UNPKG

tfl-ts

Version:

🚇 Fully-typed TypeScript client for Transport for London (TfL) API • Zero dependencies • Auto-generated types • Real-time arrivals • Journey planning • Universal compatibility

928 lines (927 loc) 43.1 kB
"use strict"; /** * Journey API Module * * Provides comprehensive journey planning functionality across the TfL transport network. * This module allows users to plan routes between any two locations in London, with support * for multi-modal journeys, accessibility preferences, cycling routes, and real-time information. * * **Features:** * - Route planning between any two locations (stations, coordinates, postcodes, or free-text) * - Multi-modal journey options (tube, bus, DLR, river bus, etc.) * - Accessibility preferences for step-free access * - Cycling journey planning with bike hire integration * - Real-time journey information with live arrivals * - Fare calculations and alternative routes * - Disambiguation handling for ambiguous locations * * @example * // Plan a basic journey * const journey = await client.journey.plan({ * from: '940GZZLUOXC', // Oxford Circus Station ID * to: '940GZZLUVIC', // Victoria Station ID * mode: ['tube', 'bus'], * timeIs: 'Departing', * date: '20241201', * time: '1430' * }); * * // Plan an accessible journey * const accessibleJourney = await client.journey.plan({ * from: 'Kings Cross', * to: 'Waterloo', * accessibilityPreference: ['StepFreeToVehicle', 'NoEscalators'], * walkingSpeed: 'Slow', * maxWalkingMinutes: '15' * }); * * // Plan a cycling journey * const cycleJourney = await client.journey.plan({ * from: 'Hyde Park', * to: 'Regents Park', * cyclePreference: 'AllTheWay', * bikeProficiency: ['Easy'], * alternativeCycle: true * }); */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Modes = exports.severityByMode = exports.severityDescriptions = exports.modeMetadata = exports.Journey = void 0; const stripTypes_1 = require("./utils/stripTypes"); const format_1 = require("./utils/format"); // Import generated metadata (NEVER hardcode!) const Meta_1 = require("./generated/meta/Meta"); Object.defineProperty(exports, "Modes", { enumerable: true, get: function () { return Meta_1.Modes; } }); // Create mode metadata from the generated Modes data const modeMetadata = Meta_1.Modes.reduce((acc, mode) => { acc[mode.modeName] = { isTflService: mode.isTflService, isFarePaying: mode.isFarePaying, isScheduledService: mode.isScheduledService }; return acc; }, {}); exports.modeMetadata = modeMetadata; // Build severity by mode mapping from generated data const buildSeverityByMode = () => { const severityMap = {}; Meta_1.Severity.forEach(severity => { if (!severityMap[severity.modeName]) { severityMap[severity.modeName] = []; } severityMap[severity.modeName].push({ level: severity.severityLevel, description: severity.description }); }); // Sort by severity level (descending) Object.keys(severityMap).forEach(mode => { severityMap[mode].sort((a, b) => b.level - a.level); }); return severityMap; }; // Build severity descriptions from generated data const buildSeverityDescriptions = () => { const descriptions = Array.from(new Set(Meta_1.Severity.map(s => s.description))); return descriptions.sort(); }; // Build severity by mode mapping const severityByMode = buildSeverityByMode(); exports.severityByMode = severityByMode; const severityDescriptions = buildSeverityDescriptions(); exports.severityDescriptions = severityDescriptions; /** * Journey class for planning routes across TfL transport network * * Provides comprehensive journey planning functionality including: * - Route planning between any two locations * - Multi-modal journey options * - Accessibility preferences * - Real-time journey information * - Fare calculations * - Disambiguation handling for ambiguous locations * * @example * // Plan a journey * const journey = await client.journey.plan({ * from: 'Oxford Circus', * to: 'Victoria', * mode: ['tube', 'bus'] * }); * * // Handle disambiguation * if (journey.disambiguation) { * console.log('Multiple options found. Please choose from:'); * journey.disambiguation.toLocationDisambiguation?.disambiguationOptions.forEach(option => { * console.log(`${option.place.commonName} (${option.parameterValue})`); * }); * } * * // Get available journey modes * const modes = await client.journey.getModes(); * * // Plan an accessible journey * const accessibleJourney = await client.journey.plan({ * from: 'Kings Cross', * to: 'Waterloo', * accessibilityPreference: ['StepFreeToVehicle', 'NoEscalators'], * walkingSpeed: 'Slow' * }); * * // Get static metadata (no HTTP request) * const modeNames = client.journey.MODE_NAMES; // ['tube', 'bus', 'dlr', ...] * const timeIsOptions = client.journey.TIME_IS_OPTIONS; // ['Arriving', 'Departing'] * * // Validate user input before making API calls * const validateJourneyOptions = (options: JourneyQuery) => { * const validModes = options.mode?.filter(mode => client.journey.MODE_NAMES.includes(mode)); * if (validModes && validModes.length !== options.mode?.length) { * const invalidModes = options.mode?.filter(mode => !client.journey.MODE_NAMES.includes(mode)); * throw new Error(`Invalid modes: ${invalidModes?.join(', ')}`); * } * return validModes; * }; */ class Journey { constructor(api) { this.api = api; // 🚨 ALWAYS use generated metadata (never hardcode!) /** Available mode names (static, no HTTP request needed) */ this.MODE_NAMES = Meta_1.Modes.map(m => m.modeName); /** Available service types (static, no HTTP request needed) */ this.SERVICE_TYPES = Meta_1.ServiceTypes; /** Available disruption categories (static, no HTTP request needed) */ this.DISRUPTION_CATEGORIES = Meta_1.DisruptionCategories; /** Available place types (static, no HTTP request needed) */ this.PLACE_TYPES = Meta_1.PlaceTypes; /** Available search providers (static, no HTTP request needed) */ this.SEARCH_PROVIDERS = Meta_1.SearchProviders; /** Available sort options (static, no HTTP request needed) */ this.SORT_OPTIONS = Meta_1.Sorts; /** Available stop types (static, no HTTP request needed) */ this.STOP_TYPES = Meta_1.StopTypes; /** Available categories with their keys (static, no HTTP request needed) */ this.CATEGORIES = Meta_1.Categories; /** All severity levels and descriptions (static, no HTTP request needed) */ this.ALL_SEVERITY = Meta_1.Severity; // Build derived metadata from generated data /** Severity levels by transport mode */ this.SEVERITY_BY_MODE = severityByMode; /** All available severity descriptions */ this.SEVERITY_DESCRIPTIONS = severityDescriptions; /** Mode metadata with service information */ this.MODE_METADATA = modeMetadata; // Journey-specific constants /** Available time options for journey planning */ this.TIME_IS_OPTIONS = ['Arriving', 'Departing']; /** Available journey preferences */ this.JOURNEY_PREFERENCES = ['LeastInterchange', 'LeastTime', 'LeastWalking']; /** Available accessibility preferences */ this.ACCESSIBILITY_PREFERENCES = [ 'NoRequirements', 'NoSolidStairs', 'NoEscalators', 'NoElevators', 'StepFreeToVehicle', 'StepFreeToPlatform' ]; /** Available walking speeds */ this.WALKING_SPEEDS = ['Slow', 'Average', 'Fast']; /** Available cycle preferences */ this.CYCLE_PREFERENCES = [ 'None', 'LeaveAtStation', 'TakeOnTransport', 'AllTheWay', 'CycleHire' ]; /** Available bike proficiency levels */ this.BIKE_PROFICIENCIES = ['Easy', 'Moderate', 'Fast']; } /** * Plan a journey between two locations * * This method plans routes between any two locations in London, with support for * multi-modal journeys, accessibility preferences, cycling routes, and real-time information. * * @param options - Journey planning options * @returns Promise resolving to journey itinerary results with simplified types * @example * const journey = await client.journey.plan({ * from: '940GZZLUOXC', * to: '940GZZLUVIC', * mode: ['tube', 'bus'], * timeIs: 'Departing', * date: '20241201', * time: '1430' * }); * * @example * // Plan a journey with accessibility requirements * const accessibleJourney = await client.journey.plan({ * from: 'Kings Cross', * to: 'Waterloo', * accessibilityPreference: ['StepFreeToVehicle', 'NoEscalators'], * walkingSpeed: 'Slow', * maxWalkingMinutes: '15' * }); * * @example * // Plan a cycling journey * const cycleJourney = await client.journey.plan({ * from: 'Hyde Park', * to: 'Regents Park', * cyclePreference: 'AllTheWay', * bikeProficiency: ['Easy'], * alternativeCycle: true * }); * * @example * // Handle disambiguation when multiple options are found * const journey = await client.journey.plan({ * from: 'Westminster', * to: 'Bank' * }); * * if (journey.disambiguation) { * console.log('Multiple options found. Please choose from:'); * * // Show 'from' options * if (journey.disambiguation.fromLocationDisambiguation) { * console.log('From options:'); * journey.disambiguation.fromLocationDisambiguation.disambiguationOptions.forEach(option => { * console.log(`- ${option.place.commonName} (${option.parameterValue})`); * }); * } * * // Show 'to' options * if (journey.disambiguation.toLocationDisambiguation) { * console.log('To options:'); * journey.disambiguation.toLocationDisambiguation.disambiguationOptions.forEach(option => { * console.log(`- ${option.place.commonName} (${option.parameterValue})`); * }); * } * * // Use a specific option for the journey * const specificJourney = await client.journey.plan({ * from: '1000266', // Westminster Station * to: '1000013' // Bank Station * }); * } * * @example * // Validate user input before making API calls * const userInput = ['tube', 'invalid-mode']; * const validModes = userInput.filter(mode => client.journey.MODE_NAMES.includes(mode)); * if (validModes.length !== userInput.length) { * throw new Error(`Invalid modes: ${userInput.filter(mode => !client.journey.MODE_NAMES.includes(mode)).join(', ')}`); * } */ async plan(options) { const { keepTflTypes = false, ...apiOptions } = options; // Fix mode parameter to be comma-separated string instead of array if (apiOptions.mode && Array.isArray(apiOptions.mode)) { apiOptions.mode = apiOptions.mode.join(','); } try { const result = await this.api.journey.journeyJourneyResults(apiOptions) .then(response => (0, stripTypes_1.stripTypeFields)(response.data, keepTflTypes)); return this.simplifyJourneyResult(result); } catch (error) { // Handle disambiguation responses (status 300) // The generated client throws the entire Response object if (error?.status === 300) { // Extract the response body to get disambiguation data let disambiguationData = undefined; try { // Clone the response and read the body const responseClone = error.clone ? error.clone() : error; if (responseClone.body && !responseClone.bodyUsed) { const text = await responseClone.text(); disambiguationData = JSON.parse(text); } } catch (parseError) { console.warn('Failed to parse disambiguation response:', parseError); } // Return a special result indicating disambiguation is needed return { journeys: [], stationNames: { from: options.from, to: options.to }, stopMessages: [`Disambiguation required. Multiple options found for "${options.from}" and/or "${options.to}". Please use specific station IDs.`], disambiguation: disambiguationData }; } // Re-throw other errors throw error; } } /** * Get available transport modes for journey planning * * This method retrieves all available transport modes that can be used * for journey planning, including their metadata and capabilities. * * @returns Promise resolving to an array of available modes * @example * const modes = await client.journey.getModes(); * console.log('Available modes:', modes.map(m => m.modeName)); * * // Use static metadata instead (no HTTP request) * const modeNames = client.journey.MODE_NAMES; // ['tube', 'bus', 'dlr', ...] */ async getModes() { return this.api.journey.journeyMeta().then(response => response.data); } /** * Extract station names from journey result * * This method extracts the actual station names from a journey result, * which can be useful for displaying user-friendly location names. * * @param journey - Journey result from plan method * @returns Object with from and to station names * @example * const journey = await client.journey.plan({ from: 'Oxford Circus', to: 'Victoria' }); * const stationNames = client.journey.getStationNames(journey); * console.log(`From: ${stationNames.from}, To: ${stationNames.to}`); */ getStationNames(journey) { const stationNames = {}; if (journey.journeys && journey.journeys.length > 0) { const firstJourney = journey.journeys[0]; if (firstJourney.legs && firstJourney.legs.length > 0) { // Get first leg departure point const firstLeg = firstJourney.legs[0]; if (firstLeg.instruction?.summary) { const summary = firstLeg.instruction.summary; // Extract station name from instruction like "Walk to Oxford Circus Underground Station" const match = summary.match(/to (.+?)(?: Underground Station| Station|$)/i); if (match) { stationNames.from = match[1]; } } // Get last leg arrival point const lastLeg = firstJourney.legs[firstJourney.legs.length - 1]; if (lastLeg.instruction?.summary) { const summary = lastLeg.instruction.summary; // Extract station name from instruction const match = summary.match(/to (.+?)(?: Underground Station| Station|$)/i); if (match) { stationNames.to = match[1]; } } } } return stationNames; } /** * Validate journey planning parameters * * This method validates journey planning options before making API calls, * helping to catch errors early and provide better error messages. * * @param options - Journey planning options to validate * @returns Object with validation results and any errors * @example * const validation = client.journey.validateOptions({ * from: 'Oxford Circus', * to: 'Victoria', * mode: ['tube', 'invalid-mode'] * }); * * if (!validation.isValid) { * console.log('Validation errors:', validation.errors); * } */ validateOptions(options) { const errors = []; // Validate required fields if (!options.from) { errors.push('Origin location (from) is required'); } if (!options.to) { errors.push('Destination location (to) is required'); } // Validate modes if provided if (options.mode && options.mode.length > 0) { const invalidModes = options.mode.filter(mode => !this.MODE_NAMES.includes(mode)); if (invalidModes.length > 0) { errors.push(`Invalid modes: ${invalidModes.join(', ')}. Valid modes: ${this.MODE_NAMES.join(', ')}`); } } // Validate timeIs if provided if (options.timeIs && !this.TIME_IS_OPTIONS.includes(options.timeIs)) { errors.push(`Invalid timeIs: ${options.timeIs}. Valid options: ${this.TIME_IS_OPTIONS.join(', ')}`); } // Validate journeyPreference if provided if (options.journeyPreference && !this.JOURNEY_PREFERENCES.includes(options.journeyPreference)) { errors.push(`Invalid journeyPreference: ${options.journeyPreference}. Valid options: ${this.JOURNEY_PREFERENCES.join(', ')}`); } // Validate accessibilityPreference if provided if (options.accessibilityPreference && options.accessibilityPreference.length > 0) { const invalidPrefs = options.accessibilityPreference.filter(pref => !this.ACCESSIBILITY_PREFERENCES.includes(pref)); if (invalidPrefs.length > 0) { errors.push(`Invalid accessibility preferences: ${invalidPrefs.join(', ')}. Valid options: ${this.ACCESSIBILITY_PREFERENCES.join(', ')}`); } } // Validate walkingSpeed if provided if (options.walkingSpeed && !this.WALKING_SPEEDS.includes(options.walkingSpeed)) { errors.push(`Invalid walkingSpeed: ${options.walkingSpeed}. Valid options: ${this.WALKING_SPEEDS.join(', ')}`); } // Validate cyclePreference if provided if (options.cyclePreference && !this.CYCLE_PREFERENCES.includes(options.cyclePreference)) { errors.push(`Invalid cyclePreference: ${options.cyclePreference}. Valid options: ${this.CYCLE_PREFERENCES.join(', ')}`); } // Validate bikeProficiency if provided if (options.bikeProficiency && options.bikeProficiency.length > 0) { const invalidProfs = options.bikeProficiency.filter(prof => !this.BIKE_PROFICIENCIES.includes(prof)); if (invalidProfs.length > 0) { errors.push(`Invalid bike proficiencies: ${invalidProfs.join(', ')}. Valid options: ${this.BIKE_PROFICIENCIES.join(', ')}`); } } return { isValid: errors.length === 0, errors }; } /** * Get journey planning metadata and constants * * This method returns all available constants and metadata for journey planning, * including transport modes, preferences, and validation options. * * @returns Object containing all journey planning constants and metadata * @example * const metadata = client.journey.getMetadata(); * console.log('Available modes:', metadata.modes); * console.log('Journey preferences:', metadata.journeyPreferences); */ getMetadata() { return { modes: this.MODE_NAMES, serviceTypes: this.SERVICE_TYPES, disruptionCategories: this.DISRUPTION_CATEGORIES, placeTypes: this.PLACE_TYPES, searchProviders: this.SEARCH_PROVIDERS, sortOptions: this.SORT_OPTIONS, stopTypes: this.STOP_TYPES, categories: this.CATEGORIES, severity: this.ALL_SEVERITY, severityByMode: this.SEVERITY_BY_MODE, severityDescriptions: this.SEVERITY_DESCRIPTIONS, modeMetadata: this.MODE_METADATA, timeIsOptions: this.TIME_IS_OPTIONS, journeyPreferences: this.JOURNEY_PREFERENCES, accessibilityPreferences: this.ACCESSIBILITY_PREFERENCES, walkingSpeeds: this.WALKING_SPEEDS, cyclePreferences: this.CYCLE_PREFERENCES, bikeProficiencies: this.BIKE_PROFICIENCIES }; } /** * Get all available mode verbs for natural language generation * * Returns a complete mapping of all transport modes to their natural language verbs, * useful for building custom natural language interfaces or validating mode verbs. * * @returns Object mapping mode names to their verb forms * @example * const modeVerbs = client.journey.getAllModeVerbs(); * console.log('Tube verbs:', modeVerbs.tube); * // Output: { imperative: 'take', gerund: 'taking', present: 'take', article: 'the' } * * // Use in custom natural language generation * const customInstruction = `Please ${modeVerbs.tube.imperative} ${modeVerbs.tube.article} tube to your destination`; */ getAllModeVerbs() { const allVerbs = {}; // Get verbs for all available modes this.MODE_NAMES.forEach(mode => { allVerbs[mode] = this.getModeVerbs(mode); }); // Add additional common modes that might not be in MODE_NAMES const additionalModes = ['interchange-keep-sitting', 'interchange-secure']; additionalModes.forEach(mode => { allVerbs[mode] = this.getModeVerbs(mode); }); return allVerbs; } /** * Get emoji icon for transport mode * * Returns an emoji icon for each transport mode that can be used * in UI displays or console output. * * @param mode - Transport mode name * @returns Emoji icon string * @example * const icon = client.journey.getModeIcon('tube'); * console.log(icon); // "🚇" * * // Use in UI * const modeIcon = client.journey.getModeIcon(leg.mode?.name || 'walking'); * console.log(`${modeIcon} ${leg.instruction?.summary}`); */ getModeIcon(mode) { const icons = { 'tube': '🚇', 'bus': '🚌', 'dlr': '🚈', 'overground': '🚆', 'elizabeth-line': '🚄', 'national-rail': '🚂', 'tram': '🚊', 'river-bus': '⛴️', 'river-tour': '🚢', 'cable-car': '🚡', 'cycle': '🚲', 'cycle-hire': '🚲', 'walking': '🚶', 'taxi': '🚕', 'coach': '🚌', 'replacement-bus': '🚌', 'interchange-keep-sitting': '🔄', 'interchange-secure': '🔒' }; return icons[mode] || '🚀'; } /** * Get mode display name for natural language generation * * Returns a human-readable name for each transport mode that can be used * in natural language sentences without emojis. * * @param mode - Transport mode name * @returns Human-readable mode name * @example * const modeName = client.journey.getModeDisplayName('tube'); * console.log(modeName); // "tube" * * const busName = client.journey.getModeDisplayName('bus'); * console.log(busName); // "bus" * * // Use in natural language * const instruction = `Take the ${client.journey.getModeDisplayName(leg.mode?.name || 'walking')} for ${duration} minutes`; */ getModeDisplayName(mode) { const displayNames = { 'tube': 'tube', 'bus': 'bus', 'dlr': 'DLR', 'overground': 'Overground', 'elizabeth-line': 'Elizabeth line', 'national-rail': 'train', 'tram': 'tram', 'river-bus': 'river bus', 'river-tour': 'river tour', 'cable-car': 'cable car', 'cycle': 'bike', 'cycle-hire': 'bike', 'walking': '', 'taxi': 'taxi', 'coach': 'coach', 'replacement-bus': 'replacement bus', 'interchange-keep-sitting': 'same vehicle', 'interchange-secure': 'transfer' }; return displayNames[mode] || mode; } /** * Get verb for transport mode for natural language generation * * Returns appropriate verbs that can be used in natural language sentences * like "take the tube", "walk", "board the bus", etc. * * @param mode - Transport mode name * @returns Object with different verb forms for the mode * @example * const verbs = client.journey.getModeVerbs('tube'); * console.log(verbs.imperative); // "take" * console.log(verbs.gerund); // "taking" * console.log(verbs.present); // "take" * * // Use in natural language * const instruction = `Please ${verbs.imperative} the ${mode} for ${duration} minutes`; */ getModeVerbs(mode) { const verbs = { 'tube': { imperative: 'take', gerund: 'taking', present: 'take', article: 'the' }, 'bus': { imperative: 'board', gerund: 'boarding', present: 'board', article: 'the' }, 'dlr': { imperative: 'take', gerund: 'taking', present: 'take', article: 'the' }, 'overground': { imperative: 'take', gerund: 'taking', present: 'take', article: 'the' }, 'elizabeth-line': { imperative: 'take', gerund: 'taking', present: 'take', article: 'the' }, 'national-rail': { imperative: 'take', gerund: 'taking', present: 'take', article: 'the' }, 'tram': { imperative: 'take', gerund: 'taking', present: 'take', article: 'the' }, 'river-bus': { imperative: 'board', gerund: 'boarding', present: 'board', article: 'the' }, 'river-tour': { imperative: 'board', gerund: 'boarding', present: 'board', article: 'the' }, 'cable-car': { imperative: 'take', gerund: 'taking', present: 'take', article: 'the' }, 'cycle': { imperative: 'cycle', gerund: 'cycling', present: 'cycle', article: '' }, 'cycle-hire': { imperative: 'cycle', gerund: 'cycling', present: 'cycle', article: '' }, 'walking': { imperative: 'walk', gerund: 'walking', present: 'walk', article: '' }, 'taxi': { imperative: 'take', gerund: 'taking', present: 'take', article: 'a' }, 'coach': { imperative: 'board', gerund: 'boarding', present: 'board', article: 'the' }, 'replacement-bus': { imperative: 'board', gerund: 'boarding', present: 'board', article: 'the' }, 'interchange-keep-sitting': { imperative: 'stay on', gerund: 'staying on', present: 'stay on', article: '' }, 'interchange-secure': { imperative: 'transfer to', gerund: 'transferring to', present: 'transfer to', article: '' } }; return verbs[mode] || { imperative: 'use', gerund: 'using', present: 'use', article: 'the' }; } /** * Generate natural language instruction for a journey leg * * Creates human-readable instructions like "Walk 3 minutes to Oxford Circus, * then take the tube for 5 minutes to Victoria" * * @param leg - Journey leg data * @param isFirst - Whether this is the first leg in the journey * @returns Natural language instruction string * @example * const journey = await client.journey.plan({ from: 'Oxford Circus', to: 'Victoria' }); * if (journey.journeys?.[0]?.legs) { * journey.journeys[0].legs.forEach((leg, index) => { * const instruction = client.journey.generateNaturalInstruction(leg, index === 0); * console.log(instruction); * }); * } */ generateNaturalInstruction(leg, isFirst = false) { const modeName = leg.mode?.name || 'walking'; const duration = leg.duration || 0; const verbs = this.getModeVerbs(modeName); const article = verbs.article; const modeDisplay = modeName === 'walking' ? 'walking' : modeName === 'cycle' || modeName === 'cycle-hire' ? 'cycling' : modeName === 'tube' ? 'tube' : modeName === 'bus' ? 'bus' : modeName === 'dlr' ? 'DLR' : modeName === 'overground' ? 'Overground' : modeName === 'elizabeth-line' ? 'Elizabeth line' : modeName === 'national-rail' ? 'train' : modeName === 'tram' ? 'tram' : modeName === 'river-bus' ? 'river bus' : modeName === 'river-tour' ? 'river tour' : modeName === 'cable-car' ? 'cable car' : modeName === 'taxi' ? 'taxi' : modeName === 'coach' ? 'coach' : modeName === 'replacement-bus' ? 'replacement bus' : modeName; // Extract bus line information from original instruction if available let lineInfo = ''; if (leg.instruction?.summary && (modeName === 'bus' || modeName === 'replacement-bus')) { // Look for bus line patterns like "390 bus", "N136 bus", etc. const busLineMatch = leg.instruction.summary.match(/(\d+|[A-Z]\d+)\s+bus/i); if (busLineMatch) { lineInfo = ` ${busLineMatch[1]}`; } } // Handle different modes with appropriate language if (modeName === 'walking') { const distance = leg.distance || 0; const distanceText = distance > 0 ? ` (${(0, format_1.formatDistance)(distance)})` : ''; return `${isFirst ? 'Walk' : 'Then walk'} ${duration} minute${duration !== 1 ? 's' : ''}${distanceText}`; } if (modeName === 'cycle' || modeName === 'cycle-hire') { return `${isFirst ? 'Cycle' : 'Then cycle'} for ${duration} minute${duration !== 1 ? 's' : ''}`; } if (modeName === 'interchange-keep-sitting') { return `${isFirst ? 'Stay on' : 'Then stay on'} the same service`; } if (modeName === 'interchange-secure') { return `${isFirst ? 'Transfer to' : 'Then transfer to'} the next service`; } // For public transport modes const transportInstruction = `${isFirst ? verbs.imperative.charAt(0).toUpperCase() + verbs.imperative.slice(1) : 'Then ' + verbs.imperative} ${article}${lineInfo} ${modeDisplay} for ${duration} minute${duration !== 1 ? 's' : ''}`; return transportInstruction; } /** * Generate complete natural language journey description * * Creates a full natural language description of the entire journey * * @param journey - Journey data * @returns Complete natural language journey description * @example * const journey = await client.journey.plan({ from: 'Oxford Circus', to: 'Victoria' }); * if (journey.journeys?.[0]) { * const description = client.journey.generateNaturalDescription(journey.journeys[0]); * console.log(description); * // Output: "Walk 2 minutes to Oxford Circus Underground Station, then take the tube for 5 minutes to Victoria" * } */ generateNaturalDescription(journey) { if (!journey.legs || journey.legs.length === 0) { return 'Journey details unavailable'; } const instructions = []; journey.legs.forEach((leg, index) => { const instruction = this.generateNaturalInstruction(leg, index === 0); instructions.push(instruction); }); return instructions.join(', '); } /** * Get API endpoint information * * This method returns information about the available API endpoints * for the Journey module. * * @returns Object containing endpoint information * @example * // Get endpoint information * const endpoints = client.journey.getEndpoints(); * console.log('Available endpoints:', Object.keys(endpoints)); * * // Get specific endpoint details * const journeyEndpoint = endpoints.JOURNEY_RESULTS; * console.log('Journey endpoint path:', journeyEndpoint.path); * console.log('Journey endpoint method:', journeyEndpoint.method); */ getEndpoints() { return Journey.ENDPOINTS; } /** * Simplify journey result for better developer experience */ simplifyJourneyResult(result) { const simplifiedJourneys = result.journeys?.map(journey => this.simplifyJourney(journey)) || []; return { journeys: simplifiedJourneys, stationNames: this.getStationNames({ journeys: simplifiedJourneys }), stopMessages: result.stopMessages }; } /** * Simplify individual journey with better undefined handling */ simplifyJourney(journey) { // Generate a meaningful description if none is provided const description = journey.description || this.generateJourneyDescription(journey); return { startDateTime: journey.startDateTime, duration: journey.duration, arrivalDateTime: journey.arrivalDateTime, description, alternativeRoute: journey.alternativeRoute || false, legs: journey.legs?.map(leg => this.simplifyLeg(leg)) || [], fare: journey.fare ? this.simplifyFare(journey.fare) : undefined }; } /** * Generate a meaningful journey description from legs */ generateJourneyDescription(journey) { if (!journey.legs || journey.legs.length === 0) { return 'Journey details unavailable'; } const modes = journey.legs .map(leg => leg.mode?.name) .filter((mode) => Boolean(mode)) .filter((mode, index, arr) => arr.indexOf(mode) === index); // Remove duplicates if (modes.length === 0) { return 'Walking journey'; } if (modes.length === 1) { return `${modes[0].charAt(0).toUpperCase() + modes[0].slice(1)} journey`; } return `${modes.length} mode journey (${modes.join(', ')})`; } /** * Simplify journey leg with better undefined handling */ simplifyLeg(leg) { // Generate meaningful instruction if none provided const instruction = leg.instruction ? { summary: leg.instruction.summary || this.generateLegSummary(leg), detailed: leg.instruction.detailed || leg.instruction.summary || this.generateLegSummary(leg) } : { summary: this.generateLegSummary(leg), detailed: this.generateLegSummary(leg) }; return { duration: leg.duration || 0, instruction, departureTime: leg.departureTime, arrivalTime: leg.arrivalTime, mode: leg.mode ? { name: leg.mode.name || 'unknown', id: leg.mode.id || leg.mode.name || 'unknown' } : { name: 'walking', id: 'walking' }, distance: leg.distance || 0, isDisrupted: leg.isDisrupted || false }; } /** * Generate a meaningful leg summary */ generateLegSummary(leg) { const modeName = leg.mode?.name || 'walking'; const duration = leg.duration || 0; if (leg.instruction?.summary) { return leg.instruction.summary; } // Use the natural language generation for better summaries const verbs = this.getModeVerbs(modeName); const article = verbs.article; const modeDisplay = modeName === 'walking' ? 'walking' : modeName === 'cycle' || modeName === 'cycle-hire' ? 'cycling' : modeName === 'tube' ? 'tube' : modeName === 'bus' ? 'bus' : modeName === 'dlr' ? 'DLR' : modeName === 'overground' ? 'Overground' : modeName === 'elizabeth-line' ? 'Elizabeth line' : modeName === 'national-rail' ? 'train' : modeName === 'tram' ? 'tram' : modeName === 'river-bus' ? 'river bus' : modeName === 'river-tour' ? 'river tour' : modeName === 'cable-car' ? 'cable car' : modeName === 'taxi' ? 'taxi' : modeName === 'coach' ? 'coach' : modeName === 'replacement-bus' ? 'replacement bus' : modeName; if (modeName === 'walking') { return `Walk for ${duration} minutes`; } if (modeName === 'cycle' || modeName === 'cycle-hire') { return `Cycle for ${duration} minutes`; } if (modeName === 'interchange-keep-sitting') { return 'Stay on the same service'; } if (modeName === 'interchange-secure') { return 'Transfer to the next service'; } return `${verbs.imperative.charAt(0).toUpperCase() + verbs.imperative.slice(1)} ${article} ${modeDisplay} for ${duration} minutes`; } /** * Simplify fare information with better undefined handling */ simplifyFare(fare) { return { totalCost: fare.totalCost || 0, fares: fare.fares?.map(f => ({ cost: f.cost || 0, chargeProfileName: f.chargeProfileName || 'Standard', peak: f.peak || 0, offPeak: f.offPeak || 0 })) || [] }; } } exports.Journey = Journey; /** API name for this module */ Journey.API_NAME = 'Journey API'; /** Available API endpoints */ Journey.ENDPOINTS = { /** Plan a journey between two locations */ JOURNEY_RESULTS: { path: '/Journey/JourneyResults/{from}/to/{to}', method: 'GET', description: 'Gets journey results between two locations', parameters: [ { name: 'from', type: 'string', required: true, description: 'Origin location' }, { name: 'to', type: 'string', required: true, description: 'Destination location' }, { name: 'mode', type: 'string[]', required: false, description: 'Transport modes to use' }, { name: 'timeIs', type: 'string', required: false, description: 'Whether time is Arriving or Departing' }, { name: 'date', type: 'string', required: false, description: 'Date in YYYYMMDD format' }, { name: 'time', type: 'string', required: false, description: 'Time in HHMM format' }, { name: 'journeyPreference', type: 'string', required: false, description: 'Journey preference' }, { name: 'accessibilityPreference', type: 'string[]', required: false, description: 'Accessibility preferences' }, { name: 'walkingSpeed', type: 'string', required: false, description: 'Walking speed preference' }, { name: 'cyclePreference', type: 'string', required: false, description: 'Cycle preference' }, { name: 'bikeProficiency', type: 'string[]', required: false, description: 'Bike proficiency levels' }, { name: 'maxWalkingMinutes', type: 'string', required: false, description: 'Maximum walking time in minutes' }, { name: 'useMultiModalCall', type: 'boolean', required: false, description: 'Use multi-modal journey planning' }, { name: 'walkingOptimization', type: 'boolean', required: false, description: 'Optimize for walking' }, { name: 'includeAlternativeRoutes', type: 'boolean', required: false, description: 'Include alternative routes' }, { name: 'useRealTimeLiveArrivals', type: 'boolean', required: false, description: 'Use real-time live arrivals' }, { name: 'alternativeCycle', type: 'boolean', required: false, description: 'Include alternative cycling routes' }, { name: 'alternativeWalking', type: 'boolean', required: false, description: 'Include alternative walking routes' }, { name: 'applyHtmlMarkup', type: 'boolean', required: false, description: 'Apply HTML markup to instructions' }, { name: 'via', type: 'string', required: false, description: 'Via location' }, { name: 'nationalSearch', type: 'boolean', required: false, description: 'Include national rail services' }, { name: 'maxChangeMinutes', type: 'string', required: false, description: 'Maximum change time in minutes' }, { name: 'changeSpeed', type: 'string', required: false, description: 'Change speed preference' }, { name: 'adjustment', type: 'string', required: false, description: 'Time adjustment' }, { name: 'bikeProficiency', type: 'string[]', required: false, description: 'Bike proficiency levels' }, { name: 'cyclePreference', type: 'string', required: false, description: 'Cycle preference' }, { name: 'accessibilityPreference', type: 'string[]', required: false, description: 'Accessibility preferences' }, { name: 'walkingSpeed', type: 'string', required: false, description: 'Walking speed preference' }, { name: 'maxWalkingMinutes', type: 'string', required: false, description: 'Maximum walking time in minutes' }, { name: 'journeyPreference', type: 'string', required: false, description: 'Journey preference' }, { name: 'timeIs', type: 'string', required: false, description: 'Whether time is Arriving or Departing' }, { name: 'time', type: 'string', required: false, description: 'Time in HHMM format' }, { name: 'date', type: 'string', required: false, description: 'Date in YYYYMMDD format' }, { name: 'mode', type: 'string[]', required: false, description: 'Transport modes to use' }, { name: 'to', type: 'string', required: true, description: 'Destination location' }, { name: 'from', type: 'string', required: true, description: 'Origin location' } ] }, /** Get available transport modes */ META_MODES: { path: '/Journey/Meta/Modes', method: 'GET', description: 'Gets available transport modes for journey planning', parameters: [] } }; /** Total number of endpoints */ Journey.TOTAL_ENDPOINTS = 2;