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

191 lines
5.7 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-Americas.svg';
const DESTINO = 'src/lib/mapa/iberoamerica.ts';
/** Los veinte, en el orden en que se leen de norte a sur. */
const PAISES = [
'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;
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(/id="([^"]+)"/)?.[1] ?? null);
continue;
}
if (cierre) continue;
const propio = atributos.match(/id="([^"]+)"/)?.[1] ?? null;
const codigo = [propio, ...[...pila].reverse()].find((c) => c && PAISES.includes(c));
const d = atributos.match(/d="([^"]+)"/)?.[1];
if (codigo && d) trozos[codigo].push(d);
void suelta;
}
/** El rectángulo que ocupa un trazado, para saber si merece la pena. */
function tamano(d) {
const numeros = [...d.matchAll(/-?\d+(?:\.\d+)?/g)].map((m) => Number(m[0]));
const xs = numeros.filter((_, i) => i % 2 === 0);
const ys = numeros.filter((_, i) => i % 2 === 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));
const contorno = {};
const centro = {};
let descartados = 0;
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;
});
contorno[codigo] = unaDecimal(buenos.join(' '));
/*
* 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);
}
/* El encuadre sale de lo que queda, no del mapa entero. */
const xs = [];
const ys = [];
for (const d of Object.values(contorno)) {
const t = tamano(d);
xs.push(...t.xs);
ys.push(...t.ys);
}
const MARGEN = 10;
const x0 = Math.min(...xs) - MARGEN;
const y0 = Math.min(...ys) - MARGEN;
const encuadre = [x0, y0, Math.max(...xs) - x0 + MARGEN, Math.max(...ys) - y0 + MARGEN]
.map((n) => n.toFixed(1))
.join(' ');
const cuerpo = PAISES.map(
(c) =>
`\t{ codigo: '${c.toUpperCase()}', 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;
/** 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 justo de los veinte países, no el del mapa original. */
export const ENCUADRE = '${encuadre}';
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(` tamaño ${kb} KB`);

Powered by TurnKey Linux.