@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
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.
|
package/dist/capas.d.ts
ADDED
|
@@ -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
|