RAVE SCOUT: análisis de aislamiento de España y visor de localizaciones
Barrido de los 498.528 km² de la península y Baleares sobre malla de 100 m (EPSG:3035) para localizar sitios alejados de todo con acceso rodado. - 12,5 M de edificios y 1,4 M de km de viales de OSM, incluidos los países vecinos para no inflar el aislamiento en la franja fronteriza - Espacios protegidos oficiales (Red Natura 2000 + designación nacional) vía la Agencia Europea de Medio Ambiente: cubren el 29 % del país frente al 15 % que tenía OSM - Modelo acústico con propagación real; es el filtro que de verdad corta, y deja 505 km² en toda España - 355 localizaciones valoradas de 1 a 5 estrellas en seis criterios - Visor autónomo con relieve, ortofoto PNOA por localización, relieve 3D y mapa deslizante con satélite, catastro y espacios protegidos
This commit is contained in:
commit
d4b3b803e0
394 changed files with 7393 additions and 0 deletions
133
src/render_map.py
Normal file
133
src/render_map.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""Renderiza el mapa de aislamiento a PNG (versión clara y oscura).
|
||||
|
||||
Se dibuja solo España porque el análisis solo es válido dentro de España: los
|
||||
datos de los vecinos entran para calcular distancias correctas en la frontera,
|
||||
pero sus celdas no se puntúan ni se muestran.
|
||||
|
||||
Dos versiones de color porque un PNG no se adapta al tema de quien lo mira, y
|
||||
el informe sí.
|
||||
"""
|
||||
import csv
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
sys.path.insert(0, str(__file__.rsplit("/", 1)[0]))
|
||||
import config as C
|
||||
|
||||
F = 8 # factor de reducción -> ~1645 px de ancho
|
||||
|
||||
# Rampa secuencial azul (100 -> 700), magnitud continua, un solo tono.
|
||||
RAMP = ["#cde2fb", "#b7d3f6", "#9ec5f4", "#86b6ef", "#6da7ec", "#5598e7",
|
||||
"#3987e5", "#2a78d6", "#256abf", "#1c5cab", "#184f95", "#104281",
|
||||
"#0d366b"]
|
||||
|
||||
# El fondo de cada versión es el mismo que el de la página, para que el mapa no
|
||||
# lleve marco y se funda con el papel.
|
||||
#
|
||||
# En claro la rampa va de claro a oscuro: más aislado = azul más denso, que
|
||||
# destaca sobre papel claro. En oscuro se invierte. No es un volteo automático
|
||||
# del gradiente sino la misma rampa recorrida al revés, porque lo que tiene que
|
||||
# conservarse es el significado: lo aislado siempre es lo que más resalta. Con
|
||||
# la rampa sin invertir, los vacíos —justo lo que buscamos— se disolverían en
|
||||
# el fondo negro.
|
||||
THEMES = {
|
||||
"light": dict(surface="#e9edf2", ring="#ffffff", label="#0f1620",
|
||||
halo="#ffffff", reverse=False),
|
||||
"dark": dict(surface="#0b0f14", ring="#0b0f14", label="#e8edf3",
|
||||
halo="#0b0f14", reverse=True),
|
||||
}
|
||||
MARKER = {"light": "#d4541f", "dark": "#f08a5a"}
|
||||
|
||||
|
||||
def hex2rgb(h):
|
||||
return tuple(int(h[i:i + 2], 16) for i in (1, 3, 5))
|
||||
|
||||
|
||||
def ramp_lut(reverse=False):
|
||||
cols = np.array([hex2rgb(h) for h in (RAMP[::-1] if reverse else RAMP)],
|
||||
dtype=np.float32)
|
||||
xs = np.linspace(0, 255, len(cols))
|
||||
lut = np.zeros((256, 3), dtype=np.uint8)
|
||||
for ch in range(3):
|
||||
lut[:, ch] = np.interp(np.arange(256), xs, cols[:, ch]).astype(np.uint8)
|
||||
return lut
|
||||
|
||||
|
||||
def block_reduce(a, f, how="mean"):
|
||||
h = (a.shape[0] // f) * f
|
||||
w = (a.shape[1] // f) * f
|
||||
a = a[:h, :w].reshape(h // f, f, w // f, f)
|
||||
return a.mean(axis=(1, 3)) if how == "mean" else a.max(axis=(1, 3))
|
||||
|
||||
|
||||
def get_font(size):
|
||||
for p in ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"):
|
||||
try:
|
||||
return ImageFont.truetype(p, size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def main():
|
||||
iso = np.load(C.INTERIM / "iso_pure.npy")
|
||||
spain = np.load(C.INTERIM / "mask_spain.npy").astype(bool)
|
||||
|
||||
vals = iso[spain & np.isfinite(iso)]
|
||||
lo, hi = np.percentile(vals, 2), np.percentile(vals, 99.5)
|
||||
print(f"escala del mapa: {lo:.0f} .. {hi:.0f} m")
|
||||
|
||||
filled = np.where(np.isfinite(iso), iso, 0.0)
|
||||
small = block_reduce(filled, F, "mean")
|
||||
land = block_reduce(spain.astype(np.float32), F, "mean") > 0.35
|
||||
|
||||
norm = np.clip((small - lo) / (hi - lo), 0, 1)
|
||||
idx = (norm * 255).astype(np.uint8)
|
||||
|
||||
with open(C.OUT / "candidatos_estrellas.csv") as fh:
|
||||
cands = [c for c in csv.DictReader(fh) if int(c["estrellas"]) >= 3]
|
||||
cands.sort(key=lambda c: -float(c["score"]))
|
||||
for i, c in enumerate(cands, 1):
|
||||
c["rank"] = i
|
||||
|
||||
for theme, col in THEMES.items():
|
||||
rgb = ramp_lut(col["reverse"])[idx]
|
||||
img_arr = np.empty(rgb.shape, dtype=np.uint8)
|
||||
img_arr[:] = hex2rgb(col["surface"])
|
||||
img_arr[land] = rgb[land]
|
||||
img = Image.fromarray(img_arr, "RGB")
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
f_lbl = get_font(15)
|
||||
for cd in cands:
|
||||
x, y = C.to_grid(float(cd["lon"]), float(cd["lat"]))
|
||||
px = (x - C.X_MIN) / C.RES / F
|
||||
py = (C.Y_MAX - y) / C.RES / F
|
||||
rank = int(cd["rank"])
|
||||
r = 6 if rank <= 10 else 4
|
||||
# anillo del color de la superficie: separa el punto del fondo
|
||||
d.ellipse([px - r - 2, py - r - 2, px + r + 2, py + r + 2],
|
||||
fill=hex2rgb(col["ring"]))
|
||||
d.ellipse([px - r, py - r, px + r, py + r], fill=hex2rgb(MARKER[theme]))
|
||||
if rank <= 10:
|
||||
t = str(rank)
|
||||
tx, ty = px + r + 5, py - 9
|
||||
for ox, oy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
|
||||
d.text((tx + ox, ty + oy), t, font=f_lbl,
|
||||
fill=hex2rgb(col["halo"]))
|
||||
d.text((tx, ty), t, font=f_lbl, fill=hex2rgb(col["label"]))
|
||||
|
||||
out = C.OUT / f"mapa_{theme}.png"
|
||||
# Paleta de 200 colores: la rampa es continua pero el ojo no distingue
|
||||
# más pasos, y el PNG baja de ~1,5 MB a ~400 KB, que importa porque va
|
||||
# incrustado en el informe.
|
||||
img.convert("P", palette=Image.ADAPTIVE, colors=200).save(out, optimize=True)
|
||||
print(f" {out.name} {img.size[0]}x{img.size[1]} "
|
||||
f"{out.stat().st_size/1024:.0f} KB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue