Some checks failed
Auto Tag on Merge to Main / auto-tag (push) Successful in 27s
CI - Lint and Build / lint-backend (push) Failing after 30s
CI - Lint and Build / lint-frontend (push) Failing after 2s
CI - Lint and Build / build-frontend (push) Has been skipped
CI - Lint and Build / docker-build-test (push) Has been skipped
Complete real-time train tracking system for Spanish railways (Renfe/Cercanías): - Backend API (Node.js/Express) with GTFS-RT polling workers - Frontend dashboard (React/Vite) with Leaflet maps - Real-time updates via Socket.io WebSocket - PostgreSQL/PostGIS database with Flyway migrations - Redis caching layer - Docker Compose configuration for development and production - Gitea CI/CD workflows (lint, auto-tag, release) - Production deployment with nginx + Let's Encrypt SSL 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
43 KiB
43 KiB
Arquitectura Sistema de Tracking de Trenes en Tiempo Real
Visión General
Sistema web para visualizar en tiempo real la posición de todos los trenes operados por Renfe en España, con capacidad de consultar histórico mediante un timeline slider.
Fuentes de Datos
📘 Nota: Para información detallada de todas las fuentes disponibles, ver FUENTES_DATOS.md
Nivel 1: Datos en Tiempo Real (Core)
GTFS-RT Vehicle Positions (IMPLEMENTADO)
- URL:
https://gtfsrt.renfe.com/vehicle_positions.pb - Formato: Protocol Buffer (GTFS Realtime)
- Frecuencia: Cada 30 segundos
- Información:
- Posición GPS (latitud, longitud)
- Estado del vehículo (parado, en movimiento)
- Velocidad y dirección (bearing)
- Identificadores (train_id, trip_id, route_id)
- Servicios: Principalmente Cercanías
- Uso: Visualización en tiempo real en mapa
GTFS-RT Trip Updates
- URL:
https://gtfsrt.renfe.com/trip_updates_cercanias.pb - Formato: Protocol Buffer (GTFS Realtime)
- Frecuencia: Cada 30 segundos
- Información:
- Cancelaciones de servicios
- Retrasos y adelantos
- Cambios de horario
- Predicciones de llegada/salida
- Servicios: Cercanías + AVE + LD + MD
- Uso: Alertas, predicciones, estadísticas de puntualidad
GTFS-RT Service Alerts
- URL:
https://gtfsrt.renfe.com/alerts.pb - Formato: Protocol Buffer (GTFS Realtime)
- Frecuencia: Cada 30 segundos
- Información:
- Incidencias en la red
- Servicios de autobús sustitutorio
- Problemas de accesibilidad
- Cierres de vías
- Uso: Notificaciones push, panel de alertas
Nivel 2: Datos Estáticos (Enriquecimiento)
GTFS Static de Renfe
- URL:
https://data.renfe.com/dataset?res_format=GTFS/ - Formato: ZIP con archivos CSV
- Frecuencia de actualización: Semanal/Mensual
- Vigencia actual: 18 marzo 2025 - 15 diciembre 2025
- Cobertura: 366 rutas, 769 paradas
- Archivos principales:
routes.txt: Rutas y líneasstops.txt: Estaciones y paradastrips.txt: Viajes programadosstop_times.txt: Horarioscalendar.txt: Días de operaciónshapes.txt: Geometría de rutas (si disponible)
- Uso:
- Enriquecer nombres de rutas
- Mostrar horarios programados vs reales
- Calcular retrasos
- Visualizar rutas en mapa
Estaciones de Renfe
- URL:
https://data.renfe.com/dataset(buscar "estaciones") - Formato: JSON, CSV, XML
- Información:
- Coordenadas GPS precisas
- Servicios disponibles
- Accesibilidad (Atendo)
- Código de estación
- Instalaciones
- Uso:
- Enriquecer mapa con marcadores de estaciones
- Filtrar por estaciones accesibles
- Mostrar servicios disponibles
Nivel 3: Infraestructura Ferroviaria
ADIF - IDEADIF (Web Map Service)
- URL:
https://ideadif.adif.es/ - Protocolo: WMS (OGC 1.1.1, 1.3.0)
- Normativa: INSPIRE (Transport Networks)
- Información:
- Trazado de vías
- Topología de la red ferroviaria
- Estaciones e instalaciones
- Túneles y puentes
- Uso:
- Overlay en mapa mostrando vías
- Visualización de infraestructura
- Contexto geográfico de posiciones
ADIF - PISERVI
- URL:
https://www.adif.es/ - Información:
- Características técnicas de instalaciones
- Terminales de mercancías
- Instalaciones de mantenimiento
- Cambiadores de ancho de vía
- Uso: Información complementaria de estaciones
Nivel 4: Datos Regionales y Complementarios
Consorcios Regionales
- CRTM (Madrid): Cercanías Madrid, Metro, Tren Ligero
- CTB (Bilbao): Cercanías Bilbao, EuskoTren
- TMB (Barcelona): Rodalies Barcelona, FGC
- Formato: GTFS, GTFS-RT
- Uso: Integración con redes locales
Datos Meteorológicos (Futuro)
- AEMET OpenData: Condiciones climáticas
- Uso: Correlación retrasos con meteorología
Datos Estadísticos
- Observatorio del Ferrocarril: Informes anuales
- Ministerio de Transportes: Estadísticas mensuales
- Uso: Analytics, KPIs, tendencias
Arquitectura Propuesta
Stack Tecnológico
Frontend
- Framework: React + TypeScript
- Mapa: Leaflet.js con tiles de OpenStreetMap
- UI Components: Tailwind CSS + shadcn/ui
- Gestión de estado: Zustand o Redux Toolkit
- Timeline: React Range Slider o Timeline personalizado
- Cliente WebSocket: native WebSocket API
Backend
- Runtime: Node.js
- Framework: Express.js o Fastify
- Base de datos: PostgreSQL + PostGIS (datos geoespaciales)
- Cache: Redis (últimas posiciones)
- WebSockets: Socket.io
- Parser GTFS-RT: gtfs-realtime-bindings (npm)
Infraestructura
- Hosting: Docker containers
- Reverse Proxy: Nginx
- Proceso de fondo: Node.js worker para polling
Diagrama de Componentes
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Mapa OSM │ │ Panel Info │ │ Timeline │ │
│ │ (Leaflet) │ │ Tren │ │ Slider │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────┘ │
│ │ │
│ WebSocket Client │
└───────────────────────────┼──────────────────────────────────┘
│
┌───────▼──────────┐
│ NGINX (proxy) │
└───────┬──────────┘
│
┌───────────────────────────┼──────────────────────────────────┐
│ BACKEND │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ WebSocket Server (Socket.io) │ │
│ │ - Broadcast posiciones en tiempo real │ │
│ │ - Gestión de salas por región/tipo tren │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼─────────────────────────────┐ │
│ │ API REST (Express/Fastify) │ │
│ │ - GET /trains/current (últimas posiciones) │ │
│ │ - GET /trains/history (histórico por rango) │ │
│ │ - GET /trains/:id (info específica) │ │
│ │ - GET /routes (rutas disponibles) │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐ │
│ │ Redis │ │PostgreSQL│ │ Worker │ │
│ │ Cache │ │ +PostGIS │ │ Process │ │
│ │ │ │ │ │ │ │
│ │ Últimas │ │ Histórico│ │ Polling │ │
│ │Posiciones│ │ Posiciones│ │ GTFS-RT │ │
│ └──────────┘ │ Rutas │ │ │ │
│ │ Paradas │ └────┬─────┘ │
│ └──────────┘ │ │
└─────────────────────────────────────────┼──────────────────┘
│
┌─────────────▼─────────────┐
│ Renfe GTFS-RT Feed │
│ vehicle_positions.pb │
│ (polling cada 30 seg) │
└───────────────────────────┘
Modelo de Datos
Base de Datos Principal (PostgreSQL + PostGIS)
Tabla: train_positions
CREATE TABLE train_positions (
id BIGSERIAL PRIMARY KEY,
train_id VARCHAR(50) NOT NULL,
trip_id VARCHAR(100),
route_id VARCHAR(50),
position GEOGRAPHY(POINT, 4326) NOT NULL, -- PostGIS
latitude DECIMAL(10, 7) NOT NULL,
longitude DECIMAL(10, 7) NOT NULL,
bearing DECIMAL(5, 2), -- Dirección 0-360°
speed DECIMAL(5, 2), -- km/h
status VARCHAR(20), -- STOPPED, MOVING, etc.
timestamp TIMESTAMP NOT NULL,
recorded_at TIMESTAMP DEFAULT NOW(),
INDEX idx_train_timestamp (train_id, timestamp),
INDEX idx_position (position) USING GIST,
INDEX idx_recorded_at (recorded_at)
);
-- Particionamiento por fecha para optimizar consultas históricas
CREATE TABLE train_positions_2025_01 PARTITION OF train_positions
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
Tabla: trains
CREATE TABLE trains (
train_id VARCHAR(50) PRIMARY KEY,
route_id VARCHAR(50),
train_type VARCHAR(50), -- AVE, Cercanías, Regional, etc.
service_name VARCHAR(100),
first_seen TIMESTAMP,
last_seen TIMESTAMP,
is_active BOOLEAN DEFAULT true
);
Tabla: routes
CREATE TABLE routes (
route_id VARCHAR(50) PRIMARY KEY,
route_name VARCHAR(200),
route_type VARCHAR(50),
origin_station VARCHAR(100),
destination_station VARCHAR(100),
color VARCHAR(7) -- Para visualización en mapa
);
Tabla: stations
CREATE TABLE stations (
station_id VARCHAR(50) PRIMARY KEY,
station_name VARCHAR(200),
position GEOGRAPHY(POINT, 4326),
latitude DECIMAL(10, 7),
longitude DECIMAL(10, 7),
station_type VARCHAR(50)
);
Cache Redis
Estructura de claves:
trains:current:{train_id} → JSON con última posición
trains:active → SET con IDs de trenes activos
trains:by_type:{type} → SET con IDs por tipo
stats:last_update → Timestamp última actualización
Flujo de Datos
1. Recolección de Datos (Worker Process)
// Pseudocódigo del worker
async function pollGTFSRT() {
const response = await fetch('https://gtfsrt.renfe.com/vehicle_positions.pb');
const buffer = await response.arrayBuffer();
const feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
new Uint8Array(buffer)
);
for (const entity of feed.entity) {
if (entity.vehicle) {
const position = {
train_id: entity.vehicle.vehicle.id,
trip_id: entity.vehicle.trip?.trip_id,
route_id: entity.vehicle.trip?.route_id,
latitude: entity.vehicle.position.latitude,
longitude: entity.vehicle.position.longitude,
bearing: entity.vehicle.position.bearing,
speed: entity.vehicle.position.speed,
status: entity.vehicle.current_status,
timestamp: new Date(entity.vehicle.timestamp * 1000)
};
// Guardar en Redis (tiempo real)
await redis.setex(
`trains:current:${position.train_id}`,
300,
JSON.stringify(position)
);
// Guardar en PostgreSQL (histórico)
await db.insertTrainPosition(position);
// Broadcast vía WebSocket
io.emit('train:update', position);
}
}
}
// Ejecutar cada 30 segundos
setInterval(pollGTFSRT, 30000);
2. API REST Endpoints
// GET /api/trains/current
// Retorna todas las posiciones actuales desde Redis
router.get('/trains/current', async (req, res) => {
const trainIds = await redis.smembers('trains:active');
const positions = await Promise.all(
trainIds.map(id => redis.get(`trains:current:${id}`))
);
res.json(positions.map(JSON.parse));
});
// GET /api/trains/history?from=timestamp&to=timestamp&train_id=xxx
// Consulta histórico de PostgreSQL
router.get('/trains/history', async (req, res) => {
const { from, to, train_id } = req.query;
const positions = await db.query(`
SELECT * FROM train_positions
WHERE timestamp BETWEEN $1 AND $2
${train_id ? 'AND train_id = $3' : ''}
ORDER BY timestamp ASC
`, [from, to, train_id].filter(Boolean));
res.json(positions);
});
// GET /api/trains/:id
// Información detallada de un tren
router.get('/trains/:id', async (req, res) => {
const train = await db.getTrainInfo(req.params.id);
const currentPosition = await redis.get(`trains:current:${req.params.id}`);
res.json({ ...train, current: JSON.parse(currentPosition) });
});
3. Frontend - Componentes Principales
Mapa (MapView.tsx)
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
function MapView({ trains, selectedTrain, onTrainClick }) {
return (
<MapContainer center={[40.4168, -3.7038]} zoom={6}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© OpenStreetMap contributors'
/>
{trains.map(train => (
<Marker
key={train.train_id}
position={[train.latitude, train.longitude]}
icon={getTrainIcon(train.train_type)}
eventHandlers={{
click: () => onTrainClick(train)
}}
>
<Popup>
<TrainInfo train={train} />
</Popup>
</Marker>
))}
</MapContainer>
);
}
Timeline Slider (TimelineSlider.tsx)
function TimelineSlider({ onTimeChange, minTime, maxTime }) {
const [currentTime, setCurrentTime] = useState(Date.now());
const [isPlaying, setIsPlaying] = useState(true);
useEffect(() => {
if (isPlaying) {
const interval = setInterval(() => {
setCurrentTime(Date.now());
onTimeChange(Date.now());
}, 1000);
return () => clearInterval(interval);
}
}, [isPlaying]);
return (
<div className="timeline-container">
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<input
type="range"
min={minTime}
max={maxTime}
value={currentTime}
onChange={(e) => {
setIsPlaying(false);
setCurrentTime(Number(e.target.value));
onTimeChange(Number(e.target.value));
}}
/>
<span>{new Date(currentTime).toLocaleString()}</span>
</div>
);
}
WebSocket Hook (useTrainUpdates.ts)
function useTrainUpdates() {
const [trains, setTrains] = useState<Train[]>([]);
useEffect(() => {
const socket = io('ws://localhost:3000');
socket.on('train:update', (position) => {
setTrains(prev => {
const idx = prev.findIndex(t => t.train_id === position.train_id);
if (idx >= 0) {
const updated = [...prev];
updated[idx] = position;
return updated;
} else {
return [...prev, position];
}
});
});
return () => socket.disconnect();
}, []);
return trains;
}
Características del Sistema
Tiempo Real
- Actualización automática cada 30 segundos desde el feed GTFS-RT
- WebSocket para push de actualizaciones al frontend sin polling
- Animación suave de movimiento de trenes entre actualizaciones
Navegación Temporal (Timeline)
- Slider para seleccionar momento específico en el tiempo
- Botones play/pause para reproducir histórico
- Controles de velocidad de reproducción (1x, 2x, 5x)
- Selección de rango de fechas
Información de Trenes
- Popup al hacer clic: ID, ruta, velocidad, dirección, estado
- Panel lateral con detalles extendidos
- Histórico de posiciones del tren seleccionado (trail en el mapa)
- Filtros por tipo de tren (AVE, Cercanías, Regional, etc.)
Optimizaciones
Backend
- Particionamiento de tablas por fecha
- Índices espaciales (PostGIS GIST)
- Cache Redis para consultas frecuentes
- Compresión de respuestas HTTP (gzip)
- Rate limiting en API
Frontend
- Clustering de markers cuando hay muchos trenes cercanos
- Lazy loading de datos históricos
- Debouncing en timeline slider
- Virtual scrolling en listas de trenes
- Service Worker para caché offline
Roadmap de Implementación
Fase 1: MVP (4-6 semanas)
-
Semana 1-2: Backend
- Setup PostgreSQL + PostGIS + Redis
- Worker para polling GTFS-RT
- API REST básica
-
Semana 3-4: Frontend
- Mapa con OpenStreetMap
- Visualización de trenes en tiempo real
- WebSocket client
-
Semana 5-6: Timeline
- Slider temporal
- Consultas históricas
- Testing y optimización
Fase 2: Mejoras (4-6 semanas)
- Integración de feeds adicionales (data.renfe.com)
- Filtros avanzados y búsqueda
- Estadísticas y analytics
- Exportación de datos
Fase 3: Avanzado
- Predicción de posiciones usando ML
- Alertas y notificaciones
- API pública
- Mobile app
Costes Estimados
Desarrollo
- Solo: 2-3 meses (una persona)
- Equipo pequeño: 1-1.5 meses (2-3 personas)
Infraestructura Mensual (estimación)
- VPS (4GB RAM, 2 CPU): ~15-25€/mes
- PostgreSQL gestionado (opcional): ~25-50€/mes
- Redis gestionado (opcional): ~10-20€/mes
- Dominio: ~10€/año
- Total: 25-95€/mes (dependiendo de servicios gestionados)
Almacenamiento
- ~1KB por posición
- 1000 trenes × 2 updates/min × 60 min × 24 h = ~2.8M registros/día
- ~2.8GB/día → ~84GB/mes
- Retención de 3 meses: ~250GB
Consideraciones Técnicas
Desafíos
- Parsing de Protocol Buffers: Usar librerías oficiales gtfs-realtime-bindings
- Escalabilidad: Particionamiento de datos históricos, archivado a cold storage
- Precisión GPS: Los datos pueden tener errores, implementar filtrado de outliers
- Frecuencia de actualización: Balancear entre tiempo real y carga del servidor
Seguridad
- Rate limiting en API
- CORS configurado correctamente
- Sanitización de inputs
- HTTPS en producción
- Autenticación para endpoints administrativos
Alternativas y Variantes
Stack Alternativo Ligero
- Backend: Python + FastAPI + SQLite/DuckDB
- Frontend: Vanilla JS + Leaflet (sin framework)
- Deploy: Single VPS con systemd
Stack Más Robusto
- Backend: Go + Gin + TimescaleDB
- Message Queue: RabbitMQ o Kafka para procesamiento
- Frontend: Next.js + React Query
- Deploy: Kubernetes cluster
Serverless
- Backend: AWS Lambda + API Gateway
- DB: DynamoDB + S3 para histórico
- Frontend: Vercel/Netlify
- Real-time: AWS IoT Core o Pusher
Referencias
- GTFS Realtime Specification: https://gtfs.org/documentation/realtime/
- Renfe Data Portal: https://data.renfe.com/
- PostGIS Documentation: https://postgis.net/
- Leaflet.js: https://leafletjs.com/
- Socket.io: https://socket.io/
Integración de Fuentes de Datos
Estrategia de Sincronización
Workers de Recolección
// Arquitectura de workers propuesta
const workers = {
// Worker 1: GTFS-RT en tiempo real (alta frecuencia)
realtimeWorker: {
interval: 30000, // 30 segundos
sources: [
{
name: 'vehicle_positions',
url: 'https://gtfsrt.renfe.com/vehicle_positions.pb',
parser: parseVehiclePositions,
destination: ['postgresql.train_positions', 'redis.current_positions']
},
{
name: 'trip_updates',
url: 'https://gtfsrt.renfe.com/trip_updates_cercanias.pb',
parser: parseTripUpdates,
destination: ['postgresql.trip_updates', 'redis.delays']
},
{
name: 'service_alerts',
url: 'https://gtfsrt.renfe.com/alerts.pb',
parser: parseServiceAlerts,
destination: ['postgresql.alerts', 'redis.active_alerts']
}
]
},
// Worker 2: GTFS Static (baja frecuencia)
staticDataWorker: {
interval: 86400000, // 24 horas
schedule: '0 3 * * *', // 3:00 AM diaria
sources: [
{
name: 'gtfs_static',
url: 'https://data.renfe.com/api/gtfs/latest.zip',
parser: parseGTFSZip,
files: ['routes', 'stops', 'trips', 'stop_times', 'calendar', 'shapes'],
destination: 'postgresql',
onUpdate: (changes) => {
// Detectar cambios en rutas o estaciones
notifyAdmins(changes);
invalidateCache(['routes', 'stations']);
}
}
]
},
// Worker 3: Estaciones y metadatos (muy baja frecuencia)
metadataWorker: {
interval: 604800000, // 7 días
schedule: '0 2 * * 0', // Domingo 2:00 AM
sources: [
{
name: 'renfe_stations',
url: 'https://data.renfe.com/api/stations.json',
parser: parseStations,
destination: 'postgresql.stations',
merge: true // Merge con datos existentes
},
{
name: 'adif_infrastructure',
type: 'wms',
url: 'https://ideadif.adif.es/wms',
parser: extractStationMetadata,
destination: 'postgresql.stations',
merge: true
}
]
}
};
Pipeline de Procesamiento de Datos
┌─────────────────────────────────────────────────────────────┐
│ INGESTA DE DATOS │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ GTFS-RT │ │ GTFS Static │ │ Metadatos │ │
│ │ (30 seg) │ │ (24 horas) │ │ (7 días) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
└─────────┼──────────────────┼──────────────────┼────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ VALIDACIÓN Y TRANSFORMACIÓN │
├─────────────────────────────────────────────────────────────┤
│ │
│ • Decodificar Protocol Buffers │
│ • Validar campos requeridos │
│ • Normalizar formatos (fechas, coordenadas) │
│ • Detectar outliers (posiciones GPS inválidas) │
│ • Enriquecer con datos estáticos (nombres, rutas) │
│ │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ALMACENAMIENTO │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ PostgreSQL │ │ Redis │ │
│ │ + PostGIS │ │ │ │
│ ├──────────────────┤ ├──────────────────┤ │
│ │ • train_positions│◄─────────────┤ • current_pos │ │
│ │ • trip_updates │ │ • active_alerts │ │
│ │ • alerts │ │ • delay_cache │ │
│ │ • routes │ │ • stats_cache │ │
│ │ • stations │ └──────────────────┘ │
│ │ • trips │ │
│ └──────────────────┘ │
│ │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ DISTRIBUCIÓN │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ WebSocket │ │ API REST │ │ Webhooks │ │
│ │ (push) │ │ (pull) │ │ (eventos) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Sincronización GTFS Static
// Módulo de sincronización GTFS Static
class GTFSStaticSyncer {
async syncGTFSStatic() {
const zipUrl = await this.getLatestGTFSUrl();
const localPath = await this.downloadGTFS(zipUrl);
const extracted = await this.extractZip(localPath);
// Calcular checksum para detectar cambios
const newChecksum = this.calculateChecksum(extracted);
const oldChecksum = await this.getStoredChecksum();
if (newChecksum === oldChecksum) {
console.log('GTFS sin cambios');
return;
}
// Procesar archivos GTFS
const data = {
routes: await this.parseGTFSFile(extracted, 'routes.txt'),
stops: await this.parseGTFSFile(extracted, 'stops.txt'),
trips: await this.parseGTFSFile(extracted, 'trips.txt'),
stopTimes: await this.parseGTFSFile(extracted, 'stop_times.txt'),
calendar: await this.parseGTFSFile(extracted, 'calendar.txt'),
shapes: await this.parseGTFSFile(extracted, 'shapes.txt')
};
// Detectar cambios
const changes = await this.detectChanges(data);
// Actualizar base de datos en transacción
await this.db.transaction(async (trx) => {
// Insertar/actualizar rutas
await this.upsertRoutes(trx, data.routes);
// Insertar/actualizar estaciones
await this.upsertStops(trx, data.stops);
// Insertar/actualizar viajes
await this.upsertTrips(trx, data.trips);
// Insertar/actualizar horarios
await this.upsertStopTimes(trx, data.stopTimes);
// Insertar/actualizar formas de rutas
if (data.shapes) {
await this.upsertShapes(trx, data.shapes);
}
// Guardar checksum
await this.storeChecksum(trx, newChecksum);
});
// Invalidar cachés
await this.invalidateCaches(['routes', 'stations', 'trips']);
// Notificar cambios
await this.notifyChanges(changes);
console.log('GTFS actualizado:', changes);
}
async detectChanges(newData) {
const changes = {
newRoutes: [],
updatedRoutes: [],
deletedRoutes: [],
newStations: [],
updatedStations: [],
deletedStations: []
};
// Comparar con datos existentes
const oldRoutes = await this.db.select('*').from('routes');
// Detectar rutas nuevas
for (const route of newData.routes) {
const existing = oldRoutes.find(r => r.route_id === route.route_id);
if (!existing) {
changes.newRoutes.push(route);
} else if (JSON.stringify(existing) !== JSON.stringify(route)) {
changes.updatedRoutes.push(route);
}
}
// Detectar rutas eliminadas
for (const oldRoute of oldRoutes) {
const existing = newData.routes.find(r => r.route_id === oldRoute.route_id);
if (!existing) {
changes.deletedRoutes.push(oldRoute);
}
}
// Similar para estaciones...
return changes;
}
}
Enriquecimiento de Datos en Tiempo Real
// Enriquecer posiciones con datos estáticos
class PositionEnricher {
async enrichPosition(position) {
// Datos básicos del feed GTFS-RT
const enriched = {
...position,
// Añadir información de la ruta
route: await this.getRoute(position.route_id),
// Añadir información del viaje
trip: await this.getTrip(position.trip_id),
// Calcular estación más cercana
nearestStation: await this.findNearestStation(
position.latitude,
position.longitude
),
// Calcular retraso si hay trip_update
delay: await this.calculateDelay(position.trip_id),
// Añadir alertas activas
alerts: await this.getActiveAlerts(position.route_id),
// Información de ocupación (si disponible)
occupancy: this.mapOccupancyStatus(position.occupancy_status)
};
return enriched;
}
async findNearestStation(lat, lon) {
// Usar PostGIS para encontrar estación más cercana
const result = await this.db.raw(`
SELECT
station_id,
station_name,
latitude,
longitude,
ST_Distance(
position,
ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography
) as distance
FROM stations
ORDER BY distance
LIMIT 1
`, [lon, lat]);
return result.rows[0];
}
async calculateDelay(tripId) {
// Buscar trip_update más reciente
const update = await this.redis.get(`trip_update:${tripId}`);
if (!update) return null;
const parsed = JSON.parse(update);
// Calcular diferencia entre horario programado y predicción
const scheduledArrival = await this.getScheduledArrival(tripId);
const predictedArrival = parsed.stop_time_update?.arrival?.time;
if (scheduledArrival && predictedArrival) {
return (predictedArrival - scheduledArrival) / 60; // Minutos
}
return null;
}
}
Features Avanzadas por Fuente de Datos
Features con GTFS-RT Vehicle Positions
1. Mapa en Tiempo Real
- Descripción: Visualización de todos los trenes en movimiento
- Implementación:
- Marcadores animados en Leaflet
- Colores por tipo de servicio (AVE, Cercanías, etc.)
- Rotación del icono según bearing
- Clustering para alta densidad
- Datos utilizados: latitude, longitude, bearing, train_type
2. Trail de Movimiento
- Descripción: Estela que muestra trayectoria reciente del tren
- Implementación:
- Polyline con últimas 10 posiciones
- Desvanecimiento gradual (opacity)
- Código de colores por velocidad
- Datos utilizados: Histórico de train_positions (últimos 5 min)
3. Heatmap de Tráfico
- Descripción: Mapa de calor mostrando zonas con más tráfico ferroviario
- Implementación:
- Leaflet.heat plugin
- Agregación por celdas geográficas
- Filtros por hora del día / día de la semana
- Datos utilizados: Agregación de train_positions
4. Timeline Playback
- Descripción: Reproducir histórico de posiciones
- Implementación:
- Slider temporal
- Controles play/pause/velocidad
- Sincronización de múltiples trenes
- Datos utilizados: train_positions con timestamp
5. Seguimiento de Tren Específico
- Descripción: Centrar cámara en un tren y seguirlo
- Implementación:
- Auto-pan del mapa
- Panel lateral con información detallada
- Notificaciones de cambios de estado
- Datos utilizados: train_positions + enriched data
Features con GTFS-RT Trip Updates
6. Monitor de Puntualidad
- Descripción: Dashboard de retrasos y cancelaciones
- Implementación:
- Gráficos de puntualidad por línea
- Ranking de rutas más puntuales
- Tendencias temporales
- Datos utilizados: trip_updates, delay calculations
7. Predicciones de Llegada
- Descripción: Mostrar hora estimada de llegada a próximas estaciones
- Implementación:
- Lista de próximas paradas
- Comparación horario programado vs predicho
- Indicador visual de retraso/adelanto
- Datos utilizados: stop_time_updates
8. Alertas de Cancelación
- Descripción: Notificaciones push de trenes cancelados
- Implementación:
- WebSocket event al detectar cancelación
- Notificación en navegador
- Email/SMS para suscripciones
- Datos utilizados: trip_updates con schedule_relationship=CANCELED
9. Análisis de Retrasos
- Descripción: Identificar patrones de retrasos
- Implementación:
- Análisis por hora del día
- Análisis por estación
- Correlación con meteorología
- Predicción ML de retrasos
- Datos utilizados: Histórico de delays, weather data
Features con GTFS-RT Service Alerts
10. Panel de Incidencias
- Descripción: Listado de alertas activas
- Implementación:
- Categorización por severidad
- Filtros por línea/estación
- Mapa con marcadores de incidencias
- Datos utilizados: alerts table
11. Notificaciones Personalizadas
- Descripción: Alertas para rutas favoritas
- Implementación:
- Sistema de suscripciones
- Push notifications
- Filtros por tipo de alerta
- Datos utilizados: alerts + user preferences
12. Histórico de Incidencias
- Descripción: Análisis de incidencias pasadas
- Implementación:
- Timeline de alertas
- Estadísticas por tipo
- Zonas problemáticas
- Datos utilizados: alerts historical data
Features con GTFS Static
13. Explorador de Rutas
- Descripción: Visualización de rutas completas
- Implementación:
- Trazar ruta en mapa usando shapes.txt
- Mostrar todas las paradas
- Horarios completos del servicio
- Datos utilizados: routes, trips, shapes, stop_times
14. Planificador de Viajes
- Descripción: Calcular mejor ruta entre dos estaciones
- Implementación:
- Algoritmo A* o Dijkstra
- Considerar transbordos
- Mostrar horarios disponibles
- Datos utilizados: routes, trips, stop_times, calendar
15. Información de Estaciones
- Descripción: Detalles completos de estaciones
- Implementación:
- Servicios disponibles
- Trenes que paran
- Próximas salidas/llegadas
- Accesibilidad
- Datos utilizados: stops, stop_times, station metadata
16. Comparador Horario vs Real
- Descripción: Overlay de horario programado sobre posiciones reales
- Implementación:
- Mostrar ruta teórica vs real
- Calcular desviaciones
- Métricas de adherencia al horario
- Datos utilizados: stop_times + train_positions
Features con Infraestructura ADIF
17. Visualización de Vías
- Descripción: Overlay de red ferroviaria
- Implementación:
- WMS layer de IDEADIF
- Estilos por tipo de vía (AVE, convencional)
- Toggle on/off
- Datos utilizados: ADIF WMS service
18. Información de Infraestructura
- Descripción: Detalles técnicos de vías y estaciones
- Implementación:
- Click en vía para ver características
- Tipo de electrificación
- Ancho de vía
- Velocidad máxima
- Datos utilizados: ADIF PISERVI data
Features con Datos Históricos
19. Análisis de Tráfico
- Descripción: Patrones de movimiento a lo largo del tiempo
- Implementación:
- Gráficos de volumen por hora/día
- Identificar horas punta
- Rutas más transitadas
- Datos utilizados: Agregaciones de train_positions
20. Estadísticas de Rendimiento
- Descripción: KPIs del sistema ferroviario
- Implementación:
- Puntualidad promedio
- Distancia recorrida
- Velocidad promedio por ruta
- Número de servicios diarios
- Datos utilizados: calculate_train_statistics function
21. Exportación de Datos
- Descripción: Descarga de datasets para análisis externo
- Implementación:
- Export a CSV/JSON/GeoJSON
- API para acceso programático
- Filtros temporales y geográficos
- Datos utilizados: Cualquier tabla
Features Avanzadas Multi-Fuente
22. Dashboard de Control
- Descripción: Vista general del sistema
- Implementación:
- Número de trenes activos
- Alertas activas
- Puntualidad global
- Gráficos en tiempo real
- Datos utilizados: Todas las fuentes
23. Sistema de Recomendaciones
- Descripción: Sugerir mejor momento para viajar
- Implementación:
- Análisis de patrones históricos de retrasos
- Ocupación típica por horario
- Condiciones meteorológicas
- Datos utilizados: Historical delays + weather + occupancy
24. Detección de Anomalías
- Descripción: Identificar comportamientos inusuales
- Implementación:
- ML para detectar patrones anormales
- Alertas automáticas a operadores
- Análisis de velocidad inusual
- Detección de paradas no programadas
- Datos utilizados: train_positions + trip_updates + ML model
25. API Pública
- Descripción: Acceso programático a datos
- Implementación:
- REST API documentada
- GraphQL endpoint
- Webhooks para eventos
- Rate limiting y autenticación
- Datos utilizados: Todas las fuentes
26. Integración con Asistentes
- Descripción: Comandos de voz y chatbots
- Implementación:
- "¿Cuándo llega el próximo tren a Madrid?"
- "¿Hay retrasos en la línea C-1?"
- Integración con Alexa/Google Assistant
- Datos utilizados: NLP + trip_updates + train_positions
27. Gamificación
- Descripción: Elementos lúdicos para engagement
- Implementación:
- Badges por trenes observados
- Ranking de usuarios
- Challenges ("visita 10 estaciones diferentes")
- Compartir en redes sociales
- Datos utilizados: User data + train_positions
Roadmap de Features
Fase 1: MVP (4-6 semanas)
- Arquitectura Docker
- Worker GTFS-RT Vehicle Positions
- Mapa en tiempo real básico
- Panel de información de tren
- Timeline básico
- API REST core
Fase 2: Enriquecimiento (4-6 semanas)
- Integración GTFS Static
- Sincronización semanal automática
- Trip Updates y Alerts
- Monitor de puntualidad
- Predicciones de llegada
- Panel de incidencias
Fase 3: Features Avanzadas (8-12 semanas)
- Heatmap de tráfico
- Análisis de retrasos
- Explorador de rutas
- Planificador de viajes
- Overlay infraestructura ADIF
- Dashboard de control
Fase 4: Analytics y ML (8-12 semanas)
- Análisis histórico de tráfico
- Estadísticas de rendimiento
- Predicción ML de retrasos
- Detección de anomalías
- Sistema de recomendaciones
- Correlación con meteorología
Fase 5: Ecosistema (Continuo)
- API pública documentada
- Webhooks
- Integraciones con asistentes
- Mobile app (iOS/Android)
- Notificaciones push
- Sistema de suscripciones
Próximos Pasos
- ✅ Arquitectura Docker Compose completa
- ✅ Base de datos con PostGIS y particiones
- ✅ Sistema de migraciones Flyway
- ✅ Identificación de fuentes de datos
- ⬜ Implementar worker GTFS-RT Vehicle Positions
- ⬜ Desarrollar API REST básica
- ⬜ Crear frontend con mapa Leaflet
- ⬜ Implementar WebSocket para tiempo real
- ⬜ Sincronización GTFS Static
- ⬜ Trip Updates y Service Alerts