Files
localesp/src/utils/ubicacion/ProveedorUbicacion.js
T
edgar.friendly 003b1e209f
deploy-branch / deploy (push) Failing after 6s
deploy-branch / teardown (push) Skipped
feat(propuesta): pasarela paso a paso con autodetección de ubicación y FAB
- 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
2026-08-16 01:35:02 +02:00

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;
}
}
}