@siva-sub/mcp-public-transport
Version:
A Model Context Protocol server for Singapore transport data with real-time information and routing
282 lines (281 loc) • 10.8 kB
JavaScript
/**
* Instruction parsing service for OneMap routing responses
*/
export class InstructionParserService {
/**
* Parse instructions from OneMap response
*/
parseInstructions(response) {
const responseType = this.getResponseType(response);
switch (responseType) {
case 'DIRECT_ROUTING':
return this.parseDirectInstructions(response);
case 'PUBLIC_TRANSPORT':
return this.parseTransitInstructions(response);
default:
return [];
}
}
/**
* Parse direct routing instructions (walk/drive/cycle)
*/
parseDirectInstructions(response) {
if (!response.route_instructions)
return [];
return response.route_instructions.map((inst, index) => ({
step: index + 1,
type: 'direct',
mode: this.mapDirectMode(inst[8]),
direction: inst[0],
streetName: inst[1],
distance: inst[2],
coordinates: this.parseCoordinates(inst[3]),
duration: inst[4],
instruction: inst[9] || `${inst[0]} for ${inst[5]}`,
estimatedContext: this.generateBasicContext(this.parseCoordinates(inst[3]))
}));
}
/**
* Parse public transport instructions
*/
parseTransitInstructions(response) {
if (!response.plan?.itineraries?.[0]?.legs)
return [];
const instructions = [];
let stepCounter = 1;
response.plan.itineraries[0].legs.forEach((leg) => {
if (leg.mode === 'WALK') {
// Walking segment
if (leg.steps && leg.steps.length > 0) {
leg.steps.forEach((step) => {
instructions.push({
step: stepCounter++,
type: 'transit_walk',
mode: 'WALK',
direction: step.relativeDirection,
streetName: step.streetName,
distance: step.distance,
coordinates: { lat: step.lat, lng: step.lon },
instruction: `${step.relativeDirection} on ${step.streetName} for ${step.distance}m`,
estimatedContext: this.generateBasicContext({ lat: step.lat, lng: step.lon })
});
});
}
else {
instructions.push({
step: stepCounter++,
type: 'transit_walk',
mode: 'WALK',
distance: leg.distance,
duration: leg.duration,
coordinates: { lat: leg.from.lat, lng: leg.from.lon },
instruction: `Walk ${Math.round(leg.distance)}m to ${leg.to.name}`,
estimatedContext: this.generateBasicContext({ lat: leg.from.lat, lng: leg.from.lon })
});
}
}
else {
// Transit segment (BUS, RAIL, etc.)
instructions.push({
step: stepCounter++,
type: 'transit',
mode: this.mapTransitMode(leg.mode),
service: leg.routeShortName || leg.route,
operator: leg.agencyName,
distance: leg.distance,
duration: leg.duration,
coordinates: { lat: leg.from.lat, lng: leg.from.lon },
instruction: `Take ${leg.routeShortName || leg.route} from ${leg.from.name} to ${leg.to.name}`,
from: {
name: leg.from.name,
stopCode: leg.from.stopCode,
coordinates: { lat: leg.from.lat, lng: leg.from.lon }
},
to: {
name: leg.to.name,
stopCode: leg.to.stopCode,
coordinates: { lat: leg.to.lat, lng: leg.to.lon }
},
intermediateStops: leg.intermediateStops?.map((stop) => ({
name: stop.name,
stopCode: stop.stopCode,
coordinates: { lat: stop.lat, lng: stop.lon }
})),
estimatedContext: this.generateBasicContext({ lat: leg.from.lat, lng: leg.from.lon })
});
}
});
return instructions;
}
/**
* Generate route summary
*/
generateSummary(response, instructions, polylineCount) {
const summary = {
responseType: this.getResponseType(response),
instructionCount: instructions.length,
polylineCount
};
if (response.route_summary) {
summary.totalTime = response.route_summary.total_time;
summary.totalDistance = response.route_summary.total_distance;
}
if (response.plan?.itineraries?.[0]) {
const itinerary = response.plan.itineraries[0];
summary.totalTime = itinerary.duration;
summary.walkDistance = itinerary.walkDistance;
summary.transfers = itinerary.transfers;
summary.fare = itinerary.fare;
summary.totalCost = parseFloat(itinerary.fare) || 0;
}
return summary;
}
/**
* Enhance instructions with contextual information
*/
enhanceInstructions(instructions) {
return instructions.map(instruction => ({
...instruction,
estimatedContext: {
area: instruction.estimatedContext?.area || 'Unknown',
timeOfDay: instruction.estimatedContext?.timeOfDay || this.getCurrentTimeContext(),
weatherNote: instruction.estimatedContext?.weatherNote || 'Check weather conditions for outdoor segments',
landmark: this.findNearbyLandmark(instruction.coordinates),
safetyNote: this.getSafetyNote(instruction.coordinates),
accessibilityInfo: this.getAccessibilityInfo(instruction.coordinates)
}
}));
}
/**
* Format instructions for human reading
*/
formatInstructionsForDisplay(instructions) {
return instructions.map(inst => {
let formatted = `Step ${inst.step}: ${inst.instruction}`;
if (inst.duration) {
const minutes = Math.round(inst.duration / 60);
const seconds = inst.duration % 60;
formatted += ` (${minutes > 0 ? `${minutes}min ` : ''}${seconds}s)`;
}
if (inst.service && inst.operator) {
formatted += ` [${inst.service} - ${inst.operator}]`;
}
if (inst.estimatedContext?.area) {
formatted += ` - ${inst.estimatedContext.area}`;
}
return formatted;
});
}
/**
* Helper methods
*/
getResponseType(response) {
if (!response)
return 'ERROR';
if (response.plan?.itineraries)
return 'PUBLIC_TRANSPORT';
if (response.route_instructions)
return 'DIRECT_ROUTING';
return 'ERROR';
}
parseCoordinates(coordString) {
if (!coordString)
return null;
const [lat, lng] = coordString.split(',').map(parseFloat);
return { lat, lng };
}
mapDirectMode(apiMode) {
switch (apiMode?.toLowerCase()) {
case 'walking':
return 'WALK';
case 'driving':
return 'TAXI';
default:
return 'WALK';
}
}
mapTransitMode(apiMode) {
switch (apiMode.toUpperCase()) {
case 'WALK':
return 'WALK';
case 'BUS':
return 'BUS';
case 'RAIL':
case 'SUBWAY':
return 'SUBWAY';
default:
return 'WALK';
}
}
generateBasicContext(coordinates) {
if (!coordinates) {
return {
area: 'Unknown',
timeOfDay: this.getCurrentTimeContext(),
weatherNote: 'Check weather conditions for outdoor segments'
};
}
return {
area: this.getAreaFromCoordinates(coordinates),
timeOfDay: this.getCurrentTimeContext(),
weatherNote: 'Check weather conditions for outdoor segments'
};
}
getAreaFromCoordinates(coordinates) {
const { lat, lng } = coordinates;
if (lat >= 1.38 && lat <= 1.45 && lng >= 103.89 && lng <= 103.95)
return 'Punggol/Sengkang';
if (lat >= 1.31 && lat <= 1.33 && lng >= 103.84 && lng <= 103.86)
return 'Novena/Toa Payoh';
if (lat >= 1.27 && lat <= 1.29 && lng >= 103.84 && lng <= 103.86)
return 'Chinatown/CBD';
if (lat >= 1.29 && lat <= 1.31 && lng >= 103.77 && lng <= 103.79)
return 'Jurong';
if (lat >= 1.30 && lat <= 1.32 && lng >= 103.81 && lng <= 103.83)
return 'Orchard/Somerset';
return 'Singapore';
}
getCurrentTimeContext() {
const hour = new Date().getHours();
if (hour >= 7 && hour <= 9)
return 'Morning Peak';
if (hour >= 17 && hour <= 19)
return 'Evening Peak';
if (hour >= 23 || hour <= 5)
return 'Late Night';
return 'Off Peak';
}
findNearbyLandmark(coordinates) {
if (!coordinates)
return '';
// This would be enhanced with actual landmark data
const area = this.getAreaFromCoordinates(coordinates);
switch (area) {
case 'Punggol/Sengkang':
return 'Near Punggol Waterway';
case 'Novena/Toa Payoh':
return 'Near Novena Medical Hub';
case 'Chinatown/CBD':
return 'Near Marina Bay';
case 'Orchard/Somerset':
return 'Near Orchard Road Shopping';
default:
return '';
}
}
getSafetyNote(coordinates) {
if (!coordinates)
return '';
const hour = new Date().getHours();
if (hour >= 22 || hour <= 6) {
return 'Well-lit area recommended for night travel';
}
return 'Safe area for travel';
}
getAccessibilityInfo(coordinates) {
if (!coordinates)
return '';
// This would be enhanced with actual accessibility data
return 'Check for wheelchair accessibility at transit stops';
}
}