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.
191 lines
5.7 KiB
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`);
|