minotor
Version:
A lightweight client-side transit routing library.
148 lines (139 loc) • 3.84 kB
text/typescript
import {
Latitude,
LocationType,
Longitude,
Platform,
SourceStopId,
Stop,
StopId,
StopsMap,
} from '../stops/stops.js';
import { Maybe, parseCsv } from './utils.js';
export type GtfsLocationType =
| 0 // simple stop or platform (can also be empty)
| 1 // station
| 2 // entrance / exit
| 3 // generic node
| 4; // boarding area
export type StopEntry = {
stop_id: SourceStopId;
stop_name: string;
stop_lat?: Latitude;
stop_lon?: Longitude;
location_type?: GtfsLocationType;
parent_station?: SourceStopId;
platform_code?: Platform;
};
type ParsedStop = Stop & {
parentSourceId?: SourceStopId;
};
export type ParsedStopsMap = Map<SourceStopId, ParsedStop>;
/**
* Parses the stops.txt file from a GTFS feed.
*
* @param stopsStream The readable stream containing the stops data.
* @return A mapping of stop IDs to corresponding stop details.
*/
export const parseStops = async (
stopsStream: NodeJS.ReadableStream,
platformParser?: (stopEntry: StopEntry) => Maybe<Platform>,
): Promise<ParsedStopsMap> => {
const parsedStops = new Map<SourceStopId, ParsedStop>();
let i = 0;
for await (const rawLine of parseCsv(stopsStream)) {
const line = rawLine as StopEntry;
const stop: ParsedStop = {
id: i,
sourceStopId: line.stop_id + '',
name: line.stop_name,
lat: line.stop_lat,
lon: line.stop_lon,
locationType: line.location_type
? parseGtfsLocationType(line.location_type)
: 'SIMPLE_STOP_OR_PLATFORM',
children: [],
...(line.parent_station && { parentSourceId: line.parent_station }),
};
if (platformParser) {
try {
const platform = platformParser(line);
if (platform) {
stop.platform = platform;
}
} catch {
console.info(`Could not parse platform for stop ${line.stop_id}.`);
}
}
parsedStops.set(line.stop_id + '', stop);
i = i + 1;
}
for (const [sourceStopId, stop] of parsedStops) {
if (stop.parentSourceId) {
const parentStop = parsedStops.get(stop.parentSourceId);
if (!parentStop) {
console.warn(
`Cannot find parent stop ${stop.parentSourceId} of ${sourceStopId}`,
);
continue;
}
stop.parent = parentStop.id;
parentStop.children.push(stop.id);
}
}
return parsedStops;
};
/**
* Builds the final stop map indexed by internal IDs.
* Excludes all stops that do not have at least one valid stopId
* as a child, a parent, or being valid itself.
*
* @param parsedStops - The map of parsed stops.
* @param validStops - A set of valid stop IDs.
* @returns A map of stops indexed by internal IDs.
*/
export const indexStops = (
parsedStops: ParsedStopsMap,
validStops?: Set<StopId>,
): StopsMap => {
const stops = new Map<StopId, Stop>();
for (const [, stop] of parsedStops) {
if (
!validStops ||
validStops.has(stop.id) ||
(stop.parent && validStops.has(stop.parent)) ||
stop.children.some((childId) => validStops.has(childId))
) {
stops.set(stop.id, {
id: stop.id,
sourceStopId: stop.sourceStopId,
name: stop.name,
lat: stop.lat,
lon: stop.lon,
locationType: stop.locationType,
platform: stop.platform,
children: stop.children.filter(
(childId) => !validStops || validStops.has(childId),
),
parent: stop.parent,
});
}
}
return stops;
};
const parseGtfsLocationType = (
gtfsLocationType: GtfsLocationType,
): LocationType => {
switch (gtfsLocationType) {
case 0:
default:
return 'SIMPLE_STOP_OR_PLATFORM';
case 1:
return 'STATION';
case 2:
return 'ENTRANCE_EXIT';
case 3:
return 'GENERIC_NODE';
case 4:
return 'BOARDING_AREA';
}
};