Skip to content
Draft
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
4 changes: 3 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import express from "express";

import mbus from "./routes/api"
import mbus from "./routes/api";
import v4 from "./routes/v4";
import * as documented from "./routes/documented";

const app = express();

app.use(express.json());
documented.addRouter(documented.globalContext, app, "/mbus/api/v3", mbus);
documented.addRouter(documented.globalContext, app, "/api/v4", v4);
app.use("/docs", express.static("docs"));

const PORT = process.env.PORT || 3000;
Expand Down
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
31 changes: 10 additions & 21 deletions src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as journeyService from '../services/journey';
import * as reminderService from '../services/reminder';
import * as graphBuilder from '../services/graphBuilder';
import { startBackgroundJobs } from '../jobs';
import { BusRouteLineSchema } from "../services/bustimeCommon";
import * as documented from "./documented";

/**
Expand Down Expand Up @@ -188,27 +189,15 @@ export function getRidePositions(req: express.Request, res: express.Response) {
}
router.get('/getRidePositions', getRidePositions);

/**
* Returns all cached route patterns.
* @param req - Express request
* @param res - Express response
* @returns JSON object with `routes` mapping route IDs to patterns.
*/
export function getAllRoutes(req: express.Request, res: express.Response) {
res.json({ routes: state.cachedRoutes });
}
router.get('/getAllRoutes', getAllRoutes);
// remove when mb2 support is dropped
router.get('/getAllRoutes', (_, res) => {
res.json({ routes: state.cachedRoutesLegacy })
});

/**
* Returns all cached ride route patterns.
* @param req - Express request
* @param res - Express response
* @returns JSON object with `routes` mapping route IDs to patterns.
*/
export function getAllRideRoutes(req: express.Request, res: express.Response) {
res.json({ routes: state.cachedRideRoutes });
}
router.get('/getAllRideRoutes', getAllRideRoutes);
// remove when mb2 support is dropped
router.get('/getAllRideRoutes', (_, res) => {
res.json({ routes: state.cachedRideRoutesLegacy });
});

/**
* Returns the route timing cache used for extrapolation.
Expand Down Expand Up @@ -381,7 +370,7 @@ documented.addGetRoute(
.transform((x) => x === undefined ? undefined : parseInt(x))
.pipe(z.optional(z.number())),
}),
resBody: z.any(),
resBody: z.object({ journeys: z.array(journeyService.ProcessedJourneySchema) }),
},
async (_, { originLat, originLon, destLat, destLon, walkingPenalty, range }) => {
try {
Expand Down
31 changes: 31 additions & 0 deletions src/routes/v4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Changes to the served api that are NOT backwards compatible with mb2 should go here.
*
* Try to use documented instead of raw express.
* @module
*/

import express from 'express';
import * as z from 'zod';
import * as state from '@/state/transitState';
import { BusRouteLineSchema } from '@/services/bustimeCommon';
import * as documented from './documented';

const router = express.Router();
const ctx = documented.globalContext;

documented.addGetRoute(
ctx, router, '/getAllRoutes',
{ ...documented.emptyFormat, resBody: z.array(BusRouteLineSchema) },
async () => documented.makeSuccessResponse(Object.values(state.cachedRoutes).flat(1)),
{ description: 'get all cached route patterns' }
);

documented.addGetRoute(
ctx, router, '/getAllRideRoutes',
{ ...documented.emptyFormat, resBody: z.array(BusRouteLineSchema) },
async () => documented.makeSuccessResponse(Object.values(state.cachedRideRoutes).flat(1)),
{ description: 'get all cached ride route patterns' },
)

export default router;
185 changes: 185 additions & 0 deletions src/services/bustimeCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import z from "zod";

// ========== patterns & bus lines =========

const PatternPtSchema = z.object({
seq: z.int(),
typ: z.string(),
stpid: z.optional(z.string()),
stpnm: z.optional(z.string()),
pdist: z.optional(z.number()),
lat: z.number(),
lon: z.number(),
}).meta({ id: 'PatternPt' });

export const PatternSchema = z.object({
pid: z.int(),
ln: z.number(),
rtdir: z.string(),
pt: z.array(PatternPtSchema),
dtrid: z.optional(z.string()),
dtrpt: z.optional(z.array(PatternPtSchema)),
}).meta({ id: 'Pattern' });
export type Pattern = z.infer<typeof PatternSchema>

export const LatLonSchema = z.object({ lat: z.number(), lon: z.number() }).meta({ id: 'LatLon' });
export type LatLon = z.infer<typeof LatLonSchema>;

export const BusStopSchema = z.object({
id: z.string(),
name: z.string(),
location: LatLonSchema,
routeId: z.string(),
rotation: z.number(),
isRide: z.boolean(),
}).meta({ id: 'BusStop' });
export type BusStop = z.infer<typeof BusStopSchema>;

export function makeBusStop(
{ id, name, lat, lon }: { id?: string, name?: string, lat?: number, lon?: number },
routeId: string, rotation: number, isRide: boolean
): BusStop {
return {
id: id ?? '',
name: name ? normalizeStopName(name) : '',
location: { lat: lat ?? 0, lon: lon ?? 0 },
routeId, rotation, isRide,
};
}

/** doesn't include color or image url, which are still handled by the frontend */
export const BusRouteLineSchema = z.object({
routeId: z.string(),
routeDirection: z.string(),
points: z.array(LatLonSchema),
stops: z.array(z.object({ index: z.int(), stop: BusStopSchema })),
}).meta({ id: 'BusRouteLine' });
export type BusRouteLine = z.infer<typeof BusRouteLineSchema>;

export function makeBusRouteLines(rt: string, pattern: Pattern, isRide: boolean): BusRouteLine[] {

const process = (pointList: Pattern['pt']): {
points: BusRouteLine['points'],
stops: BusRouteLine['stops']
} => {
const points = [];
const stops = [];
for (let i = 0; i < pointList.length; i++) {
const point = pointList[i];
const isLast = i == pointList.length - 1; // bool to check if last
points.push({ lat: point.lat, lon: point.lon });
if (point.typ === 'S') {
// get rotation of stop
let stopRotation;
if (isLast) {
// use the previous 2 points to calculate rotation
stopRotation = pointRotation(
pointList[i - 2]?.lat ?? 0,
pointList[i - 2]?.lon ?? 0,
pointList[i - 1]?.lat ?? 0,
pointList[i - 1]?.lon ?? 0,
);
} else {
// use the next 2 points to calculate rotation
stopRotation = pointRotation(
pointList[i + 1]?.lat ?? 0,
pointList[i + 1]?.lon ?? 0,
pointList[i + 2]?.lat ?? 0,
pointList[i + 2]?.lon ?? 0,
);
}
stops.push({
index: i,
stop : makeBusStop(
{ id: point.stpid, name: point.stpnm, lat: point.lat, lon: point.lon },
rt, stopRotation, isRide
)
});
}
}
return { points, stops };
}

const lines: BusRouteLine[] = [];
{
const { points, stops } = process(pattern.pt);
lines.push({ routeId: rt, points, stops, routeDirection: pattern.rtdir });
}

// Handle detour points if present
if (pattern.dtrpt) {
const { points, stops } = process(pattern.dtrpt);
lines.push({ routeId: rt, points, stops, routeDirection: pattern.rtdir });
}

return lines;
}

/**
* Function to calculate rotation angle between two geographical points
* (used for bus stop icon orientation)
*/
export function pointRotation(lat1: number, lon1: number, lat2: number, lon2: number): number {
const dLat = lat2 - lat1;
const dLon = lon2 - lon1;

// Scale longitude by cos(lat) to correct for east-west distance
const x = dLon * (Math.cos(lat1 * Math.PI / 180.0));
const y = dLat;

let angle = Math.atan2(x, y) * 180.0 / Math.PI;

// Normalize to [0, 360)
if (angle < 0) angle += 360;

return angle;
}

// KEEP THIS IN SYNC WITH THE CORRESPONDING FUNCTION IN THE FRONTEND
function normalizeStopName(rawStopName: string): string {
return rawStopName
.replaceAll('%', '')
.replaceAll(/\s+/g, ' ')
.trim();
}

// ========= predictions ==========

// fields that were not present in practice are commented out
// might've missed a field given it happened with prdctdn (in response fields table but not the xml schema)...
export const PredictionSchema = z.object({
tmstmp: z.string(),
typ: z.string(),
stpid: z.string(),
stpnm: z.string(),
vid: z.string(), // number in schema
dstp: z.number(),
rt: z.string(),
rtdd: z.string(),
rtdir: z.string(),
des: z.string(),
prdtm: z.string(),
dly: z.optional(z.boolean()),
dyn: z.number(),
tablockid: z.string(),
tatripid: z.string(),
origtatripno: z.string(),
prdctdn: z.string(), // not in xml schema
zone: z.string(),
psgld: z.string(),
// gtfsseq: z.string(),
nbus: z.optional(z.string()),
stst: z.optional(z.number()),
stsd: z.optional(z.string()), // number? in schema
// flagStop: z.number(),
}).meta({ id: 'Prediction' });
export type Prediction = z.infer<typeof PredictionSchema>;

export const GetPredictionsResponseSchema = z.object({
'bustime-response': z.object({
'prd': z.optional(z.array(PredictionSchema)),
'error': z.optional(z.unknown()),
})
}).meta({ id: 'GetPredictionsResponse' });
export type GetPredictionsResponse = z.infer<typeof GetPredictionsResponseSchema>;

Loading