| name | time-isochrones-precalculated |
| description | Pre-cálculo de isócronas reales con OSMnx + NetworkX para el proyecto Time. Genera GeoJSON basado en red viaria OSM real. Patrón para calcular isócronas sin API externa. |
| version | 1.0.0 |
| author | David Antizar |
| tags | ["isochrones","osmnx","networkx","openstreetmap","python","offline","time"] |
Isochoronas Pre-calculadas — Time
Cuándo usar esta skill
Cuando necesites:
- Calcular isócronas reales sin depender de APIs externas
- Añadir nuevas ciudades al proyecto Time
- Mejorar la precisión de las isócronas existentes
- Entender cómo funciona el cálculo offline
Arquitectura
Python (OSMnx + NetworkX) → JSON GeoJSON → Server Node.js → Frontend Leaflet
↓ ↓ ↓ ↓
Descarga grafo OSM Guarda isócronas Sirve /isochrones/ Renderiza polígonos
Scripts
scripts/precalcular-isocronas.py (principal)
Genera isócronas para TODAS las ciudades configuradas.
pip3 install osmnx networkx numpy shapely
python3 scripts/precalcular-isocronas.py
python3 scripts/precalcular-isocronas.py --ciudad bilbao
python3 scripts/precalcular-isocronas.py --listar
scripts/calcular-isocronas.py (helper)
Calcula una isócrona individual para debugging.
python3 scripts/calcular-isocronas.py --ciudad bilbao --modo car --tiempo 30
Ciudades configuradas
| Ciudad | Query OSM | Centro | Radio |
|---|
| bilbao | Bilbao, Bizkaia, España | 43.263, -2.935 | 15km |
| malaga | Málaga, Andalucía, España | 36.721, -4.421 | 15km |
| sevilla | Sevilla, Andalucía, España | 37.389, -5.984 | 15km |
| valencia | Valencia, España | 39.470, -0.376 | 15km |
| zaragoza | Zaragoza, Aragón, España | 41.649, -0.889 | 15km |
Formato de salida
JSON combinado (data/isochrones/{ciudad}.json)
{
"ciudad": "bilbao",
"centro": [43.263, -2.935],
"generado": "2026-06-25T18:00:00Z",
"isochrones": {
"car": {
"15": { "geojson": {...}, "area_km2": 12.5, "radio_m": 6000 },
"30": { "geojson": {...}, "area_km2": 45.2, "radio_m": 12000 },
"60": { "geojson": {...}, "area_km2": 156.8, "radio_m": 24000 }
},
"bike": { ... },
"foot": { ... }
}
}
JSON individual (data/isochrones/{ciudad}_{modo}_{tiempo}.json)
{
"ciudad": "bilbao",
"modo": "car",
"tiempo_min": 30,
"area_km2": 45.2,
"radio_m": 12000,
"geojson": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [...] },
"properties": { "modo": "car", "minutos": 30, "area_km2": 45.2 }
}]
}
}
Endpoints del servidor
| Endpoint | Descripción |
|---|
GET /isochrones/list | Lista ciudades disponibles con metadata |
GET /isochrones/{ciudad} | Todas las isócronas de una ciudad |
GET /isochrones/{ciudad}/{modo}/{min} | Isócrona específica |
Algoritmo OSMnx + NetworkX
import osmnx as ox
import networkx as nx
G = ox.graph_from_place("Bilbao, España", network_type='drive')
center = ox.geocode("Plaza Mayor, Bilbao")
origin_node = ox.distance.nearest_nodes(G, center[1], center[0])
lengths = nx.single_source_dijkstra_path_length(G, origin_node, cutoff=1800)
reachable_nodes = [n for n, l in lengths.items() if l <= 1800]
from shapely.geometry import MultiPoint
points = [(G.nodes[n]['x'], G.nodes[n]['y']) for n in reachable_nodes]
polygon = MultiPoint(points).convex_hull
from shapely.geometry import mapping
geojson = mapping(polygon)
Velocidades por modo y tipo de vía
| Modo | Velocidad base | Calles principales | Secundarias | Carriles bici |
|---|
| car | 50 km/h | Autovías | Urbanas | N/A |
| bike | 15 km/h | Carriles bici | Calles tranquilas | Prioridad |
| foot | 5 km/h | Aceras | Calles peatonales | N/A |
Pitfalls
- OSMnx descarga datos grandes — El grafo de una ciudad puede ocupar 50-200MB en cache. Usar
cache/ directory.
- Tiempo de cálculo — 1-5 minutos por ciudad dependiendo del tamaño. Ejecutar como batch job.
- network_type —
'drive' para coche, 'bike' para bici, 'walk' para peatón, 'all' para todos.
- cutoff en segundos — Dijkstra usa segundos, no minutos. 15min = 900s, 30min = 1800s, 60min = 3600s.
- convex hull — El polígono resultante es convex hull, no sigue la costa. Para ciudades costeras, recortar con shoreline.
- RAM — OSMnx puede consumir 1-2GB RAM para ciudades grandes. Ejecutar en máquina con suficiente memoria.
- Primera ejecución — Descarga datos de Overpass API. Necesita internet solo la primera vez.
- Actualizaciones — Los datos OSM cambian. Recalcular periódicamente (mensual recomendado).
Alternativa: OSRM Real-time (sin API key) — Boundary Detection
Cuando necesites calcular isócronas en tiempo real desde el navegador sin depender de APIs premium, usar el endpoint público de OSRM con boundary detection por dirección.
⚠️ NO usar convex hull — Produce círculos de mierda. David lo probó y corrigió. Usar boundary detection que genera formas irregulares siguiendo la red de carreteras.
Concepto
72 direcciones × 8 radios = 576 puntos radiales
→ OSRM Table endpoint (multi-batch de 89 coords)
→ Para cada dirección: interpolación lineal último alcanzable / primero que excede
→ Polígono irregular que sigue la red vial real
Algoritmo
-
Generar puntos radiales: 72 direcciones (cada 5°) × radios adaptativos
- Radios:
[2, 5, 8, 12, 18, 25, 35, 50, 65, 80] filtrados por max(minutos * 0.9, 5)
- Ejemplo 30min:
[2, 5, 8, 12, 18, 25] → 432 puntos
-
Query OSRM table por batches (max 89 coords, stagger 80ms):
GET https://router.project-osrm.org/table/v1/driving/{coords}?annotations=duration
-
Para cada dirección, encontrar boundary:
const ptsDir = resultados.filter(r => r.dir === d).sort((a,b) => a.radioKm - b.radioKm);
let lastReach = null, firstOver = null;
for (const p of ptsDir) {
if (p.dur <= targetSec) lastReach = p;
else if (!firstOver) { firstOver = p; break; }
}
if (lastReach && firstOver && firstOver.dur > lastReach.dur) {
const frac = (targetSec - lastReach.dur) / (firstOver.dur - lastReach.dur);
rBoundary = lastReach.radioKm + frac * (firstOver.radioKm - lastReach.radioKm);
}
-
Cerrar polígono — Conectar boundary points en orden angular (sin convex hull)
Resultados comparados (Madrid, 30min coche)
| Método | Área | Forma |
|---|
| ❌ Convex hull | 1250 km² | Círculo irregular |
| ✅ Boundary detection | 879 km² | Forma irregular real, sigue carreteras |
Pitfalls OSRM
- OSRM público solo tiene
driving — No hay perfil foot o bicycle. Para andar, usar ORS o simulación.
- Variable case-sensitive —
dlat ≠ dLat. JS no da error, solo undefined.
- Max ~89 coordenadas por llamada
table — Dividir en batches, stagger 80ms.
- Rate limit no documentado — En la práctica tolerante con stagger.
- Sin semáforos ni restricciones horarias — OSRM usa datos OSM básicos.
- No hay datos de elevación — La velocidad es plana. Para pendientes, usar Valhalla con DEM.
- Radios muy bajos = pocos puntos — Asegurar al menos
[2, 5] km para tiempos cortos.
Cadena de fallback recomendada
async function calcularIsocrona(lng, lat, modo, minutos) {
const apiKey = localStorage.getItem('isotime_ors_key');
if (apiKey) {
try { return await calcularIsocronaORS(lng, lat, modo, minutos, apiKey); }
catch (e) { console.warn('ORS failed:', e.message); }
}
if (modo === 'car') {
try { return await calcularIsocronaOSRM(lng, lat, minutos); }
catch (e) { console.warn('OSRM failed:', e.message); }
}
return calcularIsocronaSim(lng, lat, modo, minutos);
}
Futuras mejoras
- Shoreline clipping — Recortar isócronas costeras con Natural Earth data
- Valhalla local — Para isócronas con elevación real (desnivel)
- OTP integration — Isochrone de transporte público con transbordos reales
- Cron de actualización — Recalcular isócronas mensualmente vía cron job
- More cities — Añadir Madrid, Barcelona, etc.