minotor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspell.json +43 -0
- package/.czrc +3 -0
- package/.editorconfig +10 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +4 -0
- package/.github/workflows/minotor.yml +85 -0
- package/.prettierrc +7 -0
- package/.releaserc.json +27 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/bundle.cjs.js +16507 -0
- package/dist/bundle.cjs.js.map +1 -0
- package/dist/bundle.esm.js +16496 -0
- package/dist/bundle.esm.js.map +1 -0
- package/dist/bundle.umd.js +2 -0
- package/dist/bundle.umd.js.map +1 -0
- package/dist/cli/__tests__/minotor.test.d.ts +1 -0
- package/dist/cli/minotor.d.ts +5 -0
- package/dist/cli/repl.d.ts +1 -0
- package/dist/cli/utils.d.ts +3 -0
- package/dist/cli.mjs +20504 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/gtfs/__tests__/parser.test.d.ts +1 -0
- package/dist/gtfs/__tests__/routes.test.d.ts +1 -0
- package/dist/gtfs/__tests__/services.test.d.ts +1 -0
- package/dist/gtfs/__tests__/stops.test.d.ts +1 -0
- package/dist/gtfs/__tests__/time.test.d.ts +1 -0
- package/dist/gtfs/__tests__/transfers.test.d.ts +1 -0
- package/dist/gtfs/__tests__/trips.test.d.ts +1 -0
- package/dist/gtfs/__tests__/utils.test.d.ts +1 -0
- package/dist/gtfs/parser.d.ts +34 -0
- package/dist/gtfs/profiles/__tests__/ch.test.d.ts +1 -0
- package/dist/gtfs/profiles/ch.d.ts +2 -0
- package/dist/gtfs/profiles/standard.d.ts +2 -0
- package/dist/gtfs/routes.d.ts +11 -0
- package/dist/gtfs/services.d.ts +19 -0
- package/dist/gtfs/stops.d.ts +20 -0
- package/dist/gtfs/time.d.ts +17 -0
- package/dist/gtfs/transfers.d.ts +22 -0
- package/dist/gtfs/trips.d.ts +26 -0
- package/dist/gtfs/utils.d.ts +21 -0
- package/dist/index.d.ts +11 -0
- package/dist/routing/__tests__/router.test.d.ts +1 -0
- package/dist/routing/plotter.d.ts +11 -0
- package/dist/routing/query.d.ts +35 -0
- package/dist/routing/result.d.ts +28 -0
- package/dist/routing/route.d.ts +25 -0
- package/dist/routing/router.d.ts +33 -0
- package/dist/stops/__tests__/io.test.d.ts +1 -0
- package/dist/stops/__tests__/stopFinder.test.d.ts +1 -0
- package/dist/stops/i18n.d.ts +10 -0
- package/dist/stops/io.d.ts +4 -0
- package/dist/stops/proto/stops.d.ts +53 -0
- package/dist/stops/stops.d.ts +16 -0
- package/dist/stops/stopsIndex.d.ts +52 -0
- package/dist/timetable/__tests__/io.test.d.ts +1 -0
- package/dist/timetable/__tests__/timetable.test.d.ts +1 -0
- package/dist/timetable/duration.d.ts +51 -0
- package/dist/timetable/io.d.ts +8 -0
- package/dist/timetable/proto/timetable.d.ts +122 -0
- package/dist/timetable/time.d.ts +98 -0
- package/dist/timetable/timetable.d.ts +82 -0
- package/dist/umdIndex.d.ts +9 -0
- package/eslint.config.mjs +52 -0
- package/package.json +109 -0
- package/rollup.config.js +44 -0
- package/src/cli/__tests__/minotor.test.ts +23 -0
- package/src/cli/minotor.ts +112 -0
- package/src/cli/repl.ts +200 -0
- package/src/cli/utils.ts +36 -0
- package/src/gtfs/__tests__/parser.test.ts +591 -0
- package/src/gtfs/__tests__/resources/sample-feed/agency.txt +2 -0
- package/src/gtfs/__tests__/resources/sample-feed/calendar.txt +3 -0
- package/src/gtfs/__tests__/resources/sample-feed/calendar_dates.txt +2 -0
- package/src/gtfs/__tests__/resources/sample-feed/fare_attributes.txt +3 -0
- package/src/gtfs/__tests__/resources/sample-feed/fare_rules.txt +5 -0
- package/src/gtfs/__tests__/resources/sample-feed/frequencies.txt +12 -0
- package/src/gtfs/__tests__/resources/sample-feed/routes.txt +6 -0
- package/src/gtfs/__tests__/resources/sample-feed/sample-feed.zip +0 -0
- package/src/gtfs/__tests__/resources/sample-feed/shapes.txt +1 -0
- package/src/gtfs/__tests__/resources/sample-feed/stop_times.txt +34 -0
- package/src/gtfs/__tests__/resources/sample-feed/stops.txt +10 -0
- package/src/gtfs/__tests__/resources/sample-feed/trips.txt +13 -0
- package/src/gtfs/__tests__/resources/sample-feed.zip +0 -0
- package/src/gtfs/__tests__/routes.test.ts +63 -0
- package/src/gtfs/__tests__/services.test.ts +209 -0
- package/src/gtfs/__tests__/stops.test.ts +177 -0
- package/src/gtfs/__tests__/time.test.ts +27 -0
- package/src/gtfs/__tests__/transfers.test.ts +117 -0
- package/src/gtfs/__tests__/trips.test.ts +463 -0
- package/src/gtfs/__tests__/utils.test.ts +13 -0
- package/src/gtfs/parser.ts +154 -0
- package/src/gtfs/profiles/__tests__/ch.test.ts +43 -0
- package/src/gtfs/profiles/ch.ts +70 -0
- package/src/gtfs/profiles/standard.ts +39 -0
- package/src/gtfs/routes.ts +48 -0
- package/src/gtfs/services.ts +98 -0
- package/src/gtfs/stops.ts +112 -0
- package/src/gtfs/time.ts +33 -0
- package/src/gtfs/transfers.ts +102 -0
- package/src/gtfs/trips.ts +228 -0
- package/src/gtfs/utils.ts +42 -0
- package/src/index.ts +28 -0
- package/src/routing/__tests__/router.test.ts +760 -0
- package/src/routing/plotter.ts +70 -0
- package/src/routing/query.ts +74 -0
- package/src/routing/result.ts +108 -0
- package/src/routing/route.ts +94 -0
- package/src/routing/router.ts +262 -0
- package/src/stops/__tests__/io.test.ts +43 -0
- package/src/stops/__tests__/stopFinder.test.ts +185 -0
- package/src/stops/i18n.ts +40 -0
- package/src/stops/io.ts +94 -0
- package/src/stops/proto/stops.proto +26 -0
- package/src/stops/proto/stops.ts +445 -0
- package/src/stops/stops.ts +24 -0
- package/src/stops/stopsIndex.ts +151 -0
- package/src/timetable/__tests__/io.test.ts +175 -0
- package/src/timetable/__tests__/timetable.test.ts +180 -0
- package/src/timetable/duration.ts +85 -0
- package/src/timetable/io.ts +265 -0
- package/src/timetable/proto/timetable.proto +76 -0
- package/src/timetable/proto/timetable.ts +1304 -0
- package/src/timetable/time.ts +192 -0
- package/src/timetable/timetable.ts +286 -0
- package/src/umdIndex.ts +14 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Platform } from '../../stops/stops.js';
|
|
2
|
+
import { RouteType } from '../../timetable/timetable.js';
|
|
3
|
+
import { GtfsProfile } from '../parser.js';
|
|
4
|
+
import { StopEntry } from '../stops.js';
|
|
5
|
+
import { Maybe } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses the platform number from a stop entry.
|
|
9
|
+
* @param stopEntry The stop entry.
|
|
10
|
+
* @returns The platform corresponding to this stop.
|
|
11
|
+
*/
|
|
12
|
+
const platformParser = (stopEntry: StopEntry): Maybe<Platform> => {
|
|
13
|
+
const stopId = String(stopEntry.stop_id);
|
|
14
|
+
const stopParts = stopId.split(':');
|
|
15
|
+
if (stopParts.length > 2) {
|
|
16
|
+
return stopParts[2];
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parses the SBB extended route type and returns the corresponding basic GTFS route type.
|
|
23
|
+
* @param routeType The SBB route type to parse.
|
|
24
|
+
* @returns The corresponding GTFS route type, or undefined if the route type is not recognized.
|
|
25
|
+
*/
|
|
26
|
+
const routeTypeParser = (routeType: number): Maybe<RouteType> => {
|
|
27
|
+
switch (routeType) {
|
|
28
|
+
case 1700: // Lift
|
|
29
|
+
case 1400: // Cogwheel train, funicular
|
|
30
|
+
return 'FUNICULAR'; // Funicular
|
|
31
|
+
case 700: // Bus
|
|
32
|
+
case 705: // Night bus
|
|
33
|
+
case 710: // Panorama bus
|
|
34
|
+
case 202: // National long-distance bus
|
|
35
|
+
case 201: // International long-distance bus
|
|
36
|
+
case 702: // Express bus
|
|
37
|
+
case 715: // On-demand bus
|
|
38
|
+
return 'BUS'; // Bus
|
|
39
|
+
case 1300: // Chairlift, Gondola
|
|
40
|
+
return 'AERIAL_LIFT'; // Aerial lift
|
|
41
|
+
case 401: // Metro
|
|
42
|
+
return 'SUBWAY'; // Subway
|
|
43
|
+
case 1000: // Boat / Ship
|
|
44
|
+
return 'FERRY'; // Boat
|
|
45
|
+
case 900: // Tram
|
|
46
|
+
return 'TRAM'; // Tram
|
|
47
|
+
case 117: // Special train
|
|
48
|
+
case 102: // International train
|
|
49
|
+
case 104: // Car train
|
|
50
|
+
case 101: // International train
|
|
51
|
+
case 111: // Airport train
|
|
52
|
+
case 105: // Night train
|
|
53
|
+
case 103: // Fast train
|
|
54
|
+
case 107: // Mountain train
|
|
55
|
+
case 100: // No guaranteed train
|
|
56
|
+
case 106: // Regional train
|
|
57
|
+
case 109: // Urban train
|
|
58
|
+
case 116: // ??? train TODO figure out what this means
|
|
59
|
+
return 'RAIL'; // Train
|
|
60
|
+
case 1100: // Aircraft
|
|
61
|
+
case 1500: // Taxi
|
|
62
|
+
default:
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const chGtfsProfile: GtfsProfile = {
|
|
68
|
+
routeTypeParser,
|
|
69
|
+
platformParser,
|
|
70
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Platform } from '../../stops/stops.js';
|
|
2
|
+
import { GtfsProfile } from '../parser.js';
|
|
3
|
+
import { StopEntry } from '../stops.js';
|
|
4
|
+
import { Maybe } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
export const standardProfile: GtfsProfile = {
|
|
7
|
+
routeTypeParser: (routeType: number) => {
|
|
8
|
+
switch (routeType) {
|
|
9
|
+
case 0:
|
|
10
|
+
return 'TRAM';
|
|
11
|
+
case 1:
|
|
12
|
+
return 'SUBWAY';
|
|
13
|
+
case 2:
|
|
14
|
+
return 'RAIL';
|
|
15
|
+
case 3:
|
|
16
|
+
return 'BUS';
|
|
17
|
+
case 4:
|
|
18
|
+
return 'FERRY';
|
|
19
|
+
case 5:
|
|
20
|
+
return 'CABLE_TRAM';
|
|
21
|
+
case 6:
|
|
22
|
+
return 'AERIAL_LIFT';
|
|
23
|
+
case 7:
|
|
24
|
+
return 'FUNICULAR';
|
|
25
|
+
case 11:
|
|
26
|
+
return 'TROLLEYBUS';
|
|
27
|
+
case 12:
|
|
28
|
+
return 'MONORAIL';
|
|
29
|
+
default:
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
platformParser: (stopEntry: StopEntry): Maybe<Platform> => {
|
|
34
|
+
if (stopEntry.platform_code) {
|
|
35
|
+
return stopEntry.platform_code;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import log from 'loglevel';
|
|
2
|
+
|
|
3
|
+
import { RouteId, ServiceRoutesMap } from '../timetable/timetable.js';
|
|
4
|
+
import { GtfsProfile } from './parser.js';
|
|
5
|
+
import { standardProfile } from './profiles/standard.js';
|
|
6
|
+
import { parseCsv } from './utils.js';
|
|
7
|
+
|
|
8
|
+
// Can be a standard gtfs route type or an extended route type
|
|
9
|
+
// the profile converter handles the conversion to a route type.
|
|
10
|
+
export type GtfsRouteType = number;
|
|
11
|
+
|
|
12
|
+
type RouteEntry = {
|
|
13
|
+
route_id: RouteId;
|
|
14
|
+
agency_id: string;
|
|
15
|
+
route_short_name: string;
|
|
16
|
+
route_long_name: string;
|
|
17
|
+
route_desc: string;
|
|
18
|
+
route_type: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parses a GTFS routes.txt file and returns a map of all the valid routes.
|
|
23
|
+
*
|
|
24
|
+
* @param routesStream A readable stream for the GTFS routes.txt file.
|
|
25
|
+
* @param profile A configuration object defining the specificities of the GTFS feed.
|
|
26
|
+
* @returns A map of all the valid routes.
|
|
27
|
+
*/
|
|
28
|
+
export const parseRoutes = async (
|
|
29
|
+
routesStream: NodeJS.ReadableStream,
|
|
30
|
+
profile: GtfsProfile = standardProfile,
|
|
31
|
+
): Promise<ServiceRoutesMap> => {
|
|
32
|
+
const routes: ServiceRoutesMap = new Map();
|
|
33
|
+
for await (const rawLine of parseCsv(routesStream)) {
|
|
34
|
+
const line = rawLine as RouteEntry;
|
|
35
|
+
const routeType = profile.routeTypeParser(line.route_type);
|
|
36
|
+
if (routeType === undefined) {
|
|
37
|
+
log.info(
|
|
38
|
+
`Unsupported route type ${line.route_type} for route ${line.route_id}.`,
|
|
39
|
+
);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
routes.set(line.route_id, {
|
|
43
|
+
name: line.route_short_name + '',
|
|
44
|
+
type: routeType,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return routes;
|
|
48
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
|
|
3
|
+
import { toGtfsDate } from './time.js';
|
|
4
|
+
import { parseCsv } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export type ServiceId = string;
|
|
7
|
+
export type ServiceIds = Set<ServiceId>;
|
|
8
|
+
|
|
9
|
+
type Weekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
10
|
+
type ActiveFlag = 0 | 1;
|
|
11
|
+
type RawGtfsDate = number;
|
|
12
|
+
type ExceptionType = 1 | 2;
|
|
13
|
+
|
|
14
|
+
type CalendarEntry = {
|
|
15
|
+
service_id: string;
|
|
16
|
+
monday: ActiveFlag;
|
|
17
|
+
tuesday: ActiveFlag;
|
|
18
|
+
wednesday: ActiveFlag;
|
|
19
|
+
thursday: ActiveFlag;
|
|
20
|
+
friday: ActiveFlag;
|
|
21
|
+
saturday: ActiveFlag;
|
|
22
|
+
sunday: ActiveFlag;
|
|
23
|
+
start_date: RawGtfsDate;
|
|
24
|
+
end_date: RawGtfsDate;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type CalendarDatesEntry = {
|
|
28
|
+
service_id: ServiceId;
|
|
29
|
+
date: RawGtfsDate;
|
|
30
|
+
exception_type: ExceptionType;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const weekdays = {
|
|
34
|
+
1: 'monday',
|
|
35
|
+
2: 'tuesday',
|
|
36
|
+
3: 'wednesday',
|
|
37
|
+
4: 'thursday',
|
|
38
|
+
5: 'friday',
|
|
39
|
+
6: 'saturday',
|
|
40
|
+
7: 'sunday',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parses a GTFS calendar.txt file and finds the service_ids of a given date.
|
|
45
|
+
*
|
|
46
|
+
* @param serviceIds A map of the active service ids (will be populated with active service_ids).
|
|
47
|
+
* @param date The active date.
|
|
48
|
+
* @param calendarStream A readable stream for the GTFS calendar.txt file.
|
|
49
|
+
*/
|
|
50
|
+
export const parseCalendar = async (
|
|
51
|
+
calendarStream: NodeJS.ReadableStream,
|
|
52
|
+
serviceIds: ServiceIds,
|
|
53
|
+
date: DateTime,
|
|
54
|
+
) => {
|
|
55
|
+
const activeDate: number = toGtfsDate(date);
|
|
56
|
+
const weekday = date.weekday as Weekday;
|
|
57
|
+
const weekdayIndex = weekdays[weekday] as keyof CalendarEntry;
|
|
58
|
+
for await (const rawLine of parseCsv(calendarStream)) {
|
|
59
|
+
const line = rawLine as CalendarEntry;
|
|
60
|
+
if (activeDate < line.start_date || activeDate > line.end_date) {
|
|
61
|
+
// If the service is not valid on this date
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (line[weekdayIndex] !== 1) {
|
|
65
|
+
// If the service is not valid on this week day
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
serviceIds.add(line['service_id']);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses a gtfs calendar_dates.txt file and finds the service ids valid at a given date.
|
|
74
|
+
*
|
|
75
|
+
* @param serviceIds A map of the active service ids (will be populated and filtered).
|
|
76
|
+
* @param date The active date, in the format "YYYYMMDD".
|
|
77
|
+
* @param calendarDatesStream A readable stream for the GTFS calendar_dates.txt file.
|
|
78
|
+
*/
|
|
79
|
+
export const parseCalendarDates = async (
|
|
80
|
+
calendarDatesStream: NodeJS.ReadableStream,
|
|
81
|
+
serviceIds: ServiceIds,
|
|
82
|
+
date: DateTime,
|
|
83
|
+
) => {
|
|
84
|
+
const activeDate: number = toGtfsDate(date);
|
|
85
|
+
for await (const rawLine of parseCsv(calendarDatesStream)) {
|
|
86
|
+
const line = rawLine as CalendarDatesEntry;
|
|
87
|
+
if (line.date !== activeDate) {
|
|
88
|
+
// No rule on the active date
|
|
89
|
+
} else if (line.exception_type === 2 && serviceIds.has(line.service_id)) {
|
|
90
|
+
// Service has been removed for the specified date.
|
|
91
|
+
|
|
92
|
+
serviceIds.delete(line.service_id);
|
|
93
|
+
} else if (line.exception_type === 1) {
|
|
94
|
+
// Service is present on the active date
|
|
95
|
+
serviceIds.add(line.service_id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Latitude,
|
|
3
|
+
LocationType,
|
|
4
|
+
Longitude,
|
|
5
|
+
Platform,
|
|
6
|
+
Stop,
|
|
7
|
+
StopId,
|
|
8
|
+
StopsMap,
|
|
9
|
+
} from '../stops/stops.js';
|
|
10
|
+
import { Maybe, parseCsv } from './utils.js';
|
|
11
|
+
|
|
12
|
+
export type StopIds = Set<StopId>;
|
|
13
|
+
|
|
14
|
+
export type GtfsLocationType =
|
|
15
|
+
| 0 // simple stop or platform (can also be empty)
|
|
16
|
+
| 1 // station
|
|
17
|
+
| 2 // entrance / exit
|
|
18
|
+
| 3 // generic node
|
|
19
|
+
| 4; // boarding area
|
|
20
|
+
|
|
21
|
+
export type StopEntry = {
|
|
22
|
+
stop_id: StopId;
|
|
23
|
+
stop_name: string;
|
|
24
|
+
stop_lat?: Latitude;
|
|
25
|
+
stop_lon?: Longitude;
|
|
26
|
+
location_type?: GtfsLocationType;
|
|
27
|
+
parent_station?: StopId;
|
|
28
|
+
platform_code?: Platform;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses the stops.txt file from a GTFS feed.
|
|
33
|
+
*
|
|
34
|
+
* @param stopsStream The readable stream containing the stops data.
|
|
35
|
+
* @return A mapping of stop IDs to corresponding stop details.
|
|
36
|
+
*/
|
|
37
|
+
export const parseStops = async (
|
|
38
|
+
stopsStream: NodeJS.ReadableStream,
|
|
39
|
+
platformParser?: (stopEntry: StopEntry) => Maybe<Platform>,
|
|
40
|
+
validStops?: StopIds,
|
|
41
|
+
): Promise<StopsMap> => {
|
|
42
|
+
const stops: StopsMap = new Map();
|
|
43
|
+
|
|
44
|
+
for await (const rawLine of parseCsv(stopsStream)) {
|
|
45
|
+
const line = rawLine as StopEntry;
|
|
46
|
+
const stop: Stop = {
|
|
47
|
+
id: line.stop_id,
|
|
48
|
+
name: line.stop_name,
|
|
49
|
+
lat: line.stop_lat,
|
|
50
|
+
lon: line.stop_lon,
|
|
51
|
+
locationType: line.location_type
|
|
52
|
+
? parseGtfsLocationType(line.location_type)
|
|
53
|
+
: 'SIMPLE_STOP_OR_PLATFORM',
|
|
54
|
+
children: [],
|
|
55
|
+
...(line.parent_station && { parent: line.parent_station }),
|
|
56
|
+
};
|
|
57
|
+
if (platformParser) {
|
|
58
|
+
try {
|
|
59
|
+
const platform = platformParser(line);
|
|
60
|
+
if (platform) {
|
|
61
|
+
stop.platform = platform;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
console.info(`Could not parse platform for stop ${line.stop_id}.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
stops.set(line.stop_id, stop);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const [stopId, stop] of stops) {
|
|
71
|
+
if (stop.parent) {
|
|
72
|
+
const parentStop = stops.get(stop.parent);
|
|
73
|
+
if (!parentStop) {
|
|
74
|
+
console.warn(`Cannot find parent stop ${stop.parent} of ${stopId}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
parentStop.children.push(stopId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (validStops) {
|
|
81
|
+
// Remove all stops which don't have at least one valid stopId as a child,
|
|
82
|
+
// a parent or as its own.
|
|
83
|
+
for (const [stopId, stop] of stops) {
|
|
84
|
+
if (
|
|
85
|
+
!validStops.has(stopId) &&
|
|
86
|
+
(!stop.parent || !validStops.has(stop.parent)) &&
|
|
87
|
+
!stop.children.some((childId) => validStops.has(childId))
|
|
88
|
+
) {
|
|
89
|
+
stops.delete(stopId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return stops;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const parseGtfsLocationType = (
|
|
97
|
+
gtfsLocationType: GtfsLocationType,
|
|
98
|
+
): LocationType => {
|
|
99
|
+
switch (gtfsLocationType) {
|
|
100
|
+
case 0:
|
|
101
|
+
default:
|
|
102
|
+
return 'SIMPLE_STOP_OR_PLATFORM';
|
|
103
|
+
case 1:
|
|
104
|
+
return 'STATION';
|
|
105
|
+
case 2:
|
|
106
|
+
return 'ENTRANCE_EXIT';
|
|
107
|
+
case 3:
|
|
108
|
+
return 'GENERIC_NODE';
|
|
109
|
+
case 4:
|
|
110
|
+
return 'BOARDING_AREA';
|
|
111
|
+
}
|
|
112
|
+
};
|
package/src/gtfs/time.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
|
|
3
|
+
import { Time } from '../timetable/time.js';
|
|
4
|
+
|
|
5
|
+
export type GtfsDate = number;
|
|
6
|
+
export type GtfsTime = string;
|
|
7
|
+
|
|
8
|
+
export const toGtfsDate = (date: DateTime): GtfsDate => {
|
|
9
|
+
return parseInt(date.toFormat('yyyyLLdd'));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts a time string in the format 'HH:mm:ss' to a Time object
|
|
14
|
+
* (number of seconds since midnight).
|
|
15
|
+
*
|
|
16
|
+
* This method splits the input time string into hours, minutes, and seconds,
|
|
17
|
+
* and then calculates the total number of seconds.
|
|
18
|
+
*
|
|
19
|
+
* @param time - A string representing the time in 'HH:mm:ss' format.
|
|
20
|
+
* @returns The GTFS time as the number of seconds since midnight.
|
|
21
|
+
* @throws An error if the input time string is not in the correct format.
|
|
22
|
+
*/
|
|
23
|
+
export const toTime = (time: GtfsTime): Time => {
|
|
24
|
+
const splits = time.split(':');
|
|
25
|
+
if (!splits[0] || !splits[1] || !splits[2]) {
|
|
26
|
+
throw new Error(`Invalid time ${time}.`);
|
|
27
|
+
}
|
|
28
|
+
return Time.fromHMS(
|
|
29
|
+
parseInt(splits[0]),
|
|
30
|
+
parseInt(splits[1]),
|
|
31
|
+
parseInt(splits[2]),
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { StopId } from '../stops/stops.js';
|
|
2
|
+
import { Duration } from '../timetable/duration.js';
|
|
3
|
+
import {
|
|
4
|
+
ServiceRouteId,
|
|
5
|
+
Transfer,
|
|
6
|
+
TransferType,
|
|
7
|
+
} from '../timetable/timetable.js';
|
|
8
|
+
import { TripId } from './trips.js';
|
|
9
|
+
import { parseCsv } from './utils.js';
|
|
10
|
+
|
|
11
|
+
export type GtfsTransferType =
|
|
12
|
+
| 0 // recommended transfer point
|
|
13
|
+
| 1 // timed transfer (guaranteed)
|
|
14
|
+
| 2 // requires a minimal amount of time
|
|
15
|
+
| 3 // transfer not possible
|
|
16
|
+
| 4 // in-seat transfer
|
|
17
|
+
| 5; // in-seat transfer not allowed (must alight)
|
|
18
|
+
|
|
19
|
+
export type TransfersMap = Map<StopId, Transfer[]>;
|
|
20
|
+
|
|
21
|
+
export type TransferEntry = {
|
|
22
|
+
from_stop_id?: StopId;
|
|
23
|
+
to_stop_id?: StopId;
|
|
24
|
+
from_trip_id?: TripId;
|
|
25
|
+
to_trip_id?: TripId;
|
|
26
|
+
from_route_id?: ServiceRouteId;
|
|
27
|
+
to_route_id?: ServiceRouteId;
|
|
28
|
+
transfer_type: GtfsTransferType;
|
|
29
|
+
min_transfer_time?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parses the transfers.txt file from a GTFS feed.
|
|
34
|
+
*
|
|
35
|
+
* @param stopsStream The readable stream containing the stops data.
|
|
36
|
+
* @return A mapping of stop IDs to corresponding stop details.
|
|
37
|
+
*/
|
|
38
|
+
export const parseTransfers = async (
|
|
39
|
+
transfersStream: NodeJS.ReadableStream,
|
|
40
|
+
): Promise<TransfersMap> => {
|
|
41
|
+
const transfers: TransfersMap = new Map();
|
|
42
|
+
|
|
43
|
+
for await (const rawLine of parseCsv(transfersStream)) {
|
|
44
|
+
const transferEntry = rawLine as TransferEntry;
|
|
45
|
+
if (
|
|
46
|
+
transferEntry.transfer_type === 3 ||
|
|
47
|
+
transferEntry.transfer_type === 5
|
|
48
|
+
) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (transferEntry.from_trip_id && transferEntry.to_trip_id) {
|
|
52
|
+
console.warn(
|
|
53
|
+
`Unsupported transfer between trips ${transferEntry.from_trip_id} and ${transferEntry.to_trip_id}.`,
|
|
54
|
+
);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (transferEntry.from_route_id && transferEntry.to_route_id) {
|
|
58
|
+
console.warn(
|
|
59
|
+
`Unsupported transfer between routes ${transferEntry.from_route_id} and ${transferEntry.to_route_id}.`,
|
|
60
|
+
);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!transferEntry.from_stop_id || !transferEntry.to_stop_id) {
|
|
64
|
+
console.warn(`Missing transfer origin or destination stop.`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (transferEntry.transfer_type === 2 && !transferEntry.min_transfer_time) {
|
|
68
|
+
console.info(
|
|
69
|
+
`Missing minimum transfer time between ${transferEntry.from_stop_id} and ${transferEntry.to_stop_id}.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const transfer: Transfer = {
|
|
74
|
+
destination: transferEntry.to_stop_id,
|
|
75
|
+
type: parseGtfsTransferType(transferEntry.transfer_type),
|
|
76
|
+
...(transferEntry.min_transfer_time && {
|
|
77
|
+
minTransferTime: Duration.fromSeconds(transferEntry.min_transfer_time),
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const fromStopTransfers = transfers.get(transferEntry.from_stop_id) || [];
|
|
82
|
+
fromStopTransfers.push(transfer);
|
|
83
|
+
transfers.set(transferEntry.from_stop_id, fromStopTransfers);
|
|
84
|
+
}
|
|
85
|
+
return transfers;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const parseGtfsTransferType = (
|
|
89
|
+
gtfsTransferType: GtfsTransferType,
|
|
90
|
+
): TransferType => {
|
|
91
|
+
switch (gtfsTransferType) {
|
|
92
|
+
case 0:
|
|
93
|
+
default:
|
|
94
|
+
return 'RECOMMENDED';
|
|
95
|
+
case 1:
|
|
96
|
+
return 'GUARANTEED';
|
|
97
|
+
case 2:
|
|
98
|
+
return 'REQUIRES_MINIMAL_TIME';
|
|
99
|
+
case 4:
|
|
100
|
+
return 'IN_SEAT';
|
|
101
|
+
}
|
|
102
|
+
};
|