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>
1205 lines
43 KiB
Markdown
1205 lines
43 KiB
Markdown
# 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](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íneas
|
||
- `stops.txt`: Estaciones y paradas
|
||
- `trips.txt`: Viajes programados
|
||
- `stop_times.txt`: Horarios
|
||
- `calendar.txt`: Días de operación
|
||
- `shapes.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
|
||
```sql
|
||
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
|
||
```sql
|
||
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
|
||
```sql
|
||
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
|
||
```sql
|
||
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)
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
```javascript
|
||
// 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)
|
||
```typescript
|
||
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)
|
||
```typescript
|
||
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)
|
||
```typescript
|
||
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)
|
||
1. **Semana 1-2**: Backend
|
||
- Setup PostgreSQL + PostGIS + Redis
|
||
- Worker para polling GTFS-RT
|
||
- API REST básica
|
||
|
||
2. **Semana 3-4**: Frontend
|
||
- Mapa con OpenStreetMap
|
||
- Visualización de trenes en tiempo real
|
||
- WebSocket client
|
||
|
||
3. **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
|
||
1. **Parsing de Protocol Buffers**: Usar librerías oficiales gtfs-realtime-bindings
|
||
2. **Escalabilidad**: Particionamiento de datos históricos, archivado a cold storage
|
||
3. **Precisión GPS**: Los datos pueden tener errores, implementar filtrado de outliers
|
||
4. **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
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
```javascript
|
||
// 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)
|
||
- [x] 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
|
||
|
||
1. ✅ Arquitectura Docker Compose completa
|
||
2. ✅ Base de datos con PostGIS y particiones
|
||
3. ✅ Sistema de migraciones Flyway
|
||
4. ✅ Identificación de fuentes de datos
|
||
5. ⬜ Implementar worker GTFS-RT Vehicle Positions
|
||
6. ⬜ Desarrollar API REST básica
|
||
7. ⬜ Crear frontend con mapa Leaflet
|
||
8. ⬜ Implementar WebSocket para tiempo real
|
||
9. ⬜ Sincronización GTFS Static
|
||
10. ⬜ Trip Updates y Service Alerts
|