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
|