You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
senzapaura_es/scripts/mapa/generar.mjs

362 lines
11 KiB

/**
* Genera `src/lib/mapa/iberoamerica.ts` a partir del mapa de Wikimedia.
*
* node scripts/mapa/generar.mjs [ruta al svg]
*
* El original es `BlankMap-Americas.svg`, de Lokal_Profil, en dominio público:
* https://commons.wikimedia.org/wiki/File:BlankMap-Americas.svg
*
* De sus cincuenta y cinco países se conservan los veinte de Iberoamérica en
* América, y de cada uno todos sus trazados unidos en uno. Dos recortes bajan
* el archivo de 640 KB a algo que puede viajar en una página:
*
* - **Las coordenadas, a un decimal.** El mapa mide dos mil setecientas
* unidades de ancho y se dibuja a setecientos píxeles: la décima de unidad no
* llega ni a la décima de píxel.
* - **Fuera los islotes.** Chile trae ciento cincuenta y cinco trazados, casi
* todos rocas del sur que a este tamaño ocupan menos de un píxel. Se
* descartan los que no llegan al umbral; las islas que sí se ven —Galápagos,
* Chiloé, las Malvinas— lo pasan de sobra.
*
* Se deja como script y no como un archivo pegado a mano para que se pueda
* rehacer: el día que haga falta añadir un país o cambiar el umbral, esto es
* una línea, y a mano sería volver a empezar.
*/
import fs from 'node:fs';
const ORIGEN = process.argv[2] ?? 'scripts/mapa/BlankMap-World.svg';
const DESTINO = 'src/lib/mapa/iberoamerica.ts';
/**
* Los veintidós de Iberoamérica: los veinte de América más España y Portugal.
*
* Estaban fuera en la primera versión, hecha sobre un mapa de las Américas.
* Iberoamérica es también la Península —y España está en los datos: el bolero
* se adoptó ahí—, así que el origen pasa a ser el mapa del mundo.
*/
const PAISES = [
'es',
'pt',
'mx',
'gt',
'sv',
'hn',
'ni',
'cr',
'pa',
'cu',
'do',
'pr',
've',
'co',
'ec',
'pe',
'bo',
'py',
'cl',
'ar',
'uy',
'br'
];
/** Un trazado más pequeño que esto no se ve, y sí se nota en el peso. */
const MINIMO = 2;
/**
* El nombre en español de cada uno.
*
* Va aquí y no se lee de la tabla `pais`: el mapa dibuja los veintidós desde el
* primer día y la tabla solo tiene los países que ha traído alguna semilla. Un
* país sin nombre en la lista de al lado no se puede ni pulsar.
*/
const NOMBRES = {
es: 'España',
pt: 'Portugal',
mx: 'México',
gt: 'Guatemala',
sv: 'El Salvador',
hn: 'Honduras',
ni: 'Nicaragua',
cr: 'Costa Rica',
pa: 'Panamá',
cu: 'Cuba',
do: 'República Dominicana',
pr: 'Puerto Rico',
ve: 'Venezuela',
co: 'Colombia',
ec: 'Ecuador',
pe: 'Perú',
bo: 'Bolivia',
py: 'Paraguay',
cl: 'Chile',
ar: 'Argentina',
uy: 'Uruguay',
br: 'Brasil'
};
const svg = fs.readFileSync(ORIGEN, 'utf8');
/*
* Se recorren las etiquetas con expresión regular y no con un analizador de
* XML: hace falta saber en qué grupo va cada trazado, y para eso basta con
* llevar la cuenta de los `<g>` abiertos.
*/
const trozos = Object.fromEntries(PAISES.map((c) => [c, []]));
const pila = [];
for (const [, cierre, etiqueta, atributos, suelta] of svg.matchAll(
/<(\/?)(g|path)\b([^>]*?)(\/?)>/g
)) {
if (etiqueta === 'g') {
if (cierre) pila.pop();
else pila.push(atributos.match(/(?:^|\s)id="([^"]+)"/)?.[1] ?? null);
continue;
}
if (cierre) continue;
const propio = atributos.match(/(?:^|\s)id="([^"]+)"/)?.[1] ?? null;
const codigo = [propio, ...[...pila].reverse()].find((c) => c && PAISES.includes(c));
/*
* El límite del atributo importa: sin él, `d="…"` encaja dentro de
* `id="Mexico_mainland"` y el trazado que se extrae es el nombre del
* país. Salía un mapa vacío sin que nada fallara.
*/
const d = atributos.match(/(?:^|\s)d="([^"]+)"/)?.[1];
if (codigo && d) trozos[codigo].push(d);
void suelta;
}
/**
* Recorre un trazado y devuelve sus puntos, ya en absolutas.
*
* Hace falta porque el mapa del mundo está escrito en relativas —`m 1244,328 c
* 0.1,-0.12 …`— y ahí los números no son coordenadas sino desplazamientos:
* sumarlos como si lo fueran daba un rectángulo sin sentido y el encuadre salía
* infinito.
*
* No convierte el trazado, solo lo mide: el `d` se guarda tal cual, que dibuja
* igual y ocupa menos.
*/
function puntosDe(d) {
const piezas = d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) ?? [];
const puntos = [];
let x = 0;
let y = 0;
let inicioX = 0;
let inicioY = 0;
let comando = 'M';
let i = 0;
/** Cuántos números lleva cada comando. */
const cuantos = { M: 2, L: 2, T: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, A: 7, Z: 0 };
while (i < piezas.length) {
if (/[a-zA-Z]/.test(piezas[i])) {
comando = piezas[i];
i++;
if (comando.toUpperCase() === 'Z') {
x = inicioX;
y = inicioY;
continue;
}
}
const letra = comando.toUpperCase();
const relativo = comando !== letra;
const n = cuantos[letra];
if (n === undefined) break;
const args = piezas.slice(i, i + n).map(Number);
i += n;
if (args.length < n) break;
if (letra === 'H') x = relativo ? x + args[0] : args[0];
else if (letra === 'V') y = relativo ? y + args[0] : args[0];
else if (letra === 'A') {
// De la elipse solo interesa dónde acaba.
x = relativo ? x + args[5] : args[5];
y = relativo ? y + args[6] : args[6];
} else {
// El punto final es la última pareja; los de control quedan fuera del
// cálculo, y para medir un país la diferencia no se ve.
x = relativo ? x + args[n - 2] : args[n - 2];
y = relativo ? y + args[n - 1] : args[n - 1];
}
if (letra === 'M') {
inicioX = x;
inicioY = y;
// Una `m` con más parejas sigue como `l`; una `M`, como `L`.
comando = relativo ? 'l' : 'L';
}
puntos.push([x, y]);
}
return puntos;
}
/** El rectángulo que ocupa un trazado, para saber si merece la pena. */
function tamano(d) {
const puntos = puntosDe(d);
const xs = puntos.map((p) => p[0]);
const ys = puntos.map((p) => p[1]);
if (xs.length === 0) return { ancho: 0, alto: 0, xs, ys };
return {
ancho: Math.max(...xs) - Math.min(...xs),
alto: Math.max(...ys) - Math.min(...ys),
xs,
ys
};
}
const unaDecimal = (d) => d.replace(/-?\d+\.\d+/g, (n) => String(Math.round(Number(n) * 10) / 10));
/**
* Une varios trazados en uno.
*
* El primer salto de cada uno pasa a ser absoluto. Dentro de un `<path>` la
* posición de partida es el origen, así que `m 1244,328` y `M 1244,328` son lo
* mismo; pero al pegar el segundo detrás del primero, su `m` se sumaba a donde
* había acabado el anterior y México acababa a cuarenta mil unidades de su
* sitio. El encuadre salía de cuarenta y dos mil por cincuenta y ocho mil.
*/
const unir = (trazados) => trazados.map((d) => d.replace(/^\s*m/, 'M')).join(' ');
const contorno = {};
const centro = {};
/** El trazado mayor de cada país: es con el que se encuadra el inserto. */
const principal = {};
let descartados = 0;
/**
* Países de los que solo se dibuja el trozo mayor.
*
* España es la Península más Canarias y Baleares. Canarias cae a mil
* kilómetros al suroeste, fuera del recuadro; las Baleares son cuatro píxeles
* sueltos al este. En un mapa que trata de géneros, ninguna de las dos dice
* nada que no diga ya la Península, y sueltas al borde del encuadre solo
* ensucian.
*/
const SIN_ISLAS = ['es'];
for (const codigo of PAISES) {
const buenos = trozos[codigo].filter((d) => {
const { ancho, alto } = tamano(d);
const vale = ancho >= MINIMO || alto >= MINIMO;
if (!vale) descartados++;
return vale;
});
/*
* Un punto donde agarrar el país.
*
* Es el promedio de los vértices del trazado más grande, no el centro de su
* rectángulo: el de Cuba cae en el mar, porque la isla es alargada y curva.
* Con el promedio de la costa el punto queda dentro, y además da algo que
* pulsar en Puerto Rico o El Salvador, que a esta escala miden cuatro
* píxeles.
*/
const mayor = buenos.reduce(
(a, b) => (tamano(b).ancho * tamano(b).alto > tamano(a).ancho * tamano(a).alto ? b : a),
buenos[0] ?? ''
);
const { xs, ys } = tamano(mayor);
const medio = (ns) => ns.reduce((t, n) => t + n, 0) / ns.length;
centro[codigo] = [medio(xs), medio(ys)].map((n) => Math.round(n * 10) / 10);
principal[codigo] = mayor;
contorno[codigo] = unaDecimal(unir(SIN_ISLAS.includes(codigo) ? [mayor] : buenos));
}
/**
* Dos encuadres, no uno.
*
* Iberoamérica son dos orillas y en medio hay un océano: con un solo encuadre,
* la mitad del dibujo es Atlántico vacío y todo lo demás queda diminuto. Se
* calcula uno para América y otro para la Península, y el mapa pinta el segundo
* en un recuadro —la misma solución que usa cualquier mapa de España con
* Canarias—.
*/
const IBERIA = ['es', 'pt'];
const MARGEN = 10;
function encuadrar(codigos, soloPrincipal = false) {
const xs = [];
const ys = [];
for (const c of codigos) {
const t = tamano(soloPrincipal ? principal[c] : contorno[c]);
xs.push(...t.xs);
ys.push(...t.ys);
}
const x0 = Math.min(...xs) - MARGEN;
const y0 = Math.min(...ys) - MARGEN;
return [x0, y0, Math.max(...xs) - x0 + MARGEN, Math.max(...ys) - y0 + MARGEN]
.map((n) => n.toFixed(1))
.join(' ');
}
const encuadre = encuadrar(PAISES.filter((c) => !IBERIA.includes(c)));
/*
* El inserto encuadra solo la parte continental. Con Canarias dentro, el
* rectángulo se estiraba mil kilómetros al suroeste y la Península quedaba del
* tamaño de una mota. De España ya no se dibujan las islas; de Portugal sí, y
* Azores y Madeira caen fuera de este rectángulo, que es donde deben caer.
*/
const encuadreIberia = encuadrar(IBERIA, true);
const cuerpo = PAISES.map(
(c) =>
`\t{ codigo: '${c.toUpperCase()}', nombre: '${NOMBRES[c]}', centro: [${centro[c].join(', ')}], d: '${contorno[c]}' }`
).join(',\n');
fs.writeFileSync(
DESTINO,
`/**
* El mapa de Iberoamérica, país a país.
*
* **Generado. No se edita a mano:** \`node scripts/mapa/generar.mjs\`, que
* explica de dónde sale y qué se recorta.
*
* Los contornos son de \`BlankMap-Americas.svg\` de Wikimedia Commons, obra de
* Lokal_Profil, en dominio público.
*
* España y Portugal no están: el original es de las Américas. El día que hagan
* falta habrá que traerlos de otro mapa o enseñarlos aparte.
*/
export interface PaisDelMapa {
/** ISO 3166-1 alfa-2, como en la tabla \`pais\`. */
codigo: string;
/** En español. Lo lleva el mapa porque dibuja países que aún no están en la base. */
nombre: string;
/** Un punto dentro del país, para colgar de él lo que haya que pulsar. */
centro: [number, number];
/** El contorno, con todas sus islas en un solo trazado. */
d: string;
}
/** El encuadre de la orilla americana. */
export const ENCUADRE = '${encuadre}';
/** El de la Península, que va en su propio recuadro. */
export const ENCUADRE_IBERIA = '${encuadreIberia}';
/** Quiénes van en ese recuadro. */
export const IBERIA = ['ES', 'PT'];
export const PAISES: PaisDelMapa[] = [
${cuerpo}
];
`
);
const kb = (fs.statSync(DESTINO).size / 1024).toFixed(0);
console.log(` países ${PAISES.length}`);
console.log(` descartados ${descartados} trazados por debajo de ${MINIMO} unidades`);
console.log(` encuadre ${encuadre}`);
console.log(` — península ${encuadreIberia}`);
console.log(` tamaño ${kb} KB`);

Powered by TurnKey Linux.