Multi-país, idiomas, seguridad, instaladores y documentación
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.
This commit is contained in:
parent
d4b3b803e0
commit
f890dd9448
22 changed files with 1288 additions and 280 deletions
|
|
@ -59,7 +59,7 @@ def main():
|
|||
"c_sonido", "c_soledad", "c_agua", "c_arbolado", "c_acceso", "c_clima",
|
||||
"db_en_casa", "d_edificio", "d_carretera", "d_pista", "d_agua",
|
||||
"d_protegido", "protegido_cerca", "km_protegido", "arbolado_pct",
|
||||
"roca_pct", "pendiente", "tpi", "cota", "tmax", "tmin", "km_madrid"]
|
||||
"roca_pct", "pendiente", "tpi", "cota", "tmax", "tmin"]
|
||||
rows = []
|
||||
for i, cd in enumerate(cands):
|
||||
rec = [cd[k] for k in keys]
|
||||
|
|
@ -121,7 +121,10 @@ button,input,select{font:inherit;color:inherit}
|
|||
border-right:1px solid var(--line);display:flex;flex-direction:column;height:100vh}
|
||||
#side header{padding:16px 18px 12px;border-bottom:1px solid var(--line)}
|
||||
#side h1{margin:0;font-size:15px;letter-spacing:.02em}
|
||||
#side .sub{color:var(--muted);font-size:11.5px;margin-top:3px}
|
||||
#side .sub{color:var(--muted);font-size:11.5px;margin-top:3px;display:flex;
|
||||
justify-content:space-between;align-items:center;gap:8px}
|
||||
#lang{width:auto;padding:2px 5px;font-size:11px;background:var(--panel2);
|
||||
border:1px solid var(--line);border-radius:4px;color:var(--ink2);cursor:pointer}
|
||||
.filters{padding:14px 18px;display:flex;flex-direction:column;gap:13px;
|
||||
border-bottom:1px solid var(--line)}
|
||||
.f-row{display:flex;flex-direction:column;gap:6px}
|
||||
|
|
@ -161,12 +164,30 @@ select{width:100%;background:var(--panel2);border:1px solid var(--line);
|
|||
border-radius:5px;padding:6px 11px;font-size:12px;cursor:pointer;color:var(--ink2)}
|
||||
.toolbar button[aria-pressed=true]{background:var(--accent);border-color:var(--accent);
|
||||
color:#08131f;font-weight:600}
|
||||
.legend{position:absolute;bottom:12px;left:12px;background:rgba(21,26,33,.93);
|
||||
border:1px solid var(--line);border-radius:5px;padding:9px 12px;font-size:11px;
|
||||
display:flex;gap:13px;align-items:center;z-index:5}
|
||||
/* Barra inferior: leyenda a la izquierda y, a su derecha, los enlaces a mapas
|
||||
externos de la localización elegida. Antes había que abrir la ficha y bajar
|
||||
hasta el final para encontrarlos. */
|
||||
.bottombar{position:absolute;bottom:12px;left:12px;right:12px;z-index:5;
|
||||
display:flex;gap:10px;align-items:center;justify-content:space-between;
|
||||
flex-wrap:wrap;pointer-events:none}
|
||||
.bottombar>*{pointer-events:auto}
|
||||
.legend{background:rgba(21,26,33,.93);border:1px solid var(--line);
|
||||
border-radius:5px;padding:9px 12px;font-size:11px;display:flex;gap:13px;
|
||||
align-items:center}
|
||||
.legend i{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:4px}
|
||||
.hint{position:absolute;bottom:12px;right:12px;color:var(--muted);font-size:11px;
|
||||
background:rgba(21,26,33,.9);padding:5px 9px;border-radius:4px;z-index:5}
|
||||
.maplinks{background:rgba(21,26,33,.93);border:1px solid var(--line);
|
||||
border-radius:5px;padding:5px 6px;display:flex;gap:4px;align-items:center;
|
||||
flex-wrap:wrap;max-width:100%}
|
||||
.maplinks a{display:flex;flex-direction:column;gap:0;padding:5px 9px;
|
||||
border-radius:4px;text-decoration:none;line-height:1.2;white-space:nowrap}
|
||||
.maplinks a b{color:var(--accent);font-size:11.5px;font-weight:600}
|
||||
.maplinks a span{color:var(--muted);font-size:9.5px}
|
||||
.maplinks a:hover{background:#1d2836}
|
||||
.ml-sel{padding:5px 10px 5px 6px;border-right:1px solid var(--line);
|
||||
margin-right:3px;font-size:11px;color:var(--ink2);white-space:nowrap;
|
||||
display:flex;flex-direction:column;line-height:1.25}
|
||||
.ml-sel b{font-size:11.5px;color:var(--ink)}
|
||||
.ml-hint{color:var(--muted);font-size:11px;padding:6px 9px}
|
||||
|
||||
/* ---------- mapa de cerca (Leaflet) ---------- */
|
||||
#leaf{position:absolute;top:0;left:0;width:100%;height:100%;display:none;
|
||||
|
|
@ -174,6 +195,8 @@ select{width:100%;background:var(--panel2);border:1px solid var(--line);
|
|||
body.cerca #map{display:none}
|
||||
body.cerca #leaf{display:block}
|
||||
body.cerca .legend{display:none}
|
||||
/* los conmutadores del relieve solo tienen sentido en su propio modo */
|
||||
body.cerca .only-esp{display:none}
|
||||
#layers{position:absolute;top:52px;left:12px;z-index:6;width:186px;
|
||||
background:rgba(21,26,33,.95);border:1px solid var(--line);border-radius:6px;
|
||||
padding:9px 11px;display:none;flex-direction:column;gap:8px;font-size:12px}
|
||||
|
|
@ -211,6 +234,7 @@ body.cerca.sinsel #nosel{display:block}
|
|||
.d-head h2{margin:0 26px 0 0;font-size:16px}
|
||||
.d-head .loc{color:var(--muted);font-size:12px;margin-top:2px}
|
||||
.d-head .stars{font-size:16px;margin-top:7px;letter-spacing:1px}
|
||||
.d-head .btn-cerca{margin-top:11px}
|
||||
#close{position:absolute;top:13px;right:14px;background:none;border:0;
|
||||
color:var(--muted);font-size:20px;cursor:pointer;line-height:1;padding:2px 5px}
|
||||
#close:hover{color:var(--ink)}
|
||||
|
|
@ -289,13 +313,16 @@ body.cerca.sinsel #nosel{display:block}
|
|||
|
||||
<aside id="side">
|
||||
<header>
|
||||
<h1>Localizaciones · España</h1>
|
||||
<div class="sub">__N_CAND__ localizaciones · malla de 100 m</div>
|
||||
<h1 data-i18n="title">Localizaciones</h1>
|
||||
<div class="sub"><span id="subtxt"></span>
|
||||
<select id="lang" aria-label="Idioma"><option value="es">ES</option>
|
||||
<option value="en">EN</option><option value="fr">FR</option>
|
||||
<option value="pt">PT</option></select></div>
|
||||
</header>
|
||||
|
||||
<div class="filters">
|
||||
<div class="f-row">
|
||||
<label>Estrellas mínimas</label>
|
||||
<label data-i18n="minstars">Estrellas mínimas</label>
|
||||
<div class="starbtns" id="starf">
|
||||
<button data-s="1" aria-pressed="true">1+</button>
|
||||
<button data-s="2" aria-pressed="false">2+</button>
|
||||
|
|
@ -305,27 +332,23 @@ body.cerca.sinsel #nosel{display:block}
|
|||
</div>
|
||||
</div>
|
||||
<div class="f-row">
|
||||
<label for="reg">Comunidad</label>
|
||||
<select id="reg"><option value="">todas</option></select>
|
||||
<label for="reg" data-i18n="region">Comunidad</label>
|
||||
<select id="reg"><option value="" data-i18n="all">todas</option></select>
|
||||
</div>
|
||||
<div class="f-row">
|
||||
<label for="fmad">Distancia desde Madrid <b id="vmad">sin límite</b></label>
|
||||
<input type="range" id="fmad" min="25" max="700" step="25" value="700">
|
||||
</div>
|
||||
<div class="f-row">
|
||||
<label for="fson">Aislamiento sonoro mínimo <b id="vson">0</b></label>
|
||||
<label for="fson"><span data-i18n="fsound">Aislamiento sonoro mínimo</span> <b id="vson">0</b></label>
|
||||
<input type="range" id="fson" min="0" max="100" step="5" value="0">
|
||||
</div>
|
||||
<div class="f-row">
|
||||
<label for="fagu">Agua cerca mínimo <b id="vagu">0</b></label>
|
||||
<label for="fagu"><span data-i18n="fwater">Agua cerca mínimo</span> <b id="vagu">0</b></label>
|
||||
<input type="range" id="fagu" min="0" max="100" step="5" value="0">
|
||||
</div>
|
||||
<div class="f-row">
|
||||
<label for="farb">Arbolado mínimo <b id="varb">0</b></label>
|
||||
<label for="farb"><span data-i18n="ftrees">Arbolado mínimo</span> <b id="varb">0</b></label>
|
||||
<input type="range" id="farb" min="0" max="100" step="5" value="0">
|
||||
</div>
|
||||
<div class="f-row">
|
||||
<label for="facc">Acceso mínimo <b id="vacc">0</b></label>
|
||||
<label for="facc"><span data-i18n="faccess">Acceso mínimo</span> <b id="vacc">0</b></label>
|
||||
<input type="range" id="facc" min="0" max="100" step="5" value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -337,35 +360,38 @@ body.cerca.sinsel #nosel{display:block}
|
|||
<div id="main">
|
||||
<canvas id="map"></canvas>
|
||||
<div id="leaf"></div>
|
||||
<div id="nosel">Elige una localización en la lista<br>para verla de cerca</div>
|
||||
<div id="nosel" data-i18n="nosel">Elige una localización en la lista para verla de cerca</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button id="bEsp" aria-pressed="true">España</button>
|
||||
<button id="bCerca" aria-pressed="false">Ver de cerca</button>
|
||||
<button id="bEsp" aria-pressed="true" data-i18n="mrelief">Relieve</button>
|
||||
<button id="bMapa" aria-pressed="false" data-i18n="mmap">Mapa</button>
|
||||
<button id="bCerca" aria-pressed="false" data-i18n="mclose">Ver de cerca</button>
|
||||
<span style="width:9px"></span>
|
||||
<button id="bRel" aria-pressed="true">Relieve</button>
|
||||
<button id="bIso" aria-pressed="false">Aislamiento</button>
|
||||
<button id="bProt" aria-pressed="true">Protegidos</button>
|
||||
<button id="bFit">Encajar</button>
|
||||
<button id="bRel" aria-pressed="true" class="only-esp" data-i18n="lshade">Sombreado</button>
|
||||
<button id="bIso" aria-pressed="false" class="only-esp" data-i18n="liso">Aislamiento</button>
|
||||
<button id="bProt" aria-pressed="true" class="only-esp" data-i18n="lprot">Protegidos</button>
|
||||
<button id="bFit" class="only-esp" data-i18n="lfit">Encajar</button>
|
||||
</div>
|
||||
|
||||
<div id="layers">
|
||||
<div class="grp">
|
||||
<span class="ttl">Fondo</span>
|
||||
<label><input type="radio" name="base" value="pnoa" checked> Satélite (PNOA)</label>
|
||||
<label><input type="radio" name="base" value="mtn"> Topográfico (MTN)</label>
|
||||
<label><input type="radio" name="base" value="osm"> OpenStreetMap</label>
|
||||
<span class="ttl" data-i18n="base">Fondo</span>
|
||||
<label><input type="radio" name="base" value="pnoa" checked> <span data-i18n="bsat">Satélite (PNOA)</span></label>
|
||||
<label><input type="radio" name="base" value="mtn"> <span data-i18n="btopo">Topográfico (MTN)</span></label>
|
||||
<label><input type="radio" name="base" value="osm"> <span data-i18n="bosm">OpenStreetMap</span></label>
|
||||
</div>
|
||||
<div class="grp">
|
||||
<span class="ttl">Encima</span>
|
||||
<label><input type="checkbox" id="ovCat"> Parcelas del catastro</label>
|
||||
<label><input type="checkbox" id="ovN2k" checked> Red Natura 2000</label>
|
||||
<label><input type="checkbox" id="ovEnp" checked> Espacios protegidos</label>
|
||||
<label><input type="checkbox" id="ovPts" checked> Otras localizaciones</label>
|
||||
<span class="ttl" data-i18n="over">Encima</span>
|
||||
<label><input type="checkbox" id="ovCat"> <span data-i18n="ocad">Parcelas del catastro</span></label>
|
||||
<label><input type="checkbox" id="ovN2k" checked> <span data-i18n="on2k">Red Natura 2000</span></label>
|
||||
<label><input type="checkbox" id="ovEnp" checked> <span data-i18n="oenp">Espacios protegidos</span></label>
|
||||
<label><input type="checkbox" id="ovPts" checked> <span data-i18n="opts">Otras localizaciones</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend" id="legend"></div>
|
||||
<div class="hint">arrastra para mover · rueda para zoom · clic en un punto</div>
|
||||
<div class="bottombar">
|
||||
<div class="legend" id="legend"></div>
|
||||
<div class="maplinks" id="mapLinks"></div>
|
||||
</div>
|
||||
|
||||
<aside id="detail">
|
||||
<div class="d-head">
|
||||
|
|
@ -373,10 +399,11 @@ body.cerca.sinsel #nosel{display:block}
|
|||
<h2 id="dName">—</h2>
|
||||
<div class="loc" id="dLoc"></div>
|
||||
<div class="stars" id="dStars"></div>
|
||||
<button class="btn-cerca" id="bVerCercaTop" data-i18n="seeclose">Ver de cerca en el mapa</button>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<button id="tabSat" aria-pressed="true">Vista aérea</button>
|
||||
<button id="tab3d" aria-pressed="false">Relieve 3D</button>
|
||||
<button id="tabSat" aria-pressed="true" data-i18n="tabaer">Vista aérea</button>
|
||||
<button id="tab3d" aria-pressed="false" data-i18n="tab3d">Relieve 3D</button>
|
||||
</div>
|
||||
<img id="ortho" alt="Vista aérea de la localización" src="">
|
||||
<canvas id="tcanvas"></canvas>
|
||||
|
|
@ -386,10 +413,8 @@ body.cerca.sinsel #nosel{display:block}
|
|||
<div class="facts" id="dFacts"></div>
|
||||
<div id="dWarn"></div>
|
||||
<div style="padding:0 18px">
|
||||
<button class="btn-cerca" id="bVerCerca">Ver de cerca en el mapa</button>
|
||||
<button class="btn-cerca" id="bVerCerca" data-i18n="seeclose">Ver de cerca en el mapa</button>
|
||||
</div>
|
||||
<div class="links-ttl">Abrir en otros mapas</div>
|
||||
<div class="links" id="dLinks"></div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
|
|
@ -397,6 +422,7 @@ body.cerca.sinsel #nosel{display:block}
|
|||
<script>
|
||||
const DATA = __DATA__;
|
||||
const NT = __N__;
|
||||
const NCAND = "__N_CAND__";
|
||||
|
||||
// ---- desempaquetado ----
|
||||
const K = {}; DATA.keys.forEach((k,i)=>K[k]=i);
|
||||
|
|
@ -406,9 +432,109 @@ const TER = new Uint8Array(terrainRaw.length);
|
|||
for(let i=0;i<terrainRaw.length;i++) TER[i]=terrainRaw.charCodeAt(i);
|
||||
const TSIZE = NT*NT;
|
||||
|
||||
// Los nombres vienen de OpenStreetMap y de los ficheros oficiales, o sea de
|
||||
// terceros: cualquiera puede editar el nombre de un municipio en OSM. Todo lo
|
||||
// que salga de los datos y acabe en innerHTML pasa por aquí antes.
|
||||
const ESCMAP={'&':'&','<':'<','>':'>','"':'"',"'":'''};
|
||||
const esc = s => String(s==null?'':s).replace(/[&<>"']/g, ch => ESCMAP[ch]);
|
||||
|
||||
const SCOL = ["","#8a7a4a","#a8923f","#c9a92f","#e6c235","#ffd95e"];
|
||||
const stars = n => "★".repeat(n)+"☆".repeat(5-n);
|
||||
|
||||
|
||||
// ---- idiomas ----
|
||||
// El proyecto sirve para cualquier país del catálogo, así que la interfaz no
|
||||
// puede estar solo en castellano. Las cadenas de datos (municipios, comunidades)
|
||||
// vienen del país y no se traducen.
|
||||
const I18N = {
|
||||
title:{es:"Localizaciones",en:"Locations",fr:"Emplacements",pt:"Localizações"},
|
||||
minstars:{es:"Estrellas mínimas",en:"Minimum stars",fr:"Étoiles minimum",pt:"Estrelas mínimas"},
|
||||
region:{es:"Comunidad",en:"Region",fr:"Région",pt:"Região"},
|
||||
all:{es:"todas",en:"all",fr:"toutes",pt:"todas"},
|
||||
fsound:{es:"Aislamiento sonoro mínimo",en:"Min. sound isolation",fr:"Isolement sonore min.",pt:"Isolamento sonoro mín."},
|
||||
fwater:{es:"Agua cerca mínimo",en:"Min. water nearby",fr:"Eau à proximité min.",pt:"Água perto mín."},
|
||||
ftrees:{es:"Arbolado mínimo",en:"Min. tree cover",fr:"Couvert arboré min.",pt:"Arvoredo mínimo"},
|
||||
faccess:{es:"Acceso mínimo",en:"Min. access",fr:"Accès minimum",pt:"Acesso mínimo"},
|
||||
mrelief:{es:"Relieve",en:"Relief",fr:"Relief",pt:"Relevo"},
|
||||
mmap:{es:"Mapa",en:"Map",fr:"Carte",pt:"Mapa"},
|
||||
mclose:{es:"Ver de cerca",en:"Zoom in",fr:"Vue rapprochée",pt:"Ver de perto"},
|
||||
lshade:{es:"Sombreado",en:"Hillshade",fr:"Ombrage",pt:"Sombreado"},
|
||||
liso:{es:"Aislamiento",en:"Isolation",fr:"Isolement",pt:"Isolamento"},
|
||||
lprot:{es:"Protegidos",en:"Protected",fr:"Protégés",pt:"Protegidos"},
|
||||
lfit:{es:"Encajar",en:"Fit",fr:"Ajuster",pt:"Ajustar"},
|
||||
base:{es:"Fondo",en:"Base map",fr:"Fond",pt:"Fundo"},
|
||||
over:{es:"Encima",en:"Overlays",fr:"Calques",pt:"Camadas"},
|
||||
bsat:{es:"Satélite (PNOA)",en:"Satellite (PNOA)",fr:"Satellite (PNOA)",pt:"Satélite (PNOA)"},
|
||||
btopo:{es:"Topográfico (MTN)",en:"Topographic (MTN)",fr:"Topographique (MTN)",pt:"Topográfico (MTN)"},
|
||||
bosm:{es:"OpenStreetMap",en:"OpenStreetMap",fr:"OpenStreetMap",pt:"OpenStreetMap"},
|
||||
ocad:{es:"Parcelas del catastro",en:"Cadastral parcels",fr:"Parcelles cadastrales",pt:"Parcelas cadastrais"},
|
||||
on2k:{es:"Red Natura 2000",en:"Natura 2000 network",fr:"Réseau Natura 2000",pt:"Rede Natura 2000"},
|
||||
oenp:{es:"Espacios protegidos",en:"Protected areas",fr:"Espaces protégés",pt:"Áreas protegidas"},
|
||||
opts:{es:"Otras localizaciones",en:"Other locations",fr:"Autres emplacements",pt:"Outras localizações"},
|
||||
tabaer:{es:"Vista aérea",en:"Aerial view",fr:"Vue aérienne",pt:"Vista aérea"},
|
||||
tab3d:{es:"Relieve 3D",en:"3D relief",fr:"Relief 3D",pt:"Relevo 3D"},
|
||||
seeclose:{es:"Ver de cerca en el mapa",en:"Zoom in on the map",fr:"Voir de près sur la carte",pt:"Ver de perto no mapa"},
|
||||
nosel:{es:"Elige una localización en la lista para verla de cerca",en:"Pick a location from the list to zoom in",fr:"Choisissez un emplacement dans la liste",pt:"Escolhe uma localização na lista"},
|
||||
sound:{es:"Sonido",en:"Sound",fr:"Son",pt:"Som"},
|
||||
solitude:{es:"Soledad",en:"Solitude",fr:"Solitude",pt:"Solidão"},
|
||||
access:{es:"Acceso",en:"Access",fr:"Accès",pt:"Acesso"},
|
||||
water:{es:"Agua",en:"Water",fr:"Eau",pt:"Água"},
|
||||
trees:{es:"Arbolado",en:"Tree cover",fr:"Couvert arboré",pt:"Arvoredo"},
|
||||
climate:{es:"Clima",en:"Climate",fr:"Climat",pt:"Clima"},
|
||||
fdb:{es:"Nivel en la casa más cercana",en:"Level at nearest dwelling",fr:"Niveau à la maison la plus proche",pt:"Nível na casa mais próxima"},
|
||||
fbuild:{es:"Edificio más cercano",en:"Nearest building",fr:"Bâtiment le plus proche",pt:"Edifício mais próximo"},
|
||||
froad:{es:"Carretera más cercana",en:"Nearest road",fr:"Route la plus proche",pt:"Estrada mais próxima"},
|
||||
ftrack:{es:"Pista de acceso",en:"Access track",fr:"Piste d'accès",pt:"Pista de acesso"},
|
||||
fwat:{es:"Agua más cercana",en:"Nearest water",fr:"Eau la plus proche",pt:"Água mais próxima"},
|
||||
ftree:{es:"Arbolado alrededor",en:"Tree cover around",fr:"Couvert arboré autour",pt:"Arvoredo à volta"},
|
||||
frock:{es:"Roca desnuda",en:"Bare rock",fr:"Roche nue",pt:"Rocha nua"},
|
||||
fslope:{es:"Pendiente",en:"Slope",fr:"Pente",pt:"Declive"},
|
||||
ftpi:{es:"Hondonada (TPI)",en:"Hollow (TPI)",fr:"Cuvette (TPI)",pt:"Depressão (TPI)"},
|
||||
ftmax:{es:"Máx. mes más cálido",en:"Max. warmest month",fr:"Max. mois le plus chaud",pt:"Máx. mês mais quente"},
|
||||
ftmin:{es:"Mín. mes más frío",en:"Min. coldest month",fr:"Min. mois le plus froid",pt:"Mín. mês mais frio"},
|
||||
far:{es:"lejos",en:"far",fr:"loin",pt:"longe"},
|
||||
locs:{es:"localizaciones",en:"locations",fr:"emplacements",pt:"localizações"},
|
||||
loc1:{es:"localización",en:"location",fr:"emplacement",pt:"localização"},
|
||||
more:{es:"más (afina los filtros)",en:"more (narrow the filters)",fr:"de plus (affinez les filtres)",pt:"mais (afina os filtros)"},
|
||||
pick:{es:"elige una localización para abrirla en otros mapas",en:"pick a location to open it in other maps",fr:"choisissez un emplacement pour l'ouvrir ailleurs",pt:"escolhe uma localização para abrir noutros mapas"},
|
||||
lsat:{es:"Satélite",en:"Satellite",fr:"Satellite",pt:"Satélite"},
|
||||
lsatd:{es:"vista aérea",en:"aerial view",fr:"vue aérienne",pt:"vista aérea"},
|
||||
lsvd:{es:"entrada desde el asfalto",en:"the turn-off from the road",fr:"l'entrée depuis la route",pt:"entrada a partir do asfalto"},
|
||||
lmpd:{es:"fotos de pista",en:"track-level photos",fr:"photos de piste",pt:"fotos de pista"},
|
||||
losmd:{es:"caminos y topónimos",en:"tracks and place names",fr:"chemins et toponymes",pt:"caminhos e topónimos"},
|
||||
lcad:{es:"Catastro",en:"Cadastre",fr:"Cadastre",pt:"Cadastro"},
|
||||
lcadd:{es:"de quién es la finca",en:"who owns the land",fr:"à qui appartient le terrain",pt:"de quem é o terreno"},
|
||||
lgo:{es:"Cómo llegar",en:"Directions",fr:"Itinéraire",pt:"Como chegar"},
|
||||
lgod:{es:"ruta desde donde estés",en:"route from where you are",fr:"itinéraire depuis chez vous",pt:"rota de onde estiveres"},
|
||||
aerhint:{es:"ortofoto · 640 m de lado · pulsa para ampliar",en:"orthophoto · 640 m across · tap to zoom",fr:"orthophoto · 640 m de côté · cliquez pour agrandir",pt:"ortofoto · 640 m de lado · toca para ampliar"},
|
||||
relhint:{es:"relieve 9,6 × 9,6 km · arrastra para girar",en:"relief 9.6 × 9.6 km · drag to rotate",fr:"relief 9,6 × 9,6 km · faites glisser pour tourner",pt:"relevo 9,6 × 9,6 km · arrasta para girar"},
|
||||
protnear:{es:"Espacio protegido a",en:"Protected area at",fr:"Espace protégé à",pt:"Área protegida a"},
|
||||
protwarn:{es:"Está fuera, pero comprueba el límite exacto en el visor oficial antes de ir.",en:"It is outside, but check the exact boundary on the official viewer before going.",fr:"Il est en dehors, mais vérifiez la limite exacte avant d'y aller.",pt:"Está fora, mas confirma o limite exato antes de ir."},
|
||||
grid:{es:"malla de 100 m",en:"100 m grid",fr:"grille de 100 m",pt:"malha de 100 m"},
|
||||
legprot:{es:"espacio protegido",en:"protected area",fr:"espace protégé",pt:"área protegida"},
|
||||
noprot:{es:"Sin espacio protegido cartografiado en 12 km. Aun así, comprueba la propiedad del terreno: casi todo esto es finca privada.",en:"No mapped protected area within 12 km. Even so, check land ownership: nearly all of this is private.",fr:"Aucun espace protégé cartographié à moins de 12 km. Vérifiez tout de même la propriété du terrain.",pt:"Sem área protegida cartografada em 12 km. Ainda assim, confirma a propriedade do terreno."},
|
||||
};
|
||||
let LANG = localStorage.getItem('rs_lang') ||
|
||||
(navigator.language||'es').slice(0,2);
|
||||
if(!['es','en','fr','pt'].includes(LANG)) LANG='es';
|
||||
const t = k => (I18N[k] && (I18N[k][LANG] || I18N[k].es)) || k;
|
||||
|
||||
function applyLang(){
|
||||
document.documentElement.lang = LANG;
|
||||
document.querySelectorAll('[data-i18n]').forEach(e=>{
|
||||
e.textContent = t(e.dataset.i18n);
|
||||
});
|
||||
el('lang').value = LANG;
|
||||
el('subtxt').textContent = NCAND + ' ' + t('locs') + ' · ' + t('grid');
|
||||
el('legend').innerHTML = [5,4,3,2,1].map(s=>
|
||||
`<span><i style="background:${SCOL[s]}"></i>${s}★</span>`).join("")+
|
||||
`<span style="color:var(--muted);border-left:1px solid var(--line);padding-left:12px">`+
|
||||
`<i style="background:#c43f2f;opacity:.75"></i>${t('legprot')}</span>`;
|
||||
if(selected) select(selected); else renderMapLinks(null);
|
||||
apply();
|
||||
setTab(document.getElementById('detail').classList.contains('t3d'));
|
||||
}
|
||||
|
||||
// ---- imágenes ----
|
||||
const layers = {};
|
||||
function loadImg(name, b64){
|
||||
|
|
@ -495,16 +621,15 @@ const el = id => document.getElementById(id);
|
|||
let minStars = 1;
|
||||
function apply(){
|
||||
const reg = el('reg').value;
|
||||
const mad = +el('fmad').value, son=+el('fson').value, agu=+el('fagu').value,
|
||||
const son=+el('fson').value, agu=+el('fagu').value,
|
||||
arb=+el('farb').value, acc=+el('facc').value;
|
||||
visible = C.filter(c =>
|
||||
c[K.estrellas] >= minStars &&
|
||||
(!reg || c[K.comunidad]===reg) &&
|
||||
(mad>=700 || c[K.km_madrid]<=mad) &&
|
||||
c[K.c_sonido]>=son && c[K.c_agua]>=agu &&
|
||||
c[K.c_arbolado]>=arb && c[K.c_acceso]>=acc);
|
||||
visible.sort((a,b)=>b[K.score]-a[K.score]);
|
||||
el('cnt').textContent = visible.length + (visible.length===1?" localización":" localizaciones");
|
||||
el('cnt').textContent = visible.length + " " + (visible.length===1?t("loc1"):t("locs"));
|
||||
const n5 = visible.filter(c=>c[K.estrellas]===5).length;
|
||||
el('cnt2').textContent = n5 ? n5+" de 5★" : "";
|
||||
renderList(); draw();
|
||||
|
|
@ -518,7 +643,7 @@ function renderList(){
|
|||
d.className='item'; d.setAttribute('role','option');
|
||||
d.setAttribute('aria-selected', selected===c ? 'true':'false');
|
||||
d.innerHTML = `<span class="st" style="color:${SCOL[c[K.estrellas]]}">${"★".repeat(c[K.estrellas])}</span>`+
|
||||
`<span class="nm">${c[K.municipio]||'—'}</span>`+
|
||||
`<span class="nm">${esc(c[K.municipio])||'—'}</span>`+
|
||||
`<span class="sc mono">${c[K.score].toFixed(0)}</span>`;
|
||||
d.onclick = ()=>select(c);
|
||||
frag.appendChild(d);
|
||||
|
|
@ -527,14 +652,14 @@ function renderList(){
|
|||
if(visible.length>400){
|
||||
const m=document.createElement('div');
|
||||
m.className='item'; m.style.color='var(--muted)';
|
||||
m.innerHTML=`<span class="nm">… y ${visible.length-400} más (afina los filtros)</span>`;
|
||||
m.innerHTML=`<span class="nm">… ${visible.length-400} ${t("more")}</span>`;
|
||||
box.appendChild(m);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- panel de detalle ----
|
||||
const CRITS = [["c_sonido","Sonido"],["c_soledad","Soledad"],["c_acceso","Acceso"],
|
||||
["c_agua","Agua"],["c_arbolado","Arbolado"],["c_clima","Clima"]];
|
||||
const CRITS = [["c_sonido","sound"],["c_soledad","solitude"],["c_acceso","access"],
|
||||
["c_agua","water"],["c_arbolado","trees"],["c_clima","climate"]];
|
||||
function select(c){
|
||||
selected = c;
|
||||
el('detail').classList.add('open');
|
||||
|
|
@ -545,66 +670,52 @@ function select(c){
|
|||
|
||||
el('dCrits').innerHTML = CRITS.map(([k,lab])=>{
|
||||
const v = c[K[k]];
|
||||
return `<div class="crit"><span style="color:var(--ink2)">${lab}</span>`+
|
||||
return `<div class="crit"><span style="color:var(--ink2)">${t(lab)}</span>`+
|
||||
`<span class="bar"><i style="width:${v}%"></i></span>`+
|
||||
`<span class="v mono">${v}</span></div>`;
|
||||
}).join("");
|
||||
|
||||
const f = [
|
||||
["Nivel en la casa más cercana", c[K.db_en_casa].toFixed(0)+" dB"],
|
||||
["Edificio más cercano", (c[K.d_edificio]/1000).toFixed(1)+" km"],
|
||||
["Carretera más cercana", (c[K.d_carretera]/1000).toFixed(1)+" km"],
|
||||
["Pista de acceso", c[K.d_pista]+" m"],
|
||||
["Agua más cercana", c[K.d_agua]<10000 ? (c[K.d_agua]/1000).toFixed(1)+" km" : "lejos"],
|
||||
["Arbolado alrededor", c[K.arbolado_pct]+" %"],
|
||||
["Roca desnuda", c[K.roca_pct]+" %"],
|
||||
["Pendiente", c[K.pendiente]+"°"],
|
||||
["Hondonada (TPI)", c[K.tpi]+" m"],
|
||||
["Máx. mes más cálido", c[K.tmax].toFixed(0)+" °C"],
|
||||
["Mín. mes más frío", c[K.tmin].toFixed(0)+" °C"],
|
||||
["Desde Madrid", c[K.km_madrid]+" km en línea recta"],
|
||||
[t("fdb"), c[K.db_en_casa].toFixed(0)+" dB"],
|
||||
[t("fbuild"), (c[K.d_edificio]/1000).toFixed(1)+" km"],
|
||||
[t("froad"), (c[K.d_carretera]/1000).toFixed(1)+" km"],
|
||||
[t("ftrack"), c[K.d_pista]+" m"],
|
||||
[t("fwat"), c[K.d_agua]<10000 ? (c[K.d_agua]/1000).toFixed(1)+" km" : t("far")],
|
||||
[t("ftree"), c[K.arbolado_pct]+" %"],
|
||||
[t("frock"), c[K.roca_pct]+" %"],
|
||||
[t("fslope"), c[K.pendiente]+"°"],
|
||||
[t("ftpi"), c[K.tpi]+" m"],
|
||||
[t("ftmax"), c[K.tmax].toFixed(0)+" °C"],
|
||||
[t("ftmin"), c[K.tmin].toFixed(0)+" °C"],
|
||||
];
|
||||
el('dFacts').innerHTML = f.map(([a,b])=>`<div class="fact"><span>${a}</span><b>${b}</b></div>`).join("");
|
||||
|
||||
const pk = c[K.protegido_cerca], pkm = c[K.km_protegido];
|
||||
el('dWarn').innerHTML = pk
|
||||
? `<div class="warn"><b>Espacio protegido a ${pkm} km:</b> ${pk}.
|
||||
Está fuera, pero comprueba el límite exacto en el visor oficial antes de ir.</div>`
|
||||
: `<div class="warn">Sin espacio protegido cartografiado en 12 km. Aun así,
|
||||
comprueba la propiedad del terreno: casi todo esto es finca privada.</div>`;
|
||||
? `<div class="warn"><b>${t('protnear')} ${pkm} km:</b> ${esc(pk)}. ${t('protwarn')}</div>`
|
||||
: `<div class="warn">${t('noprot')}</div>`;
|
||||
|
||||
const la=c[K.lat], lo=c[K.lon];
|
||||
const lnk=(href,t,s)=>`<a href="${href}" target="_blank" rel="noopener">`+
|
||||
`<b>${t}</b><span>${s}</span></a>`;
|
||||
el('dLinks').innerHTML =
|
||||
lnk(`https://www.google.com/maps/@${la},${lo},1500m/data=!3m1!1e3`,
|
||||
'Google satélite','vista aérea y relieve') +
|
||||
lnk(`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${la},${lo}`,
|
||||
'Street View','la entrada desde el asfalto') +
|
||||
lnk(`https://www.mapillary.com/app/?lat=${la}&lng=${lo}&z=15`,
|
||||
'Mapillary','fotos de pista, si las hay') +
|
||||
lnk(`https://www.openstreetmap.org/#map=16/${la}/${lo}`,
|
||||
'OpenStreetMap','caminos y topónimos') +
|
||||
lnk(`https://www1.sedecatastro.gob.es/CYCBienInmueble/OVCListaBienes.aspx?`+
|
||||
`del=0&mun=0&RCCompleta=&latitud=${la}&longitud=${lo}`,
|
||||
'Catastro','de quién es la finca') +
|
||||
lnk(`https://www.google.com/maps/dir/?api=1&destination=${la},${lo}`,
|
||||
'Cómo llegar','ruta desde donde estés');
|
||||
renderMapLinks(c);
|
||||
|
||||
// La foto aérea se carga solo al seleccionar, no las 355 de golpe.
|
||||
el('ortho').src = 'ortho/' + c[K.id] + '.jpg';
|
||||
if(document.getElementById('detail').classList.contains('t3d')) drawTerrain(c);
|
||||
else curTile = c;
|
||||
// Si estamos en el mapa de cerca, que siga a la localización elegida.
|
||||
if(L2 && document.body.classList.contains('cerca')){
|
||||
// La marca siempre se mueve; recentrar solo en el modo de cerca. En el modo
|
||||
// Mapa se está mirando el país entero y no tiene sentido hacer zoom cada vez
|
||||
// que se pincha un punto.
|
||||
if(L2){
|
||||
document.body.classList.remove('sinsel');
|
||||
L2.setView([c[K.lat],c[K.lon]], Math.max(L2.getZoom(),15));
|
||||
selMarker.setLatLng([c[K.lat],c[K.lon]]).setStyle({opacity:1,fillOpacity:.28});
|
||||
selMarker.bringToFront();
|
||||
if(el('bCerca').getAttribute('aria-pressed') === 'true'){
|
||||
L2.setView([c[K.lat],c[K.lon]], Math.max(L2.getZoom(),15));
|
||||
}
|
||||
}
|
||||
renderList(); draw();
|
||||
}
|
||||
el('close').onclick = ()=>{el('detail').classList.remove('open'); selected=null; renderList(); draw();};
|
||||
el('close').onclick = ()=>{el('detail').classList.remove('open'); selected=null;
|
||||
renderMapLinks(null); renderList(); draw();};
|
||||
|
||||
// ---- terreno 3D (canvas 2D, algoritmo del pintor) ----
|
||||
const tc = document.getElementById('tcanvas'), tx = tc.getContext('2d');
|
||||
|
|
@ -665,6 +776,34 @@ function drawTerrain(c){
|
|||
tx.strokeStyle="#ff7a45"; tx.lineWidth=1.5;
|
||||
tx.beginPath(); tx.moveTo(cP[0],cP[1]-5); tx.lineTo(cP[0],cP[1]); tx.stroke();
|
||||
}
|
||||
// enlaces a mapas externos, en la barra inferior y no dentro de la ficha
|
||||
function renderMapLinks(c){
|
||||
const box = el('mapLinks');
|
||||
if(!c){
|
||||
box.innerHTML = '<span class="ml-hint">'+t('pick')+'</span>';
|
||||
return;
|
||||
}
|
||||
const la=c[K.lat], lo=c[K.lon];
|
||||
const lnk=(href,t,s)=>`<a href="${href}" target="_blank" rel="noopener" title="${s}">`+
|
||||
`<b>${t}</b><span>${s}</span></a>`;
|
||||
box.innerHTML =
|
||||
`<span class="ml-sel"><b>${esc(c[K.municipio])||'—'}</b>`+
|
||||
`<span style="color:${SCOL[c[K.estrellas]]}">${'★'.repeat(c[K.estrellas])}</span></span>` +
|
||||
lnk(`https://www.google.com/maps/@${la},${lo},1500m/data=!3m1!1e3`,
|
||||
t('lsat'),t('lsatd')) +
|
||||
lnk(`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${la},${lo}`,
|
||||
'Street View',t('lsvd')) +
|
||||
lnk(`https://www.mapillary.com/app/?lat=${la}&lng=${lo}&z=15`,
|
||||
'Mapillary',t('lmpd')) +
|
||||
lnk(`https://www.openstreetmap.org/#map=16/${la}/${lo}`,
|
||||
'OSM',t('losmd')) +
|
||||
lnk(`https://www1.sedecatastro.gob.es/CYCBienInmueble/OVCListaBienes.aspx?`+
|
||||
`del=0&mun=0&RCCompleta=&latitud=${la}&longitud=${lo}`,
|
||||
t('lcad'),t('lcadd')) +
|
||||
lnk(`https://www.google.com/maps/dir/?api=1&destination=${la},${lo}`,
|
||||
t('lgo'),t('lgod'));
|
||||
}
|
||||
|
||||
// pestañas del panel: la vista aérea es lo primero que se ve, y el 3D solo se
|
||||
// dibuja si se pide, que es lo caro
|
||||
function setTab(t3d){
|
||||
|
|
@ -672,9 +811,7 @@ function setTab(t3d){
|
|||
d.classList.toggle('t3d', t3d);
|
||||
el('tabSat').setAttribute('aria-pressed', String(!t3d));
|
||||
el('tab3d').setAttribute('aria-pressed', String(t3d));
|
||||
el('tHint').textContent = t3d
|
||||
? 'relieve 9,6 × 9,6 km · arrastra para girar'
|
||||
: 'ortofoto PNOA · 640 m de lado · pulsa para ampliar';
|
||||
el('tHint').textContent = t3d ? t('relhint') : t('aerhint');
|
||||
el('tRange').style.display = t3d ? '' : 'none';
|
||||
if(t3d && curTile) drawTerrain(curTile);
|
||||
}
|
||||
|
|
@ -731,7 +868,7 @@ function initLeaf(){
|
|||
const m = L.circleMarker([c[K.lat],c[K.lon]],{
|
||||
radius: c[K.estrellas]>=4?6:4, color:'#0a0d12', weight:1.5,
|
||||
fillColor:SCOL[c[K.estrellas]], fillOpacity:.95});
|
||||
m.bindTooltip(`${'★'.repeat(c[K.estrellas])} ${c[K.municipio]||'—'}`,{direction:'top'});
|
||||
m.bindTooltip(`${'★'.repeat(c[K.estrellas])} ${esc(c[K.municipio])||'—'}`,{direction:'top'});
|
||||
m.on('click',()=>select(c));
|
||||
ptsLayer.addLayer(m);
|
||||
}
|
||||
|
|
@ -744,10 +881,31 @@ function initLeaf(){
|
|||
selMarker.setStyle({opacity:0,fillOpacity:0});
|
||||
}
|
||||
|
||||
function setMode(m){ // 'esp' | 'mapa' | 'cerca'
|
||||
document.body.classList.toggle('cerca', m !== 'esp');
|
||||
el('bEsp').setAttribute('aria-pressed', String(m === 'esp'));
|
||||
el('bMapa').setAttribute('aria-pressed', String(m === 'mapa'));
|
||||
el('bCerca').setAttribute('aria-pressed', String(m === 'cerca'));
|
||||
}
|
||||
|
||||
// Encaja todo el país sobre el mapa real. Los límites salen de los propios
|
||||
// candidatos con margen, no de un rectángulo fijo.
|
||||
function spainBounds(){
|
||||
let a=90,b=180,c2=-90,d=-180;
|
||||
for(const c of C){ a=Math.min(a,c[K.lat]); c2=Math.max(c2,c[K.lat]);
|
||||
b=Math.min(b,c[K.lon]); d=Math.max(d,c[K.lon]); }
|
||||
return L.latLngBounds([a-0.5,b-0.7],[c2+0.5,d+0.7]);
|
||||
}
|
||||
|
||||
function goMapa(){
|
||||
setMode('mapa');
|
||||
initLeaf();
|
||||
document.body.classList.remove('sinsel');
|
||||
setTimeout(()=>{ L2.invalidateSize(); L2.fitBounds(spainBounds()); }, 30);
|
||||
}
|
||||
|
||||
function goCerca(c){
|
||||
document.body.classList.add('cerca');
|
||||
el('bEsp').setAttribute('aria-pressed','false');
|
||||
el('bCerca').setAttribute('aria-pressed','true');
|
||||
setMode('cerca');
|
||||
initLeaf();
|
||||
document.body.classList.toggle('sinsel', !c);
|
||||
setTimeout(()=>{
|
||||
|
|
@ -756,18 +914,20 @@ function goCerca(c){
|
|||
L2.setView([c[K.lat],c[K.lon]], 16);
|
||||
selMarker.setLatLng([c[K.lat],c[K.lon]]).setStyle({opacity:1,fillOpacity:.28});
|
||||
selMarker.bringToFront();
|
||||
} else {
|
||||
L2.fitBounds(spainBounds());
|
||||
}
|
||||
},30);
|
||||
}
|
||||
function goEspana(){
|
||||
document.body.classList.remove('cerca');
|
||||
el('bEsp').setAttribute('aria-pressed','true');
|
||||
el('bCerca').setAttribute('aria-pressed','false');
|
||||
setMode('esp');
|
||||
resize();
|
||||
}
|
||||
el('bCerca').onclick=()=>goCerca(selected);
|
||||
el('bMapa').onclick=goMapa;
|
||||
el('bEsp').onclick=goEspana;
|
||||
el('bVerCerca').onclick=()=>{ if(selected) goCerca(selected); };
|
||||
el('bVerCercaTop').onclick=()=>{ if(selected) goCerca(selected); };
|
||||
|
||||
document.querySelectorAll('input[name=base]').forEach(r=>{
|
||||
r.onchange=()=>{ if(!L2) return;
|
||||
|
|
@ -793,8 +953,7 @@ document.querySelectorAll('#starf button').forEach(b=>{
|
|||
o.setAttribute('aria-pressed', o===b ? 'true':'false'));
|
||||
apply(); };
|
||||
});
|
||||
const sliders = [['fmad','vmad',v=>v>=700?'sin límite':v+' km'],
|
||||
['fson','vson',v=>v], ['fagu','vagu',v=>v],
|
||||
const sliders = [['fson','vson',v=>v], ['fagu','vagu',v=>v],
|
||||
['farb','varb',v=>v], ['facc','vacc',v=>v]];
|
||||
sliders.forEach(([id,out,fmt])=>{
|
||||
const i=el(id); i.oninput=()=>{ el(out).textContent=fmt(+i.value); apply(); };
|
||||
|
|
@ -819,7 +978,10 @@ el('legend').innerHTML = [5,4,3,2,1].map(s=>
|
|||
|
||||
Promise.all([loadImg('rel',"__RELIEF__"),loadImg('prot',"__PROT__"),loadImg('iso',"__ISO__")])
|
||||
.then(()=>{ resize(); fit(); apply(); });
|
||||
apply(); resize();
|
||||
el('lang').onchange=()=>{ LANG=el('lang').value;
|
||||
localStorage.setItem('rs_lang',LANG); applyLang(); };
|
||||
renderMapLinks(null);
|
||||
apply(); resize(); applyLang();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -7,26 +7,39 @@ que es justo lo que necesitamos para "a cuántos metros está la casa más cerca
|
|||
Cubrimos península + Baleares. Canarias queda fuera: en 3035 se deforma mucho y
|
||||
además no es un destino al que se llegue conduciendo.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from pyproj import CRS, Transformer
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import regions
|
||||
|
||||
# El país se elige con la variable de entorno REGION. Por defecto España, que es
|
||||
# la región con la que se construyó el proyecto y la única con todos los datos
|
||||
# precalculados.
|
||||
CODE = os.environ.get("REGION", regions.DEFAULT).lower()
|
||||
REGION = regions.get(CODE)
|
||||
IS_DEFAULT = CODE == regions.DEFAULT
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
RAW = ROOT / "data" / "raw"
|
||||
INTERIM = ROOT / "data" / "interim"
|
||||
OUT = ROOT / "out"
|
||||
# La región por defecto publica en la raíz, para no romper rutas ya
|
||||
# documentadas; las demás van a su propio subdirectorio.
|
||||
INTERIM = ROOT / "data" / "interim" if IS_DEFAULT else ROOT / "data" / "interim" / CODE
|
||||
OUT = ROOT / "out" if IS_DEFAULT else ROOT / "out" / CODE
|
||||
for _d in (RAW, INTERIM, OUT):
|
||||
_d.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
PBF = RAW / "spain-latest.osm.pbf"
|
||||
PBF = RAW / (REGION["osm"][0].rsplit("/", 1)[-1] + "-latest.osm.pbf")
|
||||
|
||||
# Ventana geográfica: península + Baleares, con un pequeño margen.
|
||||
LON_MIN, LON_MAX = -9.60, 4.40
|
||||
LAT_MIN, LAT_MAX = 35.85, 43.90
|
||||
# Ventana geográfica de la región, con un pequeño margen.
|
||||
LON_MIN, LAT_MIN, LON_MAX, LAT_MAX = REGION["bbox"]
|
||||
|
||||
CRS_WGS84 = CRS.from_epsg(4326)
|
||||
CRS_GRID = CRS.from_epsg(3035)
|
||||
CRS_GRID = CRS.from_user_input(regions.crs_for(CODE))
|
||||
|
||||
to_grid = Transformer.from_crs(CRS_WGS84, CRS_GRID, always_xy=True).transform
|
||||
to_wgs = Transformer.from_crs(CRS_GRID, CRS_WGS84, always_xy=True).transform
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
# El bucket es público (open data en AWS), no hace falta autenticación.
|
||||
# Las teselas que caen enteras en el mar no existen y devuelven 404: se ignoran.
|
||||
set -u
|
||||
DEST=/home/sito/RAVE_SCOUT/data/raw/dem
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DEST="$ROOT/data/raw/dem"
|
||||
mkdir -p "$DEST"
|
||||
BASE=https://copernicus-dem-90m.s3.amazonaws.com
|
||||
|
||||
|
|
|
|||
191
src/regions.py
Normal file
191
src/regions.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
"""Catálogo de regiones. El país es un parámetro, no algo cableado.
|
||||
|
||||
Se procesa **una malla por país**, no una malla continental. Europa entera a
|
||||
100 m son mil millones de celdas: 4 GB por capa y unos 73 GB para las dieciocho
|
||||
del modelo, más de lo que cabe en memoria de una tacada. Por país el problema es
|
||||
tratable y además paralelizable.
|
||||
|
||||
Uso:
|
||||
REGION=pt python src/extract_osm.py
|
||||
REGION=pt make all
|
||||
|
||||
Cada entrada declara qué hay disponible de verdad para ese país, porque no todo
|
||||
existe en todas partes:
|
||||
|
||||
protegidos 'eea' Red Natura 2000 + designaciones nacionales, vía la Agencia
|
||||
Europea de Medio Ambiente. Cubre la UE y el EEE.
|
||||
None No hay fuente libre equivalente. El filtro legal NO
|
||||
funciona: hay que conseguir la cartografía del país antes
|
||||
de fiarse de un resultado.
|
||||
ortofoto 'pnoa' Ortofoto nacional descargable (solo España, IGN).
|
||||
None Sin foto aérea incrustada; en el visor queda el mapa
|
||||
deslizante, que sí funciona en todas partes.
|
||||
"""
|
||||
|
||||
# code: (nombre, bbox lon/lat, extracto(s) de Geofabrik, vecinos para no inflar
|
||||
# el aislamiento en la frontera, protegidos, ortofoto)
|
||||
REGIONS = {
|
||||
# ---------------- Península Ibérica ----------------
|
||||
"es": dict(name="España", bbox=(-9.60, 35.85, 4.40, 43.90),
|
||||
osm=["europe/spain"],
|
||||
neighbours=["europe/portugal", "europe/andorra",
|
||||
"europe/france/aquitaine", "europe/france/midi-pyrenees",
|
||||
"europe/france/languedoc-roussillon"],
|
||||
protected="eea", ortho="pnoa", default=True),
|
||||
"pt": dict(name="Portugal", bbox=(-9.60, 36.90, -6.10, 42.20),
|
||||
osm=["europe/portugal"], neighbours=["europe/spain"],
|
||||
protected="eea", ortho=None),
|
||||
"ad": dict(name="Andorra", bbox=(1.40, 42.42, 1.79, 42.66),
|
||||
osm=["europe/andorra"],
|
||||
neighbours=["europe/spain", "europe/france/midi-pyrenees"],
|
||||
protected="eea", ortho=None),
|
||||
|
||||
# ---------------- Resto de Europa occidental ----------------
|
||||
"fr": dict(name="Francia", bbox=(-5.20, 41.30, 9.60, 51.10),
|
||||
osm=["europe/france"],
|
||||
neighbours=["europe/spain", "europe/italy", "europe/germany",
|
||||
"europe/belgium", "europe/switzerland"],
|
||||
protected="eea", ortho=None),
|
||||
"it": dict(name="Italia", bbox=(6.60, 35.40, 18.60, 47.10),
|
||||
osm=["europe/italy"],
|
||||
neighbours=["europe/france", "europe/switzerland", "europe/austria",
|
||||
"europe/slovenia"],
|
||||
protected="eea", ortho=None),
|
||||
"de": dict(name="Alemania", bbox=(5.80, 47.20, 15.10, 55.10),
|
||||
osm=["europe/germany"],
|
||||
neighbours=["europe/france", "europe/poland", "europe/czech-republic",
|
||||
"europe/austria", "europe/netherlands", "europe/belgium"],
|
||||
protected="eea", ortho=None),
|
||||
"gb": dict(name="Reino Unido", bbox=(-8.70, 49.80, 1.80, 61.00),
|
||||
osm=["europe/great-britain"],
|
||||
neighbours=["europe/ireland-and-northern-ireland"],
|
||||
protected="eea", ortho=None),
|
||||
"ie": dict(name="Irlanda", bbox=(-10.70, 51.30, -5.30, 55.50),
|
||||
osm=["europe/ireland-and-northern-ireland"], neighbours=[],
|
||||
protected="eea", ortho=None),
|
||||
"ch": dict(name="Suiza", bbox=(5.90, 45.80, 10.60, 47.90),
|
||||
osm=["europe/switzerland"],
|
||||
neighbours=["europe/france", "europe/italy", "europe/germany",
|
||||
"europe/austria"],
|
||||
protected="eea", ortho=None),
|
||||
"at": dict(name="Austria", bbox=(9.50, 46.30, 17.20, 49.10),
|
||||
osm=["europe/austria"],
|
||||
neighbours=["europe/germany", "europe/italy", "europe/switzerland",
|
||||
"europe/czech-republic", "europe/slovakia", "europe/hungary",
|
||||
"europe/slovenia"],
|
||||
protected="eea", ortho=None),
|
||||
"nl": dict(name="Países Bajos", bbox=(3.30, 50.70, 7.30, 53.60),
|
||||
osm=["europe/netherlands"],
|
||||
neighbours=["europe/germany", "europe/belgium"],
|
||||
protected="eea", ortho=None),
|
||||
"be": dict(name="Bélgica", bbox=(2.50, 49.40, 6.50, 51.60),
|
||||
osm=["europe/belgium"],
|
||||
neighbours=["europe/france", "europe/germany", "europe/netherlands",
|
||||
"europe/luxembourg"],
|
||||
protected="eea", ortho=None),
|
||||
|
||||
# ---------------- Norte y este de Europa ----------------
|
||||
"no": dict(name="Noruega", bbox=(4.00, 57.90, 31.20, 71.30),
|
||||
osm=["europe/norway"], neighbours=["europe/sweden", "europe/finland"],
|
||||
protected="eea", ortho=None),
|
||||
"se": dict(name="Suecia", bbox=(10.90, 55.30, 24.20, 69.10),
|
||||
osm=["europe/sweden"], neighbours=["europe/norway", "europe/finland"],
|
||||
protected="eea", ortho=None),
|
||||
"fi": dict(name="Finlandia", bbox=(19.10, 59.70, 31.60, 70.10),
|
||||
osm=["europe/finland"], neighbours=["europe/sweden", "europe/norway"],
|
||||
protected="eea", ortho=None),
|
||||
"is": dict(name="Islandia", bbox=(-24.60, 63.20, -13.40, 66.60),
|
||||
osm=["europe/iceland"], neighbours=[], protected="eea", ortho=None),
|
||||
"pl": dict(name="Polonia", bbox=(14.10, 49.00, 24.20, 54.90),
|
||||
osm=["europe/poland"],
|
||||
neighbours=["europe/germany", "europe/czech-republic",
|
||||
"europe/slovakia"],
|
||||
protected="eea", ortho=None),
|
||||
"cz": dict(name="Chequia", bbox=(12.10, 48.50, 18.90, 51.10),
|
||||
osm=["europe/czech-republic"],
|
||||
neighbours=["europe/germany", "europe/poland", "europe/slovakia",
|
||||
"europe/austria"],
|
||||
protected="eea", ortho=None),
|
||||
"gr": dict(name="Grecia", bbox=(19.30, 34.70, 28.30, 41.80),
|
||||
osm=["europe/greece"], neighbours=["europe/albania"],
|
||||
protected="eea", ortho=None),
|
||||
"ro": dict(name="Rumanía", bbox=(20.20, 43.60, 29.70, 48.30),
|
||||
osm=["europe/romania"], neighbours=["europe/bulgaria", "europe/hungary"],
|
||||
protected="eea", ortho=None),
|
||||
"hr": dict(name="Croacia", bbox=(13.40, 42.30, 19.50, 46.60),
|
||||
osm=["europe/croatia"],
|
||||
neighbours=["europe/slovenia", "europe/bosnia-herzegovina"],
|
||||
protected="eea", ortho=None),
|
||||
|
||||
# ---------------- Norte de África ----------------
|
||||
# Sin fuente libre de espacios protegidos equivalente a la europea: el
|
||||
# filtro legal queda desactivado y hay que conseguir la cartografía del
|
||||
# país antes de dar por bueno ningún resultado.
|
||||
"ma": dict(name="Marruecos", bbox=(-13.30, 27.60, -0.90, 36.00),
|
||||
osm=["africa/morocco"], neighbours=["africa/algeria"],
|
||||
protected=None, ortho=None),
|
||||
"dz": dict(name="Argelia", bbox=(-8.70, 18.90, 12.00, 37.10),
|
||||
osm=["africa/algeria"],
|
||||
neighbours=["africa/morocco", "africa/tunisia", "africa/libya"],
|
||||
protected=None, ortho=None),
|
||||
"tn": dict(name="Túnez", bbox=(7.50, 30.20, 11.60, 37.60),
|
||||
osm=["africa/tunisia"], neighbours=["africa/algeria", "africa/libya"],
|
||||
protected=None, ortho=None),
|
||||
"ly": dict(name="Libia", bbox=(9.30, 19.50, 25.20, 33.20),
|
||||
osm=["africa/libya"], neighbours=["africa/tunisia", "africa/egypt"],
|
||||
protected=None, ortho=None),
|
||||
"eg": dict(name="Egipto", bbox=(24.60, 21.90, 37.00, 31.70),
|
||||
osm=["africa/egypt"], neighbours=["africa/libya"],
|
||||
protected=None, ortho=None),
|
||||
"mr": dict(name="Mauritania", bbox=(-17.10, 14.70, -4.80, 27.30),
|
||||
osm=["africa/mauritania"], neighbours=["africa/morocco"],
|
||||
protected=None, ortho=None),
|
||||
}
|
||||
|
||||
# EPSG:3035 (LAEA Europa) solo es válida en Europa: fuera deforma. Para las
|
||||
# regiones de África se construye una LAEA centrada en la propia región, que es
|
||||
# equiárea allí donde se usa. Europa mantiene 3035 para no invalidar la malla
|
||||
# con la que ya está calculada España.
|
||||
AFRICA = {"ma", "dz", "tn", "ly", "eg", "mr"}
|
||||
|
||||
|
||||
def crs_for(code):
|
||||
r = get(code)
|
||||
if code not in AFRICA:
|
||||
return "EPSG:3035"
|
||||
lon0 = (r["bbox"][0] + r["bbox"][2]) / 2
|
||||
lat0 = (r["bbox"][1] + r["bbox"][3]) / 2
|
||||
return (f"+proj=laea +lat_0={lat0:.4f} +lon_0={lon0:.4f} "
|
||||
"+x_0=4321000 +y_0=3210000 +datum=WGS84 +units=m +no_defs")
|
||||
|
||||
|
||||
DEFAULT = "es"
|
||||
GEOFABRIK = "https://download.geofabrik.de/{path}-latest.osm.pbf"
|
||||
|
||||
|
||||
def get(code):
|
||||
code = (code or DEFAULT).lower()
|
||||
if code not in REGIONS:
|
||||
raise SystemExit(
|
||||
f"Región '{code}' desconocida. Disponibles: "
|
||||
+ ", ".join(sorted(REGIONS)))
|
||||
return REGIONS[code]
|
||||
|
||||
|
||||
def urls(code):
|
||||
"""URLs de Geofabrik del país y de sus vecinos."""
|
||||
r = get(code)
|
||||
return ([GEOFABRIK.format(path=p) for p in r["osm"]],
|
||||
[GEOFABRIK.format(path=p) for p in r["neighbours"]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"{'código':8s}{'país':18s}{'protegidos':12s}{'ortofoto':10s}bbox")
|
||||
print("-" * 76)
|
||||
for k, v in sorted(REGIONS.items(), key=lambda kv: kv[1]["name"]):
|
||||
p = v["protected"] or "NO HAY"
|
||||
o = v["ortho"] or "—"
|
||||
b = v["bbox"]
|
||||
print(f"{k:8s}{v['name'][:17]:18s}{p:12s}{o:10s}"
|
||||
f"{b[0]:.1f},{b[1]:.1f} .. {b[2]:.1f},{b[3]:.1f}")
|
||||
print(f"\n{len(REGIONS)} regiones. Por defecto: {DEFAULT}")
|
||||
|
|
@ -46,7 +46,6 @@ def main():
|
|||
<td class="nu">{km(r['d_agua'])}</td>
|
||||
<td class="nu">{esc(r['arbolado_pct'])}</td>
|
||||
<td class="nu">{km(r['d_protegido'])}</td>
|
||||
<td class="nu">{esc(r['km_madrid'])}</td>
|
||||
<td class="lk"><a href="{gm}" target="_blank" rel="noopener">sat</a>
|
||||
<a href="{sv}" target="_blank" rel="noopener">sv</a></td>
|
||||
</tr>""")
|
||||
|
|
@ -355,7 +354,7 @@ code.inline {{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monos
|
|||
<thead><tr>
|
||||
<th>#</th><th>valor</th><th>coordenadas</th><th>municipio</th><th>comunidad</th>
|
||||
<th>dB</th><th>casa km</th><th>pista m</th><th>agua km</th><th>árbol %</th>
|
||||
<th>prot km</th><th>Madrid km</th><th>ver</th>
|
||||
<th>prot km</th><th>ver</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
|
|
@ -385,8 +384,6 @@ code.inline {{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monos
|
|||
riesgo extremo de incendio de junio a septiembre, con restricciones de acceso y
|
||||
responsabilidad penal si algo prende. El mismo punto no es el mismo sitio en marzo
|
||||
que en agosto.</li>
|
||||
<li><h3>Distancia en línea recta, no tiempo de coche</h3>La columna de Madrid es
|
||||
euclídea. Por carretera y luego pista, el tiempo real puede ser el doble.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ N_CAND = 60
|
|||
MIN_SEP = 15000 # separación mínima entre candidatos, para no devolver
|
||||
# 60 celdas del mismo valle
|
||||
|
||||
MADRID = C.to_grid(-3.7038, 40.4168)
|
||||
|
||||
|
||||
def load(name):
|
||||
|
|
@ -163,7 +162,6 @@ def main():
|
|||
"pendiente_deg": round(float(slope[r, c]), 1),
|
||||
"tpi_m": round(float(tpi[r, c]), 1),
|
||||
"edif_5km": int(dens[r, c]),
|
||||
"km_madrid": round(float(np.hypot(x - MADRID[0], y - MADRID[1]) / 1000), 1),
|
||||
})
|
||||
|
||||
# suprimimos un disco alrededor para el siguiente candidato
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ MAX_FOREST_ATT = 6.0
|
|||
MAX_TERRAIN_ATT = 12.0
|
||||
|
||||
BLOCK = 200 # celdas = 20 km
|
||||
MADRID = C.to_grid(-3.7038, 40.4168)
|
||||
|
||||
|
||||
def load(n):
|
||||
|
|
@ -259,7 +258,6 @@ def main():
|
|||
"cota": int(dem[r, c]) if np.isfinite(dem[r, c]) else 0,
|
||||
"tmax": round(float(tmax[r, c]), 1),
|
||||
"tmin": round(float(tmin[r, c]), 1),
|
||||
"km_madrid": round(float(np.hypot(x - MADRID[0], y - MADRID[1]) / 1000), 1),
|
||||
"row": r, "col": c,
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue