Seguridad - Escapado de todo lo que viene de datos de terceros antes de ir al HTML, incluidos los tooltips de Leaflet. Los nombres de municipio salen de OpenStreetMap, que cualquiera puede editar: sin escapar, era una vía de inyección de código en el navegador de quien abriera el visor. Multi-país - El país pasa a ser un parámetro (REGION). Catálogo de 27 regiones entre Europa y el norte de África, declarando por país qué datos existen de verdad. - Una malla por país y no una continental: Europa entera a 100 m son mil millones de celdas y unos 73 GB solo en capas ráster. - Proyección equiárea propia para las regiones de fuera de Europa, donde EPSG:3035 deforma. Interfaz - Idiomas: castellano, inglés, francés y portugués, con selector persistente. - Modo Mapa: el país entero sobre satélite, topográfico u OSM. - Enlaces a mapas externos en la barra inferior, siempre visibles. - Botón de ver de cerca también en la cabecera de la ficha. - Fuera la distancia desde Madrid, que centralizaba algo de uso general. Portabilidad - install.sh e install.ps1 para Linux/macOS y Windows; requirements.txt. - Sin rutas absolutas en el código. Documentación - Ocho páginas en docs/ para la wiki: instalación, uso, añadir un país, metodología, seguridad, fuentes y publicación.
199 lines
7.5 KiB
Python
199 lines
7.5 KiB
Python
"""Combina las capas en dos rankings distintos y extrae candidatos.
|
|
|
|
1) AISLAMIENTO PURO: ¿qué punto de España está más lejos de cualquier rastro de
|
|
civilización? Es la pregunta interesante, pero su respuesta suele ser una
|
|
cara norte a 2.400 m sin forma de llegar.
|
|
|
|
2) APTITUD PARA RAVE: aislamiento acotado por lo que la fiesta necesita de
|
|
verdad. Acceso rodado, suelo llano, fuera de espacio protegido y militar, y
|
|
a ser posible en una hondonada, que encierra el sonido y tapa las luces.
|
|
|
|
Las dos restricciones tiran en sentidos opuestos: cuanto más aislado, peor
|
|
acceso. El sitio bueno no es el máximo de ninguna de las dos, sino el máximo de
|
|
una con la otra acotada, que es justo lo que hace el modelo.
|
|
"""
|
|
import pickle
|
|
import sys
|
|
|
|
import numpy as np
|
|
import rasterio
|
|
from shapely.geometry import Point
|
|
from shapely.strtree import STRtree
|
|
|
|
sys.path.insert(0, str(__file__.rsplit("/", 1)[0]))
|
|
import config as C
|
|
|
|
# --- parámetros del modelo (todo en metros) ---
|
|
MIN_D_BUILD = 1500 # umbral duro: por debajo, te oyen desde casa
|
|
MAX_D_ACCESS = 600 # más lejos de un vial, no metes el equipo
|
|
MIN_D_PROT = 500 # margen al borde del espacio protegido (zona periférica)
|
|
MAX_SLOPE = 8.0 # grados; por encima no se monta ni se aparca
|
|
SAT_BUILD = 5000 # a partir de aquí el aislamiento ya no suma más
|
|
SAT_PLACE = 8000
|
|
SAT_ROAD = 5000
|
|
|
|
N_CAND = 60
|
|
MIN_SEP = 15000 # separación mínima entre candidatos, para no devolver
|
|
# 60 celdas del mismo valle
|
|
|
|
|
|
|
|
def load(name):
|
|
return np.load(C.INTERIM / f"{name}.npy")
|
|
|
|
|
|
def norm(a, sat):
|
|
return np.clip(a / sat, 0, 1)
|
|
|
|
|
|
def main():
|
|
print("cargando capas…", flush=True)
|
|
d_build = load("d_build")
|
|
d_major = load("d_major")
|
|
d_minor = load("d_minor")
|
|
d_track = load("d_track")
|
|
d_place = load("d_place_any")
|
|
dens = load("dens_build_5km")
|
|
slope = load("slope_deg")
|
|
tpi = load("tpi_2km")
|
|
spain = load("mask_spain").astype(bool)
|
|
protected = load("mask_protected").astype(bool)
|
|
military = load("mask_military").astype(bool)
|
|
water = load("mask_water").astype(bool)
|
|
|
|
d_access = np.minimum(np.minimum(d_major, d_minor), d_track)
|
|
|
|
# ---------- 1) aislamiento puro ----------
|
|
# Media geométrica de las tres distancias que definen "lejos de todo".
|
|
# Geométrica y no aritmética: así un sitio a 8 km de una casa pero a 200 m
|
|
# de una nacional no puntúa; hay que estar lejos de las tres cosas a la vez.
|
|
iso = np.cbrt(np.maximum(d_build, 1) *
|
|
np.maximum(np.minimum(d_major, d_minor), 1) *
|
|
np.maximum(d_place, 1)).astype(np.float32)
|
|
iso[~spain] = np.nan
|
|
np.save(C.INTERIM / "iso_pure.npy", iso)
|
|
|
|
valid_iso = spain & np.isfinite(iso)
|
|
print(f"\n=== AISLAMIENTO PURO ===")
|
|
print(f" celdas válidas: {valid_iso.sum():,}")
|
|
for q in (50, 90, 99, 99.9):
|
|
print(f" p{q:<5} {np.nanpercentile(iso[valid_iso], q):7.0f} m")
|
|
print(f" máximo {np.nanmax(iso):7.0f} m")
|
|
print(f" distancia máxima a un edificio en España: "
|
|
f"{np.nanmax(np.where(spain, d_build, np.nan))/1000:.1f} km")
|
|
print(f" distancia máxima a una carretera: "
|
|
f"{np.nanmax(np.where(spain, np.minimum(d_major, d_minor), np.nan))/1000:.1f} km")
|
|
|
|
# ---------- 2) aptitud para rave ----------
|
|
d_prot = load("d_protected")
|
|
d_mil = load("d_military")
|
|
ok = (spain & ~protected & ~military & ~water &
|
|
(d_build >= MIN_D_BUILD) &
|
|
(d_access <= MAX_D_ACCESS) &
|
|
(slope <= MAX_SLOPE) &
|
|
(d_prot >= MIN_D_PROT) &
|
|
np.isfinite(slope))
|
|
print(f"\n=== APTITUD PARA RAVE ===")
|
|
print(f" celdas que pasan todas las restricciones: {ok.sum():,} "
|
|
f"({ok.sum()/spain.sum()*100:.2f}% del país, "
|
|
f"{ok.sum()*0.01:,.0f} km2)")
|
|
|
|
score = (0.34 * norm(d_build, SAT_BUILD) +
|
|
0.16 * norm(d_place, SAT_PLACE) +
|
|
0.20 * norm(np.minimum(d_major, d_minor), SAT_ROAD) +
|
|
0.12 * (1 - np.clip(dens / 200.0, 0, 1)) +
|
|
0.10 * np.clip(-tpi / 30.0, 0, 1) + # hondonada
|
|
0.08 * (1 - np.clip(slope / MAX_SLOPE, 0, 1)))
|
|
score = score.astype(np.float32)
|
|
score[~ok] = np.nan
|
|
np.save(C.INTERIM / "score_rave.npy", score)
|
|
|
|
with rasterio.open(
|
|
C.OUT / "score_rave.tif", "w", driver="GTiff",
|
|
height=C.HEIGHT, width=C.WIDTH, count=1, dtype="float32",
|
|
crs=C.CRS_GRID, transform=C.TRANSFORM, nodata=np.nan,
|
|
compress="deflate", tiled=True) as dst:
|
|
dst.write(score, 1)
|
|
print(f" ráster guardado en out/score_rave.tif")
|
|
|
|
# ---------- extracción de candidatos ----------
|
|
print(f"\nextrayendo {N_CAND} candidatos con {MIN_SEP/1000:.0f} km de separación…",
|
|
flush=True)
|
|
with open(C.INTERIM / "area_admin_municipality.pkl", "rb") as fh:
|
|
munis = pickle.load(fh)
|
|
with open(C.INTERIM / "area_admin_region.pkl", "rb") as fh:
|
|
regs = [r for r in pickle.load(fh) if r["geom"].area > 1e6]
|
|
muni_tree = STRtree([m["geom"] for m in munis])
|
|
reg_tree = STRtree([r["geom"] for r in regs])
|
|
|
|
work = np.where(np.isfinite(score), score, -1.0).astype(np.float32)
|
|
rad = int(MIN_SEP / C.RES)
|
|
rows = []
|
|
for i in range(N_CAND):
|
|
idx = int(np.argmax(work))
|
|
r, c = divmod(idx, C.WIDTH)
|
|
if work[r, c] <= 0:
|
|
break
|
|
x = C.X_MIN + (c + 0.5) * C.RES
|
|
y = C.Y_MAX - (r + 0.5) * C.RES
|
|
lon, lat = C.to_wgs(x, y)
|
|
pt = Point(x, y)
|
|
|
|
def lookup(tree, recs):
|
|
for j in tree.query(pt):
|
|
if recs[j]["geom"].contains(pt):
|
|
return recs[j]["name"]
|
|
return ""
|
|
|
|
rows.append({
|
|
"rank": i + 1,
|
|
"lat": round(float(lat), 5),
|
|
"lon": round(float(lon), 5),
|
|
"municipio": lookup(muni_tree, munis),
|
|
"comunidad": lookup(reg_tree, regs),
|
|
"score": round(float(score[r, c]), 4),
|
|
"d_edificio_m": int(d_build[r, c]),
|
|
"d_carretera_m": int(min(d_major[r, c], d_minor[r, c])),
|
|
"d_acceso_m": int(d_access[r, c]),
|
|
"d_nucleo_m": int(d_place[r, c]),
|
|
"d_protegido_m": int(d_prot[r, c]),
|
|
"d_militar_m": int(d_mil[r, c]),
|
|
"cota_m": int(np.nan_to_num(load_dem_at(r, c))),
|
|
"pendiente_deg": round(float(slope[r, c]), 1),
|
|
"tpi_m": round(float(tpi[r, c]), 1),
|
|
"edif_5km": int(dens[r, c]),
|
|
})
|
|
|
|
# suprimimos un disco alrededor para el siguiente candidato
|
|
r0, r1 = max(0, r - rad), min(C.HEIGHT, r + rad + 1)
|
|
c0, c1 = max(0, c - rad), min(C.WIDTH, c + rad + 1)
|
|
yy, xx = np.ogrid[r0:r1, c0:c1]
|
|
work[r0:r1, c0:c1][((yy - r) ** 2 + (xx - c) ** 2) <= rad * rad] = -1.0
|
|
|
|
import csv
|
|
cols = list(rows[0].keys())
|
|
with open(C.OUT / "candidatos.csv", "w", newline="") as fh:
|
|
w = csv.DictWriter(fh, fieldnames=cols)
|
|
w.writeheader()
|
|
w.writerows(rows)
|
|
print(f" {len(rows)} candidatos -> out/candidatos.csv")
|
|
for r_ in rows[:15]:
|
|
print(f" #{r_['rank']:<3} {r_['lat']:8.4f},{r_['lon']:9.4f} "
|
|
f"{r_['municipio'][:26]:26s} {r_['comunidad'][:18]:18s} "
|
|
f"casa={r_['d_edificio_m']:>6}m ctra={r_['d_carretera_m']:>5}m "
|
|
f"acc={r_['d_acceso_m']:>4}m")
|
|
|
|
|
|
_dem = None
|
|
|
|
|
|
def load_dem_at(r, c):
|
|
global _dem
|
|
if _dem is None:
|
|
_dem = np.load(C.INTERIM / "dem.npy", mmap_mode="r")
|
|
v = _dem[r, c]
|
|
return 0.0 if not np.isfinite(v) else float(v)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|