- Campo universal de ubicación (Google Maps/Waze/OSM/coordenadas/dirección) sobre proveedores extensibles (src/utils/ubicacion) con resolver e inferencia inversa Nominatim (nombre/provincia pre-rellenados, alias cooficiales) - FAB flotante abajo a la derecha que abre la pasarela en un modal accesible (Escape, foco contenido, responsive); sin descripción ni puntuación; avisos de duplicado y filtro de palabras dentro del modal - Aviso PWA reposicionado: tarjeta abajo a la izquierda en móvil, botón compacto arriba a la izquierda en PC (no tapa el FAB) - Suite de pruebas: 88 unitarias/componente (vitest + RTL, offline) y 10 de integración (supertest + SQLite :memory: y Nominatim real opt-in) - CI: workflow de Gitea Actions con npm test + build en push/PR e integración manual
104 lines
4.2 KiB
JavaScript
104 lines
4.2 KiB
JavaScript
// Clase base de los proveedores de ubicación: define el contrato común que
|
|
// implementan todas las integraciones con backends de mapas (Google Maps,
|
|
// Waze, OpenStreetMap, coordenadas sueltas y Nominatim como fallback de
|
|
// texto). JS no tiene interfaces nativas, así que el contrato se documenta
|
|
// aquí y se comprueba al registrar cada instancia (ver index.js).
|
|
//
|
|
// Contrato de cada proveedor:
|
|
// - id: identificador estable del proveedor. Se copia al resultado para
|
|
// que la UI pueda etiquetar el origen de la ubicación.
|
|
// - detecta(entrada) → boolean: ¿esta entrada parece pertenecer a este
|
|
// proveedor? Debe ser barato y síncrono (normalmente un regex).
|
|
// - extrae(entrada) → Promise<Resultado|null>: extrae la información
|
|
// disponible y devuelve { proveedor, lat, lng, ...extras } o null
|
|
// si no se pudo extraer nada. Los errores de red se degradan a un
|
|
// fallo controlado (null), nunca se lanzan al consumidor.
|
|
//
|
|
// El resultado es deliberadamente extensible: los consumidores leen solo los
|
|
// campos que conocen (hoy lat/lng/nombre/provincia/direccion), de modo que
|
|
// añadir campos nuevos en el futuro (teléfono, horarios…) no rompe nada.
|
|
import { PROVINCIAS } from "../../constants/provincias.js";
|
|
|
|
export default class ProveedorUbicacion {
|
|
// Identificador del proveedor; cada subclase debe sobrescribirlo.
|
|
id = "proveedor";
|
|
|
|
// Nombres cooficiales (normalizados y sin artículo) → provincia canónica.
|
|
// Nominatim puede devolver "Bizkaia", "Girona" o "Ourense" según el idioma
|
|
// negociado; sin este mapa la provincia quedaría vacía innecesariamente.
|
|
static ALIAS_PROVINCIAS = {
|
|
bizkaia: "Vizcaya",
|
|
gipuzkoa: "Guipúzcoa",
|
|
araba: "Álava",
|
|
girona: "Gerona",
|
|
lleida: "Lérida",
|
|
ourense: "Orense",
|
|
coruna: "La Coruña",
|
|
"illes balears": "Islas Baleares",
|
|
asturies: "Asturias",
|
|
};
|
|
|
|
// ¿Reconoce esta entrada? La base no reconoce nada.
|
|
detecta(entrada) {
|
|
return false;
|
|
}
|
|
|
|
// Extrae la ubicación de la entrada. Método "abstracto": la base lanza para
|
|
// que una subclase registrada sin implementarlo falle rápido.
|
|
async extrae(entrada) {
|
|
throw new Error("sin implementar");
|
|
}
|
|
|
|
// ── Helpers compartidos por todos los proveedores ────────────────────────
|
|
|
|
// Normaliza un texto: minúsculas, sin tildes y sin espacios extremos. Se usa
|
|
// para comparar provincias y etiquetas sin depender de acentos.
|
|
static normalizar(texto) {
|
|
return (texto || "")
|
|
.toLowerCase()
|
|
.normalize("NFD")
|
|
.replace(/[\u0300-\u036f]/g, "")
|
|
.trim();
|
|
}
|
|
|
|
normalizar(texto) {
|
|
return ProveedorUbicacion.normalizar(texto);
|
|
}
|
|
|
|
// Empareja un texto libre contra el listado canónico de PROVINCIAS,
|
|
// insensible a acentos y mayúsculas y tolerante con artículos ("Comunidad
|
|
// de Madrid" → "Madrid", "A Coruña" → "La Coruña"). Devuelve "" si no casa
|
|
// con ninguna, para que la UI fuerce la selección manual (mismo
|
|
// comportamiento que tenía AutocompletarLocal).
|
|
emparejarProvincia(texto) {
|
|
const t = this.normalizar(texto);
|
|
if (!t) return "";
|
|
const sinArticulo = (s) => s.replace(/^(el|la|los|las|a|o)\s+/, "");
|
|
const tSin = sinArticulo(t);
|
|
const directa = PROVINCIAS.find((p) => {
|
|
const np = ProveedorUbicacion.normalizar(p);
|
|
const npSin = sinArticulo(np);
|
|
return t === np || t.includes(np) || tSin === npSin || tSin.includes(npSin);
|
|
});
|
|
if (directa) return directa;
|
|
// Alias de nombres cooficiales: Nominatim puede responder en euskera,
|
|
// catalán o gallego según el idioma negociado (D12 del diseño).
|
|
return ProveedorUbicacion.ALIAS_PROVINCIAS[tSin] || "";
|
|
}
|
|
|
|
// fetch a Nominatim con las cabeceras adecuadas y manejo de errores: si la
|
|
// red o el servicio fallan devuelve null (fallo controlado) en vez de
|
|
// lanzar, para que la pasarela nunca se rompa por un error de proveedor.
|
|
async fetchNominatim(url) {
|
|
try {
|
|
const respuesta = await fetch(url, {
|
|
headers: { "Accept-Language": "es", "User-Agent": "LocalesEspanoles/1.0" },
|
|
});
|
|
if (!respuesta.ok) return null;
|
|
return await respuesta.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
}
|