Skip to content
Merged
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
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ const lTrain = await mta.subway.arrivals({
stopId: "L08",
route: "L",
});

const direction = await mta.subway.direction({
route: "L",
fromStopId: "L06",
destination: "Union Sq",
});
```

Arrival rows may include display-oriented fields. `destination` depends on
Expand All @@ -34,18 +40,17 @@ generic fallback label:

```ts
for (const arrival of lTrain) {
const direction =
arrival.displayDirection ??
(arrival.destination
? `toward ${arrival.destination}`
: arrival.headsign
? `toward ${arrival.headsign}`
: arrival.direction);

console.log(`${arrival.route.shortName} ${direction} from ${arrival.stop.name}`);
console.log(
`${arrival.route.shortName} ${arrival.displayDirection ?? arrival.destination ?? "unknown direction"} from ${arrival.stop.displayName ?? arrival.stop.name}`,
);
}
```

Use `mta.subway.direction(...)` when a rider enters an intermediate destination
such as `Union Sq`; the hosted API resolves it against static GTFS route order
and returns a typed `direction`, `destinationStop`, and terminal-facing
`displayDirection` such as `toward 8 Av`.

NYC Subway realtime feeds use NYCT's `north`/`south` stop directions, even on
east-west lines. For the L train, `mta-js` accepts rider-facing `east`/`west`
aliases and maps them to the underlying feed directions.
Expand Down Expand Up @@ -110,6 +115,7 @@ override them.
## Endpoints

- `mta.subway.arrivals(...)`
- `mta.subway.direction(...)`
- `mta.bus.arrivals(...)`
- `mta.bus.vehicles(...)`
- `mta.alerts.current(...)`
Expand Down
7 changes: 7 additions & 0 deletions hosted-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,16 @@ describe.skipIf(!apiKey)("hosted integration tests", () => {
limit: 5,
});

const direction = await mta.subway.direction({
route: "L",
fromStopId: "L06",
destination: "Union Sq",
});

expect(Array.isArray(arrivals)).toBe(true);
expect(Array.isArray(stops)).toBe(true);
expect(Array.isArray(vehicles)).toBe(true);
expect(typeof direction.resolved).toBe("boolean");
expect(stops.length).toBeGreaterThan(0);
expect(stops.every((stop) => stop.routeMatch)).toBe(true);
}, 30_000);
Expand Down
53 changes: 53 additions & 0 deletions index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,59 @@ test("hosted subway arrivals infer L route and normalize east aliases before req
expect(url.searchParams.get("direction")).toBe("south");
});

test("hosted subway direction calls resolve intermediate destinations through the API", async () => {
let request: Request | undefined;
const mta = new MTA({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
fetch: (async (input, init) => {
request = input instanceof Request ? new Request(input, init) : new Request(String(input), init);
return Response.json({
route: { id: "L", shortName: "L" },
destination: "Union Sq",
normalizedDestination: "union sq",
resolved: true,
direction: "north",
displayDirection: "toward 8 Av",
terminal: "8 Av",
fromStop: {
id: "L06",
name: "1 Av",
displayName: "1 Av",
parentId: "L06",
},
destinationStop: {
id: "L03",
name: "14 St-Union Sq",
displayName: "14 St-Union Sq",
parentId: "L03",
},
});
}) as typeof fetch,
});

const resolution = await mta.subway.direction({
route: "L",
fromStopId: "L06",
destination: "Union Sq",
});

const url = new URL(request?.url ?? "");
expect(url.pathname).toBe("/api/v1/subway/direction");
expect(url.searchParams.get("route")).toBe("L");
expect(url.searchParams.get("fromStopId")).toBe("L06");
expect(url.searchParams.get("destination")).toBe("Union Sq");
expect(resolution).toMatchObject({
resolved: true,
direction: "north",
displayDirection: "toward 8 Av",
fromStop: {
displayName: "1 Av",
parentId: "L06",
},
});
});

test("subway arrivals decode GTFS realtime and join in-memory static metadata", async () => {
const feed = encodeFeedMessage({
header: { gtfsRealtimeVersion: "2.0", timestamp: 1_700_000_000 },
Expand Down
6 changes: 6 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type {
Stop,
StopsNearQuery,
SubwayArrivalQuery,
SubwayDirectionQuery,
SubwayDirectionResolution,
TransitMode,
Vehicle,
} from "./src/types";
Expand Down Expand Up @@ -133,6 +135,10 @@ class SubwayClient {
.slice(0, query.limit ?? 20);
}

direction(query: SubwayDirectionQuery): Promise<SubwayDirectionResolution> {
return this.mta.hostedJson<SubwayDirectionResolution>("/api/v1/subway/direction", query);
}

private feedForRoute(route: string) {
const feed = this.mta.endpoints.subwayFeeds[route];
if (!feed) throw new UnknownRouteError(route);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mta-js",
"version": "2.1.1",
"version": "2.2.0",
"description": "A TypeScript client for MTA realtime feeds and the hosted MTA API.",
"license": "MIT",
"repository": {
Expand Down
34 changes: 33 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export type BusStopId = AutocompleteString<KnownBusStopId>;
export type TransitMode = "subway" | "bus" | "lirr" | "metro-north";

export type Direction = "north" | "south" | "east" | "west" | "unknown";
export type SubwayResolvedDirection = "north" | "south";

export type DirectionHeadsigns = Record<string, string[]>;

export interface MTAOptions {
apiKey?: string;
Expand Down Expand Up @@ -131,17 +134,26 @@ export interface Route {
export interface Stop {
id: string;
name: string;
displayName?: string;
lat?: number;
lon?: number;
parentStation?: string;
parentId?: string;
mode?: TransitMode;
}

export type ServedRoute = Route & {
headsigns?: string[];
directionHeadsigns?: DirectionHeadsigns;
directions?: number[];
};

export type NearbyStop = Stop & {
distanceMeters?: number;
servedRoutes?: Route[];
servedRoutes?: ServedRoute[];
routeMatch?: boolean;
routeHeadsigns?: string[];
directionHeadsigns?: DirectionHeadsigns;
note?: string;
};

Expand Down Expand Up @@ -200,6 +212,26 @@ export interface SubwayArrivalQuery {
includeRaw?: boolean;
}

export interface SubwayDirectionQuery {
route: SubwayRoute;
fromStopId: SubwayStopId;
destination: string;
}

export interface SubwayDirectionResolution {
route: Route;
destination: string;
normalizedDestination: string;
resolved: boolean;
direction?: SubwayResolvedDirection;
displayDirection?: string;
terminal?: string;
fromStop?: Stop;
destinationStop?: Stop;
matches?: Stop[];
reason?: string;
}

export interface BusArrivalQuery {
stopId: BusStopId;
route?: BusRoute;
Expand Down
Loading