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.
# Instalar dependencias
pip3 install osmnx networkx numpy shapely
# Calcular todas las ciudades
python3 scripts/precalcular-isocronas.py
# Calcular una ciudad específica
python3 scripts/precalcular-isocronas.py --ciudad bilbao
# Listar ciudades disponibles
python3 scripts/precalcular-isocronas.py --listar
scripts/calcular-isocronas.py (helper)
Calcula una isócrona individual para debugging.
# Calcular isócona de coche 30min en Bilbao
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
# 1. Descargar grafo de calles
G = ox.graph_from_place("Bilbao, España", network_type='drive')
# 2. Encontrar nodo más cercano al punto
center = ox.geocode("Plaza Mayor, Bilbao")
origin_node = ox.distance.nearest_nodes(G, center[1], center[0])
# 3. Calcular distancias desde el origen (Dijkstra)
lengths = nx.single_source_dijkstra_path_length(G, origin_node, cutoff=1800) # 30 min
# 4. Filtrar nodos alcanzables
reachable_nodes = [n for n, l in lengths.items() if l <= 1800]
# 5. Generar polígono convexo hull
from shapely.geometry import MultiPoint
points = [(G.nodes[n]['x'], G.nodes[n]['y']) for n in reachable_nodes]
polygon = MultiPoint(points).convex_hull
# 6. Convertir a GeoJSON
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 |
Alternativa: CSR binario para Web Workers (ISOTime)
En vez de generar GeoJSON estático, se puede serializar el grafo OSM a binario CSR (Compressed Sparse Row) y cargarlo en un Web Worker para correr Dijkstra en el navegador. Esto permite isocronas en tiempo real 100% client-side, sin API ni servidor.
Implementado en ISOTime (github.com/Ntizar/ISOTime):
- Python (OSMnx) → binario CSR (1-2MB por ciudad) → fetch estático → Web Worker → Dijkstra + binary heap → polígono
- 40K nodos se calculan en <1s en el navegador
- Formato: header (32B) + city name (32B) + node coords (f32) + CSR offsets (u32) + edge targets/lengths/speeds
- Ver skill
routing-isochrones → references/dijkstra-web-worker-csr.md para formato completo y código
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.
- ⚠️ OSMnx OOM en VMs pequeñas (2GB RAM) — Radio de 20km para Madrid (40K+ nodos) causa OOM (exit 137, killed). Usar 12km máximo para ciudades grandes (Madrid, Barcelona, Valencia). Ciudades pequeñas (Bilbao, Zaragoza) toleran 15km. Síntoma: el proceso Python muere sin traceback, exit code 137. Solución: reducir
dist en graph_from_point() o usar graph_from_place() con un boundary más pequeño.
- Primera ejecución — Descarga datos de Overpass API. Necesita internet solo la primera vez.
- Actualizaciones — Los datos OSM cambian. Recalcular periódicamente (mensual recomendado).
- CSR binario vs GeoJSON — CSR binario es 10-50x más pequeño que GeoJSON y permite cálculo en tiempo real en el navegador. GeoJSON es más simple pero estático (no puedes cambiar el tiempo/modo sin regenerar). Usar CSR para apps client-side, GeoJSON para pre-visualización o export.
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; }
}
// Interpolación lineal
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) {
// 1. ORS (si hay key — más preciso)
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); }
}
// 2. OSRM (solo coche — routing real sin key)
if (modo === 'car') {
try { return await calcularIsocronaOSRM(lng, lat, minutos); }
catch (e) { console.warn('OSRM failed:', e.message); }
}
// 3. Simulación (fallback final)
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.
1---2name: time-isochrones-precalculated3description: 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.4---56# Isochoronas Pre-calculadas — Time78## Cuándo usar esta skill910Cuando necesites:11- Calcular isócronas reales sin depender de APIs externas12- Añadir nuevas ciudades al proyecto Time13- Mejorar la precisión de las isócronas existentes14- Entender cómo funciona el cálculo offline1516## Arquitectura1718```19Python (OSMnx + NetworkX) → JSON GeoJSON → Server Node.js → Frontend Leaflet20 ↓ ↓ ↓ ↓21Descarga grafo OSM Guarda isócronas Sirve /isochrones/ Renderiza polígonos22```2324## Scripts2526### `scripts/precalcular-isocronas.py` (principal)2728Genera isócronas para TODAS las ciudades configuradas.2930```bash31# Instalar dependencias32pip3 install osmnx networkx numpy shapely3334# Calcular todas las ciudades35python3 scripts/precalcular-isocronas.py3637# Calcular una ciudad específica38python3 scripts/precalcular-isocronas.py --ciudad bilbao3940# Listar ciudades disponibles41python3 scripts/precalcular-isocronas.py --listar42```4344### `scripts/calcular-isocronas.py` (helper)4546Calcula una isócrona individual para debugging.4748```bash49# Calcular isócona de coche 30min en Bilbao50python3 scripts/calcular-isocronas.py --ciudad bilbao --modo car --tiempo 3051```5253## Ciudades configuradas5455| Ciudad | Query OSM | Centro | Radio |56|--------|-----------|--------|-------|57| bilbao | Bilbao, Bizkaia, España | 43.263, -2.935 | 15km |58| malaga | Málaga, Andalucía, España | 36.721, -4.421 | 15km |59| sevilla | Sevilla, Andalucía, España | 37.389, -5.984 | 15km |60| valencia | Valencia, España | 39.470, -0.376 | 15km |61| zaragoza | Zaragoza, Aragón, España | 41.649, -0.889 | 15km |6263## Formato de salida6465### JSON combinado (`data/isochrones/{ciudad}.json`)6667```json68{69 "ciudad": "bilbao",70 "centro": [43.263, -2.935],71 "generado": "2026-06-25T18:00:00Z",72 "isochrones": {73 "car": {74 "15": { "geojson": {...}, "area_km2": 12.5, "radio_m": 6000 },75 "30": { "geojson": {...}, "area_km2": 45.2, "radio_m": 12000 },76 "60": { "geojson": {...}, "area_km2": 156.8, "radio_m": 24000 }77 },78 "bike": { ... },79 "foot": { ... }80 }81}82```8384### JSON individual (`data/isochrones/{ciudad}_{modo}_{tiempo}.json`)8586```json87{88 "ciudad": "bilbao",89 "modo": "car",90 "tiempo_min": 30,91 "area_km2": 45.2,92 "radio_m": 12000,93 "geojson": {94 "type": "FeatureCollection",95 "features": [{96 "type": "Feature",97 "geometry": { "type": "Polygon", "coordinates": [...] },98 "properties": { "modo": "car", "minutos": 30, "area_km2": 45.2 }99 }]100 }101}102```103104## Endpoints del servidor105106| Endpoint | Descripción |107|----------|-------------|108| `GET /isochrones/list` | Lista ciudades disponibles con metadata |109| `GET /isochrones/{ciudad}` | Todas las isócronas de una ciudad |110| `GET /isochrones/{ciudad}/{modo}/{min}` | Isócrona específica |111112## Algoritmo OSMnx + NetworkX113114```python115import osmnx as ox116import networkx as nx117118# 1. Descargar grafo de calles119G = ox.graph_from_place("Bilbao, España", network_type='drive')120121# 2. Encontrar nodo más cercano al punto122center = ox.geocode("Plaza Mayor, Bilbao")123origin_node = ox.distance.nearest_nodes(G, center[1], center[0])124125# 3. Calcular distancias desde el origen (Dijkstra)126lengths = nx.single_source_dijkstra_path_length(G, origin_node, cutoff=1800) # 30 min127128# 4. Filtrar nodos alcanzables129reachable_nodes = [n for n, l in lengths.items() if l <= 1800]130131# 5. Generar polígono convexo hull132from shapely.geometry import MultiPoint133points = [(G.nodes[n]['x'], G.nodes[n]['y']) for n in reachable_nodes]134polygon = MultiPoint(points).convex_hull135136# 6. Convertir a GeoJSON137from shapely.geometry import mapping138geojson = mapping(polygon)139```140141## Velocidades por modo y tipo de vía142143| Modo | Velocidad base | Calles principales | Secundarias | Carriles bici |144|------|---------------|-------------------|-------------|---------------|145| car | 50 km/h | Autovías | Urbanas | N/A |146| bike | 15 km/h | Carriles bici | Calles tranquilas | Prioridad |147| foot | 5 km/h | Aceras | Calles peatonales | N/A |148149## Alternativa: CSR binario para Web Workers (ISOTime)150151En vez de generar GeoJSON estático, se puede serializar el grafo OSM a **binario CSR (Compressed Sparse Row)** y cargarlo en un Web Worker para correr Dijkstra en el navegador. Esto permite isocronas en tiempo real 100% client-side, sin API ni servidor.152153**Implementado en ISOTime** (`github.com/Ntizar/ISOTime`):154- Python (OSMnx) → binario CSR (1-2MB por ciudad) → fetch estático → Web Worker → Dijkstra + binary heap → polígono155- 40K nodos se calculan en <1s en el navegador156- Formato: header (32B) + city name (32B) + node coords (f32) + CSR offsets (u32) + edge targets/lengths/speeds157- Ver skill `routing-isochrones` → `references/dijkstra-web-worker-csr.md` para formato completo y código158159## Pitfalls1601611. **OSMnx descarga datos grandes** — El grafo de una ciudad puede ocupar 50-200MB en cache. Usar `cache/` directory.1622. **Tiempo de cálculo** — 1-5 minutos por ciudad dependiendo del tamaño. Ejecutar como batch job.1633. **network_type** — `'drive'` para coche, `'bike'` para bici, `'walk'` para peatón, `'all'` para todos.1644. **cutoff en segundos** — Dijkstra usa segundos, no minutos. 15min = 900s, 30min = 1800s, 60min = 3600s.1655. **convex hull** — El polígono resultante es convex hull, no sigue la costa. Para ciudades costeras, recortar con shoreline.1666. **⚠️ OSMnx OOM en VMs pequeñas (2GB RAM)** — Radio de 20km para Madrid (40K+ nodos) causa OOM (exit 137, killed). Usar **12km máximo** para ciudades grandes (Madrid, Barcelona, Valencia). Ciudades pequeñas (Bilbao, Zaragoza) toleran 15km. Síntoma: el proceso Python muere sin traceback, exit code 137. Solución: reducir `dist` en `graph_from_point()` o usar `graph_from_place()` con un boundary más pequeño.1677. **Primera ejecución** — Descarga datos de Overpass API. Necesita internet solo la primera vez.1688. **Actualizaciones** — Los datos OSM cambian. Recalcular periódicamente (mensual recomendado).1699. **CSR binario vs GeoJSON** — CSR binario es 10-50x más pequeño que GeoJSON y permite cálculo en tiempo real en el navegador. GeoJSON es más simple pero estático (no puedes cambiar el tiempo/modo sin regenerar). Usar CSR para apps client-side, GeoJSON para pre-visualización o export.170171## Alternativa: OSRM Real-time (sin API key) — Boundary Detection172173Cuando 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**.174175**⚠️ 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.176177### Concepto178179```18072 direcciones × 8 radios = 576 puntos radiales181 → OSRM Table endpoint (multi-batch de 89 coords)182 → Para cada dirección: interpolación lineal último alcanzable / primero que excede183 → Polígono irregular que sigue la red vial real184```185186### Algoritmo1871881. **Generar puntos radiales:** 72 direcciones (cada 5°) × radios adaptativos189 - Radios: `[2, 5, 8, 12, 18, 25, 35, 50, 65, 80]` filtrados por `max(minutos * 0.9, 5)`190 - Ejemplo 30min: `[2, 5, 8, 12, 18, 25]` → 432 puntos1911922. **Query OSRM `table` por batches** (max 89 coords, stagger 80ms):193 ```194 GET https://router.project-osrm.org/table/v1/driving/{coords}?annotations=duration195 ```1961973. **Para cada dirección, encontrar boundary:**198 ```javascript199 const ptsDir = resultados.filter(r => r.dir === d).sort((a,b) => a.radioKm - b.radioKm);200 let lastReach = null, firstOver = null;201 for (const p of ptsDir) {202 if (p.dur <= targetSec) lastReach = p;203 else if (!firstOver) { firstOver = p; break; }204 }205 // Interpolación lineal206 if (lastReach && firstOver && firstOver.dur > lastReach.dur) {207 const frac = (targetSec - lastReach.dur) / (firstOver.dur - lastReach.dur);208 rBoundary = lastReach.radioKm + frac * (firstOver.radioKm - lastReach.radioKm);209 }210 ```2112124. **Cerrar polígono** — Conectar boundary points en orden angular (sin convex hull)213214### Resultados comparados (Madrid, 30min coche)215216| Método | Área | Forma |217|--------|------|-------|218| ❌ Convex hull | 1250 km² | Círculo irregular |219| ✅ Boundary detection | 879 km² | Forma irregular real, sigue carreteras |220221### Pitfalls OSRM2222231. **OSRM público solo tiene `driving`** — No hay perfil `foot` o `bicycle`. Para andar, usar ORS o simulación.2242. **Variable case-sensitive** — `dlat` ≠ `dLat`. JS no da error, solo `undefined`.2253. **Max ~89 coordenadas por llamada `table`** — Dividir en batches, stagger 80ms.2264. **Rate limit no documentado** — En la práctica tolerante con stagger.2275. **Sin semáforos ni restricciones horarias** — OSRM usa datos OSM básicos.2286. **No hay datos de elevación** — La velocidad es plana. Para pendientes, usar Valhalla con DEM.2297. **Radios muy bajos = pocos puntos** — Asegurar al menos `[2, 5]` km para tiempos cortos.230231### Cadena de fallback recomendada232233```javascript234async function calcularIsocrona(lng, lat, modo, minutos) {235 // 1. ORS (si hay key — más preciso)236 const apiKey = localStorage.getItem('isotime_ors_key');237 if (apiKey) {238 try { return await calcularIsocronaORS(lng, lat, modo, minutos, apiKey); }239 catch (e) { console.warn('ORS failed:', e.message); }240 }241 // 2. OSRM (solo coche — routing real sin key)242 if (modo === 'car') {243 try { return await calcularIsocronaOSRM(lng, lat, minutos); }244 catch (e) { console.warn('OSRM failed:', e.message); }245 }246 // 3. Simulación (fallback final)247 return calcularIsocronaSim(lng, lat, modo, minutos);248}249```250251## Futuras mejoras2522531. **Shoreline clipping** — Recortar isócronas costeras con Natural Earth data2542. **Valhalla local** — Para isócronas con elevación real (desnivel)2553. **OTP integration** — Isochrone de transporte público con transbordos reales2564. **Cron de actualización** — Recalcular isócronas mensualmente vía cron job2575. **More cities** — Añadir Madrid, Barcelona, etc.