Skip to main content

gtfs-browser-parser

Parser GTFS completo en navegador + catalogo de operadores por ciudad, busqueda por proximidad, fuentes multiples (cache, ZIP subido, localStorage, proxy), integracion mapa y PDF

Ir para a instalação

Informações da origem

Repositório
Ntizar/NtizarBrainMasterMind
Última atividade na origem
26 de junho de 2026 às 12:05
Idioma detectado do SKILL.md
espanhol
Estrelas
2
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Explorador de arquivos
10 arquivos

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
gtfs-browser-parser
description
Parser GTFS completo en navegador + catalogo de operadores por ciudad, busqueda por proximidad, fuentes multiples (cache, ZIP subido, localStorage, proxy), integracion mapa y PDF
version
1.1.0
tags
["gtfs","transporte","parser","navegador","movilidad","isocronas"]
# GTFS Browser Parser — Patrón completo de Integración GTFS ## Descripción Patrón para integrar datos GTFS de transporte público en aplicaciones web del navegador: detección de ciudad y operadores, carga selectiva de paradas cercanas, visualización en mapa Leaflet, informe PDF y exportación GeoJSON. ## Origen Derivado de [nap-dashboard](https://github.com/Ntizar/nap-dashboard) (API NAP España) y [TimeIneco](https://timeineco-ntizar-ntizar.apps.nan.builders/) v0.7 (isocronas de movilidad laboral). ## Arquitectura General ``` User input (address) → geocode → ciudad detectada → catálogo operadores → usuario selecciona → cargar GTFS (cache / upload / proxy / localStorage) → findStopsNear(punto, radio 2km) → filtrar por Haversine → UI: chips de rutas + marcadores mapa + tabla detalle → PDF: seccion "Rutas de transporte publico disponibles" → Export: GeoJSON de paradas cercanas ``` ## Estrategia de Carga: Filtrar en Origen **REGLAS DE ORO:** 1. **No descargar todo el GTFS** — un feed de ciudad puede pesar 50-200 MB. Solo extrae paradas cercanas al punto de interés del usuario. 2. **El usuario elige el operador** — muestra catálogo de operadores por ciudad, el usuario selecciona uno, solo entonces descargas/cargas su GTFS. 3. **Múltiples fuentes, por orden de prioridad:** - Cache simulado (JSON embebido para demo) - localStorage (datos previamente cargados) - ZIP subido por usuario (el método más práctico) - Proxy servidor (descarga desde URL pública) ## Patrón: Catálogo de Operadores por Ciudad ```javascript const EMPRESAS = { madrid: [ { id: 'emt-madrid', nombre: 'EMT Madrid', modo: 'bus', lineas: 217, cacheable: true, // cacheable = datos simulados embebidos gtfsUrl: 'https://opendata.emtmadrid.es/GTFS/Madrid' }, { id: 'metro-madrid', nombre: 'Metro de Madrid', modo: 'metro', lineas: 13, cacheable: false } // requiere subida de ZIP ], barcelona: [ { id: 'tmb-bus', nombre: 'TMB (Autobus)', modo: 'bus', lineas: 107, cacheable: false } ], // ~12+ ciudades espanolas con operadores catalogados }; function detectarCiudad(displayName) { // 1. Buscar en EMPRESAS // 2. Fallback: matchear contra lista de ~50 ciudades // 3. Si no hay match, mostrar opcion de subir GTFS manualmente } ``` ## Patrón: Búsqueda por Proximidad (Haversine) Solo extraer paradas dentro de un radio del punto de origen (default 2 km): ```javascript function findStopsNear(lat, lng, radiusKm = 2) { const results = []; for (const stop of stops) { const dist = haversine(lat, lng, stop.stop_lat, stop.stop_lon); if (dist <= radiusKm) { const routeIds = stopRoutes[stop.stop_id] || []; results.push({ stop, distanciaKm: dist, routes: routeIds }); } } results.sort((a, b) => a.distanciaKm - b.distanciaKm); return results; } function haversine(lat1, lon1, lat2, lon2) { const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } ``` ## Patrón: Ruteo Inferido (sin stop_times) Cuando no hay `stop_times.txt` ni `trips.txt`, inferir rutas por coincidencia semántica de nombres: ```javascript // Modo 1: Coincidencia de palabras entre route_long_name y stop_name for (const route of data.routes) { const routeWords = route.route_long_name.toLowerCase(); for (const stop of data.stops) { for (const word of routeWords.split(/[\s/]+/)) { if (word.length > 3 && stop.stop_name.toLowerCase().includes(word)) { stopRoutes[stop.stop_id].add(route.route_id); } } } } // Modo 2: Fallback — paradas céntricas reciben rutas base const centralStops = stops.filter(s => s.stop_name.includes('Sol') || s.stop_name.includes('Gran Via') || ...); ``` ## Patrón: Múltiples Fuentes de Datos ### 1. Cache simulado (JSON embebido) ```javascript function cargarDesdeCache(cacheData, operador, ciudad) { // cacheData: { routes, stops, stop_times?, trips?, shapes?, _meta } construirIndiceStopRoutes(cacheData); guardarEstado(operador, ciudad); persistirEnLocalStorage(ciudad, operador, data); } ``` ### 2. ZIP subido por usuario ```javascript async function cargarDesdeZip(file, operador, ciudad) { const zip = await JSZip.loadAsync(file); const stops = parsearCSV(await leerCSVZip(zip, 'stops.txt')); const routes = parsearCSV(await leerCSVZip(zip, 'routes.txt')); const trips = parsearCSV(await leerCSVZip(zip, 'trips.txt').catch(()=>'')); const stopTimes = parsearCSV(await leerCSVZip(zip, 'stop_times.txt').catch(()=>'')); // Construir indice stop→routes via trips+stopTimes // Cargar en estado y cachear en localStorage } ``` ### 3. Proxy servidor (para CORS) ```javascript // server.mjs if (req.url.startsWith('/gtfs-download') && req.method === 'POST') { const { url } = JSON.parse(body); https.get(url, { headers: { 'User-Agent': 'App/1.0' } }, (proxyRes) => { const chunks = []; proxyRes.on('data', chunk => chunks.push(chunk)); proxyRes.on('end', () => { res.end(Buffer.concat(chunks)); // Streamear ZIP al cliente }); }); } ``` ## Patrón: Integración con Mapa Leaflet ```javascript const iconoParada = L.divIcon({ html: '<div style="width:10px;height:10px;background:#a855f7;border:2px solid #fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,0.3)"></div>', iconSize: [10, 10], iconAnchor: [5, 5], className: '' }); for (const item of stopsNear) { L.marker([item.stop.stop_lat, item.stop.stop_lon], { icon: iconoParada }) .bindPopup(`${item.stop.stop_name} (${item.distanciaKm.toFixed(2)} km)<br>${item.routes.length} rutas`) .addTo(capaParadas); } ``` ## Patrón: Integración con PDF (jsPDF) Sección en informes PDF con: - Tabla de paradas cercanas (nombre, distancia, líneas que sirven) - Líneas destacadas con paradas que cubren - Operador, fuente de datos y modo predominante ```javascript if (gtfsData && gtfsData.totalStops > 0) { // Tabla con autoTable: cabecera morada (#a855f7) // Columnas: Parada | Distancia | Lineas // Max 12 paradas, 10 lineas destacadas } ``` ## Patrón: Exportación GeoJSON ```javascript function exportarParadasGeoJSON() { const features = stops.map(s => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [s.stop_lon, s.stop_lat] }, properties: { stop_id: s.stop_id, stop_name: s.stop_name } })); return { type: 'FeatureCollection', features, properties: { fuente: `TimeIneco GTFS — ${operador}`, totalStops: features.length } }; } ``` ## Patrón: Resumen para UI ```javascript function getRouteSummary(stopsNear) { const rutasUnicas = new Map(); for (const item of stopsNear) { for (const r of item.routes) { if (!rutasUnicas.has(r.id)) rutasUnicas.set(r.id, { ...r, paradas: [] }); rutasUnicas.get(r.id).paradas.push(item.stop.stop_name); } } return { operador, ciudad, totalStops: stopsNear.length, totalRoutes: rutasUnicas.size, rutas: [...rutasUnicas.values()], modoPredominante, datosFuente }; } ``` ## CSV Parser (con soporte de comillas) ```javascript function parsearCSV(text) { const lines = text.trim().split('\n'); if (lines.length < 2) return []; const headers = lines[0].split(',').map(h => h.trim()); const result = []; for (let i = 1; i < lines.length; i++) { const values = parsearLineaCSV(lines[i]); if (values.length !== headers.length) continue; const row = {}; for (let j = 0; j < headers.length; j++) row[headers[j]] = values[j] || ''; result.push(row); } return result; } function parsearLineaCSV(line) { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { if (line[i] === '"') inQuotes = !inQuotes; else if (line[i] === ',' && !inQuotes) { result.push(current.trim()); current = ''; } else current += line[i]; } result.push(current.trim()); return result; } ``` ## Implementación Recomendada en Hermes 1. Usar `JSZip` (CDN en index.html) para descompresión ZIP del lado cliente 2. Catálogo de operadores como constante JS (con cacheable flag) 3. Motor GTFS: `gtfs-engine.js` con estado global, findStopsNear, getRouteSummary 4. Panel NAP: `nap.js` como facade de UI con manejo de eventos delegados 5. Mapa: marcadores Leaflet con divIcon y popups informativos 6. PDF: jsPDF + autoTable, colores #a855f7 para cabeceras GTFS 7. Cache en localStorage con prefijo `timeineco_gtfs_` 8. Evento `gtfs:loaded` para sincronizar carga de GTFS con mapa ## Scripts del skill - `scripts/server-gtfs.py` — Servidor mínimo con /api/zips para auto-carga de ZIPs. `python server-gtfs.py` desde la raiz del proyecto. ## Patrón: Auto-carga de ZIPs desde servidor local (v3.0) **NUEVO en 2026-06-23:** En vez de arrastrar ZIPs manualmente, el visor se conecta a un servidor mínimo que lista los ZIPs disponibles en `data/` y los carga automáticamente. ### Servidor mínimo (server.py) ```python # visor/server.py — servidor mínimo HTTP con /api/zips import http.server, json, os from pathlib import Path class GTFSServer(http.server.SimpleHTTPRequestHandler): def do_GET(self): if self.path == '/api/zips': self.serve_zips_list() return super().do_GET() def serve_zips_list(self): zips = [] for root, dirs, files in os.walk(DATA_DIR): for f in files: if f.endswith('.zip'): full_path = Path(root) / f rel_path = full_path.relative_to(BASE_DIR) zips.append({ "name": f, "path": str(rel_path).replace("\\", "/"), "size": full_path.stat().st_size, "size_human": f"{full_path.stat().st_size / 1024 / 1024:.1f} MB" }) zips.sort(key=lambda x: x["name"]) self.send_response(200) self.send_header('Content-Type', 'application/json') self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() self.wfile.write(json.dumps(zips, indent=2).encode()) ``` ### Auto-carga en el cliente ```javascript async function autoLoadZips() { let zipsList = []; try { const resp = await fetch('/api/zips'); if (resp.ok) zipsList = await resp.json(); } catch (e) {} if (zipsList.length === 0) return; const MAX_ZIPS = 25; const toLoad = zipsList.slice(0, MAX_ZIPS); for (const zf of toLoad) { const resp = await fetch('/' + zf.path); const blob = await resp.blob(); const zip = await JSZip.loadAsync(blob); await processGTFS(zip, zf.name); } } ``` ### Launcher para Windows (.bat) ```bat @echo off chcp 65001 >nul python --version >nul 2>&1 || (echo [ERROR] Python no encontrado & pause & exit /b 1) cd visor python server.py ``` ### Patrón: Panel de horarios desplegable Al hacer clic en una parada → panel deslizante desde abajo con rutas y horarios. ```javascript function showSchedulePanel(stop) { const routesData = stop.routes.map(rid => allRoutes[rid]).filter(Boolean); // Filtros de ruta como botones clickeables // Cards expandibles por ruta con tabla de horarios const panel = document.getElementById('schedulePanel'); panel.innerHTML = htmlContent; panel.classList.add('open'); } ```
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub