@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.
Files changed (68) hide show
  1. package/README.md +133 -0
  2. package/dist/capas.d.ts +29 -0
  3. package/dist/capas.js +489 -0
  4. package/dist/capas.js.map +1 -0
  5. package/dist/doble/estiloFalso.d.ts +19 -0
  6. package/dist/doble/estiloFalso.js +33 -0
  7. package/dist/doble/estiloFalso.js.map +1 -0
  8. package/dist/doble/index.d.ts +11 -0
  9. package/dist/doble/index.js +12 -0
  10. package/dist/doble/index.js.map +1 -0
  11. package/dist/doble/mapaFalso.d.ts +75 -0
  12. package/dist/doble/mapaFalso.js +535 -0
  13. package/dist/doble/mapaFalso.js.map +1 -0
  14. package/dist/encuadre.d.ts +64 -0
  15. package/dist/encuadre.js +83 -0
  16. package/dist/encuadre.js.map +1 -0
  17. package/dist/estilo.d.ts +88 -0
  18. package/dist/estilo.js +23 -0
  19. package/dist/estilo.js.map +1 -0
  20. package/dist/eventos.d.ts +127 -0
  21. package/dist/eventos.js +69 -0
  22. package/dist/eventos.js.map +1 -0
  23. package/dist/identificadores.d.ts +109 -0
  24. package/dist/identificadores.js +104 -0
  25. package/dist/identificadores.js.map +1 -0
  26. package/dist/index.d.ts +37 -0
  27. package/dist/index.js +33 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/manifiesto.d.ts +97 -0
  30. package/dist/manifiesto.js +156 -0
  31. package/dist/manifiesto.js.map +1 -0
  32. package/dist/modelo.d.ts +154 -0
  33. package/dist/modelo.js +227 -0
  34. package/dist/modelo.js.map +1 -0
  35. package/dist/navegador/index.d.ts +70 -0
  36. package/dist/navegador/index.js +132 -0
  37. package/dist/navegador/index.js.map +1 -0
  38. package/dist/navegador/maplibre.d.ts +41 -0
  39. package/dist/navegador/maplibre.js +463 -0
  40. package/dist/navegador/maplibre.js.map +1 -0
  41. package/dist/navegador/protocolo.d.ts +4 -0
  42. package/dist/navegador/protocolo.js +55 -0
  43. package/dist/navegador/protocolo.js.map +1 -0
  44. package/dist/navegador/webgl.d.ts +24 -0
  45. package/dist/navegador/webgl.js +47 -0
  46. package/dist/navegador/webgl.js.map +1 -0
  47. package/dist/navegador/worker.d.ts +33 -0
  48. package/dist/navegador/worker.js +47 -0
  49. package/dist/navegador/worker.js.map +1 -0
  50. package/dist/paleta.d.ts +53 -0
  51. package/dist/paleta.js +53 -0
  52. package/dist/paleta.js.map +1 -0
  53. package/dist/proyecciones.d.ts +160 -0
  54. package/dist/proyecciones.js +2 -0
  55. package/dist/proyecciones.js.map +1 -0
  56. package/dist/puerto.d.ts +244 -0
  57. package/dist/puerto.js +2 -0
  58. package/dist/puerto.js.map +1 -0
  59. package/dist/sdk.d.ts +111 -0
  60. package/dist/sdk.js +552 -0
  61. package/dist/sdk.js.map +1 -0
  62. package/dist/sincronizacion.d.ts +40 -0
  63. package/dist/sincronizacion.js +144 -0
  64. package/dist/sincronizacion.js.map +1 -0
  65. package/dist/worker-de-maplibre.d.ts +36 -0
  66. package/dist/worker-de-maplibre.js +37 -0
  67. package/dist/worker-de-maplibre.js.map +1 -0
  68. package/package.json +59 -0
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @cerca.red/mapa
2
+
3
+ El SDK del mapa de Cerca: modelo, cámara, capas y eventos sobre MapLibre GL JS.
4
+
5
+ El paquete guarda su propio estado y trata el mapa como una proyección de él.
6
+ Eso es lo que hace que cambiar de tema no pierda los negocios pintados, y lo
7
+ que permite probar casi todo sin GPU.
8
+
9
+ ## Los tres puntos de entrada
10
+
11
+ | Import | Qué trae | Dónde corre |
12
+ | --------------------------- | ------------------------------------------------ | -------------------------- |
13
+ | `@cerca.red/mapa` | el modelo, el encuadre, el manifiesto, los tipos | navegador, workerd y Node |
14
+ | `@cerca.red/mapa/navegador` | `crearMapa` y el adaptador de MapLibre | solo el navegador (WebGL2) |
15
+ | `@cerca.red/mapa/doble` | un motor falso con la geometría de verdad | donde sea, sin canvas |
16
+
17
+ El punto de entrada por defecto **no alcanza `maplibre-gl`**, ni directamente
18
+ ni cinco archivos más adentro. Se puede importar desde un Server Component
19
+ sobre Cloudflare Workers sin que el build reviente. El mapa de verdad entra por
20
+ la subruta `./navegador`, que es explícita a propósito: importarla se ve en el
21
+ diff. Dos pruebas lo comprueban, una dentro de workerd y otra leyendo el grafo
22
+ de imports.
23
+
24
+ ## Resultados y clusterización híbrida
25
+
26
+ El backend consumidor decide qué devolver para la caja y el zoom actuales. La
27
+ actualización es una sola orden y los dos modos son mutuamente excluyentes:
28
+
29
+ ```ts
30
+ mapa.ponerResultados({
31
+ modo: "grupos",
32
+ grupos: [
33
+ {
34
+ id: "z12:1234:2187",
35
+ cantidad: 384,
36
+ ubicacion: { lon: -75.696, lat: 4.813 },
37
+ caja: [-75.71, 4.8, -75.68, 4.83],
38
+ },
39
+ ],
40
+ });
41
+
42
+ mapa.ponerResultados({ modo: "negocios", negocios });
43
+ ```
44
+
45
+ Los grupos del servidor tienen fuente y capas propias y no vuelven a
46
+ clusterizarse. Los negocios individuales conservan la agrupación visual local
47
+ de MapLibre. `ponerNegocios(negocios)` sigue siendo compatible y equivale al
48
+ segundo ejemplo.
49
+
50
+ `grupo:clic` discrimina `origen: "servidor" | "local"`. Un grupo del servidor
51
+ incluye su `caja` y el SDK la encuadra; uno local conserva el acercamiento
52
+ incremental existente.
53
+
54
+ ## Next.js y los bundlers que pierden `import.meta.url`
55
+
56
+ MapLibre 6 reparte el motor en dos archivos. `maplibre-gl.mjs` corre en el hilo
57
+ principal; `maplibre-gl-worker.mjs` corre en un Web Worker y hace el trabajo
58
+ pesado. El segundo no se importa: el primero deduce su URL a partir de su
59
+ propio `import.meta.url` y llama a `new Worker(...)`.
60
+
61
+ Un bundler que reescriba `import.meta.url` rompe esa deducción. Medido en
62
+ `cerca-pwa` con Next.js 16 sobre OpenNext: con Turbopack y con `--webpack`, el
63
+ motor recibe la URL de la propia página y pide un archivo que no está ahí. Con
64
+ Vite no pasa, y por eso el mismo paquete pinta el mapa en `laboratorio/` y no
65
+ lo pinta bajo Next.
66
+
67
+ La salida es decirle la URL:
68
+
69
+ ```ts
70
+ import { crearMapa } from "@cerca.red/mapa/navegador";
71
+
72
+ const mapa = crearMapa({
73
+ contenedor,
74
+ estilo,
75
+ urlDeTeselas,
76
+ urlDelWorker: "/maplibre/maplibre-gl-worker.mjs",
77
+ });
78
+ ```
79
+
80
+ Con `<Mapa>` de `@cerca.red/mapa-react` es la prop `urlDelWorker`. Y quien
81
+ prefiera fijarla una sola vez al arrancar tiene `fijarUrlDelWorker(url)` en la
82
+ misma subruta. En los tres casos hay que hacerlo **antes del primer mapa**: el
83
+ pool de workers de MapLibre es único para toda la página, se crea dentro del
84
+ constructor de `Map` y no vuelve a leer la URL mientras quede un mapa vivo.
85
+ Fijarla después no da error ni aviso, y tampoco hace nada.
86
+
87
+ Sin la opción no se toca la configuración de MapLibre, que es lo correcto bajo
88
+ Vite.
89
+
90
+ ### Qué archivo hay que servir
91
+
92
+ El `maplibre-gl-worker.mjs` de la **misma versión** de `maplibre-gl` que
93
+ resuelve el proyecto. El protocolo entre el hilo principal y el worker es
94
+ interno de MapLibre y no está versionado: un worker de otra versión no da un
95
+ error que lo diga, da un mapa que no pinta teselas.
96
+
97
+ Para copiarlo en la compilación y para comprobarlo, el punto de entrada por
98
+ defecto exporta dos cadenas —no importan `maplibre-gl`, así que se pueden leer
99
+ desde un guion de Node:
100
+
101
+ ```ts
102
+ import {
103
+ RUTA_DEL_WORKER_DE_MAPLIBRE, // "maplibre-gl/dist/maplibre-gl-worker.mjs"
104
+ VERSION_DE_MAPLIBRE, // la versión contra la que se compiló este paquete
105
+ } from "@cerca.red/mapa";
106
+ ```
107
+
108
+ El guion que lo copia resuelve el especificador en vez de unir cadenas, para
109
+ que salga el archivo del `maplibre-gl` que ese proyecto resolvió:
110
+
111
+ ```ts
112
+ import { copyFileSync, mkdirSync } from "node:fs";
113
+ import { createRequire } from "node:module";
114
+ import { RUTA_DEL_WORKER_DE_MAPLIBRE } from "@cerca.red/mapa";
115
+
116
+ const origen = createRequire(import.meta.url).resolve(
117
+ RUTA_DEL_WORKER_DE_MAPLIBRE,
118
+ );
119
+ mkdirSync("public/maplibre", { recursive: true });
120
+ copyFileSync(origen, "public/maplibre/maplibre-gl-worker.mjs");
121
+ ```
122
+
123
+ Y la prueba que impide la pareja desigual compara la versión que resuelve la
124
+ superficie con `VERSION_DE_MAPLIBRE`. Dentro de este repositorio esa
125
+ comparación la hace `src/worker-de-maplibre.test.ts`.
126
+
127
+ ## Cómo se prueba
128
+
129
+ `@cerca.red/mapa/doble` da un motor falso con la geometría de verdad, para
130
+ comprobar sin levantar un contexto WebGL que al tocar un resultado de la lista
131
+ el mapa recibió la orden de seleccionarlo. Lo que sí necesita un navegador —un
132
+ objetivo táctil sobre el canvas, un radio de toque— se comprueba tocando, con
133
+ `map.project` para calcular el punto; nunca comparando píxeles.
@@ -0,0 +1,29 @@
1
+ import type { CapaDelEstilo } from "./estilo.ts";
2
+ import { type Anclas } from "./identificadores.ts";
3
+ import type { PaletaDelMapa } from "./paleta.ts";
4
+ /**
5
+ * Las capas de Cerca, como datos.
6
+ *
7
+ * Cada función devuelve una lista de capas y **el ancla donde van**. El SDK
8
+ * las inserta en ese orden; no hay ninguna que dependa de dónde quedó otra.
9
+ *
10
+ * Se rehacen enteras cada vez que cambia el tema, porque cambiar de tema en
11
+ * MapLibre es reemplazar el estilo completo y el estilo nuevo llega sin nada
12
+ * de esto. Que sean funciones puras de la paleta es lo que hace que esa
13
+ * reconstrucción no pueda quedar a medias.
14
+ */
15
+ /** Una capa con el ancla debajo de la cual se inserta. */
16
+ export interface CapaConAncla {
17
+ readonly capa: CapaDelEstilo;
18
+ readonly antesDe: string;
19
+ }
20
+ /** Las áreas: isócronas, zonas de cobertura y halos de imprecisión. */
21
+ export declare function capasDeAreas(paleta: PaletaDelMapa, anclas: Anclas): readonly CapaConAncla[];
22
+ /** Las líneas: rutas, con su contorno y sus alternativas. */
23
+ export declare function capasDeLineas(paleta: PaletaDelMapa, anclas: Anclas): readonly CapaConAncla[];
24
+ /** Los puntos: grupos, negocios, sedes, vehículos y la ubicación del usuario. */
25
+ export declare function capasDePuntos(paleta: PaletaDelMapa, anclas: Anclas): readonly CapaConAncla[];
26
+ /** La selección: encima de todo, con anillo además de color. */
27
+ export declare function capasDeSeleccion(paleta: PaletaDelMapa): readonly CapaConAncla[];
28
+ /** Todas las capas de Cerca, en el orden en que se insertan. */
29
+ export declare function todasLasCapas(paleta: PaletaDelMapa, anclas: Anclas): readonly CapaConAncla[];
package/dist/capas.js ADDED
@@ -0,0 +1,489 @@
1
+ import { CAPAS, FUENTES, RADIO_TACTIL_MINIMO_EN_PX, } from "./identificadores.js";
2
+ /** Radio del punto de un negocio, por énfasis. El tamaño distingue tanto como el color. */
3
+ const RADIO_POR_ENFASIS = {
4
+ atenuado: 4,
5
+ normal: 6,
6
+ destacado: 9,
7
+ };
8
+ /**
9
+ * La expresión que elige radio según el énfasis y el zoom.
10
+ *
11
+ * Interpola con el zoom porque un punto de radio fijo se ve enorme al alejarse
12
+ * —cuando hay cien en pantalla— y minúsculo al acercarse. Los valores están
13
+ * atados a los tres énfasis para que la diferencia entre destacado y atenuado
14
+ * siga siendo visible a cualquier zoom.
15
+ */
16
+ function radioDeNegocio(multiplicador) {
17
+ return [
18
+ "interpolate",
19
+ ["linear"],
20
+ ["zoom"],
21
+ 10,
22
+ [
23
+ "*",
24
+ multiplicador * 0.7,
25
+ [
26
+ "match",
27
+ ["get", "enfasis"],
28
+ "destacado",
29
+ RADIO_POR_ENFASIS.destacado,
30
+ "atenuado",
31
+ RADIO_POR_ENFASIS.atenuado,
32
+ RADIO_POR_ENFASIS.normal,
33
+ ],
34
+ ],
35
+ 16,
36
+ [
37
+ "*",
38
+ multiplicador,
39
+ [
40
+ "match",
41
+ ["get", "enfasis"],
42
+ "destacado",
43
+ RADIO_POR_ENFASIS.destacado,
44
+ "atenuado",
45
+ RADIO_POR_ENFASIS.atenuado,
46
+ RADIO_POR_ENFASIS.normal,
47
+ ],
48
+ ],
49
+ ];
50
+ }
51
+ /**
52
+ * Las capas invisibles que reciben los toques.
53
+ *
54
+ * Radio 22 px, o sea un objetivo de 44 (CLAUDE.md §1). No se comprueba
55
+ * leyendo este número: se comprueba tocando, calculando el punto con la
56
+ * proyección del mapa y tocando dentro y fuera del radio. Un radio que nadie
57
+ * tocó no está comprobado.
58
+ */
59
+ function capaDeToque(id, fuente, filter) {
60
+ return {
61
+ id,
62
+ type: "circle",
63
+ source: fuente,
64
+ ...(filter === undefined ? {} : { filter }),
65
+ paint: {
66
+ "circle-radius": RADIO_TACTIL_MINIMO_EN_PX,
67
+ // Cero, no `visibility: none`. La consulta de entidades dibujadas
68
+ // devuelve capas transparentes y no devuelve capas apagadas: si esto
69
+ // fuera `none`, los alfileres dejarían de ser tocables sin que nada
70
+ // cambiara de aspecto.
71
+ "circle-opacity": 0,
72
+ },
73
+ };
74
+ }
75
+ /** Las áreas: isócronas, zonas de cobertura y halos de imprecisión. */
76
+ export function capasDeAreas(paleta, anclas) {
77
+ const antesDe = anclas.lineas;
78
+ return [
79
+ {
80
+ antesDe,
81
+ capa: {
82
+ id: CAPAS.isocronas,
83
+ type: "fill",
84
+ source: FUENTES.isocronas,
85
+ paint: {
86
+ "fill-color": paleta.isocrona,
87
+ // Las bandas se superponen: la de 15 minutos contiene a la de 5.
88
+ // Con opacidad baja, la acumulación es justo lo que hace legible
89
+ // que el centro se alcanza antes.
90
+ "fill-opacity": [
91
+ "interpolate",
92
+ ["linear"],
93
+ ["get", "minutos"],
94
+ 5,
95
+ 0.28,
96
+ 30,
97
+ 0.1,
98
+ ],
99
+ },
100
+ },
101
+ },
102
+ {
103
+ antesDe,
104
+ capa: {
105
+ id: CAPAS.isocronasBorde,
106
+ type: "line",
107
+ source: FUENTES.isocronas,
108
+ paint: {
109
+ "line-color": paleta.isocrona,
110
+ "line-width": 1,
111
+ "line-opacity": 0.6,
112
+ },
113
+ },
114
+ },
115
+ {
116
+ antesDe,
117
+ capa: {
118
+ id: CAPAS.zonas,
119
+ type: "fill",
120
+ source: FUENTES.zonas,
121
+ paint: {
122
+ "fill-color": paleta.zona,
123
+ "fill-opacity": [
124
+ "match",
125
+ ["get", "enfasis"],
126
+ "destacado",
127
+ 0.28,
128
+ "atenuado",
129
+ 0.08,
130
+ 0.16,
131
+ ],
132
+ },
133
+ },
134
+ },
135
+ {
136
+ antesDe,
137
+ capa: {
138
+ id: CAPAS.zonasBorde,
139
+ type: "line",
140
+ source: FUENTES.zonas,
141
+ paint: { "line-color": paleta.bordeDeZona, "line-width": 1.5 },
142
+ },
143
+ },
144
+ {
145
+ antesDe,
146
+ capa: {
147
+ id: CAPAS.imprecision,
148
+ type: "fill",
149
+ source: FUENTES.imprecision,
150
+ paint: { "fill-color": paleta.imprecision, "fill-opacity": 0.18 },
151
+ },
152
+ },
153
+ {
154
+ antesDe,
155
+ capa: {
156
+ id: CAPAS.imprecisionBorde,
157
+ type: "line",
158
+ source: FUENTES.imprecision,
159
+ paint: {
160
+ "line-color": paleta.bordeDeImprecision,
161
+ "line-width": 1.5,
162
+ // Discontinuo a propósito: es la forma, no el color, la que dice
163
+ // «esto es una estimación». Quien no distinga los dos morados sigue
164
+ // viendo que este borde no es como los demás.
165
+ "line-dasharray": [2, 2],
166
+ },
167
+ },
168
+ },
169
+ ];
170
+ }
171
+ /** Las líneas: rutas, con su contorno y sus alternativas. */
172
+ export function capasDeLineas(paleta, anclas) {
173
+ const antesDe = anclas.puntos;
174
+ return [
175
+ {
176
+ antesDe,
177
+ capa: {
178
+ id: CAPAS.rutasAlternativas,
179
+ type: "line",
180
+ source: FUENTES.rutas,
181
+ filter: ["!", ["get", "principal"]],
182
+ layout: { "line-cap": "round", "line-join": "round" },
183
+ paint: {
184
+ "line-color": paleta.rutaAlternativa,
185
+ "line-width": 4,
186
+ "line-dasharray": [2, 1.5],
187
+ },
188
+ },
189
+ },
190
+ {
191
+ antesDe,
192
+ capa: {
193
+ id: CAPAS.rutaPrincipalContorno,
194
+ type: "line",
195
+ source: FUENTES.rutas,
196
+ filter: ["get", "principal"],
197
+ layout: { "line-cap": "round", "line-join": "round" },
198
+ // El contorno claro por debajo es lo que hace que la ruta se lea
199
+ // sobre una avenida del mismo grosor y color parecido. Sin él, la
200
+ // línea desaparece justo donde va por la vía que sigue.
201
+ paint: { "line-color": paleta.contornoDeRuta, "line-width": 9 },
202
+ },
203
+ },
204
+ {
205
+ antesDe,
206
+ capa: {
207
+ id: CAPAS.rutaPrincipal,
208
+ type: "line",
209
+ source: FUENTES.rutas,
210
+ filter: ["get", "principal"],
211
+ layout: { "line-cap": "round", "line-join": "round" },
212
+ paint: { "line-color": paleta.ruta, "line-width": 5 },
213
+ },
214
+ },
215
+ {
216
+ antesDe,
217
+ capa: {
218
+ id: CAPAS.rutasToque,
219
+ type: "line",
220
+ source: FUENTES.rutas,
221
+ paint: {
222
+ "line-width": RADIO_TACTIL_MINIMO_EN_PX * 2,
223
+ "line-opacity": 0,
224
+ },
225
+ },
226
+ },
227
+ ];
228
+ }
229
+ /** Los puntos: grupos, negocios, sedes, vehículos y la ubicación del usuario. */
230
+ export function capasDePuntos(paleta, anclas) {
231
+ const antesDe = anclas.seleccion;
232
+ return [
233
+ {
234
+ antesDe,
235
+ capa: {
236
+ id: CAPAS.ubicacionHalo,
237
+ type: "fill",
238
+ source: FUENTES.ubicacion,
239
+ // El halo es un **polígono**, no un círculo de MapLibre, y el filtro
240
+ // lo separa del punto que va en la misma fuente. La razón está en
241
+ // `circuloComoPoligono`: el radio de un círculo de MapLibre va en
242
+ // píxeles, así que un halo de «30 metros de incertidumbre» dibujado
243
+ // así mediría kilómetros al alejarse. Un polígono está en el mundo y
244
+ // se queda donde está.
245
+ filter: ["==", ["geometry-type"], "Polygon"],
246
+ paint: {
247
+ "fill-color": paleta.haloDeUbicacion,
248
+ "fill-opacity": 0.15,
249
+ },
250
+ },
251
+ },
252
+ {
253
+ antesDe,
254
+ capa: {
255
+ id: CAPAS.ubicacionPunto,
256
+ type: "circle",
257
+ source: FUENTES.ubicacion,
258
+ filter: ["==", ["geometry-type"], "Point"],
259
+ paint: {
260
+ "circle-radius": 6,
261
+ "circle-color": paleta.ubicacion,
262
+ "circle-stroke-width": 2,
263
+ "circle-stroke-color": paleta.contornoDePunto,
264
+ },
265
+ },
266
+ },
267
+ {
268
+ antesDe,
269
+ capa: {
270
+ id: CAPAS.gruposServidorCirculo,
271
+ type: "circle",
272
+ source: FUENTES.grupos,
273
+ paint: {
274
+ "circle-color": paleta.grupo,
275
+ "circle-radius": ["step", ["get", "cantidad"], 16, 10, 20, 50, 26],
276
+ "circle-stroke-width": 2,
277
+ "circle-stroke-color": paleta.contornoDePunto,
278
+ },
279
+ },
280
+ },
281
+ {
282
+ antesDe,
283
+ capa: {
284
+ id: CAPAS.gruposServidorCantidad,
285
+ type: "symbol",
286
+ source: FUENTES.grupos,
287
+ layout: {
288
+ "text-field": [
289
+ "coalesce",
290
+ ["get", "rotulo"],
291
+ ["to-string", ["get", "cantidad"]],
292
+ ],
293
+ "text-size": 12,
294
+ "text-allow-overlap": true,
295
+ },
296
+ paint: { "text-color": paleta.textoDeGrupo },
297
+ },
298
+ },
299
+ {
300
+ antesDe,
301
+ capa: {
302
+ id: CAPAS.gruposCirculo,
303
+ type: "circle",
304
+ source: FUENTES.negocios,
305
+ filter: ["has", "point_count"],
306
+ paint: {
307
+ "circle-color": paleta.grupo,
308
+ // El círculo crece con lo que contiene, en escalones. Un grupo de
309
+ // 200 y uno de 3 tienen que verse distintos antes de leer el número.
310
+ "circle-radius": ["step", ["get", "point_count"], 16, 10, 20, 50, 26],
311
+ "circle-stroke-width": 2,
312
+ "circle-stroke-color": paleta.contornoDePunto,
313
+ },
314
+ },
315
+ },
316
+ {
317
+ antesDe,
318
+ capa: {
319
+ id: CAPAS.gruposCantidad,
320
+ type: "symbol",
321
+ source: FUENTES.negocios,
322
+ filter: ["has", "point_count"],
323
+ layout: {
324
+ "text-field": ["get", "point_count_abbreviated"],
325
+ "text-size": 12,
326
+ "text-allow-overlap": true,
327
+ },
328
+ paint: { "text-color": paleta.textoDeGrupo },
329
+ },
330
+ },
331
+ {
332
+ antesDe,
333
+ capa: {
334
+ id: CAPAS.sedesPunto,
335
+ type: "circle",
336
+ source: FUENTES.sedes,
337
+ paint: {
338
+ "circle-radius": 4,
339
+ "circle-color": paleta.negocioAtenuado,
340
+ "circle-stroke-width": 1.5,
341
+ "circle-stroke-color": paleta.contornoDePunto,
342
+ },
343
+ },
344
+ },
345
+ {
346
+ antesDe,
347
+ capa: {
348
+ id: CAPAS.negociosPunto,
349
+ type: "circle",
350
+ source: FUENTES.negocios,
351
+ filter: ["!", ["has", "point_count"]],
352
+ paint: {
353
+ "circle-radius": radioDeNegocio(1),
354
+ "circle-color": [
355
+ "match",
356
+ ["get", "enfasis"],
357
+ "destacado",
358
+ paleta.negocioDestacado,
359
+ "atenuado",
360
+ paleta.negocioAtenuado,
361
+ paleta.negocio,
362
+ ],
363
+ "circle-stroke-width": [
364
+ "case",
365
+ ["boolean", ["feature-state", "encima"], false],
366
+ 3,
367
+ 2,
368
+ ],
369
+ "circle-stroke-color": paleta.contornoDePunto,
370
+ "circle-opacity": ["match", ["get", "enfasis"], "atenuado", 0.65, 1],
371
+ },
372
+ },
373
+ },
374
+ {
375
+ antesDe,
376
+ capa: {
377
+ id: CAPAS.negociosSimbolo,
378
+ type: "symbol",
379
+ source: FUENTES.negocios,
380
+ filter: ["all", ["!", ["has", "point_count"]], ["has", "simbolo"]],
381
+ layout: {
382
+ "icon-image": ["get", "simbolo"],
383
+ "icon-size": 0.8,
384
+ "icon-allow-overlap": true,
385
+ "text-field": ["coalesce", ["get", "rotulo"], ""],
386
+ "text-size": 11,
387
+ "text-offset": [0, 1.3],
388
+ "text-anchor": "top",
389
+ "text-optional": true,
390
+ },
391
+ paint: {
392
+ "icon-color": paleta.contornoDePunto,
393
+ "text-color": paleta.rotulo,
394
+ "text-halo-color": paleta.haloDeRotulo,
395
+ "text-halo-width": 1.2,
396
+ },
397
+ },
398
+ },
399
+ {
400
+ antesDe,
401
+ capa: {
402
+ id: CAPAS.vehiculos,
403
+ type: "symbol",
404
+ source: FUENTES.vehiculos,
405
+ layout: {
406
+ "icon-image": ["coalesce", ["get", "simbolo"], "cerca-vehiculo"],
407
+ "icon-size": 0.9,
408
+ "icon-allow-overlap": true,
409
+ "icon-rotate": ["coalesce", ["get", "rumbo"], 0],
410
+ "icon-rotation-alignment": "map",
411
+ },
412
+ paint: { "icon-color": paleta.vehiculo },
413
+ },
414
+ },
415
+ {
416
+ antesDe,
417
+ capa: capaDeToque(CAPAS.gruposServidorToque, FUENTES.grupos),
418
+ },
419
+ {
420
+ antesDe,
421
+ capa: capaDeToque(CAPAS.gruposToque, FUENTES.negocios, [
422
+ "has",
423
+ "point_count",
424
+ ]),
425
+ },
426
+ { antesDe, capa: capaDeToque(CAPAS.sedesToque, FUENTES.sedes) },
427
+ {
428
+ antesDe,
429
+ capa: capaDeToque(CAPAS.negociosToque, FUENTES.negocios, [
430
+ "!",
431
+ ["has", "point_count"],
432
+ ]),
433
+ },
434
+ { antesDe, capa: capaDeToque(CAPAS.vehiculosToque, FUENTES.vehiculos) },
435
+ ];
436
+ }
437
+ /** La selección: encima de todo, con anillo además de color. */
438
+ export function capasDeSeleccion(paleta) {
439
+ return [
440
+ {
441
+ // Sin ancla: la selección es lo último y va arriba del todo. Es la
442
+ // única familia de capas que no recibe `anclas`, y por eso su firma es
443
+ // distinta — fingir que la recibe para igualar las firmas dejaría un
444
+ // parámetro que nadie lee y que el siguiente lector creería que hace
445
+ // algo.
446
+ antesDe: "",
447
+ capa: {
448
+ id: CAPAS.seleccionArea,
449
+ type: "fill",
450
+ source: FUENTES.seleccion,
451
+ // Un negocio seleccionado con ubicación aproximada llega aquí como
452
+ // polígono, no como punto: ver `coleccionDeSeleccion`.
453
+ filter: ["==", ["geometry-type"], "Polygon"],
454
+ paint: {
455
+ "fill-color": paleta.seleccion,
456
+ "fill-opacity": 0.25,
457
+ "fill-outline-color": paleta.seleccion,
458
+ },
459
+ },
460
+ },
461
+ {
462
+ antesDe: "",
463
+ capa: {
464
+ id: CAPAS.seleccion,
465
+ type: "circle",
466
+ source: FUENTES.seleccion,
467
+ filter: ["==", ["geometry-type"], "Point"],
468
+ paint: {
469
+ "circle-radius": radioDeNegocio(1.35),
470
+ "circle-color": paleta.seleccion,
471
+ // El anillo grueso es la señal que no depende del color: quien no
472
+ // distinga el naranja del azul sigue viendo cuál está elegido.
473
+ "circle-stroke-width": 4,
474
+ "circle-stroke-color": paleta.contornoDePunto,
475
+ },
476
+ },
477
+ },
478
+ ];
479
+ }
480
+ /** Todas las capas de Cerca, en el orden en que se insertan. */
481
+ export function todasLasCapas(paleta, anclas) {
482
+ return [
483
+ ...capasDeAreas(paleta, anclas),
484
+ ...capasDeLineas(paleta, anclas),
485
+ ...capasDePuntos(paleta, anclas),
486
+ ...capasDeSeleccion(paleta),
487
+ ];
488
+ }
489
+ //# sourceMappingURL=capas.js.map