diff --git a/.gitignore b/.gitignore index 111ad14..6423b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,32 @@ data/ __pycache__/ *.pyc -# Salidas pesadas que se regeneran y no hacen falta para publicar el visor -out/score_rave.tif -out/relieve.png -out/protegidos.png -out/aislamiento_ov.png +# Salidas pesadas que se regeneran y no hacen falta para publicar el visor. +# Con ** para que valga también en los out/ de las demás regiones. +out/**/score_rave.tif +out/**/relieve.png +out/**/protegidos.png +out/**/aislamiento_ov.png + +# De cada país entra lo que hace falta para mirarlo y nada más: el visor (que es +# autónomo, lleva el relieve y el terreno incrustados) y los datos en CSV. Son +# unos 39 MB los 22 países. +# +# Fuera se quedan los WebP —van ya dentro del visor, versionarlos es duplicar—, +# los PNG de los mapas (solo los usa el informe, que únicamente existe para +# España) y el GeoTIFF del score. Eso son otros 130 MB que nadie abre. +# +# Ojo con regenerar: el visor es binario a efectos de git, así que cada +# recálculo mete una copia entera en el historial y esa ya no sale. +out/*/* +!out/ortho/ +!out/ortho/** +!out/*/visor.html +!out/*/candidatos_estrellas.csv +!out/*/stats.json + +# Paquetes por país listos para subir a una release +dist/ # Restos de trabajo *.log @@ -19,4 +40,4 @@ out/aislamiento_ov.png # sondeos temporales de las APIs *.json -!out/stats.json +!out/**/stats.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aca5a01..3bab872 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,6 +4,10 @@ # en copiar out/ a public/, que es la carpeta que GitLab Pages sirve, y poner # un index que lleve a los dos. # +# Sale solo España, que es lo único que va en el repositorio. Para publicar +# también los demás países, añade antes del cp (con python3 en la imagen): +# python3 src/descargar.py todos || true +# # La URL queda en https://.gitlab.io// pages: diff --git a/Makefile b/Makefile index 52249c9..b246929 100644 --- a/Makefile +++ b/Makefile @@ -2,15 +2,15 @@ # así que se puede reejecutar solo la parte que interese. PY := ./.venv/bin/python -.PHONY: all datos rasters modelo salidas cobertura revisar limpio +.PHONY: all datos rasters modelo salidas cobertura revisar empaquetar descargar selector limpio all: datos rasters modelo salidas revisar ## --- descargas (~2,5 GB, una vez) --- datos: + bash src/dl_osm.sh bash src/dl_dem.sh $(PY) src/dl_protected.py - @echo "OSM: descarga los .pbf de Geofabrik en data/raw/ (ver README)" ## --- extracción y rásters --- rasters: @@ -23,18 +23,20 @@ rasters: $(PY) src/build_rasters2.py ## --- modelo de estrellas --- +# score.py deja iso_pure.npy y score_rave.tif, que necesitan los mapas. modelo: + $(PY) src/score.py $(PY) src/stars.py ## --- visor, informe y mapas --- salidas: $(PY) src/render_relief.py $(PY) src/render_map.py - $(PY) -c "from PIL import Image; \ - [Image.open(f'out/{n}.png').save(f'out/{n}.webp','WEBP',quality=q,method=6) \ + $(PY) -c "import sys; sys.path.insert(0,'src'); import config as C; \ + from PIL import Image; \ + [Image.open(C.OUT/f'{n}.png').save(C.OUT/f'{n}.webp','WEBP',quality=q,method=6) \ for n,q in (('relieve',88),('protegidos',80),('aislamiento_ov',80))]" $(PY) src/dl_orthos.py - rm -rf out/ortho && cp -r data/interim/ortho out/ortho $(PY) src/build_viewer.py $(PY) src/report.py @@ -50,5 +52,18 @@ cobertura: revisar: $(PY) src/revisar.py +## --- países: paquetes para publicar y descarga de los publicados --- +# El repositorio lleva España. Los demás se calculan (REGION=xx make all) o se +# bajan ya hechos, que son unos pocos MB en vez de un par de horas. +empaquetar: + $(PY) src/empaquetar.py + +descargar: + $(PY) src/descargar.py $(or $(PAIS),todos) + +# Vuelve a poner al día el desplegable de país de todos los visores en disco. +selector: + $(PY) src/paises.py + limpio: rm -rf out/*.html out/*.png out/*.webp out/ortho diff --git a/README.md b/README.md index dc7d9ef..7e53e8b 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ No hace falta servidor ni conexión. Ojo: `visor.html` necesita la carpeta | `out/ortho/` | Ortofoto PNOA de cada localización, 640 m de lado | | `out/stats.json` | Cifras del modelo, que leen el informe y el visor | | `src/` | El pipeline entero, reproducible | +| `out//` | Lo mismo para cada país calculado. No van al repositorio: se publican como paquetes (`make empaquetar`) y se bajan con `make descargar` | ## Cómo se calcula @@ -66,20 +67,106 @@ exigiendo después 10 km de separación entre los elegidos. ![cobertura](docs/cobertura.png) -El país es un parámetro. Hay 27 declarados entre Europa y el norte de África, -pero **solo España está calculada**: el resto hay que procesarlo. +El país es un parámetro. Hay 27 en el catálogo entre Europa y el norte de África +y **están los 27 calculados**: 7.337 localizaciones en total, de las que 1.417 +son de cinco estrellas. Se cambia de uno a otro desde el propio visor. + +| País | Localizaciones | ★★★★★ | km² bajo 45 dB | Protegido | +|---|---:|---:|---:|---:| +| Argelia | 1.959 | 310 | 51.438 | 5.5 % | +| Libia | 1.246 | 434 | 52.100 | 0.0 % | +| Egipto | 753 | 175 | 22.120 | 6.7 % | +| Mauritania | 674 | 150 | 21.997 | 0.6 % | +| Marruecos | 640 | 230 | 25.173 | 1.7 % | +| Suecia | 429 | 18 | 3.985 | 18.8 % | +| España | 355 | 11 | 505 | 29.0 % | +| Rumanía | 274 | 31 | 1.075 | 24.3 % | +| Túnez | 213 | 22 | 6.085 | 0.8 % | +| Grecia | 155 | 13 | 336 | 36.2 % | +| Islandia | 143 | 15 | 1.147 | 19.8 % | +| Noruega | 142 | 6 | 176 | 18.2 % | +| Finlandia | 121 | 0 | 630 | 15.8 % | +| Reino Unido | 88 | 1 | 150 | 20.6 % | +| Italia | 46 | 0 | 7 | 22.1 % | +| Croacia | 46 | 1 | 86 | 39.5 % | +| Portugal | 16 | 0 | 13 | 23.1 % | +| Francia | 16 | 0 | 2 | 29.5 % | +| Irlanda | 12 | 0 | 4 | 16.7 % | +| Chequia | 4 | 0 | 0 | 23.1 % | +| Polonia | 2 | 0 | 1 | 40.6 % | +| Austria | 2 | 0 | 0 | 30.5 % | +| Alemania | 1 | 0 | 0 | 41.7 % | +| Andorra, Países Bajos, Bélgica, Suiza | 0 | 0 | 0 | — | + +La columna que manda es la del sonido, y el reparto no tiene nada que ver con el +tamaño de los países. **El norte de África concentra 5.272 de las 7.337 +localizaciones**; la Europa central industrializada —Alemania, Polonia, Austria, +Chequia, Bélgica, Países Bajos y Suiza juntos, más de un millón de km²— suma +**nueve**. Alemania tiene exactamente una: Bad Arolsen, a 44,4 dB. Y Holanda, +Bélgica, Suiza y Andorra, ninguna. + +Lo que decide es cómo está repartida la gente, no cuánta hay. La mediana de +distancia al edificio más cercano va de los 141 m de los Países Bajos a los +48 km de Libia, pasando por 316 m en Polonia y 1.020 m en España. Suiza es el +caso curioso: aislamiento le sobra, pero solo el 37,9 % del país baja de 10° de +pendiente y el cruce con "a más de 2 km de una casa" deja 430 km², que no +sobreviven al resto de filtros. + +**Dos avisos sobre las cifras extremas.** Primero, las estrellas no son +comparables entre países: las bandas son absolutas (5★ a partir de 72 puntos) +pero los criterios saturan en extremos opuestos. En Libia el 72 % de los +candidatos tiene el sonido —que pesa un 30 %— clavado en el máximo, y en España +el 0 %; los 121 candidatos finlandeses tienen exactamente la misma nota de clima +porque el umbral del frío se acaba en −8 °C y allí el mínimo va de −11 a −20. + +Segundo, **la columna `db_en_casa` da negativos en el desierto** y eso no +significa nada. El mejor candidato argelino tiene la casa más cercana a 265 km, +y el término de absorción atmosférica del modelo (0,5 dB/km) extrapolado a esa +distancia son 132 dB él solo, de donde salen los −111 dB. La aproximación vale +para unos pocos kilómetros, no para cientos. Por debajo del ruido ambiente de +una noche en el desierto, unos 20-30 dB, todo es igual de inaudible. + +**Ojo al comparar estrellas entre países.** El criterio de clima penaliza con +umbrales absolutos —por encima de 30 °C y por debajo de −1 °C— y en el norte +satura: los 121 candidatos finlandeses tienen exactamente la misma nota de +clima, así que ese 8 % del modelo deja de distinguir y les baja la banda de +estrellas a todos por igual. Finlandia no sale con cero cinco-estrellas porque +sus sitios sean peores. + +## Bajarse un país, o calcular otro + +El repositorio lleva España. Los demás se bajan ya hechos, que son unos pocos MB +y un momento, en vez de un par de horas y 25 GB: + +```bash +make descargar PAIS=is # solo Islandia +make descargar # todos los publicados +``` + +Aparecen en el desplegable de país del visor automáticamente. Y para calcular +uno que no esté: ```bash ./.venv/bin/python src/regions.py # ver el catálogo -REGION=pt make all # procesar Portugal +REGION=hr make all # procesar Croacia entera ``` +Sin ortofoto nacional descargable, el visor de Portugal abre la ficha en el +relieve 3D en vez de en la foto aérea, y el mapa deslizante va sobre OSM en vez +de sobre las teselas del IGN. Tampoco lleva `informe.html`: ese no es una +plantilla sino un artículo escrito sobre el barrido español, así que solo se +genera para España. + Una malla por país, no una continental: Europa entera a 100 m son mil millones de celdas, 73 GB solo en capas ráster. Ver [Añadir un país](docs/Anadir-un-pais.md). -Ojo con lo que hay disponible en cada sitio: en el norte de África **no hay -fuente libre de espacios protegidos**, así que el filtro legal no se aplica y -hay que conseguir la cartografía del país aparte. +Ojo con lo que hay disponible en cada sitio. Comprobado consultando los +servicios de la Agencia Europea de Medio Ambiente país por país, que estar en +Europa no basta: **Andorra y Reino Unido devuelven cero espacios protegidos** +—Andorra nunca estuvo en la UE y el Reino Unido salió— igual que todo el norte +de África, y Grecia solo tiene Natura 2000, sin figuras nacionales. Donde no hay +fuente oficial se cae a los polígonos de OpenStreetMap, que son incompletos y no +son un límite legal; hay que conseguir la cartografía del país aparte. ## Reproducirlo diff --git a/abrir.sh b/abrir.sh index ecba6b2..9eda358 100755 --- a/abrir.sh +++ b/abrir.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Abre el visor de localizaciones en el navegador por defecto. -# Es un fichero local: no necesita servidor, ni conexión, ni nada de Claude. +# Es un fichero local: no necesita servidor ni conexión. # # ./abrir.sh -> visor interactivo (mapa + 3D + filtros) # ./abrir.sh informe -> informe con el método y la tabla diff --git a/docs/Anadir-un-pais.md b/docs/Anadir-un-pais.md index 7a791b8..f8ee061 100644 --- a/docs/Anadir-un-pais.md +++ b/docs/Anadir-un-pais.md @@ -51,6 +51,26 @@ En `src/regions.py`: vacía: el extracto está recortado por la frontera y al otro lado no hay edificios en los datos aunque sí los haya en el terreno. +`make datos` baja el extracto del país y los de sus vecinos con `src/dl_osm.sh`, +y `extract_osm.py` los procesa solos con sufijo `_n1`, `_n2`… Si falta alguno, +avisa por pantalla en vez de dar el resultado por bueno. + +Los espacios protegidos van a `data/raw/prot//`, uno por región. Antes +había un solo directorio con un "si ya existe, no lo bajes", y cualquier país +nuevo se quedaba con los polígonos de España. + +## Niveles administrativos + +El `admin_level` que cubre el país **no es el mismo en todas partes**, y de ahí +sale la máscara del país y la etiqueta gruesa del candidato. En España el 4 son +las comunidades y cubre los 498.542 km²; en Portugal el 4 se queda en 2.338 km² +y quien cubre el país son el 6 (distritos, 89.082 km²) y el 8 (freguesías). + +No hay que tocar nada: `config.admin_polygons()` extrae los niveles 2, 4, 6 y 8 +y se queda con el primero que llegue al 98 % del que más superficie cubra. Pero +conviene mirar el km² que imprime `build_rasters`: si no se parece a la +superficie real del país, el resultado no vale nada. + ## Coste aproximado Por país mediano: 2-6 GB de descarga, ~25 GB de disco temporal y un par de horas. diff --git a/docs/Home.md b/docs/Home.md index dcd3adc..ccbacf3 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -24,9 +24,14 @@ demasiada pendiente) y puntúa lo que queda en seis criterios. | | Países | Qué significa | |---|---|---| -| 🔵 Procesado | 1 (España) | 355 localizaciones calculadas y publicadas | -| 🟡 En catálogo, con protegidos | 20 | El código los soporta y hay datos oficiales de espacios protegidos. Falta ejecutar el pipeline | -| 🟠 En catálogo, sin protegidos | 6 | Todo el norte de África. **No hay fuente libre de espacios protegidos**: el filtro legal no se aplica | +| Procesado | **27 de 27** | 7.337 localizaciones, 1.417 de cinco estrellas | +| Sin ninguna | 4 | Andorra, Países Bajos, Bélgica y Suiza. No es un fallo: no queda una sola celda que pase todos los filtros | +| Sin protegidos oficiales | 8 | El norte de África, más Andorra y Reino Unido. **No hay fuente libre**: se cae a los polígonos de OpenStreetMap, que no son un límite legal. Libia es el peor caso, con 10 polígonos y el 0,0 % del país | + +Comprobado consultando los servicios de la EEA país por país, no dando por hecho +que Europa esté cubierta: Andorra nunca estuvo en la UE y el Reino Unido salió, +y los dos devuelven cero sitios. Grecia solo tiene Natura 2000, sin figuras +nacionales. Procesar un país cuesta unas 2 horas y 25 GB. **No se procesa Europa entera de una vez**: a 100 m son mil millones de celdas y unos 73 GB solo en capas ráster, diff --git a/docs/Publicar.md b/docs/Publicar.md index 9dc677f..0828080 100644 --- a/docs/Publicar.md +++ b/docs/Publicar.md @@ -1,5 +1,39 @@ # Publicar +## Los países, fuera del repositorio + +En el repositorio va **España y nada más**. Cada visor lleva el relieve y un +recorte de terreno por candidato incrustados en base64, así que pesa entre 1 y +4 MB y cualquier recálculo reescribe el fichero entero: doce países en el +historial de git son cientos de MB en unos pocos recálculos, y el clon deja de +ser algo que la gente pueda bajarse sin pensárselo. + +Los demás se publican como adjuntos de una release: + +```bash +make empaquetar # deja dist/rave-scout-.zip, 13 MB los once +``` + +Se suben a una release con la etiqueta `paises`. A partir de ahí, cualquiera: + +```bash +make descargar PAIS=is +make descargar # todos los publicados +``` + +El paquete lleva el visor y los CSV, que es lo que hace falta para mirar el +país. Fuera se quedan los PNG de los mapas (solo los usa el informe, que +únicamente existe para España), los WebP (ya van dentro del visor) y el GeoTIFF +del score. Al descomprimir se actualiza solo el desplegable de país de todos los +visores que haya en disco, que si no el recién bajado no aparecería en los otros. + +Si lo publicas en otro sitio, `RAVE_SCOUT_PAISES` manda sobre la dirección por +defecto: + +```bash +RAVE_SCOUT_PAISES=https://mi-servidor/rave-scout make descargar PAIS=no +``` + ## Este repositorio está en Gitea **Gitea no tiene Pages integrado**, a diferencia de GitLab. Opciones: diff --git a/docs/cobertura.png b/docs/cobertura.png index 28e1851..7a40084 100644 Binary files a/docs/cobertura.png and b/docs/cobertura.png differ diff --git a/out/candidatos.csv b/out/candidatos.csv deleted file mode 100644 index e92d51c..0000000 --- a/out/candidatos.csv +++ /dev/null @@ -1,61 +0,0 @@ -rank,lat,lon,municipio,comunidad,score,d_edificio_m,d_carretera_m,d_acceso_m,d_nucleo_m,d_protegido_m,d_militar_m,cota_m,pendiente_deg,tpi_m,edif_5km,km_madrid -1,38.06287,-5.1757,Villanueva del Rey,Andalucía,0.9962,6576,5423,412,14268,9141,26148,395,0.4,-34.2,0,287.8 -2,38.34667,-3.28549,Santisteban del Puerto,Andalucía,0.9954,5336,5069,100,10580,7357,22934,447,0.5,-109.9,0,232.4 -3,38.55127,-4.52142,Almodóvar del Campo,Castilla-La Mancha,0.9946,5115,5223,447,8213,7269,44969,709,0.5,-50.7,0,217.0 -4,38.43556,-3.68651,Viso del Marqués,Castilla-La Mancha,0.9929,5560,6082,100,11364,8884,15945,963,0.7,-76.1,0,219.2 -5,38.44065,-2.92249,Chiclana de Segura,Andalucía,0.989,5941,6040,100,8050,11200,21982,707,1.1,-67.7,0,229.8 -6,39.24702,-4.99906,Herrera del Duque,Extremadura,0.9872,5333,4925,500,8993,13600,61175,491,1.0,-33.4,0,169.2 -7,39.22167,-6.90758,Alburquerque,Extremadura,0.9826,7278,4738,500,8041,20341,19383,278,0.7,-34.1,0,303.0 -8,39.10688,-6.21413,Montánchez,Extremadura,0.9823,5108,4900,100,8205,2475,17836,302,1.4,-32.9,0,257.8 -9,40.07779,-6.88696,Cilleros,Extremadura,0.976,5240,5936,412,8547,13384,80146,355,0.5,-24.3,0,273.4 -10,37.87092,-7.14462,Rosal de la Frontera,Andalucía,0.9755,8300,5131,565,9140,728,84791,266,2.5,-50.8,0,406.3 -11,38.76912,-5.96255,Don Benito,Extremadura,0.9714,5314,5536,0,7083,26444,40383,431,1.0,-30.3,0,264.3 -12,38.90002,-5.29879,Monterrubio de la Serena,Extremadura,0.9706,6600,6363,282,8354,7843,55276,389,0.3,-21.9,0,215.0 -13,38.23908,-3.77675,Baños de la Encina,Andalucía,0.9687,5536,4785,100,7566,6447,10186,368,1.4,-62.2,0,240.8 -14,37.69508,-7.20742,Paymogo,Andalucía,0.9673,7333,5103,100,7623,17844,69148,137,0.8,-24.9,0,423.8 -15,41.4182,0.22977,Fraga,Aragón,0.9664,5366,5434,583,7518,8309,93148,174,2.4,-71.9,0,349.0 -16,39.05018,-4.89497,Fuenlabrada de los Montes,Extremadura,0.966,6281,4360,0,7840,24298,72713,569,0.5,-32.9,0,181.2 -17,40.59572,-0.89423,Orrios,Aragón,0.965,5162,5872,0,6705,5162,67727,1376,0.9,-38.0,0,239.4 -18,38.20084,-6.14295,Montemolín,Extremadura,0.9648,6381,4924,200,7877,16611,44949,566,2.8,-29.5,0,320.5 -19,39.42412,-4.34442,Retuerta del Bullaque,Castilla-La Mancha,0.9646,5500,4850,0,6789,4964,51145,692,0.5,-33.8,0,122.0 -20,39.46578,-3.74467,Consuegra,Castilla-La Mancha,0.9639,5728,4967,100,6649,22173,24207,728,0.5,-29.2,0,105.3 -21,37.53579,-7.01779,Alosno,Andalucía,0.9628,5385,7892,500,8876,28205,45848,109,1.2,-22.4,0,425.4 -22,38.30851,-6.6121,Jerez de los Caballeros,Extremadura,0.9627,5950,5192,565,6926,9130,65547,290,0.1,-25.6,0,339.7 -23,41.36079,-0.17822,Sástago,Aragón,0.9624,6313,4780,100,11763,12727,66636,292,0.4,-22.6,0,314.5 -24,39.16856,-4.62131,Arroba de los Montes,Castilla-La Mancha,0.9623,4997,5140,0,6795,16277,79107,557,1.4,-54.2,0,157.9 -25,39.35078,-4.18342,Retuerta del Bullaque,Castilla-La Mancha,0.9622,4920,4802,0,7816,6082,49880,825,2.1,-77.7,0,124.3 -26,41.60134,-0.05886,Villanueva de Sigena,Aragón,0.9609,4940,7206,0,7752,39926,66038,391,1.5,-25.5,0,332.7 -27,39.17435,-5.28458,Talarrubias,Extremadura,0.9565,6264,4472,223,8500,4579,38327,482,2.2,-36.2,0,191.6 -28,40.30502,-6.57399,Villasrubias,Castilla y León,0.9529,4808,5189,400,6720,1264,93209,910,0.9,-53.7,0,244.5 -29,38.41832,-5.89163,Valencia de las Torres,Extremadura,0.9522,7023,5440,100,5640,16199,54423,440,0.1,-33.7,0,288.3 -30,39.4711,-4.0508,Los Yébenes,Castilla-La Mancha,0.9482,4981,5675,0,5909,5903,32491,1108,0.9,-30.9,0,108.4 -31,38.42337,-4.17661,Fuencaliente,Castilla-La Mancha,0.9482,4972,5800,583,10057,3935,51072,768,0.6,-16.9,0,223.6 -32,38.90876,-4.74684,Valdemanco del Esteras,Castilla-La Mancha,0.9474,7262,4143,600,7963,39906,60421,581,1.8,-37.6,0,188.1 -33,39.05771,-4.40184,Piedrabuena,Castilla-La Mancha,0.9461,4891,4101,0,12650,25205,57346,694,1.1,-41.2,0,161.0 -34,38.69587,-4.5906,Almodóvar del Campo,Castilla-La Mancha,0.9449,4992,5269,223,8340,20631,48304,605,5.5,-37.2,0,204.0 -35,38.47308,-3.21349,Villamanrique,Castilla-La Mancha,0.9442,4272,7962,0,9213,13333,32224,674,0.6,-61.8,0,219.8 -36,39.27436,-3.84608,Los Yébenes,Castilla-La Mancha,0.9401,7218,4669,282,6441,11067,36293,756,1.6,-48.6,0,126.8 -37,38.73027,-3.60681,Moral de Calatrava,Castilla-La Mancha,0.9391,5780,4295,565,10625,21888,25091,665,0.4,-21.4,0,186.9 -38,38.01888,-6.15112,Monesterio,Extremadura,0.9377,5166,4860,400,6140,5200,35867,699,2.0,-42.0,0,336.5 -39,37.55272,-5.54487,Carmona,Andalucía,0.9369,5813,4801,565,7937,12924,10259,86,0.8,-16.4,0,352.2 -40,40.00496,-3.2808,Villarrubia de Santiago,Castilla-La Mancha,0.9361,6280,4981,100,5470,13336,12778,598,1.3,-43.3,0,58.6 -41,38.06582,-5.51439,Hornachuelos,Andalucía,0.9356,4729,4785,360,12949,500,17101,319,3.7,-46.1,0,301.2 -42,36.41683,-5.82344,Medina Sidonia,Andalucía,0.9333,5126,5188,0,5126,4000,21005,38,0.9,-35.8,0,476.1 -43,37.1289,-5.34582,Morón de la Frontera,Andalucía,0.9249,6307,3590,0,7746,27549,10568,265,1.4,-53.0,0,388.2 -44,39.61766,-1.49845,Mira,Castilla-La Mancha,0.9247,6053,4365,424,5813,8237,62721,862,0.6,-32.0,0,209.8 -45,38.5549,-3.85046,Calzada de Calatrava,Castilla-La Mancha,0.9242,4539,4816,282,6413,15851,35609,530,0.5,-43.8,0,206.2 -46,39.57463,-6.90807,Alcántara,Extremadura,0.9201,4992,5536,360,11795,538,47327,264,1.2,-9.8,0,288.2 -47,39.60971,-6.00399,Trujillo,Extremadura,0.9191,3981,5532,100,9617,17426,34753,320,1.2,-31.5,0,214.8 -48,39.0247,-6.60132,Montijo,Extremadura,0.9187,7632,5215,200,9192,24481,20934,242,0.5,-7.1,0,290.5 -49,41.40568,-0.3686,Velilla de Ebro,Aragón,0.9169,5020,4252,0,6802,18577,50195,249,1.1,-24.5,0,301.0 -50,41.40387,-1.90091,Villalengua,Aragón,0.9157,5028,5797,0,6113,26121,23361,939,1.3,-19.9,0,186.2 -51,39.7348,-3.07369,Villanueva de Alcardete,Castilla-La Mancha,0.9154,5208,5124,0,8683,20208,25941,731,0.4,-5.7,0,93.5 -52,38.61326,-6.02991,Puebla de la Reina,Extremadura,0.9139,5246,3560,412,7184,29044,48662,393,1.2,-32.1,0,280.4 -53,40.25601,-2.91121,Comunidad de Albalate de Zorita e Illana,Castilla-La Mancha,0.9136,4904,4522,100,6741,40231,28313,765,1.7,-24.4,0,70.1 -54,42.68931,-4.73128,Congosto de Valdavia,Castilla y León,0.9134,4838,4827,100,4967,16007,69579,1049,0.8,-40.5,0,267.4 -55,37.44485,-4.96863,Écija,Andalucía,0.9118,7705,4738,0,7002,9481,15469,156,0.6,-14.5,0,344.6 -56,38.19032,-4.62323,Adamuz,Andalucía,0.9117,4883,4110,565,14651,18703,16773,595,1.9,-22.3,0,257.5 -57,41.67708,-2.96155,Talveila,Castilla y León,0.9102,4833,4726,0,5154,8402,12756,1014,0.8,-29.2,0,152.2 -58,39.94034,-3.08974,Fuente de Pedro Naharro,Castilla-La Mancha,0.9096,5124,5099,0,6977,30910,18767,774,0.2,-9.5,0,75.0 -59,39.94011,-6.74119,Moraleja,Extremadura,0.909,6685,3466,424,6800,15065,60360,227,0.6,-47.4,0,263.7 -60,39.09732,-4.22891,Piedrabuena,Castilla-La Mancha,0.9076,7002,3482,565,7202,22830,44201,566,0.8,-27.8,0,152.1 diff --git a/src/build_protected.py b/src/build_protected.py index d57319d..00fc765 100644 --- a/src/build_protected.py +++ b/src/build_protected.py @@ -58,22 +58,43 @@ def load(path, kind): def main(): t0 = time.time() - recs = (load(C.RAW / "prot" / "natura2000.geojson", "Natura 2000") + - load(C.RAW / "prot" / "natda.geojson", "Designación nacional")) + src = C.RAW / "prot" if C.IS_DEFAULT else C.RAW / "prot" / C.CODE + files = [(src / "natura2000.geojson", "Natura 2000"), + (src / "natda.geojson", "Designación nacional")] + recs = [r for p, kind in files if p.exists() for r in load(p, kind)] + if not recs: + # Sin cartografía oficial (norte de África, Andorra, Reino Unido) se cae + # a los polígonos de OpenStreetMap. Están peor —para eso se buscó la + # fuente oficial— pero dejar la máscara vacía es mucho peor: sería + # decirle al modelo que en el país no hay nada protegido. + print(f"\n [SIN FUENTE OFICIAL] no hay espacios protegidos para " + f"{C.REGION['name']} en {src}.") + print(" Se usan los de OpenStreetMap. Son incompletos y no son un " + "límite legal:") + print(" consigue la cartografía oficial del país antes de fiarte de " + "ningún resultado.") + with open(C.INTERIM / "area_protected.pkl", "rb") as fh: + recs = [{"geom": r["geom"], "name": r.get("name", ""), "code": "", + "kind": "OpenStreetMap"} for r in pickle.load(fh) + if not r["geom"].is_empty] + print(f" {len(recs):,} espacios de OSM") with open(C.INTERIM / "prot_official.pkl", "wb") as fh: pickle.dump(recs, fh, protocol=4) print("rasterizando…", flush=True) - mask = rasterio.features.rasterize( - [(mapping(r["geom"]), 1) for r in recs], - out_shape=(C.HEIGHT, C.WIDTH), transform=C.TRANSFORM, - fill=0, dtype=np.uint8, all_touched=True) # all_touched: conservador + if recs: + mask = rasterio.features.rasterize( + [(mapping(r["geom"]), 1) for r in recs], + out_shape=(C.HEIGHT, C.WIDTH), transform=C.TRANSFORM, + fill=0, dtype=np.uint8, all_touched=True) # all_touched: conservador + else: + mask = np.zeros((C.HEIGHT, C.WIDTH), dtype=np.uint8) np.save(C.INTERIM / "mask_protected_official.npy", mask) spain = np.load(C.INTERIM / "mask_spain.npy").astype(bool) old = np.load(C.INTERIM / "mask_protected.npy").astype(bool) new = mask.astype(bool) - print(f"\ncobertura sobre España:") + print(f"\ncobertura sobre {C.REGION['name']}:") print(f" OSM {(old & spain).sum()/spain.sum()*100:5.1f}% " f"({(old & spain).sum()*0.01:>8,.0f} km2)") print(f" oficial {(new & spain).sum()/spain.sum()*100:5.1f}% " diff --git a/src/build_rasters.py b/src/build_rasters.py index f0b696a..ddf435b 100644 --- a/src/build_rasters.py +++ b/src/build_rasters.py @@ -30,8 +30,12 @@ def load_points(base): valga en cualquier región sin tocar el código. """ xs, ys = [], [] - for p in sorted(C.INTERIM.glob(f"{base}.npy")) + \ - sorted(C.INTERIM.glob(f"{base}_*.npy")): + # _good y _bad no son vecinos: son la clasificación de las pistas que hace + # extract_extra, y son un subconjunto de line_track. Colarlas aquí no + # cambiaba el resultado, pero el patrón se las tragaba. + vecinos = [p for p in sorted(C.INTERIM.glob(f"{base}_*.npy")) + if not p.stem.endswith(("_good", "_bad"))] + for p in sorted(C.INTERIM.glob(f"{base}.npy")) + vecinos: if p.exists(): a = np.load(p) if a.shape[1]: @@ -79,11 +83,16 @@ def rasterize_areas(cat, names_out=None): def main(): - print("=== máscara de España (unión de comunidades) ===", flush=True) - with open(C.INTERIM / "area_admin_region.pkl", "rb") as fh: - regions = [r for r in pickle.load(fh) if r["geom"].area > 1e6] - print(f" {len(regions)} comunidades, " - f"{sum(r['geom'].area for r in regions)/1e6:,.0f} km2", flush=True) + print(f"=== máscara de {C.REGION['name']} ===", flush=True) + # El nivel se elige por cobertura administrativa y punto. Que los límites + # de OSM se metan en el mar lo arregla el recorte por DEM de más abajo, no + # esta elección: intentar esquivar el agua eligiendo nivel se llevaba por + # delante a Andorra, que solo tiene un polígono municipal de 26 km2 y + # necesita el límite del país. + _, regions = C.admin_polygons( + ("country", "region", "province", "municipality"), report=True) + if not regions: + raise SystemExit("sin límites administrativos: falta extract_areas.py") spain = rasterio.features.rasterize( [(mapping(r["geom"]), 1) for r in regions], out_shape=(C.HEIGHT, C.WIDTH), transform=C.TRANSFORM, @@ -102,7 +111,23 @@ def main(): if dropped: print(f" [ventana] {dropped*0.01:,.0f} km2 fuera de la ventana de " f"análisis descartados (territorios sin datos de puntos)") - spain = (spain.astype(bool) & window.astype(bool)).astype(np.uint8) + # Fuera el mar. Los límites administrativos de OSM se meten en el agua: el + # del país suele traer las aguas territoriales y las provincias costeras + # también (las županije croatas añaden 30.000 km2 de Adriático). El DEM de + # Copernicus vale como detector: el océano es exactamente 0.0, y la tierra + # a 0.0000 clavado no existe apenas —en Croacia son 5 km2 de 56.439—. Los + # países bajo el nivel del mar salen negativos, no cero, así que se quedan. + dem_path = C.INTERIM / "dem.npy" + if dem_path.exists(): + sea = np.load(dem_path) == 0.0 + n_sea = int((spain.astype(bool) & sea).sum()) + if n_sea: + print(f" [mar] {n_sea*0.01:,.0f} km2 a cota 0 exacta descartados " + f"({n_sea/max(spain.sum(),1)*100:.1f}% de la máscara)") + spain = (spain.astype(bool) & ~sea).astype(np.uint8) + else: + print(" [aviso] sin dem.npy: no se puede quitar el mar de la máscara") + np.save(C.INTERIM / "mask_spain.npy", spain) print(f" {spain.sum()*0.01:,.0f} km2 rasterizados " f"({spain.mean()*100:.1f}% de la malla)", flush=True) diff --git a/src/build_viewer.py b/src/build_viewer.py index 1bb44cc..657c242 100644 --- a/src/build_viewer.py +++ b/src/build_viewer.py @@ -28,6 +28,18 @@ def b64(path): return base64.b64encode(path.read_bytes()).decode("ascii") +def paises_options(): + """Opciones del selector de país (ver src/paises.py). + + Cada visor es un fichero de varios MB con su relieve y su terreno dentro, + así que el selector salta al visor del otro país en vez de cargar los dos a + la vez. Las rutas son relativas, para que out/ siga siendo movible. + """ + import paises + + return paises.opciones(C.OUT, C.CODE) + + def terrain_tiles(cands, dem): """Recorte de relieve alrededor de cada candidato, uint8 por tesela.""" buf = np.zeros((len(cands), N * N), dtype=np.uint8) @@ -74,6 +86,7 @@ def main(): "img_w": (C.WIDTH // F), "img_h": (C.HEIGHT // F), } + paises, n_paises = paises_options() vendor = C.ROOT / "vendor" html = TEMPLATE for token, val in ( @@ -86,6 +99,14 @@ def main(): ("__DATA__", json.dumps(payload, ensure_ascii=False, separators=(",", ":"))), ("__N_CAND__", f"{len(cands):,}".replace(",", ".")), ("__N__", str(N)), + ("__TITLE__", f"Visor de localizaciones · {C.REGION['name']}"), + # Las teselas del IGN, el catastro y las WMS del MITECO son españolas: + # fuera de España salen en blanco, así que ni se ofrecen. + ("__ES__", "true" if C.CODE == "es" else "false"), + ("__HAS_ORTHO__", "true" if C.REGION["ortho"] else "false"), + ("__PAISES__", paises), + # Con un solo país calculado el desplegable no tiene nada que ofrecer. + ("__N_PAISES__", str(n_paises)), ): html = html.replace(token, val) @@ -99,7 +120,7 @@ TEMPLATE = r""" -Visor de localizaciones · España +__TITLE__