From 553ab7049c7962e773c36e18f014bf029d59f677 Mon Sep 17 00:00:00 2001 From: Edward Zhang Date: Sat, 11 Jul 2026 20:49:48 -0700 Subject: [PATCH 1/3] feat(service): add schema validation for patterns --- src/services/bustimeTypes.ts | 25 +++++++++++++++++++++++++ src/services/graphBuilder.ts | 30 +++++++++++++++--------------- src/services/mbus.ts | 8 ++++++-- src/services/ride.ts | 7 +++++-- src/state/transitState.ts | 8 +++++--- 5 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 src/services/bustimeTypes.ts diff --git a/src/services/bustimeTypes.ts b/src/services/bustimeTypes.ts new file mode 100644 index 0000000..f34cf5b --- /dev/null +++ b/src/services/bustimeTypes.ts @@ -0,0 +1,25 @@ +import z from "zod"; + +const PatternPtSchema = z.object({ + seq: z.number(), + typ: z.string(), + stpid: z.optional(z.string()), + stpnm: z.optional(z.string()), + pdist: z.optional(z.number()), + lat: z.number(), + lon: z.number(), +}); + +export const PatternSchema = z.object({ + pid: z.number(), + ln: z.number(), + rtdir: z.string(), + pt: z.array(PatternPtSchema), + dtrid: z.optional(z.string()), + dtrpt: z.optional(z.array(PatternPtSchema)), +}); + +export const PatternsArraySchema = z.array(PatternSchema); + +export type Pattern = z.infer + diff --git a/src/services/graphBuilder.ts b/src/services/graphBuilder.ts index 68ac957..7a8bcca 100644 --- a/src/services/graphBuilder.ts +++ b/src/services/graphBuilder.ts @@ -57,8 +57,8 @@ export async function rebuildGraph() { try { console.log(`Rebuilding graph...`); const allStopIds = new Set(); - Object.values(state.cachedRoutes).forEach((patterns: any) => { - patterns?.forEach((p: any) => p.pt?.forEach((pt: any) => { + Object.values(state.cachedRoutes).forEach((patterns) => { + patterns.forEach((p) => p.pt.forEach((pt) => { if (pt.stpid) allStopIds.add(pt.stpid); })); }); @@ -74,8 +74,8 @@ export async function rebuildGraph() { // extra stuff to update the busses for the ride const rideStopIds = new Set(); - Object.values(state.cachedRideRoutes).forEach((patterns: any) => { - patterns?.forEach((p: any) => p.pt?.forEach((pt: any) => { + Object.values(state.cachedRideRoutes).forEach((patterns) => { + patterns.forEach((p) => p.pt.forEach((pt) => { if (pt.stpid) rideStopIds.add(pt.stpid); })); }); @@ -100,8 +100,8 @@ export async function rebuildGraph() { * @param preds List of processed predictions */ function populateLookupMaps(preds: any[]) { - Object.values(state.cachedRoutes).forEach((patterns: any) => { - patterns?.forEach((p: any) => p.pt?.forEach((pt: any) => { + Object.values(state.cachedRoutes).forEach((patterns) => { + patterns.forEach((p) => p.pt.forEach((pt) => { if (pt.stpid && pt.stpnm) { state.stopIdToName[pt.stpid] = pt.stpnm; } @@ -130,8 +130,8 @@ function populateLookupMaps(preds: any[]) { * @param preds List of processed predictions from the ride */ function populateRideLookupMaps(preds: any[]) { - Object.values(state.cachedRideRoutes).forEach((patterns: any) => { - patterns?.forEach((p: any) => p.pt?.forEach((pt: any) => { + Object.values(state.cachedRideRoutes).forEach((patterns) => { + patterns.forEach((p) => p.pt.forEach((pt) => { if (pt.stpid && pt.stpnm) { state.rideStopIdToName[pt.stpid] = pt.stpnm; } @@ -152,10 +152,10 @@ function populateRideLookupMaps(preds: any[]) { */ function buildStopLocationMap() { const locs: Record = {}; - Object.values(state.cachedRoutes).forEach((patterns: any) => { - patterns?.forEach((p: any) => p.pt?.forEach((pt: any) => { + Object.values(state.cachedRoutes).forEach((patterns) => { + patterns.forEach((p) => p.pt.forEach((pt) => { if (pt.stpid && pt.lat) { - locs[pt.stpid] = { name: pt.stpnm, lat: parseFloat(pt.lat), lon: parseFloat(pt.lon) }; + locs[pt.stpid] = { name: pt.stpnm, lat: pt.lat, lon: pt.lon }; } })); }); @@ -168,10 +168,10 @@ function buildStopLocationMap() { */ function buildRideStops() { const locs: Record = {}; - Object.values(state.cachedRideRoutes).forEach((patterns: any) => { - patterns?.forEach((p: any) => p.pt?.forEach((pt: any) => { + Object.values(state.cachedRideRoutes).forEach((patterns) => { + patterns.forEach((p) => p.pt.forEach((pt) => { if (pt.stpid && pt.lat) { - locs[pt.stpid] = { name: pt.stpnm, lat: parseFloat(pt.lat), lon: parseFloat(pt.lon) }; + locs[pt.stpid] = { name: pt.stpnm, lat: pt.lat, lon: pt.lon }; } })); }); @@ -242,7 +242,7 @@ function processPredictions(rawChunks: any[]) { // build index maps const routeInfoFilter: Record = {}; - for (const [routeName, routeList] of Object.entries(state.cachedRoutes as Record)) { + for (const [routeName, routeList] of Object.entries(state.cachedRoutes)) { for (const route of routeList) { const rtdir = route.rtdir; const routeKey = routeName + rtdir; diff --git a/src/services/mbus.ts b/src/services/mbus.ts index a479f5a..168977e 100644 --- a/src/services/mbus.ts +++ b/src/services/mbus.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import * as process from "node:process"; import dotenv from "dotenv"; +import { Pattern, PatternsArraySchema } from './bustimeTypes'; dotenv.config(); @@ -44,13 +45,16 @@ export async function fetchRoutes() { } /** Fetches route patterns (path points) for a specific route. */ -export async function fetchPatterns(rt: string) { +export async function fetchPatterns(rt: string): Promise { try { const res = await client.get('/getpatterns', { params: { requestType: 'getpatterns', rt: rt, rtpidatafeed: 'bustime' } }); - return res.data['bustime-response']?.ptr || []; + const resData = res.data['bustime-response']?.ptr as unknown; + const patterns = PatternsArraySchema.parse(resData); + return patterns; } catch (e) { + console.error("Fetch Patterns failed", e); return []; } } diff --git a/src/services/ride.ts b/src/services/ride.ts index bd8fd31..38ca704 100644 --- a/src/services/ride.ts +++ b/src/services/ride.ts @@ -3,6 +3,7 @@ import axios from 'axios'; import * as process from "node:process"; import dotenv from "dotenv"; +import { Pattern, PatternsArraySchema } from './bustimeTypes'; dotenv.config(); @@ -46,13 +47,15 @@ export async function fetchRoutes() { } /** Fetches route patterns (path points) for a specific route. */ -export async function fetchPatterns(rt: string) { +export async function fetchPatterns(rt: string): Promise { try { const res = await client.get('/getpatterns', { params: { requestType: 'getpatterns', rt: rt, rtpidatafeed: 'bustime' } }); - return res.data['bustime-response']?.ptr || []; + const resData = res.data['bustime-response']?.ptr as unknown; + return PatternsArraySchema.parse(resData); } catch (e) { + console.error("Fetch Patterns failed", e); return []; } } diff --git a/src/state/transitState.ts b/src/state/transitState.ts index 6686a27..2d69ecc 100644 --- a/src/state/transitState.ts +++ b/src/state/transitState.ts @@ -1,3 +1,4 @@ +import { Pattern } from "@/services/bustimeTypes"; import { Trip, TransfersByOrigin, Interchange } from "../raptor/types"; /** Current positions of all buses. */ @@ -5,9 +6,9 @@ export const curBusPositions = { buses: [] as any[] }; /** Current positions of all ride buses. */ export const curRidePositions = { buses: [] as any[] }; /** Cache of route patterns and static data. */ -export const cachedRoutes: Record = {}; +export const cachedRoutes: Record = {}; /** Cache of route patterns and static data for the ride. */ -export const cachedRideRoutes: Record = {}; +export const cachedRideRoutes: Record = {}; /** Represents a bus prediction. */ export type Prediction = { @@ -51,7 +52,8 @@ export let cachedStopLocations: Record = {}; /** Cache of timing differences between stops for extrapolation. */ -export const routeTimingCache: Record>> = { +export const routeTimingCache: Record>> = { "CN": { "N434NORTHBOUND": { "N500": { "diff": 5, "rtdir": "SOUTHBOUND", "rtNext": "CS" } From b1c386cba509c42ce4d12cbb9131d23a40d22480 Mon Sep 17 00:00:00 2001 From: Edward Zhang Date: Sat, 20 Jun 2026 12:45:51 -0700 Subject: [PATCH 2/3] refactor(journey): improve type safety Replace the any returned by processJourneys with an actual type that models the two variant nature of the processed legs. Did a similar thing with the journey leg type in `McRaptorAlgorithm.ts`. Both changes should have no semantic difference but behavior and performance should ideally both be checked. --- src/raptor/McRaptorAlgorithm.ts | 31 +++++++++----- src/services/journey.ts | 75 ++++++++++++++++++++++++--------- 2 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/raptor/McRaptorAlgorithm.ts b/src/raptor/McRaptorAlgorithm.ts index 164b1f8..7cb655e 100644 --- a/src/raptor/McRaptorAlgorithm.ts +++ b/src/raptor/McRaptorAlgorithm.ts @@ -15,23 +15,32 @@ export interface Journey { } } -/** - * Represents a single segment of a journey, either a transit trip or a walking transfer. - */ -export interface JourneyLeg { - type: 'Trip' | 'Transfer'; +interface JourneyLegCommon { origin: StopID; destination: StopID; startTime: number; endTime: number; - trip?: Trip; - transfer?: Transfer; duration: number; originID: StopID; destinationID: StopID; +}; + +interface JourneyLegTrip extends JourneyLegCommon { + type: 'Trip'; + trip: Trip; rt?: string; - stopTimes?: StopTime[]; -} + stopTimes: StopTime[]; +}; + +interface JourneyLegTransfer extends JourneyLegCommon { + type: 'Transfer', + transfer: Transfer, +}; + +/** + * Represents a single segment of a journey, either a transit trip or a walking transfer. + */ +export type JourneyLeg = JourneyLegTransfer | JourneyLegTrip; /** * Implementation of the McRAPTOR (Multi-Criteria Round-Based Public Transit Routing) algorithm. @@ -360,8 +369,8 @@ export class McRaptorAlgorithm { for (const j of allJourneys) { const tripsSignature = j.legs - .filter(l => l.type === 'Trip' && l.trip) - .map(l => l.trip!.tripId) + .filter((l) => l.type === 'Trip') + .map(l => l.trip.tripId) .join('|'); if (!tripsSignature) { diff --git a/src/services/journey.ts b/src/services/journey.ts index f5a0776..f941684 100644 --- a/src/services/journey.ts +++ b/src/services/journey.ts @@ -1,6 +1,7 @@ import * as state from '../state/transitState'; import * as walking from '../walking/walkingMap'; import { McRaptorAlgorithm, Journey, JourneyLeg } from "../raptor/McRaptorAlgorithm"; +import { StopTime, Trip } from '@/raptor/types'; /** * Plans a journey between two coordinates using the McRaptor algorithm. @@ -75,12 +76,44 @@ export async function planJourney( return processJourneys(journeys, oLat, oLon, dLat, dLon); } +interface FormattedLegCommon { + origin_id: string, + origin: string, + destination_id: string, + destination: string, + destinationName: string, + startTime: number, + endTime: number, + duration: number, + originID: string, + destinationID: string, +}; + +interface FormattedLegWalk extends + FormattedLegCommon, + Partial> // leaves just the path_coords field for now +{ + mode: 'walk' +}; + +interface FormattedLegBus extends FormattedLegCommon { + mode: 'bus', + stopTimes: StopTime[], + trip: Trip, + tripId: string, + rt: string, + vid: string | null, +}; + +type FormattedLeg = FormattedLegWalk | FormattedLegBus + async function processJourneys(journeys: Journey[], oLat: number, oLon: number, dLat: number, dLon: number) { const processLeg = async (leg: JourneyLeg) => { - const isWalk = !leg.trip; + const isWalk = leg.type === 'Transfer'; - const formattedLeg: any = { + let formattedLeg: FormattedLeg; + const formattedLegCommon: FormattedLegCommon = { origin_id: leg.origin, origin: leg.origin === 'VIRTUAL_ORIGIN' ? 'Start' : (leg.origin === 'VIRTUAL_DESTINATION' ? 'End' : (state.stopIdToName[leg.origin] || leg.origin)), destination_id: leg.destination, @@ -89,28 +122,26 @@ async function processJourneys(journeys: Journey[], oLat: number, oLon: number, startTime: Math.round(leg.startTime), endTime: Math.round(leg.endTime), duration: Math.round(leg.duration), - mode: isWalk ? 'walk' : 'bus', originID: leg.originID, destinationID: leg.destinationID, - stopTimes: leg.stopTimes, - trip: leg.trip, - rt: leg.rt }; - if (leg.trip) { - formattedLeg.tripId = leg.trip.tripId; - formattedLeg.vid = leg.trip.vid; - if (!formattedLeg.rt) { - const firstStop = leg.trip.stopTimes[0]; - formattedLeg.rt = firstStop.rt || state.tatripidToRt[leg.trip.tripId] || 'UNKNOWN'; - } - } - - if (isWalk) { + if (!isWalk) { + formattedLeg = { + ...formattedLegCommon, + mode: 'bus', + stopTimes: leg.stopTimes, + trip: leg.trip, + tripId: leg.trip.tripId, + vid: leg.trip.vid, + // fallback to route of the first stop or the route associated with the trip id + rt: leg.rt || leg.trip.stopTimes[0].rt || state.tatripidToRt[leg.trip.tripId] || 'UNKNOWN' + }; + } else { const cached = walking.getCachedWalk(leg.origin, leg.destination); if (cached) { - Object.assign(formattedLeg, cached); + formattedLeg = {...formattedLegCommon, ...cached, mode: 'walk'} } else { const l1 = leg.origin === 'VIRTUAL_ORIGIN' ? { lat: oLat, lon: oLon } : state.cachedStopLocations[leg.origin]; const l2 = leg.destination === 'VIRTUAL_DESTINATION' ? { lat: dLat, lon: dLon } : state.cachedStopLocations[leg.destination]; @@ -119,10 +150,12 @@ async function processJourneys(journeys: Journey[], oLat: number, oLon: number, try { const data = await walking.getWalkingResponse(l1.lat, l1.lon, l2.lat, l2.lon); data.duration = Math.round(data.duration); - Object.assign(formattedLeg, data); + formattedLeg = {...formattedLegCommon, ...data, mode: 'walk'} } catch (e) { - formattedLeg.path_coords = []; + formattedLeg = {...formattedLegCommon, path_coords: [], mode: 'walk'} } + } else { + formattedLeg = {...formattedLegCommon, mode: 'walk'} } } } @@ -144,8 +177,8 @@ async function processJourneys(journeys: Journey[], oLat: number, oLon: number, })); return processedList - .filter((j: any) => j !== null) - .sort((a: any, b: any) => + .filter((j) => j !== null) + .sort((a, b) => a.arrivalTime - b.arrivalTime || a.criteria.walkingDistance - b.criteria.walkingDistance ); From 9bd0171f019abf2de5a9d765ea1e5ef81e361084 Mon Sep 17 00:00:00 2001 From: Edward Zhang Date: Fri, 24 Jul 2026 21:14:53 -0700 Subject: [PATCH 3/3] docs: fix unexported definitions --- src/raptor/McRaptorAlgorithm.ts | 4 ++-- src/services/journey.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/raptor/McRaptorAlgorithm.ts b/src/raptor/McRaptorAlgorithm.ts index 7cb655e..43d1bba 100644 --- a/src/raptor/McRaptorAlgorithm.ts +++ b/src/raptor/McRaptorAlgorithm.ts @@ -25,14 +25,14 @@ interface JourneyLegCommon { destinationID: StopID; }; -interface JourneyLegTrip extends JourneyLegCommon { +export interface JourneyLegTrip extends JourneyLegCommon { type: 'Trip'; trip: Trip; rt?: string; stopTimes: StopTime[]; }; -interface JourneyLegTransfer extends JourneyLegCommon { +export interface JourneyLegTransfer extends JourneyLegCommon { type: 'Transfer', transfer: Transfer, }; diff --git a/src/services/journey.ts b/src/services/journey.ts index f941684..a9c3a3a 100644 --- a/src/services/journey.ts +++ b/src/services/journey.ts @@ -89,14 +89,14 @@ interface FormattedLegCommon { destinationID: string, }; -interface FormattedLegWalk extends +export interface FormattedLegWalk extends FormattedLegCommon, Partial> // leaves just the path_coords field for now { mode: 'walk' }; -interface FormattedLegBus extends FormattedLegCommon { +export interface FormattedLegBus extends FormattedLegCommon { mode: 'bus', stopTimes: StopTime[], trip: Trip, @@ -105,7 +105,7 @@ interface FormattedLegBus extends FormattedLegCommon { vid: string | null, }; -type FormattedLeg = FormattedLegWalk | FormattedLegBus +export type FormattedLeg = FormattedLegWalk | FormattedLegBus async function processJourneys(journeys: Journey[], oLat: number, oLon: number, dLat: number, dLon: number) {