Skip to main content

routing-isochrones

Patrones para construir herramientas de isocronas y routing: OpenRouteService, OpenTripPlanner, GTFS/NAP, geocodificación Nominatim. Arquitectura de plugins para motores de routing intercambiables.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
Ntizar/MasterMind
آخر نشاط في المصدر
١٥ سبتمبر ٢٠٢٦ في ١٥:٢٠
لغة SKILL.md المكتشفة
الإسبانية
النجوم
٢
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
25 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
routing-isochrones
description
Patrones para construir herramientas de isocronas y routing: OpenRouteService, OpenTripPlanner, GTFS/NAP, geocodificación Nominatim. Arquitectura de plugins para motores de routing intercambiables.
version
1.2.0
author
David Antizar
tags
["routing","isochrones","gtfs","openrouteservice","opentripplanner","nominatim","leaflet","vanilla-js","mobility"]
# Routing & Isocronas — Patrón de Herramienta ## Cuándo cargar esta skill Cuando el usuario pida: isocronas, mapas de accesibilidad, cálculo de rutas, transporte público con horarios, planes de movilidad, "hasta dónde llego en X minutos", routing multi-modal, GTFS, NAP transportes. ## Concepto Herramienta web que calcula isocronas y rutas de movilidad desde cualquier punto: coche, bicicleta, peatón y transporte público. Pones origen + destino + horario objetivo, obtienes un informe con isocronas y rutas de bus disponibles. **Arquitectura clave:** Sistema de plugins para motores de routing intercambiables. Cada backend (ORS, OTP, NAP) implementa la misma interfaz. --- ## Patrones de UI (dos variantes) ### Variante A: Punto de interés (simple) Cuando el usuario quiere "hasta dónde llego desde X" — un solo punto, no formulario de ruta: ``` ┌─────────────────────────────────────────────┐ │ Header oscuro (título + subtítulo) │ ├──────────┬──────────────────────────────────┤ │ Sidebar │ Mapa (CARTO light tiles) │ │ │ │ │ Modo │ [click en mapa → punto] │ │ (4 btns) │ │ │ │ │ │ Tiempo │ │ │ (slider) │ │ │ │ │ │ Dirección│ │ │ (input) │ │ │ │ │ │ Calcular │ │ │ │ │ │ PDF │ │ │ │ │ │ Resultados│ │ │ (KPIs) │ │ └──────────┴──────────────────────────────────┘ ``` - **4 botones de modo:** coche 🚗, bici 🚲, andando 🚶, bus 🚌 - **Slider de tiempo:** 5-60 min con presets rápidos (10, 15, 30, 45, 60) - **Input de dirección:** con debounce 800ms + click en mapa para poner punto - **Sidebar limpia:** fondo blanco, bordes sutiles, sin gradientes - **Mapa:** CARTO light tiles, Canvas renderer ### Variante B: Origen + Destino (completa) Para planes de movilidad laboral con horarios GTFS: ``` Origen (casa) + Destino (oficina) + Horario → Isocronas + Rutas bus ``` --- ## Diseño visual — Reglas críticas **David odia el "look de IA" (dark, neón, glass, gradientes).** Para herramientas de movilidad: - ✅ **Header oscuro** (`#1a1a2e`) + **sidebar blanca** + **mapa CARTO light** - ✅ **Botones con bordes sutiles**, colores por modo (azul=bici, naranja=coche, verde=andando, púrpura=bus) - ✅ **Tipografía system font** (-apple-system, BlinkMacSystemFont, Segoe UI) - ✅ **KPIs en grid 2x2** con fondo gris claro - ❌ **NUNCA** gradientes Aurora, glassmorphism, efectos neón - ❌ **NUNCA** fondo oscuro en la app completa - ❌ **NUNCA** decoraciones innecesarias **CSS base:** `background: #f8f9fa`, `color: #1a1a2e`, `font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` --- ## Arquitectura de Plugins ```javascript // js/plugins.js const PLUGINS = { ors: ORSRouter, // OpenRouteService (coche/bici/peatón) otp: OTPRouter, // OpenTripPlanner (transit con transbordos) nap: GTFSNapRouter // NAP/GTFS España (horarios reales) }; // Registrar nuevo plugin registerPlugin('name', { resolve(origin, dest, mode) { ... }, getIsochrones(point, time, mode) { ... } }); ``` **Para añadir un nuevo motor:** 1. Crear `js/routing-{name}.js` 2. Implementar `resolve()` y `getIsochrones()` 3. Registrar: `registerPlugin('name', router)` ### Interfaz IRouter ```javascript class IRouter { // Calcular ruta entre origen y destino async resolve(origin, dest, mode) { // origin: {lat, lng, name} // dest: {lat, lng, name} // mode: 'car' | 'bike' | 'walk' | 'transit' // Returns: {distance, duration, geometry, steps, mode} } // Calcular isocrona desde un punto async getIsochrones(point, time, mode) { // point: {lat, lng} // time: segundos // Returns: {geojson, area, success} // geojson: FeatureCollection (Polygon) // area: km² (number) // success: boolean // error: string (solo si success=false) } } ``` **⚠️ Shape consistente:** El return de `getIsochrones` debe tener SIEMPRE la misma forma. Si el backend real falla o no hay API key, devolver `{ geojson: simulatedData, area: estimatedArea, success: true }` en vez de tirar error. --- ## Stack tecnológico | Componente | Tecnología | Justificación | |---|---|---| | Mapa | Leaflet (Canvas renderer) | Ligero, sin framework, ya probado | | Isocronas | OpenRouteService API | Gratis, 3 modos, desnivel incluido | | Routing TP | OpenTripPlanner | Transbordos reales, GTFS | | Geocodificación | Nominatim (OSM) | Gratis, no requiere key | | PDF | jsPDF + autoTable + html2canvas | Generación cliente con captura de mapa | | CSS | Simple/clean (NO Aurora glass) | Header oscuro + sidebar blanca + CARTO light | | JS | Vanilla ES modules | Sin bundler, un solo HTML | --- ## OpenRouteService (ORS) v2 **CRITICAL:** The v2 API changed from the v1 format shown in old docs. The correct endpoint and body format are below. ### Dos patrones de acceso a ORS #### Patrón A: Server-side proxy (recomendado para apps privadas/produción) **NO llamar a ORS directamente desde el navegador** si la API key es del servidor — se expondría. Usar proxy: ```javascript // server.mjs — proxy endpoint if (req.method === 'POST' && req.url.startsWith('/isochrone')) { const ORS_KEY = process.env.ORS_API_KEY; if (!ORS_KEY) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'ORS_API_KEY no configurada', fallback: true })); return; } let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { const { profile, locations, range } = JSON.parse(body); const bodyObj = { locations: [locations], range, range_type: 'time', attributes: ['area'] }; // ⚠️ NO incluir 'interval' para rango único — ORS lo rechaza con 400 if (range.length > 1) bodyObj.interval = range[0]; const options = { hostname: 'api.openrouteservice.org', path: `/v2/isochrones/${profile}`, method: 'POST', headers: { 'Authorization': ORS_KEY, 'Content-Type': 'application/json; charset=utf-8', 'Accept': 'application/json, application/geo+json' } }; const proxyReq = https.request(options, (proxyRes) => { let data = ''; proxyRes.on('data', chunk => data += chunk); proxyRes.on('end', () => { res.writeHead(proxyRes.statusCode, { 'Content-Type': 'application/json' }); res.end(data); }); }); proxyReq.on('error', (err) => { res.writeHead(502); res.end(JSON.stringify({ error: err.message, fallback: true })); }); proxyReq.write(JSON.stringify(bodyObj)); proxyReq.end(); }); return; } ``` ### Request body (correct v2 format) ``` POST https://api.openrouteservice.org/v2/isochrones/{profile} Headers: Authorization: {api_key} Content-Type: application/json; charset=utf-8 Accept: application/json, application/geo+json Body: { "locations": [[lng, lat]], // single pair, NOT [{lat, lng}] "range": [900], // seconds (15 min) "range_type": "time", "attributes": ["area"] // returns area in m² // DO NOT include "interval" for single-range (see quirk below) } ``` ### ⚠️ ORS v2 API quirks 1. **`interval` quirk (CRITICAL):** Adding `"interval": [900]` with a single-range request causes ORS to respond `400: Parameter 'interval' has incorrect value or format.` Never include `interval` for single-range requests. Only use it when auto-generating multiple ranges (e.g. range: [900], interval: 300). 2. **Error response format:** ORS returns errors as string or object. Parse defensively: ```javascript const errMsg = typeof errData.error === 'string' ? errData.error : errData.error?.message || errData.error?.error || resp.statusText; ``` 3. **Rate limiting (429):** Free ORS tier ≈ 1 req/s. With 12 isochrones (4×3), parallel `Promise.all()` triggers 429. Solution: stagger sequentially with 300-1000ms delay between each request. 4. **"Access to this API has been disallowed" (403):** The key exists but lacks isochrone permissions. Some ORS keys work for routing (`/v2/directions`) but NOT for isochrones (`/v2/isochrones`). This is a permissions issue, not a format issue. **Diagnose:** call `/isochrone` from server and check healthz `ors_api` field. If `ors_api: false`, the key is invalid or lacks permissions. **Fix:** create a new key at openrouteservice.org (free tier includes isochrones if registered). Old keys from v1 era may not have isochrone scope. 5. **No transit profile in ORS** — Use `driving-car` as approximation for bus, metro, and tram. These are NOT accurate — they show road travel range, not transit network range. Label them clearly in the UI as "aproximación por carretera" and note that real transit data comes from GTFS/NAP. 6. **Area from ORS:** `features[0].properties.area` is in **m²**. Divide by 1,000,000 for km². ### Pattern: Async fallback con stagger ```javascript const ORS_PROFILES = {\n car: 'driving-car', bike: 'cycling-regular',\n foot: 'foot-walking', bus: 'driving-car',\n metro: 'driving-car', tram: 'driving-car'\n}; export async function calcularIsocronaAsync(lng, lat, modo, minutos) { try { // Intentar ORS real const resp = await fetch('/isochrone', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: ORS_PROFILES[modo], locations: [lng, lat], range: [minutos * 60] }), signal: AbortSignal.timeout(15000) }); if (!resp.ok) { const errData = await resp.json().catch(() => ({})); if (errData.fallback) throw new Error('ORS no disponible (sin key)'); const errMsg = typeof errData.error === 'string' ? errData.error : errData.error?.message || errData.error?.error || resp.statusText; throw new Error(`ORS HTTP ${resp.status}: ${errMsg}`); } const data = await resp.json(); const areaKm2 = (data.features?.[0]?.properties?.area || 0) / 1_000_000; return { geojson: data, areaKm2, real: true }; } catch (err) { console.warn(`⚠️ ORS fallback ${modo} ${minutos}min: ${err.message}`); } return { ...calcularIsocronaSim(lng, lat, modo, minutos), real: false }; } export async function calcularTodasAsync(punto, modos, tiempos) { const resultados = []; for (const modo of modos) { for (const min of tiempos) { const r = await calcularIsocronaAsync(punto.lng, punto.lat, modo, min).catch( e => ({ modo, minutos: min, geojson: null, areaKm2: 0, error: e.message, real: false }) ); resultados.push({ modo, minutos: min, ...r }); await new Promise(r => setTimeout(r, 300)); // stagger } } return resultados; } ``` ### Health check con validación de key ```javascript // server.mjs /healthz res.end(JSON.stringify({ status: 'ready', uptime: process.uptime(), checks: { ors_api: typeof ORS_KEY === 'string' && ORS_KEY.length > 20 } })); // ↑ Más robusto que !!ORS_KEY (detecta strings vacíos) ``` #### Patrón B: Client-side directo (para herramientas públicas con key del usuario) **Cuándo usar:** Herramientas públicas tipo Pages donde el usuario provee su propia API key de ORS (free tier 2000 req/día). La key se almacena en `localStorage` del usuario, no en el código. **Ventaja:** Sin servidor. Despliegue 100% estático (GitHub Pages, Netlify, etc.) **Riesgo:** La key es visible en DevTools. Aceptable para keys free-tier de uso personal. **ISOTime** (`github.com/Ntizar/ISOTime`) es un ejemplo funcional de este patrón: - HTML único + ES modules, sin bundler - ORS API v2 llamado directamente desde `fetch()` con `Authorization: key` - Key en `localStorage` (modal de setup首次, luego se lee) - Fallback simulado si no hay key (polígono con jitter) - Export GeoJSON + SHP binario en-browser (JSZip) - Tiles IGN WMTS (EPSG:3857) como alternativa a CARTO light ```javascript // Patrón ISOTime: acceso directo ORS desde el navegador async function calcularIsocrona(lng, lat, modo, minutos) { const apiKey = localStorage.getItem('ors_api_key'); if (!apiKey) return calcularIsocronaSim(lng, lat, modo, minutos); const profile = { car: 'driving-car', walk: 'foot-walking', bike: 'cycling-regular' }[modo]; const resp = await fetch(`https://api.openrouteservice.org/v2/isochrones/${profile}`, { method: 'POST', headers: { 'Authorization': apiKey, 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub