Skip to content
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
29 changes: 9 additions & 20 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
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, '/getAllMbusRoutes',
{ ...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;
144 changes: 144 additions & 0 deletions src/services/bustimeCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import z from "zod";

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 PatternsArraySchema = z.array(PatternSchema);

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

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();
}

25 changes: 0 additions & 25 deletions src/services/bustimeTypes.ts

This file was deleted.

Loading