/** * 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 `` 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`);