index y datos

This commit is contained in:
2026-03-12 01:22:15 +01:00
commit eabf7ada4f
2 changed files with 16716 additions and 0 deletions
+557
View File
@@ -0,0 +1,557 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ADIF Limitaciones Temporales de Velocidad</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet MarkerCluster -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
background: #1a1a2e;
color: #e0e0e0;
}
header {
background: #16213e;
padding: 10px 18px;
display: flex;
align-items: center;
gap: 14px;
border-bottom: 2px solid #0f3460;
flex-shrink: 0;
z-index: 1000;
}
header h1 {
font-size: 1.1rem;
font-weight: 600;
color: #e94560;
letter-spacing: 0.5px;
}
header .subtitle { font-size: 0.78rem; color: #888; }
#counter-badge {
margin-left: auto;
background: #0f3460;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.82rem;
color: #a8d8ea;
white-space: nowrap;
}
.main-layout { display: flex; flex: 1; overflow: hidden; }
/* ── Sidebar ── */
#sidebar {
width: 300px;
min-width: 260px;
background: #16213e;
border-right: 1px solid #0f3460;
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 500;
}
.panel-section {
padding: 11px 14px;
border-bottom: 1px solid #0f3460;
flex-shrink: 0;
}
.panel-section h2 {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 1px;
color: #555;
margin-bottom: 8px;
}
/* Layer toggle buttons */
.layer-btns { display: flex; gap: 6px; flex-wrap: wrap; }
.layer-btn {
padding: 5px 10px;
border-radius: 6px;
font-size: 0.75rem;
cursor: pointer;
border: 1.5px solid #1a4a80;
background: #0f3460;
color: #a8d8ea;
transition: background 0.15s, border-color 0.15s;
font-weight: 500;
}
.layer-btn.active { background: #e94560; border-color: #e94560; color: #fff; }
.layer-btn:hover:not(.active) { border-color: #a8d8ea; }
/* Overlay toggle */
.overlay-row {
display: flex;
align-items: center;
gap: 10px;
margin-top: 6px;
}
.toggle-switch {
position: relative; width: 38px; height: 20px; cursor: pointer;
}
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute; inset: 0;
background: #0f3460; border-radius: 20px;
transition: background 0.2s;
}
.toggle-slider:before {
content: ''; position: absolute;
width: 14px; height: 14px; border-radius: 50%;
background: #888; left: 3px; top: 3px;
transition: transform 0.2s, background 0.2s;
}
.toggle-switch input:checked + .toggle-slider { background: #0f3460; }
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(18px); background: #e94560;
}
.overlay-label { font-size: 0.82rem; color: #ccc; }
.overlay-sublabel { font-size: 0.7rem; color: #666; }
/* Search */
#search-input {
width: 100%;
padding: 7px 10px;
background: #0f3460;
border: 1px solid #1a4a80;
border-radius: 6px;
color: #e0e0e0;
font-size: 0.85rem;
outline: none;
}
#search-input:focus { border-color: #e94560; }
#search-input::placeholder { color: #555; }
/* Speed chips */
.speed-filters { display: flex; flex-wrap: wrap; gap: 6px; }
.speed-chip {
padding: 4px 9px;
border-radius: 12px;
font-size: 0.73rem;
cursor: pointer;
border: 2px solid transparent;
font-weight: 700;
color: #fff;
user-select: none;
transition: opacity 0.2s, border-color 0.2s;
}
.speed-chip.inactive { opacity: 0.3; }
.speed-chip:hover { border-color: rgba(255,255,255,0.45); }
/* Legend */
.legend-grid {
display: grid;
grid-template-columns: 13px 1fr;
gap: 5px 8px;
align-items: center;
}
.legend-dot {
width: 13px; height: 13px; border-radius: 50%;
border: 1.5px solid rgba(255,255,255,0.25);
}
.legend-label { font-size: 0.78rem; color: #aaa; }
/* Results list */
#results-list { flex: 1; overflow-y: auto; padding: 6px 0; }
#results-list::-webkit-scrollbar { width: 5px; }
#results-list::-webkit-scrollbar-thumb { background: #0f3460; border-radius: 3px; }
.result-item {
padding: 8px 14px;
border-bottom: 1px solid #0d1b38;
cursor: pointer;
transition: background 0.12s;
}
.result-item:hover { background: #0f3460; }
.ri-line {
font-size: 0.68rem; color: #777;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-bottom: 2px;
}
.ri-station {
font-size: 0.83rem; font-weight: 500; color: #ddd;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ri-meta { margin-top: 4px; display: flex; gap: 6px; align-items: center; }
.ri-speed-badge {
font-size: 0.71rem; padding: 1px 7px;
border-radius: 9px; color: #fff; font-weight: 700; flex-shrink: 0;
}
.ri-reason {
font-size: 0.69rem; color: #666;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Map */
#map { flex: 1; }
/* Leaflet layer control ajuste dark */
.leaflet-control-layers {
background: #16213e !important;
color: #ccc !important;
border: 1px solid #0f3460 !important;
}
.leaflet-control-layers label { color: #ccc !important; }
.leaflet-control-layers-separator { border-top-color: #0f3460 !important; }
/* Popup */
.ltv-popup { min-width: 240px; font-family: 'Segoe UI', system-ui, sans-serif; }
.ltv-popup .pop-header {
background: #16213e; color: #e94560;
padding: 7px 10px; font-weight: 700; font-size: 0.82rem;
border-radius: 3px 3px 0 0;
margin: -5px -12px 8px -12px;
}
.ltv-popup table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.ltv-popup td:first-child { color: #888; padding: 3px 8px 3px 0; white-space: nowrap; vertical-align: top; }
.ltv-popup td:last-child { color: #111; font-weight: 500; }
.ltv-popup .speed-big { font-size: 1.5rem; font-weight: 800; line-height: 1; }
</style>
</head>
<body>
<header>
<div>
<h1>🚆 ADIF Limitaciones Temporales de Velocidad</h1>
<div class="subtitle">Red ferroviaria española &nbsp;·&nbsp; Datos LTV activos</div>
</div>
<div id="counter-badge">Cargando…</div>
</header>
<div class="main-layout">
<div id="sidebar">
<!-- Mapa base -->
<div class="panel-section">
<h2>Mapa base</h2>
<div class="layer-btns">
<button class="layer-btn active" data-base="osm">OSM Estándar</button>
<button class="layer-btn" data-base="topo">OSM Topo</button>
<button class="layer-btn" data-base="dark">Dark</button>
<button class="layer-btn" data-base="satellite">Satélite</button>
</div>
</div>
<!-- Capa ferroviaria -->
<div class="panel-section">
<h2>Capa ferroviaria OpenRailwayMap</h2>
<div class="overlay-row">
<label class="toggle-switch">
<input type="checkbox" id="toggle-railway" checked />
<span class="toggle-slider"></span>
</label>
<div>
<div class="overlay-label">Infraestructura ferroviaria</div>
<div class="overlay-sublabel">Líneas, estaciones y señales</div>
</div>
</div>
<div style="margin-top:8px;" class="layer-btns" id="orm-style-btns">
<button class="layer-btn active" data-orm="standard">Estándar</button>
<button class="layer-btn" data-orm="maxspeed">Velocidades</button>
<button class="layer-btn" data-orm="signals">Señales</button>
<button class="layer-btn" data-orm="electrification">Electrif.</button>
</div>
</div>
<!-- Búsqueda -->
<div class="panel-section">
<h2>Buscar restricción</h2>
<input id="search-input" type="text" placeholder="Línea, estación, motivo…" />
</div>
<!-- Filtro velocidad -->
<div class="panel-section">
<h2>Filtrar por velocidad</h2>
<div class="speed-filters" id="speed-filters"></div>
</div>
<!-- Leyenda -->
<div class="panel-section">
<h2>Leyenda LTV</h2>
<div class="legend-grid" id="legend-grid"></div>
</div>
<div id="results-list"></div>
</div>
<div id="map"></div>
</div>
<script>
// ── Rangos de velocidad ────────────────────────────────────────────────────
const SPEED_RANGES = [
{ max: 30, color: '#d63031', label: '≤ 30 km/h' },
{ max: 60, color: '#e17055', label: '31 60 km/h' },
{ max: 100, color: '#fdcb6e', label: '61 100 km/h' },
{ max: 160, color: '#00b894', label: '101 160 km/h' },
{ max: Infinity, color: '#74b9ff', label: '> 160 km/h' },
];
function speedColor(s) {
return SPEED_RANGES.find(r => s <= r.max)?.color ?? '#74b9ff';
}
function rangeLabel(s) {
return SPEED_RANGES.find(r => s <= r.max)?.label ?? '';
}
function makeIcon(color) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="30" viewBox="0 0 22 30">
<path d="M11 0C5 0 0 5 0 11c0 8.3 11 19 11 19S22 19.3 22 11C22 5 17 0 11 0z"
fill="${color}" stroke="rgba(0,0,0,0.5)" stroke-width="1.2"/>
<circle cx="11" cy="11" r="4.5" fill="rgba(255,255,255,0.9)"/>
</svg>`;
return L.divIcon({
html: svg, className: '',
iconSize: [22, 30], iconAnchor: [11, 30], popupAnchor: [0, -30],
});
}
// ── Capas base ─────────────────────────────────────────────────────────────
const baseLayers = {
osm: L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19,
}),
topo: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://opentopomap.org">OpenTopoMap</a>',
maxZoom: 17,
}),
dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd', maxZoom: 19,
}),
satellite: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '&copy; <a href="https://www.esri.com/">Esri</a>',
maxZoom: 19,
}),
};
// ── Capas OpenRailwayMap ───────────────────────────────────────────────────
const ORM_ATTR = 'Mapa: &copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> | '
+ 'Ferroviario: &copy; <a href="https://www.openrailwaymap.org/">OpenRailwayMap</a>';
const ormLayers = {
standard: L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', { attribution: ORM_ATTR, maxZoom: 19, opacity: 0.85 }),
maxspeed: L.tileLayer('https://{s}.tiles.openrailwaymap.org/maxspeed/{z}/{x}/{y}.png', { attribution: ORM_ATTR, maxZoom: 19, opacity: 0.85 }),
signals: L.tileLayer('https://{s}.tiles.openrailwaymap.org/signals/{z}/{x}/{y}.png', { attribution: ORM_ATTR, maxZoom: 19, opacity: 0.85 }),
electrification:L.tileLayer('https://{s}.tiles.openrailwaymap.org/electrification/{z}/{x}/{y}.png',{ attribution: ORM_ATTR, maxZoom: 19, opacity: 0.85 }),
};
// ── Inicializar mapa ───────────────────────────────────────────────────────
const map = L.map('map', { zoomControl: true }).setView([40.4, -3.7], 6);
let currentBase = baseLayers.osm;
let currentOrm = ormLayers.standard;
let ormVisible = true;
currentBase.addTo(map);
currentOrm.addTo(map);
// ── Cluster ────────────────────────────────────────────────────────────────
const clusterGroup = L.markerClusterGroup({
maxClusterRadius: 40,
iconCreateFunction: cluster => {
const count = cluster.getChildCount();
const size = count < 10 ? 30 : count < 50 ? 36 : 44;
return L.divIcon({
html: `<div style="
background: rgba(233,69,96,0.85);
border: 2px solid rgba(255,255,255,0.6);
border-radius: 50%;
width:${size}px; height:${size}px;
display:flex; align-items:center; justify-content:center;
color:#fff; font-weight:700; font-size:${size < 36 ? 11 : 13}px;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
">${count}</div>`,
className: '', iconSize: [size, size],
});
},
});
map.addLayer(clusterGroup);
// ── Estado ─────────────────────────────────────────────────────────────────
let allItems = [];
let activeRanges = new Set(SPEED_RANGES.map(r => r.label));
let searchText = '';
let markerMap = new Map();
// ── Construir UI estática ──────────────────────────────────────────────────
const legendGrid = document.getElementById('legend-grid');
const filtersDiv = document.getElementById('speed-filters');
SPEED_RANGES.forEach(r => {
legendGrid.innerHTML +=
`<div class="legend-dot" style="background:${r.color}"></div>
<div class="legend-label">${r.label}</div>`;
const chip = document.createElement('div');
chip.className = 'speed-chip';
chip.style.background = r.color;
chip.textContent = r.label;
chip.dataset.range = r.label;
chip.addEventListener('click', () => {
if (activeRanges.has(r.label)) {
activeRanges.delete(r.label);
chip.classList.add('inactive');
} else {
activeRanges.add(r.label);
chip.classList.remove('inactive');
}
applyFilters();
});
filtersDiv.appendChild(chip);
});
// ── Popup ──────────────────────────────────────────────────────────────────
function buildPopup(item) {
const color = speedColor(item.speed);
return `<div class="ltv-popup">
<div class="pop-header">${item.trayectoEstacion || '—'}</div>
<table>
<tr><td>Línea</td><td>${item.line}</td></tr>
<tr><td>PK inicio</td><td>${item.pk}</td></tr>
<tr><td>PK fin</td><td>${item.pkEnd}</td></tr>
<tr><td>Vía</td><td>${item.track}</td></tr>
<tr><td>Velocidad</td><td>
<span class="speed-big" style="color:${color}">${item.speed}</span>
<small style="color:#888"> km/h</small>
</td></tr>
<tr><td>Motivo</td><td>${item.reason}</td></tr>
<tr><td>Código LTV</td><td style="font-family:monospace">${item.codigoLtv}</td></tr>
${item.horarioLtv ? `<tr><td>Horario</td><td>${item.horarioLtv}</td></tr>` : ''}
</table>
</div>`;
}
// ── Filtros ────────────────────────────────────────────────────────────────
function matchesFilters(item) {
if (!activeRanges.has(rangeLabel(item.speed))) return false;
if (searchText) {
const q = searchText.toLowerCase();
return (
item.line?.toLowerCase().includes(q) ||
item.trayectoEstacion?.toLowerCase().includes(q) ||
item.reason?.toLowerCase().includes(q) ||
item.codigoLtv?.toLowerCase().includes(q)
);
}
return true;
}
function applyFilters() {
clusterGroup.clearLayers();
const list = document.getElementById('results-list');
list.innerHTML = '';
let count = 0;
allItems.forEach((item, idx) => {
if (!matchesFilters(item)) return;
count++;
const marker = markerMap.get(idx);
if (marker) clusterGroup.addLayer(marker);
const color = speedColor(item.speed);
const div = document.createElement('div');
div.className = 'result-item';
div.innerHTML = `
<div class="ri-line">${item.line}</div>
<div class="ri-station">${item.trayectoEstacion || 'Sin estación'}</div>
<div class="ri-meta">
<span class="ri-speed-badge" style="background:${color}">${item.speed} km/h</span>
<span class="ri-reason" title="${item.reason}">${item.reason}</span>
</div>`;
div.addEventListener('click', () => {
const m = markerMap.get(idx);
if (!m) return;
map.setView([item.lat, item.lng], 14);
clusterGroup.zoomToShowLayer(m, () => m.openPopup());
});
list.appendChild(div);
});
document.getElementById('counter-badge').textContent =
`${count} restricción${count !== 1 ? 'es' : ''}`;
}
// ── Cargar JSON ────────────────────────────────────────────────────────────
fetch('adif.json')
.then(r => r.json())
.then(data => {
allItems = data.items;
allItems.forEach((item, idx) => {
if (!item.lat || !item.lng) return;
const marker = L.marker([item.lat, item.lng], { icon: makeIcon(speedColor(item.speed)) })
.bindPopup(buildPopup(item), { maxWidth: 320 });
markerMap.set(idx, marker);
});
applyFilters();
})
.catch(() => {
document.getElementById('counter-badge').textContent = 'Error al cargar datos';
});
// ── Controles de capa base ─────────────────────────────────────────────────
document.querySelectorAll('[data-base]').forEach(btn => {
btn.addEventListener('click', () => {
const key = btn.dataset.base;
if (!baseLayers[key] || currentBase === baseLayers[key]) return;
map.removeLayer(currentBase);
currentBase = baseLayers[key];
currentBase.addTo(map);
currentBase.bringToBack();
// asegurar que ORM queda encima
if (ormVisible) currentOrm.bringToFront();
document.querySelectorAll('[data-base]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// ── Controles ORM style ────────────────────────────────────────────────────
document.querySelectorAll('[data-orm]').forEach(btn => {
btn.addEventListener('click', () => {
const key = btn.dataset.orm;
if (!ormLayers[key] || currentOrm === ormLayers[key]) return;
map.removeLayer(currentOrm);
currentOrm = ormLayers[key];
if (ormVisible) currentOrm.addTo(map);
document.querySelectorAll('[data-orm]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// ── Toggle capa ferroviaria ────────────────────────────────────────────────
document.getElementById('toggle-railway').addEventListener('change', e => {
ormVisible = e.target.checked;
if (ormVisible) {
currentOrm.addTo(map);
} else {
map.removeLayer(currentOrm);
}
});
// ── Búsqueda ───────────────────────────────────────────────────────────────
document.getElementById('search-input').addEventListener('input', e => {
searchText = e.target.value.trim();
applyFilters();
});
</script>
</body>
</html>