@cerca.red/mapa-react 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Mapa.d.ts +106 -0
- package/dist/Mapa.js +190 -0
- package/dist/Mapa.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/suscripciones.d.ts +46 -0
- package/dist/suscripciones.js +40 -0
- package/dist/suscripciones.js.map +1 -0
- package/package.json +43 -0
package/dist/Mapa.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from "react";
|
|
2
|
+
import type { Caja, Coordenada } from "@cerca.red/geo";
|
|
3
|
+
import type { BandaDeIsocrona, EventosDelMapa, FabricaDeEstilo, MapaDeCerca, Margenes, NegocioEnMapa, PaletaDelMapa, ResultadosEnMapa, RutaEnMapa, SimboloDeMapa, Tema, UbicacionDeUsuarioEnMapa, VehiculoEnMapa, ZonaEnMapa } from "@cerca.red/mapa";
|
|
4
|
+
/**
|
|
5
|
+
* `<Mapa>` — el mapa de Cerca dentro de React.
|
|
6
|
+
*
|
|
7
|
+
* ## Las dos cosas que este componente hace por dentro para que nadie tenga
|
|
8
|
+
* que acordarse
|
|
9
|
+
*
|
|
10
|
+
* **Carga el motor de forma diferida y solo en el cliente.** El `import()` de
|
|
11
|
+
* `@cerca.red/mapa/navegador` está dentro de un efecto, y los efectos no
|
|
12
|
+
* corren en el servidor: así el build de Next sobre Workers nunca ve
|
|
13
|
+
* `maplibre-gl` en el grafo del servidor, sin que la superficie tenga que
|
|
14
|
+
* envolver nada. Se hace con `import()` y no con `next/dynamic` para que este
|
|
15
|
+
* paquete sirva también fuera de Next — la landing, el laboratorio, lo que
|
|
16
|
+
* venga.
|
|
17
|
+
*
|
|
18
|
+
* Como efecto secundario, el motor tampoco entra en la primera pintura: en
|
|
19
|
+
* una pantalla de resultados el estado por defecto es la lista, y el
|
|
20
|
+
* presupuesto de carga no paga los 250 KB hasta que alguien pide el mapa.
|
|
21
|
+
*
|
|
22
|
+
* **Exige un respaldo.** `respaldo` no lleva `?`, y esa es toda la diferencia
|
|
23
|
+
* entre una degradación que existe y una que se documentó. MapLibre 6 exige
|
|
24
|
+
* WebGL2 sin alternativa; cuando no lo hay, lo que se ve sin respaldo es un
|
|
25
|
+
* rectángulo vacío que parece estar cargando para siempre. SDD §13 pide la
|
|
26
|
+
* degradación mapa→lista, y aquí no se puede omitir porque el tipo no deja
|
|
27
|
+
* compilar sin ella.
|
|
28
|
+
*/
|
|
29
|
+
interface MapaPropsBase {
|
|
30
|
+
/** Quien sabe construir el estilo. Normalmente, el de `@cerca.red/mapa-estilos`. */
|
|
31
|
+
readonly estilo: FabricaDeEstilo;
|
|
32
|
+
/** La URL del archivo de teselas, tal como vino del manifiesto. */
|
|
33
|
+
readonly urlDeTeselas: string;
|
|
34
|
+
/**
|
|
35
|
+
* Lo que se enseña cuando no hay mapa: sin WebGL2, o mientras el motor
|
|
36
|
+
* carga, o si la carga falla.
|
|
37
|
+
*
|
|
38
|
+
* **No es opcional.** Ver arriba.
|
|
39
|
+
*/
|
|
40
|
+
readonly respaldo: ReactNode;
|
|
41
|
+
readonly seleccionado?: string | undefined;
|
|
42
|
+
readonly ubicacionDeUsuario?: UbicacionDeUsuarioEnMapa | undefined;
|
|
43
|
+
readonly vehiculos?: readonly VehiculoEnMapa[];
|
|
44
|
+
readonly rutas?: readonly RutaEnMapa[];
|
|
45
|
+
readonly zonas?: readonly ZonaEnMapa[];
|
|
46
|
+
readonly isocronas?: readonly BandaDeIsocrona[];
|
|
47
|
+
readonly simbolos?: readonly SimboloDeMapa[];
|
|
48
|
+
readonly tema?: Tema;
|
|
49
|
+
readonly idioma?: string;
|
|
50
|
+
readonly margenes?: Margenes;
|
|
51
|
+
readonly colores?: Partial<PaletaDelMapa>;
|
|
52
|
+
readonly centro?: Coordenada;
|
|
53
|
+
readonly zoom?: number;
|
|
54
|
+
readonly cajaMaxima?: Caja;
|
|
55
|
+
/**
|
|
56
|
+
* De dónde bajar `maplibre-gl-worker.mjs`.
|
|
57
|
+
*
|
|
58
|
+
* Hace falta en Next.js, que reescribe el `import.meta.url` con el que
|
|
59
|
+
* MapLibre deduce esa URL y deja al motor pidiendo un archivo que no
|
|
60
|
+
* existe. Bajo Vite no hace falta.
|
|
61
|
+
*
|
|
62
|
+
* Es una prop y no una llamada suelta al arrancar porque este componente
|
|
63
|
+
* carga el motor de forma diferida: `@cerca.red/mapa/navegador` no existe
|
|
64
|
+
* hasta que el efecto lo importa, y llamar antes a `fijarUrlDelWorker`
|
|
65
|
+
* obligaría a la superficie a importar el motor en el módulo — que es justo
|
|
66
|
+
* lo que la carga diferida evita.
|
|
67
|
+
*/
|
|
68
|
+
readonly urlDelWorker?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Adónde lleva el enlace de licencias en la esquina inferior derecha.
|
|
71
|
+
*
|
|
72
|
+
* Se pasa tal cual a `crearMapa`; sin esta prop no se añade el enlace. No
|
|
73
|
+
* reemplaza la atribución de OpenStreetMap del control de MapLibre —esa
|
|
74
|
+
* viene sola desde los metadatos del `.pmtiles`—, es el enlace al
|
|
75
|
+
* documento que junta esa atribución con el resto de la cadena de
|
|
76
|
+
* licencias.
|
|
77
|
+
*/
|
|
78
|
+
readonly urlDeLicencias?: string;
|
|
79
|
+
readonly onListo?: (evento: EventosDelMapa["mapa:listo"]) => void;
|
|
80
|
+
readonly onNegocioClic?: (evento: EventosDelMapa["negocio:clic"]) => void;
|
|
81
|
+
readonly onNegocioEncima?: (evento: EventosDelMapa["negocio:encima"]) => void;
|
|
82
|
+
readonly onSedeClic?: (evento: EventosDelMapa["sede:clic"]) => void;
|
|
83
|
+
readonly onGrupoClic?: (evento: EventosDelMapa["grupo:clic"]) => void;
|
|
84
|
+
readonly onRutaEleccion?: (evento: EventosDelMapa["ruta:eleccion"]) => void;
|
|
85
|
+
readonly onVehiculoClic?: (evento: EventosDelMapa["vehiculo:clic"]) => void;
|
|
86
|
+
readonly onMovimiento?: (evento: EventosDelMapa["mapa:movimiento"]) => void;
|
|
87
|
+
readonly onError?: (evento: EventosDelMapa["mapa:error"]) => void;
|
|
88
|
+
/** Para llamar a `encuadrar`, `volarA` y demás desde fuera. */
|
|
89
|
+
readonly ref?: Ref<MapaDeCerca | null>;
|
|
90
|
+
readonly className?: string;
|
|
91
|
+
readonly "aria-label"?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `resultados` reemplaza grupos o negocios en una sola orden. `negocios`
|
|
95
|
+
* sigue disponible para consumidores anteriores, pero no se pueden combinar:
|
|
96
|
+
* no habría una regla inequívoca para decidir cuál prop gana.
|
|
97
|
+
*/
|
|
98
|
+
export type MapaProps = MapaPropsBase & ({
|
|
99
|
+
readonly resultados: ResultadosEnMapa;
|
|
100
|
+
readonly negocios?: never;
|
|
101
|
+
} | {
|
|
102
|
+
readonly resultados?: never;
|
|
103
|
+
readonly negocios?: readonly NegocioEnMapa[];
|
|
104
|
+
});
|
|
105
|
+
export declare function Mapa({ estilo, urlDeTeselas, respaldo, resultados, negocios, seleccionado, ubicacionDeUsuario, vehiculos, rutas, zonas, isocronas, simbolos, tema, idioma, margenes, colores, centro, zoom, cajaMaxima, urlDelWorker, urlDeLicencias, onListo, onNegocioClic, onNegocioEncima, onSedeClic, onGrupoClic, onRutaEleccion, onVehiculoClic, onMovimiento, onError, ref, className, "aria-label": etiqueta, }: MapaProps): import("react").JSX.Element;
|
|
106
|
+
export {};
|
package/dist/Mapa.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useImperativeHandle, useRef, useState, } from "react";
|
|
4
|
+
import { atarEventos } from "./suscripciones.js";
|
|
5
|
+
export function Mapa({ estilo, urlDeTeselas, respaldo, resultados, negocios, seleccionado, ubicacionDeUsuario, vehiculos, rutas, zonas, isocronas, simbolos, tema = "claro", idioma = "es", margenes, colores, centro, zoom, cajaMaxima, urlDelWorker, urlDeLicencias, onListo, onNegocioClic, onNegocioEncima, onSedeClic, onGrupoClic, onRutaEleccion, onVehiculoClic, onMovimiento, onError, ref, className, "aria-label": etiqueta = "Mapa de negocios cercanos", }) {
|
|
6
|
+
if (resultados !== undefined && negocios !== undefined) {
|
|
7
|
+
throw new Error("Mapa: usa `resultados` o `negocios`, no ambas props a la vez.");
|
|
8
|
+
}
|
|
9
|
+
const contenedor = useRef(null);
|
|
10
|
+
const mapa = useRef(null);
|
|
11
|
+
const [estado, setEstado] = useState("cargando");
|
|
12
|
+
/**
|
|
13
|
+
* Los callbacks más recientes, en una caja que no cambia de identidad.
|
|
14
|
+
*
|
|
15
|
+
* Sin esto habría que volver a suscribirse en cada render en el que la
|
|
16
|
+
* superficie pase una función nueva —o sea, en cada render, porque casi
|
|
17
|
+
* nadie memoriza sus manejadores—, y cada re-suscripción es un `off` y un
|
|
18
|
+
* `on` sobre el motor. Con la caja, la suscripción se hace una vez y
|
|
19
|
+
* siempre llama a la última versión.
|
|
20
|
+
*/
|
|
21
|
+
const manejadores = useRef({
|
|
22
|
+
onListo,
|
|
23
|
+
onNegocioClic,
|
|
24
|
+
onNegocioEncima,
|
|
25
|
+
onSedeClic,
|
|
26
|
+
onGrupoClic,
|
|
27
|
+
onRutaEleccion,
|
|
28
|
+
onVehiculoClic,
|
|
29
|
+
onMovimiento,
|
|
30
|
+
onError,
|
|
31
|
+
});
|
|
32
|
+
manejadores.current = {
|
|
33
|
+
onListo,
|
|
34
|
+
onNegocioClic,
|
|
35
|
+
onNegocioEncima,
|
|
36
|
+
onSedeClic,
|
|
37
|
+
onGrupoClic,
|
|
38
|
+
onRutaEleccion,
|
|
39
|
+
onVehiculoClic,
|
|
40
|
+
onMovimiento,
|
|
41
|
+
onError,
|
|
42
|
+
};
|
|
43
|
+
// Sin array de dependencias: se recalcula en cada render, que es lo que
|
|
44
|
+
// hace que la referencia deje de ser `null` en cuanto el mapa existe. Con
|
|
45
|
+
// `[estado]` el linter avisa de que la dependencia no se usa dentro, y
|
|
46
|
+
// tiene razón: lo que cambia no es `estado`, es `mapa.current`, que es un
|
|
47
|
+
// ref y por definición no dispara renders.
|
|
48
|
+
// Los dos parámetros de tipo explícitos: sin ellos, React infiere el
|
|
49
|
+
// primero del `Ref` y el segundo del valor devuelto, y se queja de que
|
|
50
|
+
// `null` no encaja en `MapaDeCerca`. `null` es justo lo que hay antes de
|
|
51
|
+
// que el motor cargue, así que el tipo correcto lo incluye.
|
|
52
|
+
useImperativeHandle(ref, () => mapa.current);
|
|
53
|
+
// — montaje —
|
|
54
|
+
//
|
|
55
|
+
// Sin dependencias: el mapa se construye una vez y vive hasta que el
|
|
56
|
+
// componente se va. Cambiar el estilo, el tema o los datos son órdenes que
|
|
57
|
+
// se le dan al mapa vivo, no un motivo para tirarlo y volver a levantarlo
|
|
58
|
+
// —que costaría un contexto WebGL nuevo y una recarga entera de teselas—.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let cancelado = false;
|
|
61
|
+
void (async () => {
|
|
62
|
+
try {
|
|
63
|
+
const { crearMapa, hayWebGL2 } = await import("@cerca.red/mapa/navegador");
|
|
64
|
+
// Dos comprobaciones y no una: el componente puede haberse desmontado
|
|
65
|
+
// mientras se descargaba el motor, y en React 19 en modo estricto eso
|
|
66
|
+
// pasa siempre en desarrollo.
|
|
67
|
+
if (cancelado)
|
|
68
|
+
return;
|
|
69
|
+
if (!hayWebGL2()) {
|
|
70
|
+
setEstado("sin-webgl");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const elemento = contenedor.current;
|
|
74
|
+
if (elemento === null)
|
|
75
|
+
return;
|
|
76
|
+
const construido = crearMapa({
|
|
77
|
+
contenedor: elemento,
|
|
78
|
+
estilo,
|
|
79
|
+
urlDeTeselas,
|
|
80
|
+
tema,
|
|
81
|
+
idioma,
|
|
82
|
+
...(centro === undefined ? {} : { centro }),
|
|
83
|
+
...(zoom === undefined ? {} : { zoom }),
|
|
84
|
+
...(margenes === undefined ? {} : { margenes }),
|
|
85
|
+
...(colores === undefined ? {} : { colores }),
|
|
86
|
+
...(cajaMaxima === undefined ? {} : { cajaMaxima }),
|
|
87
|
+
...(simbolos === undefined ? {} : { simbolos }),
|
|
88
|
+
...(urlDelWorker === undefined ? {} : { urlDelWorker }),
|
|
89
|
+
...(urlDeLicencias === undefined ? {} : { urlDeLicencias }),
|
|
90
|
+
});
|
|
91
|
+
if (cancelado) {
|
|
92
|
+
// Se desmontó entre el `hayWebGL2` y aquí. Sin este destruir, el
|
|
93
|
+
// contexto WebGL y el protocolo pmtiles se quedan colgados, y un
|
|
94
|
+
// navegador aguanta muy pocos contextos a la vez.
|
|
95
|
+
construido.destruir();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
mapa.current = construido;
|
|
99
|
+
atarEventos(construido, manejadores);
|
|
100
|
+
setEstado("listo");
|
|
101
|
+
}
|
|
102
|
+
catch (fallo) {
|
|
103
|
+
if (cancelado)
|
|
104
|
+
return;
|
|
105
|
+
setEstado("fallo");
|
|
106
|
+
manejadores.current.onError?.({
|
|
107
|
+
error: fallo instanceof Error ? fallo : new Error(String(fallo)),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
return () => {
|
|
112
|
+
cancelado = true;
|
|
113
|
+
mapa.current?.destruir();
|
|
114
|
+
mapa.current = null;
|
|
115
|
+
};
|
|
116
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
117
|
+
}, []);
|
|
118
|
+
// — datos —
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (estado !== "listo")
|
|
121
|
+
return;
|
|
122
|
+
mapa.current?.ponerResultados(resultados ?? { modo: "negocios", negocios: negocios ?? [] });
|
|
123
|
+
}, [estado, negocios, resultados]);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (estado !== "listo")
|
|
126
|
+
return;
|
|
127
|
+
// Sin `emitirEvento`: seleccionar desde una prop **no** debe emitir
|
|
128
|
+
// `negocio:clic`. Si lo hiciera, tocar un resultado en la lista abriría la
|
|
129
|
+
// ficha dos veces —una por el toque y otra por el eco del mapa— y la
|
|
130
|
+
// pantalla entraría en un ciclo que no se ve como un ciclo.
|
|
131
|
+
mapa.current?.seleccionarNegocio(resultados?.modo === "grupos" ? undefined : seleccionado);
|
|
132
|
+
}, [estado, seleccionado, negocios, resultados]);
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (estado !== "listo")
|
|
135
|
+
return;
|
|
136
|
+
mapa.current?.ponerUbicacionDeUsuario(ubicacionDeUsuario);
|
|
137
|
+
}, [estado, ubicacionDeUsuario]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (estado !== "listo")
|
|
140
|
+
return;
|
|
141
|
+
mapa.current?.ponerVehiculos(vehiculos ?? []);
|
|
142
|
+
}, [estado, vehiculos]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (estado !== "listo")
|
|
145
|
+
return;
|
|
146
|
+
mapa.current?.mostrarRuta(rutas ?? []);
|
|
147
|
+
}, [estado, rutas]);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (estado !== "listo")
|
|
150
|
+
return;
|
|
151
|
+
mapa.current?.mostrarZonaDeCobertura(zonas ?? []);
|
|
152
|
+
}, [estado, zonas]);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (estado !== "listo")
|
|
155
|
+
return;
|
|
156
|
+
mapa.current?.mostrarIsocrona(isocronas ?? []);
|
|
157
|
+
}, [estado, isocronas]);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (estado !== "listo" || simbolos === undefined)
|
|
160
|
+
return;
|
|
161
|
+
mapa.current?.registrarSimbolos(simbolos);
|
|
162
|
+
}, [estado, simbolos]);
|
|
163
|
+
// — apariencia —
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (estado !== "listo")
|
|
166
|
+
return;
|
|
167
|
+
mapa.current?.ponerTema(tema);
|
|
168
|
+
}, [estado, tema]);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (estado !== "listo")
|
|
171
|
+
return;
|
|
172
|
+
mapa.current?.ponerIdioma(idioma);
|
|
173
|
+
}, [estado, idioma]);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (estado !== "listo" || margenes === undefined)
|
|
176
|
+
return;
|
|
177
|
+
mapa.current?.ponerMargenes(margenes);
|
|
178
|
+
}, [estado, margenes]);
|
|
179
|
+
const enContenedor = useCallback((elemento) => {
|
|
180
|
+
contenedor.current = elemento;
|
|
181
|
+
}, []);
|
|
182
|
+
if (estado === "sin-webgl" || estado === "fallo")
|
|
183
|
+
return _jsx(_Fragment, { children: respaldo });
|
|
184
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: enContenedor, className: className,
|
|
185
|
+
// El canvas no es accesible por sí solo: quien navegue con teclado o
|
|
186
|
+
// con lector de pantalla necesita la lista, no esto. El rol y la
|
|
187
|
+
// etiqueta son lo mínimo para que no se anuncie como un div vacío.
|
|
188
|
+
role: "application", "aria-label": etiqueta, style: { width: "100%", height: "100%" } }), estado === "cargando" ? respaldo : null] }));
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=Mapa.js.map
|
package/dist/Mapa.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Mapa.js","sourceRoot":"","sources":["../src/Mapa.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type ReactNode,\n type Ref,\n} from \"react\";\n\nimport type { Caja, Coordenada } from \"@cerca.red/geo\";\nimport { atarEventos, type ManejadoresDelMapa } from \"./suscripciones.ts\";\nimport type {\n BandaDeIsocrona,\n EventosDelMapa,\n FabricaDeEstilo,\n MapaDeCerca,\n Margenes,\n NegocioEnMapa,\n PaletaDelMapa,\n ResultadosEnMapa,\n RutaEnMapa,\n SimboloDeMapa,\n Tema,\n UbicacionDeUsuarioEnMapa,\n VehiculoEnMapa,\n ZonaEnMapa,\n} from \"@cerca.red/mapa\";\n\n/**\n * `<Mapa>` — el mapa de Cerca dentro de React.\n *\n * ## Las dos cosas que este componente hace por dentro para que nadie tenga\n * que acordarse\n *\n * **Carga el motor de forma diferida y solo en el cliente.** El `import()` de\n * `@cerca.red/mapa/navegador` está dentro de un efecto, y los efectos no\n * corren en el servidor: así el build de Next sobre Workers nunca ve\n * `maplibre-gl` en el grafo del servidor, sin que la superficie tenga que\n * envolver nada. Se hace con `import()` y no con `next/dynamic` para que este\n * paquete sirva también fuera de Next — la landing, el laboratorio, lo que\n * venga.\n *\n * Como efecto secundario, el motor tampoco entra en la primera pintura: en\n * una pantalla de resultados el estado por defecto es la lista, y el\n * presupuesto de carga no paga los 250 KB hasta que alguien pide el mapa.\n *\n * **Exige un respaldo.** `respaldo` no lleva `?`, y esa es toda la diferencia\n * entre una degradación que existe y una que se documentó. MapLibre 6 exige\n * WebGL2 sin alternativa; cuando no lo hay, lo que se ve sin respaldo es un\n * rectángulo vacío que parece estar cargando para siempre. SDD §13 pide la\n * degradación mapa→lista, y aquí no se puede omitir porque el tipo no deja\n * compilar sin ella.\n */\n\ninterface MapaPropsBase {\n /** Quien sabe construir el estilo. Normalmente, el de `@cerca.red/mapa-estilos`. */\n readonly estilo: FabricaDeEstilo;\n /** La URL del archivo de teselas, tal como vino del manifiesto. */\n readonly urlDeTeselas: string;\n /**\n * Lo que se enseña cuando no hay mapa: sin WebGL2, o mientras el motor\n * carga, o si la carga falla.\n *\n * **No es opcional.** Ver arriba.\n */\n readonly respaldo: ReactNode;\n\n readonly seleccionado?: string | undefined;\n readonly ubicacionDeUsuario?: UbicacionDeUsuarioEnMapa | undefined;\n readonly vehiculos?: readonly VehiculoEnMapa[];\n readonly rutas?: readonly RutaEnMapa[];\n readonly zonas?: readonly ZonaEnMapa[];\n readonly isocronas?: readonly BandaDeIsocrona[];\n readonly simbolos?: readonly SimboloDeMapa[];\n\n readonly tema?: Tema;\n readonly idioma?: string;\n readonly margenes?: Margenes;\n readonly colores?: Partial<PaletaDelMapa>;\n readonly centro?: Coordenada;\n readonly zoom?: number;\n readonly cajaMaxima?: Caja;\n /**\n * De dónde bajar `maplibre-gl-worker.mjs`.\n *\n * Hace falta en Next.js, que reescribe el `import.meta.url` con el que\n * MapLibre deduce esa URL y deja al motor pidiendo un archivo que no\n * existe. Bajo Vite no hace falta.\n *\n * Es una prop y no una llamada suelta al arrancar porque este componente\n * carga el motor de forma diferida: `@cerca.red/mapa/navegador` no existe\n * hasta que el efecto lo importa, y llamar antes a `fijarUrlDelWorker`\n * obligaría a la superficie a importar el motor en el módulo — que es justo\n * lo que la carga diferida evita.\n */\n readonly urlDelWorker?: string;\n /**\n * Adónde lleva el enlace de licencias en la esquina inferior derecha.\n *\n * Se pasa tal cual a `crearMapa`; sin esta prop no se añade el enlace. No\n * reemplaza la atribución de OpenStreetMap del control de MapLibre —esa\n * viene sola desde los metadatos del `.pmtiles`—, es el enlace al\n * documento que junta esa atribución con el resto de la cadena de\n * licencias.\n */\n readonly urlDeLicencias?: string;\n\n readonly onListo?: (evento: EventosDelMapa[\"mapa:listo\"]) => void;\n readonly onNegocioClic?: (evento: EventosDelMapa[\"negocio:clic\"]) => void;\n readonly onNegocioEncima?: (evento: EventosDelMapa[\"negocio:encima\"]) => void;\n readonly onSedeClic?: (evento: EventosDelMapa[\"sede:clic\"]) => void;\n readonly onGrupoClic?: (evento: EventosDelMapa[\"grupo:clic\"]) => void;\n readonly onRutaEleccion?: (evento: EventosDelMapa[\"ruta:eleccion\"]) => void;\n readonly onVehiculoClic?: (evento: EventosDelMapa[\"vehiculo:clic\"]) => void;\n readonly onMovimiento?: (evento: EventosDelMapa[\"mapa:movimiento\"]) => void;\n readonly onError?: (evento: EventosDelMapa[\"mapa:error\"]) => void;\n\n /** Para llamar a `encuadrar`, `volarA` y demás desde fuera. */\n readonly ref?: Ref<MapaDeCerca | null>;\n readonly className?: string;\n readonly \"aria-label\"?: string;\n}\n\n/**\n * `resultados` reemplaza grupos o negocios en una sola orden. `negocios`\n * sigue disponible para consumidores anteriores, pero no se pueden combinar:\n * no habría una regla inequívoca para decidir cuál prop gana.\n */\nexport type MapaProps = MapaPropsBase &\n (\n | {\n readonly resultados: ResultadosEnMapa;\n readonly negocios?: never;\n }\n | {\n readonly resultados?: never;\n readonly negocios?: readonly NegocioEnMapa[];\n }\n );\n\ntype EstadoDeCarga = \"cargando\" | \"listo\" | \"sin-webgl\" | \"fallo\";\n\nexport function Mapa({\n estilo,\n urlDeTeselas,\n respaldo,\n resultados,\n negocios,\n seleccionado,\n ubicacionDeUsuario,\n vehiculos,\n rutas,\n zonas,\n isocronas,\n simbolos,\n tema = \"claro\",\n idioma = \"es\",\n margenes,\n colores,\n centro,\n zoom,\n cajaMaxima,\n urlDelWorker,\n urlDeLicencias,\n onListo,\n onNegocioClic,\n onNegocioEncima,\n onSedeClic,\n onGrupoClic,\n onRutaEleccion,\n onVehiculoClic,\n onMovimiento,\n onError,\n ref,\n className,\n \"aria-label\": etiqueta = \"Mapa de negocios cercanos\",\n}: MapaProps) {\n if (resultados !== undefined && negocios !== undefined) {\n throw new Error(\n \"Mapa: usa `resultados` o `negocios`, no ambas props a la vez.\",\n );\n }\n\n const contenedor = useRef<HTMLDivElement | null>(null);\n const mapa = useRef<MapaDeCerca | null>(null);\n const [estado, setEstado] = useState<EstadoDeCarga>(\"cargando\");\n\n /**\n * Los callbacks más recientes, en una caja que no cambia de identidad.\n *\n * Sin esto habría que volver a suscribirse en cada render en el que la\n * superficie pase una función nueva —o sea, en cada render, porque casi\n * nadie memoriza sus manejadores—, y cada re-suscripción es un `off` y un\n * `on` sobre el motor. Con la caja, la suscripción se hace una vez y\n * siempre llama a la última versión.\n */\n const manejadores = useRef<ManejadoresDelMapa>({\n onListo,\n onNegocioClic,\n onNegocioEncima,\n onSedeClic,\n onGrupoClic,\n onRutaEleccion,\n onVehiculoClic,\n onMovimiento,\n onError,\n });\n manejadores.current = {\n onListo,\n onNegocioClic,\n onNegocioEncima,\n onSedeClic,\n onGrupoClic,\n onRutaEleccion,\n onVehiculoClic,\n onMovimiento,\n onError,\n };\n\n // Sin array de dependencias: se recalcula en cada render, que es lo que\n // hace que la referencia deje de ser `null` en cuanto el mapa existe. Con\n // `[estado]` el linter avisa de que la dependencia no se usa dentro, y\n // tiene razón: lo que cambia no es `estado`, es `mapa.current`, que es un\n // ref y por definición no dispara renders.\n // Los dos parámetros de tipo explícitos: sin ellos, React infiere el\n // primero del `Ref` y el segundo del valor devuelto, y se queja de que\n // `null` no encaja en `MapaDeCerca`. `null` es justo lo que hay antes de\n // que el motor cargue, así que el tipo correcto lo incluye.\n useImperativeHandle<MapaDeCerca | null, MapaDeCerca | null>(\n ref,\n () => mapa.current,\n );\n\n // — montaje —\n //\n // Sin dependencias: el mapa se construye una vez y vive hasta que el\n // componente se va. Cambiar el estilo, el tema o los datos son órdenes que\n // se le dan al mapa vivo, no un motivo para tirarlo y volver a levantarlo\n // —que costaría un contexto WebGL nuevo y una recarga entera de teselas—.\n useEffect(() => {\n let cancelado = false;\n\n void (async () => {\n try {\n const { crearMapa, hayWebGL2 } =\n await import(\"@cerca.red/mapa/navegador\");\n\n // Dos comprobaciones y no una: el componente puede haberse desmontado\n // mientras se descargaba el motor, y en React 19 en modo estricto eso\n // pasa siempre en desarrollo.\n if (cancelado) return;\n\n if (!hayWebGL2()) {\n setEstado(\"sin-webgl\");\n return;\n }\n\n const elemento = contenedor.current;\n if (elemento === null) return;\n\n const construido = crearMapa({\n contenedor: elemento,\n estilo,\n urlDeTeselas,\n tema,\n idioma,\n ...(centro === undefined ? {} : { centro }),\n ...(zoom === undefined ? {} : { zoom }),\n ...(margenes === undefined ? {} : { margenes }),\n ...(colores === undefined ? {} : { colores }),\n ...(cajaMaxima === undefined ? {} : { cajaMaxima }),\n ...(simbolos === undefined ? {} : { simbolos }),\n ...(urlDelWorker === undefined ? {} : { urlDelWorker }),\n ...(urlDeLicencias === undefined ? {} : { urlDeLicencias }),\n });\n\n if (cancelado) {\n // Se desmontó entre el `hayWebGL2` y aquí. Sin este destruir, el\n // contexto WebGL y el protocolo pmtiles se quedan colgados, y un\n // navegador aguanta muy pocos contextos a la vez.\n construido.destruir();\n return;\n }\n\n mapa.current = construido;\n atarEventos(construido, manejadores);\n setEstado(\"listo\");\n } catch (fallo: unknown) {\n if (cancelado) return;\n setEstado(\"fallo\");\n manejadores.current.onError?.({\n error: fallo instanceof Error ? fallo : new Error(String(fallo)),\n });\n }\n })();\n\n return () => {\n cancelado = true;\n mapa.current?.destruir();\n mapa.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // — datos —\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerResultados(\n resultados ?? { modo: \"negocios\", negocios: negocios ?? [] },\n );\n }, [estado, negocios, resultados]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n // Sin `emitirEvento`: seleccionar desde una prop **no** debe emitir\n // `negocio:clic`. Si lo hiciera, tocar un resultado en la lista abriría la\n // ficha dos veces —una por el toque y otra por el eco del mapa— y la\n // pantalla entraría en un ciclo que no se ve como un ciclo.\n mapa.current?.seleccionarNegocio(\n resultados?.modo === \"grupos\" ? undefined : seleccionado,\n );\n }, [estado, seleccionado, negocios, resultados]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerUbicacionDeUsuario(ubicacionDeUsuario);\n }, [estado, ubicacionDeUsuario]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerVehiculos(vehiculos ?? []);\n }, [estado, vehiculos]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.mostrarRuta(rutas ?? []);\n }, [estado, rutas]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.mostrarZonaDeCobertura(zonas ?? []);\n }, [estado, zonas]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.mostrarIsocrona(isocronas ?? []);\n }, [estado, isocronas]);\n\n useEffect(() => {\n if (estado !== \"listo\" || simbolos === undefined) return;\n mapa.current?.registrarSimbolos(simbolos);\n }, [estado, simbolos]);\n\n // — apariencia —\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerTema(tema);\n }, [estado, tema]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerIdioma(idioma);\n }, [estado, idioma]);\n\n useEffect(() => {\n if (estado !== \"listo\" || margenes === undefined) return;\n mapa.current?.ponerMargenes(margenes);\n }, [estado, margenes]);\n\n const enContenedor = useCallback((elemento: HTMLDivElement | null) => {\n contenedor.current = elemento;\n }, []);\n\n if (estado === \"sin-webgl\" || estado === \"fallo\") return <>{respaldo}</>;\n\n return (\n <>\n <div\n ref={enContenedor}\n className={className}\n // El canvas no es accesible por sí solo: quien navegue con teclado o\n // con lector de pantalla necesita la lista, no esto. El rol y la\n // etiqueta son lo mínimo para que no se anuncie como un div vacío.\n role=\"application\"\n aria-label={etiqueta}\n style={{ width: \"100%\", height: \"100%\" }}\n />\n {estado === \"cargando\" ? respaldo : null}\n </>\n );\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cerca.red/mapa-react` — el mapa de Cerca dentro de React.
|
|
3
|
+
*
|
|
4
|
+
* El único paquete de este repositorio con JSX, y el único que puede
|
|
5
|
+
* renderizar. Lo que hace por dentro, y que por eso ninguna superficie tiene
|
|
6
|
+
* que repetir: cargar `maplibre-gl` de forma diferida y solo en el cliente, y
|
|
7
|
+
* exigir un respaldo para cuando no hay WebGL2.
|
|
8
|
+
*/
|
|
9
|
+
export { Mapa, type MapaProps } from "./Mapa.tsx";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cerca.red/mapa-react` — el mapa de Cerca dentro de React.
|
|
3
|
+
*
|
|
4
|
+
* El único paquete de este repositorio con JSX, y el único que puede
|
|
5
|
+
* renderizar. Lo que hace por dentro, y que por eso ninguna superficie tiene
|
|
6
|
+
* que repetir: cargar `maplibre-gl` de forma diferida y solo en el cliente, y
|
|
7
|
+
* exigir un respaldo para cuando no hay WebGL2.
|
|
8
|
+
*/
|
|
9
|
+
export { Mapa } from "./Mapa.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAkB,MAAM,WAAY,CAAC","sourcesContent":["/**\n * `@cerca.red/mapa-react` — el mapa de Cerca dentro de React.\n *\n * El único paquete de este repositorio con JSX, y el único que puede\n * renderizar. Lo que hace por dentro, y que por eso ninguna superficie tiene\n * que repetir: cargar `maplibre-gl` de forma diferida y solo en el cliente, y\n * exigir un respaldo para cuando no hay WebGL2.\n */\nexport { Mapa, type MapaProps } from \"./Mapa.tsx\";\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Desuscribir, EventosDelMapa, MapaDeCerca } from "@cerca.red/mapa";
|
|
2
|
+
/**
|
|
3
|
+
* Lo único de este paquete que no es React, sacado del componente para poder
|
|
4
|
+
* probarlo.
|
|
5
|
+
*
|
|
6
|
+
* `<Mapa>` es efectos y renderizado: montar, desmontar, reaccionar a props.
|
|
7
|
+
* Medir eso con vitest exigiría un DOM falso y una biblioteca de renderizado,
|
|
8
|
+
* y lo que se comprobaría sería sobre todo React. Esto de aquí sí tiene
|
|
9
|
+
* comportamiento propio —qué eventos se atan y a qué versión del callback— y
|
|
10
|
+
* se prueba con un objeto plano.
|
|
11
|
+
*/
|
|
12
|
+
/** Los manejadores que la superficie pasó, en su versión más reciente. */
|
|
13
|
+
export interface ManejadoresDelMapa {
|
|
14
|
+
onListo?: (evento: EventosDelMapa["mapa:listo"]) => void;
|
|
15
|
+
onNegocioClic?: (evento: EventosDelMapa["negocio:clic"]) => void;
|
|
16
|
+
onNegocioEncima?: (evento: EventosDelMapa["negocio:encima"]) => void;
|
|
17
|
+
onSedeClic?: (evento: EventosDelMapa["sede:clic"]) => void;
|
|
18
|
+
onGrupoClic?: (evento: EventosDelMapa["grupo:clic"]) => void;
|
|
19
|
+
onRutaEleccion?: (evento: EventosDelMapa["ruta:eleccion"]) => void;
|
|
20
|
+
onVehiculoClic?: (evento: EventosDelMapa["vehiculo:clic"]) => void;
|
|
21
|
+
onMovimiento?: (evento: EventosDelMapa["mapa:movimiento"]) => void;
|
|
22
|
+
onError?: (evento: EventosDelMapa["mapa:error"]) => void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Una caja que no cambia de identidad y siempre tiene lo último.
|
|
26
|
+
*
|
|
27
|
+
* Es la forma de un `useRef`, escrita como interfaz para que esta función no
|
|
28
|
+
* dependa de React y se pueda probar con `{ current: … }`.
|
|
29
|
+
*/
|
|
30
|
+
export interface CajaDeManejadores {
|
|
31
|
+
readonly current: ManejadoresDelMapa;
|
|
32
|
+
}
|
|
33
|
+
/** Los eventos que este componente reenvía, para poder afirmarlo en una prueba. */
|
|
34
|
+
export declare const EVENTOS_ATADOS: readonly (keyof EventosDelMapa)[];
|
|
35
|
+
/**
|
|
36
|
+
* Ata cada evento del SDK a la prop que le corresponde.
|
|
37
|
+
*
|
|
38
|
+
* **Se suscribe una vez y lee el callback en el momento de llamarlo**, en vez
|
|
39
|
+
* de volver a suscribirse en cada render. La diferencia importa porque casi
|
|
40
|
+
* nadie memoriza sus manejadores: sin la caja, cada render de la superficie
|
|
41
|
+
* sería un `off` y un `on` sobre el motor por cada uno de los nueve eventos.
|
|
42
|
+
*
|
|
43
|
+
* Devuelve cómo soltar todo, aunque `destruir()` del SDK ya cancela sus
|
|
44
|
+
* propias suscripciones: el componente puede querer soltarlas antes.
|
|
45
|
+
*/
|
|
46
|
+
export declare function atarEventos(mapa: MapaDeCerca, manejadores: CajaDeManejadores): Desuscribir;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Qué evento del SDK va a qué prop. Un solo sitio, para que no falte ninguno. */
|
|
2
|
+
const ATADURAS = [
|
|
3
|
+
["mapa:listo", "onListo"],
|
|
4
|
+
["negocio:clic", "onNegocioClic"],
|
|
5
|
+
["negocio:encima", "onNegocioEncima"],
|
|
6
|
+
["sede:clic", "onSedeClic"],
|
|
7
|
+
["grupo:clic", "onGrupoClic"],
|
|
8
|
+
["ruta:eleccion", "onRutaEleccion"],
|
|
9
|
+
["vehiculo:clic", "onVehiculoClic"],
|
|
10
|
+
["mapa:movimiento", "onMovimiento"],
|
|
11
|
+
["mapa:error", "onError"],
|
|
12
|
+
];
|
|
13
|
+
/** Los eventos que este componente reenvía, para poder afirmarlo en una prueba. */
|
|
14
|
+
export const EVENTOS_ATADOS = ATADURAS.map(([evento]) => evento);
|
|
15
|
+
/**
|
|
16
|
+
* Ata cada evento del SDK a la prop que le corresponde.
|
|
17
|
+
*
|
|
18
|
+
* **Se suscribe una vez y lee el callback en el momento de llamarlo**, en vez
|
|
19
|
+
* de volver a suscribirse en cada render. La diferencia importa porque casi
|
|
20
|
+
* nadie memoriza sus manejadores: sin la caja, cada render de la superficie
|
|
21
|
+
* sería un `off` y un `on` sobre el motor por cada uno de los nueve eventos.
|
|
22
|
+
*
|
|
23
|
+
* Devuelve cómo soltar todo, aunque `destruir()` del SDK ya cancela sus
|
|
24
|
+
* propias suscripciones: el componente puede querer soltarlas antes.
|
|
25
|
+
*/
|
|
26
|
+
export function atarEventos(mapa, manejadores) {
|
|
27
|
+
const cancelar = ATADURAS.map(([evento, prop]) => mapa.cuando(evento, (dato) => {
|
|
28
|
+
// La lectura va **dentro**: `manejadores.current[prop]` en el momento
|
|
29
|
+
// de suscribir congelaría la primera versión del callback, y la
|
|
30
|
+
// superficie que cambia de manejador entre renders dejaría de recibir
|
|
31
|
+
// eventos sin que nada fallara.
|
|
32
|
+
const manejador = manejadores.current[prop];
|
|
33
|
+
manejador?.(dato);
|
|
34
|
+
}));
|
|
35
|
+
return () => {
|
|
36
|
+
for (const soltar of cancelar)
|
|
37
|
+
soltar();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=suscripciones.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suscripciones.js","sourceRoot":"","sources":["../src/suscripciones.ts"],"names":[],"mappings":"AAoCA,kFAAkF;AAClF,MAAM,QAAQ,GAAG;IACf,CAAC,YAAY,EAAE,SAAS,CAAC;IACzB,CAAC,cAAc,EAAE,eAAe,CAAC;IACjC,CAAC,gBAAgB,EAAE,iBAAiB,CAAC;IACrC,CAAC,WAAW,EAAE,YAAY,CAAC;IAC3B,CAAC,YAAY,EAAE,aAAa,CAAC;IAC7B,CAAC,eAAe,EAAE,gBAAgB,CAAC;IACnC,CAAC,eAAe,EAAE,gBAAgB,CAAC;IACnC,CAAC,iBAAiB,EAAE,cAAc,CAAC;IACnC,CAAC,YAAY,EAAE,SAAS,CAAC;CAG1B,CAAC;AAEF,mFAAmF;AACnF,MAAM,CAAC,MAAM,cAAc,GAAsC,QAAQ,CAAC,GAAG,CAC3E,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CACrB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CACzB,IAAiB,EACjB,WAA8B;IAE9B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAC/C,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QAC3B,sEAAsE;QACtE,gEAAgE;QAChE,sEAAsE;QACtE,gCAAgC;QAChC,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3C,SAAqD,EAAE,CAAC,IAAI,CAAC,CAAC;IACjE,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,GAAG,EAAE;QACV,KAAK,MAAM,MAAM,IAAI,QAAQ;YAAE,MAAM,EAAE,CAAC;IAC1C,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { Desuscribir, EventosDelMapa, MapaDeCerca } from \"@cerca.red/mapa\";\n\n/**\n * Lo único de este paquete que no es React, sacado del componente para poder\n * probarlo.\n *\n * `<Mapa>` es efectos y renderizado: montar, desmontar, reaccionar a props.\n * Medir eso con vitest exigiría un DOM falso y una biblioteca de renderizado,\n * y lo que se comprobaría sería sobre todo React. Esto de aquí sí tiene\n * comportamiento propio —qué eventos se atan y a qué versión del callback— y\n * se prueba con un objeto plano.\n */\n\n/** Los manejadores que la superficie pasó, en su versión más reciente. */\nexport interface ManejadoresDelMapa {\n onListo?: (evento: EventosDelMapa[\"mapa:listo\"]) => void;\n onNegocioClic?: (evento: EventosDelMapa[\"negocio:clic\"]) => void;\n onNegocioEncima?: (evento: EventosDelMapa[\"negocio:encima\"]) => void;\n onSedeClic?: (evento: EventosDelMapa[\"sede:clic\"]) => void;\n onGrupoClic?: (evento: EventosDelMapa[\"grupo:clic\"]) => void;\n onRutaEleccion?: (evento: EventosDelMapa[\"ruta:eleccion\"]) => void;\n onVehiculoClic?: (evento: EventosDelMapa[\"vehiculo:clic\"]) => void;\n onMovimiento?: (evento: EventosDelMapa[\"mapa:movimiento\"]) => void;\n onError?: (evento: EventosDelMapa[\"mapa:error\"]) => void;\n}\n\n/**\n * Una caja que no cambia de identidad y siempre tiene lo último.\n *\n * Es la forma de un `useRef`, escrita como interfaz para que esta función no\n * dependa de React y se pueda probar con `{ current: … }`.\n */\nexport interface CajaDeManejadores {\n readonly current: ManejadoresDelMapa;\n}\n\n/** Qué evento del SDK va a qué prop. Un solo sitio, para que no falte ninguno. */\nconst ATADURAS = [\n [\"mapa:listo\", \"onListo\"],\n [\"negocio:clic\", \"onNegocioClic\"],\n [\"negocio:encima\", \"onNegocioEncima\"],\n [\"sede:clic\", \"onSedeClic\"],\n [\"grupo:clic\", \"onGrupoClic\"],\n [\"ruta:eleccion\", \"onRutaEleccion\"],\n [\"vehiculo:clic\", \"onVehiculoClic\"],\n [\"mapa:movimiento\", \"onMovimiento\"],\n [\"mapa:error\", \"onError\"],\n] as const satisfies ReadonlyArray<\n readonly [keyof EventosDelMapa, keyof ManejadoresDelMapa]\n>;\n\n/** Los eventos que este componente reenvía, para poder afirmarlo en una prueba. */\nexport const EVENTOS_ATADOS: readonly (keyof EventosDelMapa)[] = ATADURAS.map(\n ([evento]) => evento,\n);\n\n/**\n * Ata cada evento del SDK a la prop que le corresponde.\n *\n * **Se suscribe una vez y lee el callback en el momento de llamarlo**, en vez\n * de volver a suscribirse en cada render. La diferencia importa porque casi\n * nadie memoriza sus manejadores: sin la caja, cada render de la superficie\n * sería un `off` y un `on` sobre el motor por cada uno de los nueve eventos.\n *\n * Devuelve cómo soltar todo, aunque `destruir()` del SDK ya cancela sus\n * propias suscripciones: el componente puede querer soltarlas antes.\n */\nexport function atarEventos(\n mapa: MapaDeCerca,\n manejadores: CajaDeManejadores,\n): Desuscribir {\n const cancelar = ATADURAS.map(([evento, prop]) =>\n mapa.cuando(evento, (dato) => {\n // La lectura va **dentro**: `manejadores.current[prop]` en el momento\n // de suscribir congelaría la primera versión del callback, y la\n // superficie que cambia de manejador entre renders dejaría de recibir\n // eventos sin que nada fallara.\n const manejador = manejadores.current[prop];\n (manejador as ((evento: unknown) => void) | undefined)?.(dato);\n }),\n );\n\n return () => {\n for (const soltar of cancelar) soltar();\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cerca.red/mapa-react",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "La envoltura React del mapa de Cerca. Carga el motor de forma diferida y solo en el cliente, y exige un respaldo para cuando no hay WebGL2.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@cerca.red/geo": "0.2.0",
|
|
22
|
+
"@cerca.red/mapa": "0.2.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"maplibre-gl": "^6.8.0",
|
|
26
|
+
"pmtiles": "^4.5.0",
|
|
27
|
+
"react": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^19.2.0",
|
|
31
|
+
"maplibre-gl": "6.8.0",
|
|
32
|
+
"pmtiles": "4.5.0",
|
|
33
|
+
"react": "^19.2.0"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Exus-Agencia-Web/cerca-maps.git",
|
|
38
|
+
"directory": "packages/mapa-react"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"construir": "tsc -p tsconfig.build.json"
|
|
42
|
+
}
|
|
43
|
+
}
|