@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
JavaScript
/**
* 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'
}
];
}
}