UNPKG

@siva-sub/mcp-public-transport

Version:

A Model Context Protocol server for Singapore transport data with real-time information and routing

438 lines (437 loc) 18.2 kB
/** * Enhanced LTA Service with comprehensive API integration * Dynamically integrates all LTA DataMall APIs for intelligent transport planning */ import { LTAService } from './lta.js'; import { logger } from '../utils/logger.js'; export class LTAEnhancedService extends LTAService { cacheService; CACHE_DURATION = { TRAFFIC_INCIDENTS: 300, // 5 minutes TRAVEL_TIMES: 300, // 5 minutes BUS_ARRIVALS: 60, // 1 minute PASSENGER_VOLUME: 3600, // 1 hour TRAFFIC_FLOW: 300, // 5 minutes FACILITY_MAINTENANCE: 1800, // 30 minutes TAXI_AVAILABILITY: 300, // 5 minutes }; constructor(apiKey, cacheService, requestTimeout = 10000) { super(apiKey, cacheService, requestTimeout); this.cacheService = cacheService; } /** * Make a request to LTA API (protected method access) */ async makeRequest(endpoint, params) { return this.client.get(endpoint, { params }); } /** * Get comprehensive route intelligence for journey planning */ async getRouteIntelligence(startLat, startLng, endLat, endLng, radius = 2000) { try { const [trafficIncidents, estimatedTravelTimes, busArrivals, facilityDisruptions, passengerVolume, taxiAvailability, faultyTrafficLights, roadWorks, vmsSigns, taxiStands] = await Promise.allSettled([ this.getTrafficIncidentsInArea(startLat, startLng, endLat, endLng, radius), this.getEstimatedTravelTimes(), this.getBusArrivalsInArea(startLat, startLng, radius), this.getFacilityMaintenance(), this.getPassengerVolumeInsights(), this.getTaxiAvailabilityInArea(startLat, startLng, endLat, endLng), this.getFaultyTrafficLights(), this.getRoadWorks(), this.getVMSSigns(), this.getTaxiStands() ]); const intelligence = { trafficConditions: trafficIncidents.status === 'fulfilled' ? trafficIncidents.value : [], estimatedTravelTimes: estimatedTravelTimes.status === 'fulfilled' ? estimatedTravelTimes.value : [], busLoadFactors: new Map(), facilityDisruptions: facilityDisruptions.status === 'fulfilled' ? facilityDisruptions.value : [], passengerVolumeInsights: passengerVolume.status === 'fulfilled' ? passengerVolume.value : [], taxiAvailability: taxiAvailability.status === 'fulfilled' ? taxiAvailability.value : [], faultyTrafficLights: faultyTrafficLights.status === 'fulfilled' ? faultyTrafficLights.value : [], roadWorks: roadWorks.status === 'fulfilled' ? roadWorks.value : [], vmsSigns: vmsSigns.status === 'fulfilled' ? vmsSigns.value : [], taxiStands: taxiStands.status === 'fulfilled' ? taxiStands.value : [], routeRecommendations: [] }; // Generate intelligent recommendations intelligence.routeRecommendations = this.generateRouteRecommendations(intelligence); return intelligence; } catch (error) { logger.error('Failed to get route intelligence', error); throw error; } } /** * Get traffic incidents in a specific area */ async getTrafficIncidentsInArea(startLat, startLng, endLat, endLng, radius) { const cacheKey = `traffic_incidents_${startLat}_${startLng}_${endLat}_${endLng}_${radius}`; const incidentsFromCache = this.cacheService.get(cacheKey); if (incidentsFromCache) { return incidentsFromCache; } try { const response = await this.makeRequest('/TrafficIncidents'); const fetchedIncidents = (response.value || []) .filter((incident) => this.isWithinArea(incident.Latitude, incident.Longitude, startLat, startLng, endLat, endLng, radius)) .map((incident) => ({ type: incident.Type, latitude: incident.Latitude, longitude: incident.Longitude, message: incident.Message, severity: this.assessIncidentSeverity(incident.Message), affectedRoutes: this.extractAffectedRoutes(incident.Message) })); this.cacheService.set(cacheKey, fetchedIncidents, this.CACHE_DURATION.TRAFFIC_INCIDENTS); return fetchedIncidents; } catch (error) { logger.error('Failed to get traffic incidents', error); return []; } } /** * Get estimated travel times for expressways */ async getEstimatedTravelTimes() { const cacheKey = 'estimated_travel_times'; const travelTimesFromCache = this.cacheService.get(cacheKey); if (travelTimesFromCache) { return travelTimesFromCache; } try { const response = await this.makeRequest('/EstTravelTimes'); const fetchedTravelTimes = (response.value || []).map((time) => ({ name: time.Name, direction: time.Direction, farEndPoint: time.FarEndPoint, startPoint: time.StartPoint, endPoint: time.EndPoint, estimatedTime: time.EstTime })); this.cacheService.set(cacheKey, fetchedTravelTimes, this.CACHE_DURATION.TRAVEL_TIMES); return fetchedTravelTimes; } catch (error) { logger.error('Failed to get estimated travel times', error); return []; } } /** * Get bus arrivals in a specific area */ async getBusArrivalsInArea(lat, lng, radius) { try { // Get bus stops in the area first const busStops = await this.getAllBusStops(); const nearbyStops = busStops.filter(stop => this.calculateDistance(lat, lng, stop.latitude, stop.longitude) <= radius); // Get arrivals for nearby stops (limit to prevent API overload) const arrivalPromises = nearbyStops.slice(0, 10).map(stop => this.getBusArrival(stop.busStopCode).catch(e => { logger.warn(`Failed to get bus arrival for ${stop.busStopCode}: ${e.message}`); return null; })); const fetchedArrivals = await Promise.all(arrivalPromises); return fetchedArrivals.filter(arrival => arrival !== null).map((busServices, index) => { const busStopCode = nearbyStops[index]?.busStopCode || 'N/A'; return { busStopCode: busStopCode, services: busServices }; }); } catch (error) { logger.error('Failed to get bus arrivals in area', error); return []; } } /** * Get facility maintenance information */ async getFacilityMaintenance() { const cacheKey = 'facility_maintenance'; const maintenanceFromCache = this.cacheService.get(cacheKey); if (maintenanceFromCache) { return maintenanceFromCache; } try { const response = await this.makeRequest('/FacilitiesMaintenance'); const fetchedMaintenance = (response.value || []).map((facility) => ({ stationCode: facility.StationCode, stationName: facility.StationName, affectedFacilities: facility.AffectedFacilities?.split(',') || [], startDate: facility.StartDate, endDate: facility.EndDate, description: facility.Description })); this.cacheService.set(cacheKey, fetchedMaintenance, this.CACHE_DURATION.FACILITY_MAINTENANCE); return fetchedMaintenance; } catch (error) { logger.error('Failed to get facility maintenance', error); return []; } } /** * Get passenger volume insights */ async getPassengerVolumeInsights() { const cacheKey = 'passenger_volume_insights'; let insights = this.cacheService.get(cacheKey); if (insights) { return insights; } try { // This would typically fetch from historical data APIs // For now, return mock insights based on common patterns const generatedInsights = this.generatePassengerVolumeInsights(); this.cacheService.set(cacheKey, generatedInsights, this.CACHE_DURATION.PASSENGER_VOLUME); return generatedInsights; } catch (error) { logger.error('Failed to get passenger volume insights', error); return []; } } /** * Get taxi availability in area */ async getTaxiAvailabilityInArea(startLat, startLng, endLat, endLng) { const cacheKey = `taxi_availability_${startLat}_${startLng}_${endLat}_${endLng}`; const availabilityFromCache = this.cacheService.get(cacheKey); if (availabilityFromCache) { return availabilityFromCache; } try { const response = await this.makeRequest('/TaxiAvailability'); const fetchedAvailability = (response.value || []).map((taxi) => ({ timestamp: taxi.Timestamp, taxiCount: taxi.NoOfTaxis, coordinates: [taxi.Latitude, taxi.Longitude] })); this.cacheService.set(cacheKey, fetchedAvailability, this.CACHE_DURATION.TAXI_AVAILABILITY); return fetchedAvailability; } catch (error) { logger.error('Failed to get taxi availability', error); return []; } } /** * Get faulty traffic lights information */ async getFaultyTrafficLights() { const cacheKey = 'faulty_traffic_lights'; const lightsFromCache = this.cacheService.get(cacheKey); if (lightsFromCache) { return lightsFromCache; } try { const response = await this.makeRequest('/FaultyTrafficLights'); const fetchedLights = (response.value || []).map((light) => ({ latitude: light.Latitude, longitude: light.Longitude, message: light.Message, })); this.cacheService.set(cacheKey, fetchedLights, this.CACHE_DURATION.TRAFFIC_INCIDENTS); return fetchedLights; } catch (error) { logger.error('Failed to get faulty traffic lights', error); return []; } } /** * Get road works information */ async getRoadWorks() { const cacheKey = 'road_works'; const worksFromCache = this.cacheService.get(cacheKey); if (worksFromCache) { return worksFromCache; } try { const response = await this.makeRequest('/RoadWorks'); const fetchedWorks = (response.value || []).map((work) => ({ eventID: work.EventID, startDate: work.StartDate, endDate: work.EndDate, serviceDept: work.SvcDept, roadName: work.RoadName, other: work.Other, })); this.cacheService.set(cacheKey, fetchedWorks, this.CACHE_DURATION.TRAFFIC_INCIDENTS); return fetchedWorks; } catch (error) { logger.error('Failed to get road works', error); return []; } } /** * Get VMS (Variable Message Signs) information */ async getVMSSigns() { const cacheKey = 'vms_signs'; const signsFromCache = this.cacheService.get(cacheKey); if (signsFromCache) { return signsFromCache; } try { const response = await this.makeRequest('/VMS'); const fetchedSigns = (response.value || []).map((sign) => ({ equipmentID: sign.EquipmentID, latitude: sign.Latitude, longitude: sign.Longitude, message: sign.Message, })); this.cacheService.set(cacheKey, fetchedSigns, this.CACHE_DURATION.TRAFFIC_INCIDENTS); return fetchedSigns; } catch (error) { logger.error('Failed to get VMS signs', error); return []; } } /** * Get Taxi Stands information */ async getTaxiStands() { const cacheKey = 'taxi_stands'; const standsFromCache = this.cacheService.get(cacheKey); if (standsFromCache) { return standsFromCache; } try { const response = await this.makeRequest('/TaxiStands'); const fetchedStands = (response.value || []).map((stand) => ({ taxiCode: stand.TaxiCode, latitude: stand.Latitude, longitude: stand.Longitude, bfa: stand.Bfa, ownership: stand.Ownership, type: stand.Type, name: stand.Name, })); this.cacheService.set(cacheKey, fetchedStands, this.CACHE_DURATION.TRAFFIC_INCIDENTS); return fetchedStands; } catch (error) { logger.error('Failed to get taxi stands', error); return []; } } /** * Generate intelligent route recommendations */ generateRouteRecommendations(intelligence) { const recommendations = []; // Traffic-based recommendations if (intelligence.trafficConditions.length > 0) { const highSeverityIncidents = intelligence.trafficConditions.filter(i => i.severity === 'high'); if (highSeverityIncidents.length > 0) { recommendations.push('Consider alternative routes due to major traffic incidents'); } } // Travel time recommendations const slowRoutes = intelligence.estimatedTravelTimes.filter(t => t.estimatedTime > 10); if (slowRoutes.length > 0) { recommendations.push('Some expressway segments experiencing delays - consider public transport'); } // Facility disruption recommendations if (intelligence.facilityDisruptions.length > 0) { recommendations.push('Some MRT facilities under maintenance - check alternative stations'); } // Passenger volume recommendations const currentHour = new Date().getHours(); if (currentHour >= 7 && currentHour <= 9) { recommendations.push('Peak hour - expect crowded public transport, consider earlier/later travel'); } // Taxi availability recommendations const totalTaxis = intelligence.taxiAvailability.reduce((sum, t) => sum + t.taxiCount, 0); if (totalTaxis < 50) { recommendations.push('Low taxi availability - consider booking in advance or using public transport'); } return recommendations; } /** * Assess incident severity based on message content */ assessIncidentSeverity(message) { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes('accident') || lowerMessage.includes('breakdown') || lowerMessage.includes('closure')) { return 'high'; } if (lowerMessage.includes('slow') || lowerMessage.includes('heavy') || lowerMessage.includes('congestion')) { return 'medium'; } return 'low'; } /** * Extract affected routes from incident message */ extractAffectedRoutes(message) { const routes = []; const routePatterns = [ /\b(AYE|PIE|CTE|ECP|KJE|SLE|TPE|BKE|MCE)\b/gi, /\b(\d+[A-Z]*)\b/g // Bus route numbers ]; routePatterns.forEach(pattern => { const matches = message.match(pattern); if (matches) { routes.push(...matches); } }); return [...new Set(routes)]; // Remove duplicates } /** * Check if coordinates are within specified area */ isWithinArea(lat, lng, startLat, startLng, endLat, endLng, radius) { const centerLat = (startLat + endLat) / 2; const centerLng = (startLng + endLng) / 2; const distance = this.calculateDistance(lat, lng, centerLat, centerLng); return distance <= radius; } /** * Calculate distance between two coordinates */ calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371e3; // Earth's 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; } /** * Generate passenger volume insights based on common patterns */ generatePassengerVolumeInsights() { const currentHour = new Date().getHours(); const isWeekend = [0, 6].includes(new Date().getDay()); // Mock insights based on typical Singapore transport patterns return [ { originBusStop: 'CBD Area', destinationBusStop: 'Residential Areas', totalTrips: currentHour >= 17 && currentHour <= 19 ? 5000 : 2000, timeOfDay: currentHour >= 17 && currentHour <= 19 ? 'evening_peak' : 'off_peak', dayType: isWeekend ? 'weekend' : 'weekday' }, { originTrainStation: 'Jurong East', destinationTrainStation: 'City Hall', totalTrips: currentHour >= 7 && currentHour <= 9 ? 8000 : 3000, timeOfDay: currentHour >= 7 && currentHour <= 9 ? 'morning_peak' : 'off_peak', dayType: isWeekend ? 'weekend' : 'weekday' } ]; } }