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/NtizarBrainMasterMind
最近来源活动
2026年7月1日 06:55
检测到的 SKILL.md 语言
西班牙语
星标
2
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
13 个文件

正在显示 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 查看