@cerca.red/mapa 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/README.md +133 -0
- package/dist/capas.d.ts +29 -0
- package/dist/capas.js +489 -0
- package/dist/capas.js.map +1 -0
- package/dist/doble/estiloFalso.d.ts +19 -0
- package/dist/doble/estiloFalso.js +33 -0
- package/dist/doble/estiloFalso.js.map +1 -0
- package/dist/doble/index.d.ts +11 -0
- package/dist/doble/index.js +12 -0
- package/dist/doble/index.js.map +1 -0
- package/dist/doble/mapaFalso.d.ts +75 -0
- package/dist/doble/mapaFalso.js +535 -0
- package/dist/doble/mapaFalso.js.map +1 -0
- package/dist/encuadre.d.ts +64 -0
- package/dist/encuadre.js +83 -0
- package/dist/encuadre.js.map +1 -0
- package/dist/estilo.d.ts +88 -0
- package/dist/estilo.js +23 -0
- package/dist/estilo.js.map +1 -0
- package/dist/eventos.d.ts +127 -0
- package/dist/eventos.js +69 -0
- package/dist/eventos.js.map +1 -0
- package/dist/identificadores.d.ts +109 -0
- package/dist/identificadores.js +104 -0
- package/dist/identificadores.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/manifiesto.d.ts +97 -0
- package/dist/manifiesto.js +156 -0
- package/dist/manifiesto.js.map +1 -0
- package/dist/modelo.d.ts +154 -0
- package/dist/modelo.js +227 -0
- package/dist/modelo.js.map +1 -0
- package/dist/navegador/index.d.ts +70 -0
- package/dist/navegador/index.js +132 -0
- package/dist/navegador/index.js.map +1 -0
- package/dist/navegador/maplibre.d.ts +41 -0
- package/dist/navegador/maplibre.js +463 -0
- package/dist/navegador/maplibre.js.map +1 -0
- package/dist/navegador/protocolo.d.ts +4 -0
- package/dist/navegador/protocolo.js +55 -0
- package/dist/navegador/protocolo.js.map +1 -0
- package/dist/navegador/webgl.d.ts +24 -0
- package/dist/navegador/webgl.js +47 -0
- package/dist/navegador/webgl.js.map +1 -0
- package/dist/navegador/worker.d.ts +33 -0
- package/dist/navegador/worker.js +47 -0
- package/dist/navegador/worker.js.map +1 -0
- package/dist/paleta.d.ts +53 -0
- package/dist/paleta.js +53 -0
- package/dist/paleta.js.map +1 -0
- package/dist/proyecciones.d.ts +160 -0
- package/dist/proyecciones.js +2 -0
- package/dist/proyecciones.js.map +1 -0
- package/dist/puerto.d.ts +244 -0
- package/dist/puerto.js +2 -0
- package/dist/puerto.js.map +1 -0
- package/dist/sdk.d.ts +111 -0
- package/dist/sdk.js +552 -0
- package/dist/sdk.js.map +1 -0
- package/dist/sincronizacion.d.ts +40 -0
- package/dist/sincronizacion.js +144 -0
- package/dist/sincronizacion.js.map +1 -0
- package/dist/worker-de-maplibre.d.ts +36 -0
- package/dist/worker-de-maplibre.js +37 -0
- package/dist/worker-de-maplibre.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Caja } from "@cerca.red/geo";
|
|
2
|
+
import type { DestinoDeCamara, Margenes, OpcionesDeEncuadre, VistaDelMapa } from "./puerto.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Cómo se encuadra algo, decidido **antes** de tocar el mapa.
|
|
5
|
+
*
|
|
6
|
+
* ## El fallo que este archivo existe para evitar
|
|
7
|
+
*
|
|
8
|
+
* `fitBounds` resuelve el zoom que hace caber una caja en la pantalla. Para
|
|
9
|
+
* una caja de **área cero** —un solo negocio, o tres sedes en la misma
|
|
10
|
+
* dirección— ese zoom es el máximo que el motor admite. El usuario aterriza
|
|
11
|
+
* encima de un tejado, sin una calle con nombre a la vista, y el mapa parece
|
|
12
|
+
* roto aunque hizo exactamente lo que se le pidió.
|
|
13
|
+
*
|
|
14
|
+
* No es un caso raro: es el caso más común de la ficha de un negocio.
|
|
15
|
+
*
|
|
16
|
+
* ## Por qué se devuelve un plan en vez de llamar al mapa
|
|
17
|
+
*
|
|
18
|
+
* Porque así se prueba sin mapa. La decisión —no moverse, volar a un punto
|
|
19
|
+
* con tope, o encuadrar una caja— es toda la lógica; ejecutarla son tres
|
|
20
|
+
* líneas. Partirlo deja la parte que se puede equivocar en Node y bajo
|
|
21
|
+
* pruebas, y la parte que necesita WebGL reducida a lo que no puede fallar.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* El zoom al que se aterriza cuando lo que hay que encuadrar es un punto.
|
|
25
|
+
*
|
|
26
|
+
* 16 y no 18: los archivos de país se construyen hasta z14 (ADR-024, que lo
|
|
27
|
+
* decidió midiendo contra el techo de caché del borde), así que por encima de
|
|
28
|
+
* ese nivel MapLibre sobreescala las teselas que ya tiene. Dos niveles de
|
|
29
|
+
* sobreescalado se ven bien —los rótulos y las líneas siguen nítidos porque
|
|
30
|
+
* son vectores—; cuatro se ven como un mapa borroso al que le falta detalle
|
|
31
|
+
* que no existe.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ZOOM_DE_UN_PUNTO = 16;
|
|
34
|
+
/** El margen que se deja alrededor de una caja encuadrada, si no dicen otro. */
|
|
35
|
+
export declare const MARGENES_POR_DEFECTO: Margenes;
|
|
36
|
+
export type PlanDeEncuadre = {
|
|
37
|
+
readonly tipo: "no-mover";
|
|
38
|
+
readonly porque: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly tipo: "camara";
|
|
41
|
+
readonly destino: DestinoDeCamara;
|
|
42
|
+
readonly porque: string;
|
|
43
|
+
} | {
|
|
44
|
+
readonly tipo: "caja";
|
|
45
|
+
readonly caja: Caja;
|
|
46
|
+
readonly porque: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Qué hacer para encuadrar `caja`, sabiendo dónde está la cámara.
|
|
50
|
+
*
|
|
51
|
+
* `porque` no es decoración: es lo que aparece en el mensaje de una prueba que
|
|
52
|
+
* falla y lo que permite distinguir «no se movió porque no había nada» de «no
|
|
53
|
+
* se movió y no sabemos por qué».
|
|
54
|
+
*/
|
|
55
|
+
export declare function planearEncuadre(caja: Caja | undefined, vistaActual: VistaDelMapa, opciones?: Pick<OpcionesDeEncuadre, "zoomMaximo">): PlanDeEncuadre;
|
|
56
|
+
/**
|
|
57
|
+
* El modo de movimiento que corresponde a un plan.
|
|
58
|
+
*
|
|
59
|
+
* El vuelo de MapLibre es una curva que se aleja y se vuelve a acercar. Para
|
|
60
|
+
* cruzar la ciudad se ve bien; para ir a un punto que ya está en pantalla se
|
|
61
|
+
* ve como un tirón hacia atrás, y es exactamente lo que pasa al tocar un
|
|
62
|
+
* resultado de la lista con el mapa ya centrado en él.
|
|
63
|
+
*/
|
|
64
|
+
export declare function modoParaPlan(plan: PlanDeEncuadre): "vuelo" | "suave" | "salto";
|
package/dist/encuadre.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { centroDeCaja, esCajaDegenerada } from "@cerca.red/geo";
|
|
2
|
+
/**
|
|
3
|
+
* Cómo se encuadra algo, decidido **antes** de tocar el mapa.
|
|
4
|
+
*
|
|
5
|
+
* ## El fallo que este archivo existe para evitar
|
|
6
|
+
*
|
|
7
|
+
* `fitBounds` resuelve el zoom que hace caber una caja en la pantalla. Para
|
|
8
|
+
* una caja de **área cero** —un solo negocio, o tres sedes en la misma
|
|
9
|
+
* dirección— ese zoom es el máximo que el motor admite. El usuario aterriza
|
|
10
|
+
* encima de un tejado, sin una calle con nombre a la vista, y el mapa parece
|
|
11
|
+
* roto aunque hizo exactamente lo que se le pidió.
|
|
12
|
+
*
|
|
13
|
+
* No es un caso raro: es el caso más común de la ficha de un negocio.
|
|
14
|
+
*
|
|
15
|
+
* ## Por qué se devuelve un plan en vez de llamar al mapa
|
|
16
|
+
*
|
|
17
|
+
* Porque así se prueba sin mapa. La decisión —no moverse, volar a un punto
|
|
18
|
+
* con tope, o encuadrar una caja— es toda la lógica; ejecutarla son tres
|
|
19
|
+
* líneas. Partirlo deja la parte que se puede equivocar en Node y bajo
|
|
20
|
+
* pruebas, y la parte que necesita WebGL reducida a lo que no puede fallar.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* El zoom al que se aterriza cuando lo que hay que encuadrar es un punto.
|
|
24
|
+
*
|
|
25
|
+
* 16 y no 18: los archivos de país se construyen hasta z14 (ADR-024, que lo
|
|
26
|
+
* decidió midiendo contra el techo de caché del borde), así que por encima de
|
|
27
|
+
* ese nivel MapLibre sobreescala las teselas que ya tiene. Dos niveles de
|
|
28
|
+
* sobreescalado se ven bien —los rótulos y las líneas siguen nítidos porque
|
|
29
|
+
* son vectores—; cuatro se ven como un mapa borroso al que le falta detalle
|
|
30
|
+
* que no existe.
|
|
31
|
+
*/
|
|
32
|
+
export const ZOOM_DE_UN_PUNTO = 16;
|
|
33
|
+
/** El margen que se deja alrededor de una caja encuadrada, si no dicen otro. */
|
|
34
|
+
export const MARGENES_POR_DEFECTO = {
|
|
35
|
+
arriba: 48,
|
|
36
|
+
derecha: 48,
|
|
37
|
+
abajo: 48,
|
|
38
|
+
izquierda: 48,
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Qué hacer para encuadrar `caja`, sabiendo dónde está la cámara.
|
|
42
|
+
*
|
|
43
|
+
* `porque` no es decoración: es lo que aparece en el mensaje de una prueba que
|
|
44
|
+
* falla y lo que permite distinguir «no se movió porque no había nada» de «no
|
|
45
|
+
* se movió y no sabemos por qué».
|
|
46
|
+
*/
|
|
47
|
+
export function planearEncuadre(caja, vistaActual, opciones = {}) {
|
|
48
|
+
if (caja === undefined) {
|
|
49
|
+
return {
|
|
50
|
+
tipo: "no-mover",
|
|
51
|
+
porque: "no hay nada que encuadrar",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const zoomMaximo = opciones.zoomMaximo ?? ZOOM_DE_UN_PUNTO;
|
|
55
|
+
if (esCajaDegenerada(caja)) {
|
|
56
|
+
// El punto entero de este archivo. Se va al centro con un zoom con tope,
|
|
57
|
+
// y **sin alejarse**: si la cámara ya estaba más cerca que el tope
|
|
58
|
+
// —porque el usuario había hecho zoom a mano— acercarse menos sería
|
|
59
|
+
// deshacerle el gesto.
|
|
60
|
+
return {
|
|
61
|
+
tipo: "camara",
|
|
62
|
+
destino: {
|
|
63
|
+
centro: centroDeCaja(caja),
|
|
64
|
+
zoom: Math.max(vistaActual.zoom, zoomMaximo),
|
|
65
|
+
},
|
|
66
|
+
porque: "la caja mide menos que una manzana: encuadrarla llevaría al zoom " +
|
|
67
|
+
"máximo, encima del tejado",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { tipo: "caja", caja, porque: "hay una caja con área que encuadrar" };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* El modo de movimiento que corresponde a un plan.
|
|
74
|
+
*
|
|
75
|
+
* El vuelo de MapLibre es una curva que se aleja y se vuelve a acercar. Para
|
|
76
|
+
* cruzar la ciudad se ve bien; para ir a un punto que ya está en pantalla se
|
|
77
|
+
* ve como un tirón hacia atrás, y es exactamente lo que pasa al tocar un
|
|
78
|
+
* resultado de la lista con el mapa ya centrado en él.
|
|
79
|
+
*/
|
|
80
|
+
export function modoParaPlan(plan) {
|
|
81
|
+
return plan.tipo === "camara" ? "suave" : "vuelo";
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=encuadre.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encuadre.js","sourceRoot":"","sources":["../src/encuadre.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAS3E;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAEnC,gFAAgF;AAChF,MAAM,CAAC,MAAM,oBAAoB,GAAa;IAC5C,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,KAAK,EAAE,EAAE;IACT,SAAS,EAAE,EAAE;CACd,CAAC;AAeF;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAsB,EACtB,WAAyB,EACzB,WAAmD,EAAE;IAErD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO;YACL,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,2BAA2B;SACpC,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,QAAQ,CAAC,UAAU,IAAI,gBAAgB,CAAC;IAE3D,IAAI,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,yEAAyE;QACzE,mEAAmE;QACnE,oEAAoE;QACpE,uBAAuB;QACvB,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE;gBACP,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC;gBAC1B,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC;aAC7C;YACD,MAAM,EACJ,mEAAmE;gBACnE,2BAA2B;SAC9B,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,qCAAqC,EAAE,CAAC;AAC/E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAoB;IAEpB,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AACpD,CAAC","sourcesContent":["import { type Caja, centroDeCaja, esCajaDegenerada } from \"@cerca.red/geo\";\n\nimport type {\n DestinoDeCamara,\n Margenes,\n OpcionesDeEncuadre,\n VistaDelMapa,\n} from \"./puerto.ts\";\n\n/**\n * Cómo se encuadra algo, decidido **antes** de tocar el mapa.\n *\n * ## El fallo que este archivo existe para evitar\n *\n * `fitBounds` resuelve el zoom que hace caber una caja en la pantalla. Para\n * una caja de **área cero** —un solo negocio, o tres sedes en la misma\n * dirección— ese zoom es el máximo que el motor admite. El usuario aterriza\n * encima de un tejado, sin una calle con nombre a la vista, y el mapa parece\n * roto aunque hizo exactamente lo que se le pidió.\n *\n * No es un caso raro: es el caso más común de la ficha de un negocio.\n *\n * ## Por qué se devuelve un plan en vez de llamar al mapa\n *\n * Porque así se prueba sin mapa. La decisión —no moverse, volar a un punto\n * con tope, o encuadrar una caja— es toda la lógica; ejecutarla son tres\n * líneas. Partirlo deja la parte que se puede equivocar en Node y bajo\n * pruebas, y la parte que necesita WebGL reducida a lo que no puede fallar.\n */\n\n/**\n * El zoom al que se aterriza cuando lo que hay que encuadrar es un punto.\n *\n * 16 y no 18: los archivos de país se construyen hasta z14 (ADR-024, que lo\n * decidió midiendo contra el techo de caché del borde), así que por encima de\n * ese nivel MapLibre sobreescala las teselas que ya tiene. Dos niveles de\n * sobreescalado se ven bien —los rótulos y las líneas siguen nítidos porque\n * son vectores—; cuatro se ven como un mapa borroso al que le falta detalle\n * que no existe.\n */\nexport const ZOOM_DE_UN_PUNTO = 16;\n\n/** El margen que se deja alrededor de una caja encuadrada, si no dicen otro. */\nexport const MARGENES_POR_DEFECTO: Margenes = {\n arriba: 48,\n derecha: 48,\n abajo: 48,\n izquierda: 48,\n};\n\nexport type PlanDeEncuadre =\n | { readonly tipo: \"no-mover\"; readonly porque: string }\n | {\n readonly tipo: \"camara\";\n readonly destino: DestinoDeCamara;\n readonly porque: string;\n }\n | {\n readonly tipo: \"caja\";\n readonly caja: Caja;\n readonly porque: string;\n };\n\n/**\n * Qué hacer para encuadrar `caja`, sabiendo dónde está la cámara.\n *\n * `porque` no es decoración: es lo que aparece en el mensaje de una prueba que\n * falla y lo que permite distinguir «no se movió porque no había nada» de «no\n * se movió y no sabemos por qué».\n */\nexport function planearEncuadre(\n caja: Caja | undefined,\n vistaActual: VistaDelMapa,\n opciones: Pick<OpcionesDeEncuadre, \"zoomMaximo\"> = {},\n): PlanDeEncuadre {\n if (caja === undefined) {\n return {\n tipo: \"no-mover\",\n porque: \"no hay nada que encuadrar\",\n };\n }\n\n const zoomMaximo = opciones.zoomMaximo ?? ZOOM_DE_UN_PUNTO;\n\n if (esCajaDegenerada(caja)) {\n // El punto entero de este archivo. Se va al centro con un zoom con tope,\n // y **sin alejarse**: si la cámara ya estaba más cerca que el tope\n // —porque el usuario había hecho zoom a mano— acercarse menos sería\n // deshacerle el gesto.\n return {\n tipo: \"camara\",\n destino: {\n centro: centroDeCaja(caja),\n zoom: Math.max(vistaActual.zoom, zoomMaximo),\n },\n porque:\n \"la caja mide menos que una manzana: encuadrarla llevaría al zoom \" +\n \"máximo, encima del tejado\",\n };\n }\n\n return { tipo: \"caja\", caja, porque: \"hay una caja con área que encuadrar\" };\n}\n\n/**\n * El modo de movimiento que corresponde a un plan.\n *\n * El vuelo de MapLibre es una curva que se aleja y se vuelve a acercar. Para\n * cruzar la ciudad se ve bien; para ir a un punto que ya está en pantalla se\n * ve como un tirón hacia atrás, y es exactamente lo que pasa al tocar un\n * resultado de la lista con el mapa ya centrado en él.\n */\nexport function modoParaPlan(\n plan: PlanDeEncuadre,\n): \"vuelo\" | \"suave\" | \"salto\" {\n return plan.tipo === \"camara\" ? \"suave\" : \"vuelo\";\n}\n"]}
|
package/dist/estilo.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* La forma de un estilo de MapLibre, declarada aquí en vez de importada.
|
|
3
|
+
*
|
|
4
|
+
* ## Por qué no se importa de `maplibre-gl`
|
|
5
|
+
*
|
|
6
|
+
* Porque este archivo lo lee el núcleo del SDK, que corre en Node y dentro de
|
|
7
|
+
* workerd, y `maplibre-gl` toca `window` al importarse. Importar «solo el
|
|
8
|
+
* tipo» tampoco vale: un `import type` desaparece al compilar, pero mete el
|
|
9
|
+
* paquete en las dependencias de un módulo que tiene que poder instalarse sin
|
|
10
|
+
* un navegador, y basta con que alguien quite el `type` un día para que el
|
|
11
|
+
* punto de entrada por defecto arrastre 250 KB al bundle del servidor.
|
|
12
|
+
*
|
|
13
|
+
* Lo que hay aquí es la parte de la especificación 8 que este SDK escribe: lo
|
|
14
|
+
* suficiente para construir capas y fuentes con el editor ayudando, y no la
|
|
15
|
+
* especificación entera. `[clave: string]: unknown` deja pasar lo demás, que
|
|
16
|
+
* es lo que hace que un estilo real de `@cerca.red/mapa-estilos` encaje sin
|
|
17
|
+
* conversión.
|
|
18
|
+
*
|
|
19
|
+
* Y esto es exactamente lo que ADR-027 llama «el puerto»: la lista escrita de
|
|
20
|
+
* lo que hay que revisar el día que salga MapLibre 7.
|
|
21
|
+
*/
|
|
22
|
+
/** Los tipos de capa que este SDK dibuja. No es la lista completa de la especificación. */
|
|
23
|
+
export type TipoDeCapa = "background" | "fill" | "line" | "symbol" | "circle" | "raster" | (string & {});
|
|
24
|
+
/**
|
|
25
|
+
* Una expresión de la especificación de estilos: `["get", "nombre"]`,
|
|
26
|
+
* `["case", …]`, un número, un color.
|
|
27
|
+
*
|
|
28
|
+
* Se deja abierta a propósito. Tipar las expresiones de MapLibre de verdad
|
|
29
|
+
* cuesta cientos de líneas de tipos recursivos que el compilador tarda en
|
|
30
|
+
* resolver, y lo que se gana es atrapar errores que el propio MapLibre ya
|
|
31
|
+
* reporta en carga con un mensaje mejor.
|
|
32
|
+
*/
|
|
33
|
+
export type Expresion = unknown;
|
|
34
|
+
export interface CapaDelEstilo {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly type: TipoDeCapa;
|
|
37
|
+
readonly source?: string;
|
|
38
|
+
readonly "source-layer"?: string;
|
|
39
|
+
readonly filter?: Expresion;
|
|
40
|
+
readonly minzoom?: number;
|
|
41
|
+
readonly maxzoom?: number;
|
|
42
|
+
readonly layout?: Readonly<Record<string, Expresion>>;
|
|
43
|
+
readonly paint?: Readonly<Record<string, Expresion>>;
|
|
44
|
+
readonly metadata?: unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface EspecificacionDeEstilo {
|
|
47
|
+
readonly version: 8;
|
|
48
|
+
readonly name?: string;
|
|
49
|
+
readonly sources: Readonly<Record<string, unknown>>;
|
|
50
|
+
readonly layers: readonly CapaDelEstilo[];
|
|
51
|
+
readonly sprite?: string | readonly unknown[];
|
|
52
|
+
readonly glyphs?: string;
|
|
53
|
+
readonly [clave: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
/** Los dos temas que el mapa sabe pintar. */
|
|
56
|
+
export type Tema = "claro" | "oscuro";
|
|
57
|
+
/**
|
|
58
|
+
* Lo que el SDK le pide a quien sepa construir un estilo.
|
|
59
|
+
*
|
|
60
|
+
* Es una **función pura**: mismas opciones, mismo documento. Eso es lo que
|
|
61
|
+
* permite regenerar el estilo entero al cambiar de tema —que es como se cambia
|
|
62
|
+
* el tema en MapLibre— sin que el SDK tenga que saber una sola cosa sobre
|
|
63
|
+
* colores.
|
|
64
|
+
*
|
|
65
|
+
* La URL de teselas llega ya compuesta desde el manifiesto (ADR-024): quien
|
|
66
|
+
* implemente esto no la construye, la recibe.
|
|
67
|
+
*
|
|
68
|
+
* **No lleva la atribución, y esa ausencia es deliberada.** El texto de
|
|
69
|
+
* OpenStreetMap viaja dentro del `.pmtiles`, en sus metadatos, y
|
|
70
|
+
* `new Protocol({ metadata: true })` lo publica solo — por eso esa petición
|
|
71
|
+
* extra no es opcional. Ofrecer aquí un canal para pasarla sería ofrecer un
|
|
72
|
+
* canal para duplicarla, y un texto legal duplicado es como una superficie
|
|
73
|
+
* termina sirviendo un mapa sin él. Si un archivo llegara sin ese metadato,
|
|
74
|
+
* eso es un defecto del archivo, no algo que el estilo deba tapar.
|
|
75
|
+
*
|
|
76
|
+
* Sobre `tema`: el SDK sabe pintar lo suyo —alfileres, rutas, zonas— en claro
|
|
77
|
+
* y en oscuro. Que el **mapa base** también cambie depende de que la fábrica
|
|
78
|
+
* sepa hacerlo; si no lo sabe, cambiarán los colores de Cerca sobre un mapa
|
|
79
|
+
* base que se queda igual. Quien implemente esto y solo tenga un tema, que
|
|
80
|
+
* estreche con su propia guarda de tipos, como se hace con el idioma.
|
|
81
|
+
*/
|
|
82
|
+
export interface FabricaDeEstilo {
|
|
83
|
+
(opciones: {
|
|
84
|
+
readonly urlDeTeselas: string;
|
|
85
|
+
readonly tema: Tema;
|
|
86
|
+
readonly idioma: string;
|
|
87
|
+
}): EspecificacionDeEstilo;
|
|
88
|
+
}
|
package/dist/estilo.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* La forma de un estilo de MapLibre, declarada aquí en vez de importada.
|
|
3
|
+
*
|
|
4
|
+
* ## Por qué no se importa de `maplibre-gl`
|
|
5
|
+
*
|
|
6
|
+
* Porque este archivo lo lee el núcleo del SDK, que corre en Node y dentro de
|
|
7
|
+
* workerd, y `maplibre-gl` toca `window` al importarse. Importar «solo el
|
|
8
|
+
* tipo» tampoco vale: un `import type` desaparece al compilar, pero mete el
|
|
9
|
+
* paquete en las dependencias de un módulo que tiene que poder instalarse sin
|
|
10
|
+
* un navegador, y basta con que alguien quite el `type` un día para que el
|
|
11
|
+
* punto de entrada por defecto arrastre 250 KB al bundle del servidor.
|
|
12
|
+
*
|
|
13
|
+
* Lo que hay aquí es la parte de la especificación 8 que este SDK escribe: lo
|
|
14
|
+
* suficiente para construir capas y fuentes con el editor ayudando, y no la
|
|
15
|
+
* especificación entera. `[clave: string]: unknown` deja pasar lo demás, que
|
|
16
|
+
* es lo que hace que un estilo real de `@cerca.red/mapa-estilos` encaje sin
|
|
17
|
+
* conversión.
|
|
18
|
+
*
|
|
19
|
+
* Y esto es exactamente lo que ADR-027 llama «el puerto»: la lista escrita de
|
|
20
|
+
* lo que hay que revisar el día que salga MapLibre 7.
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=estilo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"estilo.js","sourceRoot":"","sources":["../src/estilo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG","sourcesContent":["/**\n * La forma de un estilo de MapLibre, declarada aquí en vez de importada.\n *\n * ## Por qué no se importa de `maplibre-gl`\n *\n * Porque este archivo lo lee el núcleo del SDK, que corre en Node y dentro de\n * workerd, y `maplibre-gl` toca `window` al importarse. Importar «solo el\n * tipo» tampoco vale: un `import type` desaparece al compilar, pero mete el\n * paquete en las dependencias de un módulo que tiene que poder instalarse sin\n * un navegador, y basta con que alguien quite el `type` un día para que el\n * punto de entrada por defecto arrastre 250 KB al bundle del servidor.\n *\n * Lo que hay aquí es la parte de la especificación 8 que este SDK escribe: lo\n * suficiente para construir capas y fuentes con el editor ayudando, y no la\n * especificación entera. `[clave: string]: unknown` deja pasar lo demás, que\n * es lo que hace que un estilo real de `@cerca.red/mapa-estilos` encaje sin\n * conversión.\n *\n * Y esto es exactamente lo que ADR-027 llama «el puerto»: la lista escrita de\n * lo que hay que revisar el día que salga MapLibre 7.\n */\n\n/** Los tipos de capa que este SDK dibuja. No es la lista completa de la especificación. */\nexport type TipoDeCapa =\n | \"background\"\n | \"fill\"\n | \"line\"\n | \"symbol\"\n | \"circle\"\n | \"raster\"\n // Cualquier otro tipo que traiga un estilo construido por otro —el sabor de\n // Protomaps trae `hillshade` y `color-relief`, y MapLibre añade tipos en\n // cada versión menor—. Enumerarlos todos convertiría este tipo en una lista\n // que envejece con el upstream y que, al quedarse corta, obligaría a forzar\n // el estilo entero con un `as`: ahí se pierde la comprobación de todo lo\n // demás, que es lo que sí importa. Los seis de arriba son los que este SDK\n // **escribe**, y siguen apareciendo en el autocompletado.\n | (string & {});\n\n/**\n * Una expresión de la especificación de estilos: `[\"get\", \"nombre\"]`,\n * `[\"case\", …]`, un número, un color.\n *\n * Se deja abierta a propósito. Tipar las expresiones de MapLibre de verdad\n * cuesta cientos de líneas de tipos recursivos que el compilador tarda en\n * resolver, y lo que se gana es atrapar errores que el propio MapLibre ya\n * reporta en carga con un mensaje mejor.\n */\nexport type Expresion = unknown;\n\nexport interface CapaDelEstilo {\n readonly id: string;\n readonly type: TipoDeCapa;\n readonly source?: string;\n readonly \"source-layer\"?: string;\n readonly filter?: Expresion;\n readonly minzoom?: number;\n readonly maxzoom?: number;\n readonly layout?: Readonly<Record<string, Expresion>>;\n readonly paint?: Readonly<Record<string, Expresion>>;\n // `unknown` y no un objeto: la especificación no dice nada sobre lo que\n // cabe aquí, y tiparlo más estrecho hace que un estilo válido de terceros\n // deje de encajar.\n readonly metadata?: unknown;\n}\n\nexport interface EspecificacionDeEstilo {\n readonly version: 8;\n readonly name?: string;\n readonly sources: Readonly<Record<string, unknown>>;\n readonly layers: readonly CapaDelEstilo[];\n readonly sprite?: string | readonly unknown[];\n readonly glyphs?: string;\n readonly [clave: string]: unknown;\n}\n\n/** Los dos temas que el mapa sabe pintar. */\nexport type Tema = \"claro\" | \"oscuro\";\n\n/**\n * Lo que el SDK le pide a quien sepa construir un estilo.\n *\n * Es una **función pura**: mismas opciones, mismo documento. Eso es lo que\n * permite regenerar el estilo entero al cambiar de tema —que es como se cambia\n * el tema en MapLibre— sin que el SDK tenga que saber una sola cosa sobre\n * colores.\n *\n * La URL de teselas llega ya compuesta desde el manifiesto (ADR-024): quien\n * implemente esto no la construye, la recibe.\n *\n * **No lleva la atribución, y esa ausencia es deliberada.** El texto de\n * OpenStreetMap viaja dentro del `.pmtiles`, en sus metadatos, y\n * `new Protocol({ metadata: true })` lo publica solo — por eso esa petición\n * extra no es opcional. Ofrecer aquí un canal para pasarla sería ofrecer un\n * canal para duplicarla, y un texto legal duplicado es como una superficie\n * termina sirviendo un mapa sin él. Si un archivo llegara sin ese metadato,\n * eso es un defecto del archivo, no algo que el estilo deba tapar.\n *\n * Sobre `tema`: el SDK sabe pintar lo suyo —alfileres, rutas, zonas— en claro\n * y en oscuro. Que el **mapa base** también cambie depende de que la fábrica\n * sepa hacerlo; si no lo sabe, cambiarán los colores de Cerca sobre un mapa\n * base que se queda igual. Quien implemente esto y solo tenga un tema, que\n * estreche con su propia guarda de tipos, como se hace con el idioma.\n */\nexport interface FabricaDeEstilo {\n (opciones: {\n readonly urlDeTeselas: string;\n readonly tema: Tema;\n readonly idioma: string;\n }): EspecificacionDeEstilo;\n}\n"]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Caja, Coordenada } from "@cerca.red/geo";
|
|
2
|
+
import type { Desuscribir, OrigenDeMovimiento, PuntoEnPantalla, VistaDelMapa } from "./puerto.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Los eventos que emite el SDK, y el emisor que los reparte.
|
|
5
|
+
*
|
|
6
|
+
* ## Las dos decisiones que hay aquí, y que no son de estilo
|
|
7
|
+
*
|
|
8
|
+
* **Los eventos de cámara llevan quién los causó.** Es lo que rompe el bucle
|
|
9
|
+
* lista↔mapa. Una pantalla de resultados escucha `mapa:movimiento` para
|
|
10
|
+
* recargar; si el evento no dijera el origen, recargaría también cuando el
|
|
11
|
+
* movimiento lo hizo ella misma al encuadrar lo que el usuario tocó en la
|
|
12
|
+
* lista — y esa recarga mueve el mapa, que emite otro evento. El bucle no se
|
|
13
|
+
* ve como un bucle: se ve como una pantalla que parpadea y consulta de más.
|
|
14
|
+
*
|
|
15
|
+
* **Los eventos de puntero dicen si hubo un puntero.** Un canvas no es
|
|
16
|
+
* accesible por sí solo, así que el recorrido con teclado va por una lista
|
|
17
|
+
* real al lado. Quien la escucha necesita poder decidir distinto: abrir una
|
|
18
|
+
* tarjeta flotante bajo el dedo tiene sentido con un dedo, y no lo tiene con
|
|
19
|
+
* la tecla Tab.
|
|
20
|
+
*/
|
|
21
|
+
export interface EventoDeMapaListo {
|
|
22
|
+
readonly vista: VistaDelMapa;
|
|
23
|
+
}
|
|
24
|
+
export interface EventoDeCamaraPublico {
|
|
25
|
+
readonly vista: VistaDelMapa;
|
|
26
|
+
readonly origen: OrigenDeMovimiento;
|
|
27
|
+
}
|
|
28
|
+
export interface EventoDeClicEnMapa {
|
|
29
|
+
readonly coordenada: Coordenada;
|
|
30
|
+
readonly punto: PuntoEnPantalla;
|
|
31
|
+
readonly desdePuntero: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface EventoDeNegocio {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly ubicacion: Coordenada;
|
|
36
|
+
readonly desdePuntero: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface EventoDeSede {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly negocio: string;
|
|
41
|
+
readonly ubicacion: Coordenada;
|
|
42
|
+
readonly desdePuntero: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Se tocó un grupo.
|
|
46
|
+
*
|
|
47
|
+
* Lleva la cantidad porque quien escucha suele querer decidir: expandir el
|
|
48
|
+
* grupo acercando la cámara, o abrir una lista con los que hay dentro. El SDK
|
|
49
|
+
* ya acerca la cámara por su cuenta; el evento existe para que la superficie
|
|
50
|
+
* pueda hacer además lo suyo.
|
|
51
|
+
*/
|
|
52
|
+
interface EventoDeGrupoBase {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly cantidad: number;
|
|
55
|
+
readonly ubicacion: Coordenada;
|
|
56
|
+
readonly desdePuntero: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** Un grupo calculado por MapLibre o entregado ya agregado por el servidor. */
|
|
59
|
+
export type EventoDeGrupo = (EventoDeGrupoBase & {
|
|
60
|
+
readonly origen: "local";
|
|
61
|
+
readonly caja?: undefined;
|
|
62
|
+
}) | (EventoDeGrupoBase & {
|
|
63
|
+
readonly origen: "servidor";
|
|
64
|
+
readonly caja: Caja;
|
|
65
|
+
});
|
|
66
|
+
export interface EventoDeRuta {
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly desdePuntero: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface EventoDeVehiculo {
|
|
71
|
+
readonly id: string;
|
|
72
|
+
readonly ubicacion: Coordenada;
|
|
73
|
+
readonly desdePuntero: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** El puntero entró o salió de un negocio. `id` es `undefined` al salir. */
|
|
76
|
+
export interface EventoDeNegocioEncima {
|
|
77
|
+
readonly id: string | undefined;
|
|
78
|
+
}
|
|
79
|
+
/** La ubicación del usuario que el SDK tiene puesta cambió. `undefined` si se quitó. */
|
|
80
|
+
export interface EventoDeUbicacion {
|
|
81
|
+
readonly ubicacion: Coordenada | undefined;
|
|
82
|
+
}
|
|
83
|
+
export interface EventoDeError {
|
|
84
|
+
readonly error: Error;
|
|
85
|
+
}
|
|
86
|
+
/** El catálogo completo de eventos del SDK. */
|
|
87
|
+
export interface EventosDelMapa {
|
|
88
|
+
readonly "mapa:listo": EventoDeMapaListo;
|
|
89
|
+
readonly "mapa:clic": EventoDeClicEnMapa;
|
|
90
|
+
readonly "mapa:movimiento": EventoDeCamaraPublico;
|
|
91
|
+
readonly "mapa:zoom": EventoDeCamaraPublico;
|
|
92
|
+
readonly "mapa:error": EventoDeError;
|
|
93
|
+
readonly "negocio:clic": EventoDeNegocio;
|
|
94
|
+
readonly "negocio:encima": EventoDeNegocioEncima;
|
|
95
|
+
readonly "sede:clic": EventoDeSede;
|
|
96
|
+
readonly "grupo:clic": EventoDeGrupo;
|
|
97
|
+
readonly "ruta:eleccion": EventoDeRuta;
|
|
98
|
+
readonly "vehiculo:clic": EventoDeVehiculo;
|
|
99
|
+
readonly "ubicacion:cambio": EventoDeUbicacion;
|
|
100
|
+
}
|
|
101
|
+
export type TipoDeEvento = keyof EventosDelMapa;
|
|
102
|
+
type Manejador<T extends TipoDeEvento> = (evento: EventosDelMapa[T]) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Un emisor pequeño, con dos propiedades que sí importan.
|
|
105
|
+
*
|
|
106
|
+
* **Un manejador que lanza no se lleva a los demás por delante.** Si el
|
|
107
|
+
* tercero de cinco oyentes de `negocio:clic` revienta, los otros cuatro
|
|
108
|
+
* tienen que recibir el evento igual: son componentes distintos de la
|
|
109
|
+
* pantalla y uno roto no debe apagar el resto. El error sale por
|
|
110
|
+
* `mapa:error`, que es donde se puede ver.
|
|
111
|
+
*
|
|
112
|
+
* **Se recorre una copia de la lista.** Un manejador que se da de baja a sí
|
|
113
|
+
* mismo dentro del propio evento —lo que hace `unaVez`— modificaría el
|
|
114
|
+
* conjunto que se está recorriendo, y eso salta oyentes en silencio.
|
|
115
|
+
*/
|
|
116
|
+
export declare class EmisorDeEventos {
|
|
117
|
+
#private;
|
|
118
|
+
/** Dónde van los errores que lanzan los manejadores. */
|
|
119
|
+
alFallar(manejador: (error: Error, tipo: TipoDeEvento) => void): void;
|
|
120
|
+
cuando<T extends TipoDeEvento>(tipo: T, manejador: Manejador<T>): Desuscribir;
|
|
121
|
+
unaVez<T extends TipoDeEvento>(tipo: T, manejador: Manejador<T>): Desuscribir;
|
|
122
|
+
emitir<T extends TipoDeEvento>(tipo: T, evento: EventosDelMapa[T]): void;
|
|
123
|
+
/** Cuántos oyentes hay de un tipo. Existe para que `destruir` se pueda comprobar. */
|
|
124
|
+
cuantosOyentes(tipo?: TipoDeEvento): number;
|
|
125
|
+
quitarTodos(): void;
|
|
126
|
+
}
|
|
127
|
+
export {};
|
package/dist/eventos.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Un emisor pequeño, con dos propiedades que sí importan.
|
|
3
|
+
*
|
|
4
|
+
* **Un manejador que lanza no se lleva a los demás por delante.** Si el
|
|
5
|
+
* tercero de cinco oyentes de `negocio:clic` revienta, los otros cuatro
|
|
6
|
+
* tienen que recibir el evento igual: son componentes distintos de la
|
|
7
|
+
* pantalla y uno roto no debe apagar el resto. El error sale por
|
|
8
|
+
* `mapa:error`, que es donde se puede ver.
|
|
9
|
+
*
|
|
10
|
+
* **Se recorre una copia de la lista.** Un manejador que se da de baja a sí
|
|
11
|
+
* mismo dentro del propio evento —lo que hace `unaVez`— modificaría el
|
|
12
|
+
* conjunto que se está recorriendo, y eso salta oyentes en silencio.
|
|
13
|
+
*/
|
|
14
|
+
export class EmisorDeEventos {
|
|
15
|
+
#oyentes = new Map();
|
|
16
|
+
#alFallarUnManejador;
|
|
17
|
+
/** Dónde van los errores que lanzan los manejadores. */
|
|
18
|
+
alFallar(manejador) {
|
|
19
|
+
this.#alFallarUnManejador = manejador;
|
|
20
|
+
}
|
|
21
|
+
cuando(tipo, manejador) {
|
|
22
|
+
const existentes = this.#oyentes.get(tipo) ?? new Set();
|
|
23
|
+
existentes.add(manejador);
|
|
24
|
+
this.#oyentes.set(tipo, existentes);
|
|
25
|
+
return () => {
|
|
26
|
+
existentes.delete(manejador);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
unaVez(tipo, manejador) {
|
|
30
|
+
const quitar = this.cuando(tipo, ((evento) => {
|
|
31
|
+
quitar();
|
|
32
|
+
manejador(evento);
|
|
33
|
+
}));
|
|
34
|
+
return quitar;
|
|
35
|
+
}
|
|
36
|
+
emitir(tipo, evento) {
|
|
37
|
+
const oyentes = this.#oyentes.get(tipo);
|
|
38
|
+
if (oyentes === undefined || oyentes.size === 0)
|
|
39
|
+
return;
|
|
40
|
+
for (const manejador of [...oyentes]) {
|
|
41
|
+
try {
|
|
42
|
+
manejador(evento);
|
|
43
|
+
}
|
|
44
|
+
catch (fallo) {
|
|
45
|
+
const error = fallo instanceof Error ? fallo : new Error(String(fallo));
|
|
46
|
+
// Un manejador de `mapa:error` que lanza no puede reentrar en el
|
|
47
|
+
// propio `mapa:error`: eso es una recursión infinita disfrazada de
|
|
48
|
+
// manejo de errores.
|
|
49
|
+
if (tipo === "mapa:error")
|
|
50
|
+
continue;
|
|
51
|
+
this.#alFallarUnManejador?.(error, tipo);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Cuántos oyentes hay de un tipo. Existe para que `destruir` se pueda comprobar. */
|
|
56
|
+
cuantosOyentes(tipo) {
|
|
57
|
+
if (tipo !== undefined)
|
|
58
|
+
return this.#oyentes.get(tipo)?.size ?? 0;
|
|
59
|
+
let total = 0;
|
|
60
|
+
for (const oyentes of this.#oyentes.values())
|
|
61
|
+
total += oyentes.size;
|
|
62
|
+
return total;
|
|
63
|
+
}
|
|
64
|
+
quitarTodos() {
|
|
65
|
+
this.#oyentes.clear();
|
|
66
|
+
this.#alFallarUnManejador = undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=eventos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventos.js","sourceRoot":"","sources":["../src/eventos.ts"],"names":[],"mappings":"AA+HA;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,eAAe;IACjB,QAAQ,GAAG,IAAI,GAAG,EAA8C,CAAC;IAC1E,oBAAoB,CACuC;IAE3D,wDAAwD;IACxD,QAAQ,CAAC,SAAqD;QAC5D,IAAI,CAAC,oBAAoB,GAAG,SAAS,CAAC;IACxC,CAAC;IAED,MAAM,CACJ,IAAO,EACP,SAAuB;QAEvB,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;QACxD,UAAU,CAAC,GAAG,CAAC,SAAoC,CAAC,CAAC;QACrD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACpC,OAAO,GAAG,EAAE;YACV,UAAU,CAAC,MAAM,CAAC,SAAoC,CAAC,CAAC;QAC1D,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,CACJ,IAAO,EACP,SAAuB;QAEvB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,MAAyB,EAAE,EAAE;YAC9D,MAAM,EAAE,CAAC;YACT,SAAS,CAAC,MAAM,CAAC,CAAC;QACpB,CAAC,CAAiB,CAAC,CAAC;QACpB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,CAAyB,IAAO,EAAE,MAAyB;QAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QAExD,KAAK,MAAM,SAAS,IAAI,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC;gBACF,SAA0B,CAAC,MAAM,CAAC,CAAC;YACtC,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBACxE,iEAAiE;gBACjE,mEAAmE;gBACnE,qBAAqB;gBACrB,IAAI,IAAI,KAAK,YAAY;oBAAE,SAAS;gBACpC,IAAI,CAAC,oBAAoB,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IAED,qFAAqF;IACrF,cAAc,CAAC,IAAmB;QAChC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC;QAClE,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;YAAE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;QACpE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,WAAW;QACT,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,oBAAoB,GAAG,SAAS,CAAC;IACxC,CAAC;CACF","sourcesContent":["import type { Caja, Coordenada } from \"@cerca.red/geo\";\n\nimport type {\n Desuscribir,\n OrigenDeMovimiento,\n PuntoEnPantalla,\n VistaDelMapa,\n} from \"./puerto.ts\";\n\n/**\n * Los eventos que emite el SDK, y el emisor que los reparte.\n *\n * ## Las dos decisiones que hay aquí, y que no son de estilo\n *\n * **Los eventos de cámara llevan quién los causó.** Es lo que rompe el bucle\n * lista↔mapa. Una pantalla de resultados escucha `mapa:movimiento` para\n * recargar; si el evento no dijera el origen, recargaría también cuando el\n * movimiento lo hizo ella misma al encuadrar lo que el usuario tocó en la\n * lista — y esa recarga mueve el mapa, que emite otro evento. El bucle no se\n * ve como un bucle: se ve como una pantalla que parpadea y consulta de más.\n *\n * **Los eventos de puntero dicen si hubo un puntero.** Un canvas no es\n * accesible por sí solo, así que el recorrido con teclado va por una lista\n * real al lado. Quien la escucha necesita poder decidir distinto: abrir una\n * tarjeta flotante bajo el dedo tiene sentido con un dedo, y no lo tiene con\n * la tecla Tab.\n */\n\nexport interface EventoDeMapaListo {\n readonly vista: VistaDelMapa;\n}\n\nexport interface EventoDeCamaraPublico {\n readonly vista: VistaDelMapa;\n readonly origen: OrigenDeMovimiento;\n}\n\nexport interface EventoDeClicEnMapa {\n readonly coordenada: Coordenada;\n readonly punto: PuntoEnPantalla;\n readonly desdePuntero: boolean;\n}\n\nexport interface EventoDeNegocio {\n readonly id: string;\n readonly ubicacion: Coordenada;\n readonly desdePuntero: boolean;\n}\n\nexport interface EventoDeSede {\n readonly id: string;\n readonly negocio: string;\n readonly ubicacion: Coordenada;\n readonly desdePuntero: boolean;\n}\n\n/**\n * Se tocó un grupo.\n *\n * Lleva la cantidad porque quien escucha suele querer decidir: expandir el\n * grupo acercando la cámara, o abrir una lista con los que hay dentro. El SDK\n * ya acerca la cámara por su cuenta; el evento existe para que la superficie\n * pueda hacer además lo suyo.\n */\ninterface EventoDeGrupoBase {\n readonly id: string;\n readonly cantidad: number;\n readonly ubicacion: Coordenada;\n readonly desdePuntero: boolean;\n}\n\n/** Un grupo calculado por MapLibre o entregado ya agregado por el servidor. */\nexport type EventoDeGrupo =\n | (EventoDeGrupoBase & {\n readonly origen: \"local\";\n readonly caja?: undefined;\n })\n | (EventoDeGrupoBase & {\n readonly origen: \"servidor\";\n readonly caja: Caja;\n });\n\nexport interface EventoDeRuta {\n readonly id: string;\n readonly desdePuntero: boolean;\n}\n\nexport interface EventoDeVehiculo {\n readonly id: string;\n readonly ubicacion: Coordenada;\n readonly desdePuntero: boolean;\n}\n\n/** El puntero entró o salió de un negocio. `id` es `undefined` al salir. */\nexport interface EventoDeNegocioEncima {\n readonly id: string | undefined;\n}\n\n/** La ubicación del usuario que el SDK tiene puesta cambió. `undefined` si se quitó. */\nexport interface EventoDeUbicacion {\n readonly ubicacion: Coordenada | undefined;\n}\n\nexport interface EventoDeError {\n readonly error: Error;\n}\n\n/** El catálogo completo de eventos del SDK. */\nexport interface EventosDelMapa {\n readonly \"mapa:listo\": EventoDeMapaListo;\n readonly \"mapa:clic\": EventoDeClicEnMapa;\n readonly \"mapa:movimiento\": EventoDeCamaraPublico;\n readonly \"mapa:zoom\": EventoDeCamaraPublico;\n readonly \"mapa:error\": EventoDeError;\n readonly \"negocio:clic\": EventoDeNegocio;\n readonly \"negocio:encima\": EventoDeNegocioEncima;\n readonly \"sede:clic\": EventoDeSede;\n readonly \"grupo:clic\": EventoDeGrupo;\n readonly \"ruta:eleccion\": EventoDeRuta;\n readonly \"vehiculo:clic\": EventoDeVehiculo;\n readonly \"ubicacion:cambio\": EventoDeUbicacion;\n}\n\nexport type TipoDeEvento = keyof EventosDelMapa;\n\ntype Manejador<T extends TipoDeEvento> = (evento: EventosDelMapa[T]) => void;\n\n/**\n * Un emisor pequeño, con dos propiedades que sí importan.\n *\n * **Un manejador que lanza no se lleva a los demás por delante.** Si el\n * tercero de cinco oyentes de `negocio:clic` revienta, los otros cuatro\n * tienen que recibir el evento igual: son componentes distintos de la\n * pantalla y uno roto no debe apagar el resto. El error sale por\n * `mapa:error`, que es donde se puede ver.\n *\n * **Se recorre una copia de la lista.** Un manejador que se da de baja a sí\n * mismo dentro del propio evento —lo que hace `unaVez`— modificaría el\n * conjunto que se está recorriendo, y eso salta oyentes en silencio.\n */\nexport class EmisorDeEventos {\n readonly #oyentes = new Map<TipoDeEvento, Set<Manejador<TipoDeEvento>>>();\n #alFallarUnManejador:\n ((error: Error, tipo: TipoDeEvento) => void) | undefined;\n\n /** Dónde van los errores que lanzan los manejadores. */\n alFallar(manejador: (error: Error, tipo: TipoDeEvento) => void): void {\n this.#alFallarUnManejador = manejador;\n }\n\n cuando<T extends TipoDeEvento>(\n tipo: T,\n manejador: Manejador<T>,\n ): Desuscribir {\n const existentes = this.#oyentes.get(tipo) ?? new Set();\n existentes.add(manejador as Manejador<TipoDeEvento>);\n this.#oyentes.set(tipo, existentes);\n return () => {\n existentes.delete(manejador as Manejador<TipoDeEvento>);\n };\n }\n\n unaVez<T extends TipoDeEvento>(\n tipo: T,\n manejador: Manejador<T>,\n ): Desuscribir {\n const quitar = this.cuando(tipo, ((evento: EventosDelMapa[T]) => {\n quitar();\n manejador(evento);\n }) as Manejador<T>);\n return quitar;\n }\n\n emitir<T extends TipoDeEvento>(tipo: T, evento: EventosDelMapa[T]): void {\n const oyentes = this.#oyentes.get(tipo);\n if (oyentes === undefined || oyentes.size === 0) return;\n\n for (const manejador of [...oyentes]) {\n try {\n (manejador as Manejador<T>)(evento);\n } catch (fallo: unknown) {\n const error = fallo instanceof Error ? fallo : new Error(String(fallo));\n // Un manejador de `mapa:error` que lanza no puede reentrar en el\n // propio `mapa:error`: eso es una recursión infinita disfrazada de\n // manejo de errores.\n if (tipo === \"mapa:error\") continue;\n this.#alFallarUnManejador?.(error, tipo);\n }\n }\n }\n\n /** Cuántos oyentes hay de un tipo. Existe para que `destruir` se pueda comprobar. */\n cuantosOyentes(tipo?: TipoDeEvento): number {\n if (tipo !== undefined) return this.#oyentes.get(tipo)?.size ?? 0;\n let total = 0;\n for (const oyentes of this.#oyentes.values()) total += oyentes.size;\n return total;\n }\n\n quitarTodos(): void {\n this.#oyentes.clear();\n this.#alFallarUnManejador = undefined;\n }\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Los identificadores de todo lo que este SDK mete en un estilo.
|
|
3
|
+
*
|
|
4
|
+
* Están aquí, en un solo archivo y como constantes, por una razón práctica:
|
|
5
|
+
* un identificador de capa escrito a mano en dos sitios es un `quitarCapa`
|
|
6
|
+
* que no quita nada, y `quitarCapa` de algo que no existe **no falla en
|
|
7
|
+
* MapLibre**. La capa se queda dibujada para siempre y nadie sabe por qué.
|
|
8
|
+
*
|
|
9
|
+
* El prefijo `cerca-` está para que se distingan a simple vista de las capas
|
|
10
|
+
* del mapa base, que vienen de Protomaps y no se tocan nunca.
|
|
11
|
+
*/
|
|
12
|
+
/** Las fuentes de datos que el SDK crea y mantiene. */
|
|
13
|
+
export declare const FUENTES: {
|
|
14
|
+
readonly negocios: "cerca-negocios";
|
|
15
|
+
/** Agregados entregados por el backend, nunca clusterizados por MapLibre. */
|
|
16
|
+
readonly grupos: "cerca-grupos";
|
|
17
|
+
readonly sedes: "cerca-sedes";
|
|
18
|
+
/**
|
|
19
|
+
* La selección va en su **propia fuente** y no en un estado de la fuente de
|
|
20
|
+
* negocios. No es duplicación: la clave de ordenación de símbolos de
|
|
21
|
+
* MapLibre no lee el estado de entidad, así que un negocio marcado como
|
|
22
|
+
* seleccionado seguiría dibujándose debajo de sus vecinos. Una fuente
|
|
23
|
+
* aparte, en una capa por encima, es la única forma de que lo elegido se
|
|
24
|
+
* vea encima de todo.
|
|
25
|
+
*/
|
|
26
|
+
readonly seleccion: "cerca-seleccion";
|
|
27
|
+
/** El halo de las ubicaciones aproximadas. Área, no alfiler. */
|
|
28
|
+
readonly imprecision: "cerca-imprecision";
|
|
29
|
+
readonly ubicacion: "cerca-ubicacion";
|
|
30
|
+
readonly vehiculos: "cerca-vehiculos";
|
|
31
|
+
readonly rutas: "cerca-rutas";
|
|
32
|
+
readonly zonas: "cerca-zonas";
|
|
33
|
+
readonly isocronas: "cerca-isocronas";
|
|
34
|
+
};
|
|
35
|
+
/** Las capas que el SDK dibuja, de abajo hacia arriba dentro de cada anclaje. */
|
|
36
|
+
export declare const CAPAS: {
|
|
37
|
+
readonly isocronas: "cerca-isocronas-relleno";
|
|
38
|
+
readonly isocronasBorde: "cerca-isocronas-borde";
|
|
39
|
+
readonly zonas: "cerca-zonas-relleno";
|
|
40
|
+
readonly zonasBorde: "cerca-zonas-borde";
|
|
41
|
+
readonly imprecision: "cerca-imprecision-relleno";
|
|
42
|
+
readonly imprecisionBorde: "cerca-imprecision-borde";
|
|
43
|
+
readonly rutasAlternativas: "cerca-rutas-alternativas";
|
|
44
|
+
readonly rutaPrincipalContorno: "cerca-ruta-principal-contorno";
|
|
45
|
+
readonly rutaPrincipal: "cerca-ruta-principal";
|
|
46
|
+
readonly ubicacionHalo: "cerca-ubicacion-halo";
|
|
47
|
+
readonly ubicacionPunto: "cerca-ubicacion-punto";
|
|
48
|
+
readonly gruposCirculo: "cerca-grupos-circulo";
|
|
49
|
+
readonly gruposCantidad: "cerca-grupos-cantidad";
|
|
50
|
+
readonly gruposToque: "cerca-grupos-toque";
|
|
51
|
+
readonly gruposServidorCirculo: "cerca-grupos-servidor-circulo";
|
|
52
|
+
readonly gruposServidorCantidad: "cerca-grupos-servidor-cantidad";
|
|
53
|
+
readonly gruposServidorToque: "cerca-grupos-servidor-toque";
|
|
54
|
+
readonly negociosPunto: "cerca-negocios-punto";
|
|
55
|
+
readonly negociosSimbolo: "cerca-negocios-simbolo";
|
|
56
|
+
readonly sedesPunto: "cerca-sedes-punto";
|
|
57
|
+
readonly vehiculos: "cerca-vehiculos-simbolo";
|
|
58
|
+
readonly seleccion: "cerca-seleccion-punto";
|
|
59
|
+
readonly seleccionArea: "cerca-seleccion-area";
|
|
60
|
+
/**
|
|
61
|
+
* Las capas invisibles que reciben los toques.
|
|
62
|
+
*
|
|
63
|
+
* Un alfiler de 9 px de radio se ve bien y **no se puede tocar** en un
|
|
64
|
+
* teléfono: CLAUDE.md §1 pide objetivos de 44 px, o sea 22 de radio.
|
|
65
|
+
* Agrandar el círculo pintado sería taparlo todo, así que encima va otro
|
|
66
|
+
* círculo del tamaño tocable con opacidad cero.
|
|
67
|
+
*
|
|
68
|
+
* Funciona porque la consulta de entidades dibujadas de MapLibre lee el
|
|
69
|
+
* índice espacial y **sí devuelve capas con opacidad cero** — lo que no
|
|
70
|
+
* devuelve es capas con la visibilidad apagada, que es una cosa distinta.
|
|
71
|
+
* Es la diferencia entre «transparente» y «no está».
|
|
72
|
+
*/
|
|
73
|
+
readonly negociosToque: "cerca-negocios-toque";
|
|
74
|
+
readonly sedesToque: "cerca-sedes-toque";
|
|
75
|
+
readonly vehiculosToque: "cerca-vehiculos-toque";
|
|
76
|
+
readonly rutasToque: "cerca-rutas-toque";
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Los puntos de inserción que el estilo base deja preparados.
|
|
80
|
+
*
|
|
81
|
+
* Son capas vacías con identificador estable que solo existen para tener
|
|
82
|
+
* dónde insertar. La alternativa —referenciar una capa concreta de Protomaps,
|
|
83
|
+
* como `roads_labels_minor`— se rompe en cada actualización del upstream, y
|
|
84
|
+
* se rompe en silencio: `agregarCapa` con un `antesDe` que no existe lanza en
|
|
85
|
+
* MapLibre, pero un identificador que cambió de significado no lanza nada,
|
|
86
|
+
* solo deja la capa en el sitio equivocado.
|
|
87
|
+
*
|
|
88
|
+
* El orden, de abajo hacia arriba: mapa base sin rótulos → **áreas** →
|
|
89
|
+
* **líneas** → rótulos del mapa base → **puntos** → **selección**.
|
|
90
|
+
*
|
|
91
|
+
* Los rótulos del mapa base van en medio a propósito: el nombre de la calle
|
|
92
|
+
* se tiene que seguir leyendo por debajo de un polígono de cobertura, y los
|
|
93
|
+
* alfileres tienen que quedar por encima de todo. Un mapa que tapa los
|
|
94
|
+
* nombres de las calles no sirve para lo único que se le pide.
|
|
95
|
+
*/
|
|
96
|
+
export declare const ANCLAS_POR_DEFECTO: {
|
|
97
|
+
readonly areas: "cerca-ancla-areas";
|
|
98
|
+
readonly lineas: "cerca-ancla-lineas";
|
|
99
|
+
readonly puntos: "cerca-ancla-puntos";
|
|
100
|
+
readonly seleccion: "cerca-ancla-seleccion";
|
|
101
|
+
};
|
|
102
|
+
export type Anclas = {
|
|
103
|
+
readonly areas: string;
|
|
104
|
+
readonly lineas: string;
|
|
105
|
+
readonly puntos: string;
|
|
106
|
+
readonly seleccion: string;
|
|
107
|
+
};
|
|
108
|
+
/** El radio en píxeles que hace que un objetivo mida los 44 px que pide CLAUDE.md §1. */
|
|
109
|
+
export declare const RADIO_TACTIL_MINIMO_EN_PX = 22;
|