feat: Initial commit - Train tracking system
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
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>
This commit is contained in:
53
frontend/src/utils/bearing.js
Normal file
53
frontend/src/utils/bearing.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Calculate bearing (direction) between two geographic points
|
||||
* @param {number} lat1 - Starting latitude in degrees
|
||||
* @param {number} lon1 - Starting longitude in degrees
|
||||
* @param {number} lat2 - Ending latitude in degrees
|
||||
* @param {number} lon2 - Ending longitude in degrees
|
||||
* @returns {number} Bearing in degrees (0-360, where 0=North, 90=East, 180=South, 270=West)
|
||||
*/
|
||||
export function calculateBearing(lat1, lon1, lat2, lon2) {
|
||||
// Convert to radians
|
||||
const φ1 = lat1 * Math.PI / 180;
|
||||
const φ2 = lat2 * Math.PI / 180;
|
||||
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
||||
|
||||
const y = Math.sin(Δλ) * Math.cos(φ2);
|
||||
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
|
||||
|
||||
let θ = Math.atan2(y, x);
|
||||
|
||||
// Convert to degrees and normalize to 0-360
|
||||
let bearing = (θ * 180 / Math.PI + 360) % 360;
|
||||
|
||||
return bearing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points in meters (Haversine formula)
|
||||
* @param {number} lat1 - Starting latitude
|
||||
* @param {number} lon1 - Starting longitude
|
||||
* @param {number} lat2 - Ending latitude
|
||||
* @param {number} lon2 - Ending longitude
|
||||
* @returns {number} Distance in meters
|
||||
*/
|
||||
export function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371000; // Earth radius in meters
|
||||
const φ1 = lat1 * Math.PI / 180;
|
||||
const φ2 = lat2 * Math.PI / 180;
|
||||
const Δφ = (lat2 - lat1) * Math.PI / 180;
|
||||
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
||||
|
||||
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
||||
Math.cos(φ1) * Math.cos(φ2) *
|
||||
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum distance (in meters) to consider for bearing calculation
|
||||
* Too small distances can result in inaccurate bearings
|
||||
*/
|
||||
export const MIN_DISTANCE_FOR_BEARING = 10;
|
||||
126
frontend/src/utils/trainTypes.js
Normal file
126
frontend/src/utils/trainTypes.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Infer train type based on train ID ranges
|
||||
* Based on official Renfe train numbering system
|
||||
*/
|
||||
|
||||
const TRAIN_TYPE_RANGES = [
|
||||
{ min: 0, max: 1999, type: 'LONG_DISTANCE', name: 'Larga Distancia' },
|
||||
{ min: 2000, max: 8999, type: 'MEDIUM_DISTANCE', name: 'Media Distancia' },
|
||||
{ min: 9000, max: 9099, type: 'HIGH_SPEED', name: 'Media Distancia AV / AVE LD' },
|
||||
{ min: 9100, max: 9299, type: 'HIGH_SPEED', name: 'Talgo 200' },
|
||||
{ min: 9300, max: 9499, type: 'HIGH_SPEED', name: 'AVE Programacion Especial' },
|
||||
{ min: 9500, max: 9899, type: 'HIGH_SPEED', name: 'AV MD / AVE LD' },
|
||||
{ min: 9900, max: 9999, type: 'SERVICE', name: 'Servicio Interno AV' },
|
||||
{ min: 10000, max: 10399, type: 'LONG_DISTANCE', name: 'Ramas Larga Distancia' },
|
||||
{ min: 14000, max: 15999, type: 'LONG_DISTANCE', name: 'Prog. Especial LD' },
|
||||
{ min: 16000, max: 26999, type: 'COMMUTER', name: 'Cercanias' },
|
||||
{ min: 27000, max: 29999, type: 'COMMUTER', name: 'Prog. Especial Cercanias' },
|
||||
{ min: 30000, max: 30999, type: 'LONG_DISTANCE', name: 'Especiales LD' },
|
||||
{ min: 31000, max: 31999, type: 'MEDIUM_DISTANCE', name: 'Especiales MD' },
|
||||
{ min: 32000, max: 33999, type: 'MEDIUM_DISTANCE', name: 'Prog. Especial MD' },
|
||||
{ min: 34000, max: 35999, type: 'COMMUTER', name: 'Especiales Cercanias' },
|
||||
{ min: 36000, max: 36999, type: 'SPECIAL', name: 'Prog. Especial Varios Op.' },
|
||||
{ min: 37000, max: 39999, type: 'SPECIAL', name: 'Especiales Varios Op.' },
|
||||
{ min: 40000, max: 49999, type: 'FREIGHT', name: 'Internacional Mercancias' },
|
||||
{ min: 50000, max: 50999, type: 'FREIGHT', name: 'TECOS Red Terrestre' },
|
||||
{ min: 51000, max: 51999, type: 'FREIGHT', name: 'TECOS Red Maritima' },
|
||||
{ min: 52000, max: 52999, type: 'FREIGHT', name: 'TECOS Internacional' },
|
||||
{ min: 53000, max: 53999, type: 'FREIGHT', name: 'Petroquimicos' },
|
||||
{ min: 54000, max: 54999, type: 'FREIGHT', name: 'Material Agricola' },
|
||||
{ min: 55000, max: 55999, type: 'FREIGHT', name: 'Minero/Construccion' },
|
||||
{ min: 56000, max: 56999, type: 'FREIGHT', name: 'Prod. Manufacturados' },
|
||||
{ min: 57000, max: 57999, type: 'FREIGHT', name: 'Portacoches' },
|
||||
{ min: 58000, max: 58999, type: 'FREIGHT', name: 'Siderurgicos' },
|
||||
{ min: 59000, max: 59999, type: 'FREIGHT', name: 'Auto/Sider. Internacional' },
|
||||
{ min: 60000, max: 60999, type: 'FREIGHT', name: 'Surcos Polivalentes' },
|
||||
{ min: 61000, max: 61999, type: 'FREIGHT', name: 'Servicio Interno Merc.' },
|
||||
{ min: 62000, max: 79999, type: 'FREIGHT', name: 'Mercancias Varios Op.' },
|
||||
{ min: 80000, max: 80999, type: 'FREIGHT', name: 'TECO P.E.' },
|
||||
{ min: 81000, max: 81999, type: 'FREIGHT', name: 'Manufacturados/Agricola' },
|
||||
{ min: 82000, max: 82999, type: 'FREIGHT', name: 'Prod. Industriales' },
|
||||
{ min: 83000, max: 84999, type: 'FREIGHT', name: 'Polivalentes' },
|
||||
{ min: 85000, max: 85999, type: 'FREIGHT', name: 'Estacional/Militar' },
|
||||
{ min: 86000, max: 86999, type: 'FREIGHT', name: 'Servicio Interno Merc.' },
|
||||
{ min: 87000, max: 89999, type: 'FREIGHT', name: 'Prog. Especial Varios Op.' },
|
||||
{ min: 90000, max: 90999, type: 'FREIGHT', name: 'Transportes Excepcionales' },
|
||||
{ min: 91000, max: 92999, type: 'FREIGHT', name: 'Excepc. Intermodales' },
|
||||
{ min: 93000, max: 94999, type: 'FREIGHT', name: 'Excepc. Prod. Industriales' },
|
||||
{ min: 95000, max: 96999, type: 'FREIGHT', name: 'Excepc. Polivalente' },
|
||||
{ min: 97000, max: 99999, type: 'FREIGHT', name: 'Excepc. Varios Op.' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Get train type info from train ID
|
||||
* @param {string|number} trainId - The train ID
|
||||
* @returns {{ type: string, name: string } | null}
|
||||
*/
|
||||
export function getTrainTypeFromId(trainId) {
|
||||
// Extract numeric part from train_id (handle formats like "15000", "AVE-15000", etc.)
|
||||
const numericMatch = String(trainId).match(/\d+/);
|
||||
if (!numericMatch) return null;
|
||||
|
||||
const numericId = parseInt(numericMatch[0], 10);
|
||||
|
||||
for (const range of TRAIN_TYPE_RANGES) {
|
||||
if (numericId >= range.min && numericId <= range.max) {
|
||||
return {
|
||||
type: range.type,
|
||||
name: range.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format train type for display
|
||||
* @param {string} type - Train type code
|
||||
* @param {string} inferredName - Inferred name from ID
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatTrainType(type, inferredName = null) {
|
||||
const typeMap = {
|
||||
'HIGH_SPEED': 'Alta Velocidad (AVE)',
|
||||
'LONG_DISTANCE': 'Larga Distancia',
|
||||
'MEDIUM_DISTANCE': 'Media Distancia',
|
||||
'REGIONAL': 'Regional',
|
||||
'COMMUTER': 'Cercanias',
|
||||
'FREIGHT': 'Mercancias',
|
||||
'SERVICE': 'Servicio Interno',
|
||||
'SPECIAL': 'Especial',
|
||||
'RAIL': 'Ferrocarril',
|
||||
'UNKNOWN': 'No especificado',
|
||||
};
|
||||
|
||||
// If we have an inferred name, use it for more detail
|
||||
if (inferredName) {
|
||||
const baseType = typeMap[type] || type;
|
||||
if (inferredName !== baseType) {
|
||||
return `${baseType} - ${inferredName}`;
|
||||
}
|
||||
return baseType;
|
||||
}
|
||||
|
||||
return typeMap[type] || type || 'No especificado';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for train type (for map markers)
|
||||
* @param {string} type - Train type code
|
||||
* @returns {string} - Hex color
|
||||
*/
|
||||
export function getTrainTypeColor(type) {
|
||||
const colorMap = {
|
||||
'HIGH_SPEED': '#E74C3C', // Red - AVE
|
||||
'LONG_DISTANCE': '#3498DB', // Blue - Long distance
|
||||
'MEDIUM_DISTANCE': '#9B59B6', // Purple - Medium distance
|
||||
'COMMUTER': '#2ECC71', // Green - Cercanias
|
||||
'FREIGHT': '#95A5A6', // Gray - Freight
|
||||
'SERVICE': '#F39C12', // Orange - Service
|
||||
'SPECIAL': '#E67E22', // Dark orange - Special
|
||||
'REGIONAL': '#1ABC9C', // Teal - Regional
|
||||
};
|
||||
|
||||
return colorMap[type] || '#1a1a2e'; // Default dark blue
|
||||
}
|
||||
Reference in New Issue
Block a user