Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions src/raptor/McRaptorAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

export interface JourneyLegTrip extends JourneyLegCommon {
type: 'Trip';
trip: Trip;
rt?: string;
stopTimes?: StopTime[];
}
stopTimes: StopTime[];
};

export 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.
Expand Down Expand Up @@ -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) {
Expand Down
25 changes: 25 additions & 0 deletions src/services/bustimeTypes.ts
Original file line number Diff line number Diff line change
@@ -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<typeof PatternSchema>

30 changes: 15 additions & 15 deletions src/services/graphBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export async function rebuildGraph() {
try {
console.log(`Rebuilding graph...`);
const allStopIds = new Set<string>();
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);
}));
});
Expand All @@ -74,8 +74,8 @@ export async function rebuildGraph() {

// extra stuff to update the busses for the ride
const rideStopIds = new Set<string>();
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);
}));
});
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -152,10 +152,10 @@ function populateRideLookupMaps(preds: any[]) {
*/
function buildStopLocationMap() {
const locs: Record<string, 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.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 };
}
}));
});
Expand All @@ -168,10 +168,10 @@ function buildStopLocationMap() {
*/
function buildRideStops() {
const locs: Record<string, 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.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 };
}
}));
});
Expand Down Expand Up @@ -242,7 +242,7 @@ function processPredictions(rawChunks: any[]) {

// build index maps
const routeInfoFilter: Record<string, { stpid: string; rtdir: string }[]> = {};
for (const [routeName, routeList] of Object.entries(state.cachedRoutes as Record<string, any[]>)) {
for (const [routeName, routeList] of Object.entries(state.cachedRoutes)) {
for (const route of routeList) {
const rtdir = route.rtdir;
const routeKey = routeName + rtdir;
Expand Down
75 changes: 54 additions & 21 deletions src/services/journey.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
};

export interface FormattedLegWalk extends
FormattedLegCommon,
Partial<Omit<walking.WalkingResponse, "duration" | "distance">> // leaves just the path_coords field for now
{
mode: 'walk'
};

export interface FormattedLegBus extends FormattedLegCommon {
mode: 'bus',
stopTimes: StopTime[],
trip: Trip,
tripId: string,
rt: string,
vid: string | null,
};

export 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,
Expand All @@ -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];
Expand All @@ -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'}
}
}
}
Expand All @@ -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
);
Expand Down
8 changes: 6 additions & 2 deletions src/services/mbus.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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<Pattern[]> {
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 [];
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/services/ride.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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<Pattern[]> {
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 [];
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/state/transitState.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Pattern } from "@/services/bustimeTypes";
import { Trip, TransfersByOrigin, Interchange } from "../raptor/types";

/** Current positions of all buses. */
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<string, any> = {};
export const cachedRoutes: Record<string, Pattern[]> = {};
/** Cache of route patterns and static data for the ride. */
export const cachedRideRoutes: Record<string, any> = {};
export const cachedRideRoutes: Record<string, Pattern[]> = {};

/** Represents a bus prediction. */
export type Prediction = {
Expand Down Expand Up @@ -51,7 +52,8 @@ export let cachedStopLocations: Record<string, { name: string, lat: number, lon:
export let cachedRideStopLocations: Record<string, { name: string, lat: number, lon: number }> = {};

/** Cache of timing differences between stops for extrapolation. */
export const routeTimingCache: Record<string, Record<string, Record<string, { diff: number, rtdir: string, rtNext: string }>>> = {
export const routeTimingCache: Record<string, Record<string, Record<string,
{ diff: number, rtdir: string, rtNext: string }>>> = {
"CN": {
"N434NORTHBOUND": {
"N500": { "diff": 5, "rtdir": "SOUTHBOUND", "rtNext": "CS" }
Expand Down