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.
Files changed (131) hide show
  1. package/.cspell.json +43 -0
  2. package/.czrc +3 -0
  3. package/.editorconfig +10 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +4 -0
  8. package/.github/workflows/minotor.yml +85 -0
  9. package/.prettierrc +7 -0
  10. package/.releaserc.json +27 -0
  11. package/CHANGELOG.md +6 -0
  12. package/LICENSE +21 -0
  13. package/README.md +166 -0
  14. package/dist/bundle.cjs.js +16507 -0
  15. package/dist/bundle.cjs.js.map +1 -0
  16. package/dist/bundle.esm.js +16496 -0
  17. package/dist/bundle.esm.js.map +1 -0
  18. package/dist/bundle.umd.js +2 -0
  19. package/dist/bundle.umd.js.map +1 -0
  20. package/dist/cli/__tests__/minotor.test.d.ts +1 -0
  21. package/dist/cli/minotor.d.ts +5 -0
  22. package/dist/cli/repl.d.ts +1 -0
  23. package/dist/cli/utils.d.ts +3 -0
  24. package/dist/cli.mjs +20504 -0
  25. package/dist/cli.mjs.map +1 -0
  26. package/dist/gtfs/__tests__/parser.test.d.ts +1 -0
  27. package/dist/gtfs/__tests__/routes.test.d.ts +1 -0
  28. package/dist/gtfs/__tests__/services.test.d.ts +1 -0
  29. package/dist/gtfs/__tests__/stops.test.d.ts +1 -0
  30. package/dist/gtfs/__tests__/time.test.d.ts +1 -0
  31. package/dist/gtfs/__tests__/transfers.test.d.ts +1 -0
  32. package/dist/gtfs/__tests__/trips.test.d.ts +1 -0
  33. package/dist/gtfs/__tests__/utils.test.d.ts +1 -0
  34. package/dist/gtfs/parser.d.ts +34 -0
  35. package/dist/gtfs/profiles/__tests__/ch.test.d.ts +1 -0
  36. package/dist/gtfs/profiles/ch.d.ts +2 -0
  37. package/dist/gtfs/profiles/standard.d.ts +2 -0
  38. package/dist/gtfs/routes.d.ts +11 -0
  39. package/dist/gtfs/services.d.ts +19 -0
  40. package/dist/gtfs/stops.d.ts +20 -0
  41. package/dist/gtfs/time.d.ts +17 -0
  42. package/dist/gtfs/transfers.d.ts +22 -0
  43. package/dist/gtfs/trips.d.ts +26 -0
  44. package/dist/gtfs/utils.d.ts +21 -0
  45. package/dist/index.d.ts +11 -0
  46. package/dist/routing/__tests__/router.test.d.ts +1 -0
  47. package/dist/routing/plotter.d.ts +11 -0
  48. package/dist/routing/query.d.ts +35 -0
  49. package/dist/routing/result.d.ts +28 -0
  50. package/dist/routing/route.d.ts +25 -0
  51. package/dist/routing/router.d.ts +33 -0
  52. package/dist/stops/__tests__/io.test.d.ts +1 -0
  53. package/dist/stops/__tests__/stopFinder.test.d.ts +1 -0
  54. package/dist/stops/i18n.d.ts +10 -0
  55. package/dist/stops/io.d.ts +4 -0
  56. package/dist/stops/proto/stops.d.ts +53 -0
  57. package/dist/stops/stops.d.ts +16 -0
  58. package/dist/stops/stopsIndex.d.ts +52 -0
  59. package/dist/timetable/__tests__/io.test.d.ts +1 -0
  60. package/dist/timetable/__tests__/timetable.test.d.ts +1 -0
  61. package/dist/timetable/duration.d.ts +51 -0
  62. package/dist/timetable/io.d.ts +8 -0
  63. package/dist/timetable/proto/timetable.d.ts +122 -0
  64. package/dist/timetable/time.d.ts +98 -0
  65. package/dist/timetable/timetable.d.ts +82 -0
  66. package/dist/umdIndex.d.ts +9 -0
  67. package/eslint.config.mjs +52 -0
  68. package/package.json +109 -0
  69. package/rollup.config.js +44 -0
  70. package/src/cli/__tests__/minotor.test.ts +23 -0
  71. package/src/cli/minotor.ts +112 -0
  72. package/src/cli/repl.ts +200 -0
  73. package/src/cli/utils.ts +36 -0
  74. package/src/gtfs/__tests__/parser.test.ts +591 -0
  75. package/src/gtfs/__tests__/resources/sample-feed/agency.txt +2 -0
  76. package/src/gtfs/__tests__/resources/sample-feed/calendar.txt +3 -0
  77. package/src/gtfs/__tests__/resources/sample-feed/calendar_dates.txt +2 -0
  78. package/src/gtfs/__tests__/resources/sample-feed/fare_attributes.txt +3 -0
  79. package/src/gtfs/__tests__/resources/sample-feed/fare_rules.txt +5 -0
  80. package/src/gtfs/__tests__/resources/sample-feed/frequencies.txt +12 -0
  81. package/src/gtfs/__tests__/resources/sample-feed/routes.txt +6 -0
  82. package/src/gtfs/__tests__/resources/sample-feed/sample-feed.zip +0 -0
  83. package/src/gtfs/__tests__/resources/sample-feed/shapes.txt +1 -0
  84. package/src/gtfs/__tests__/resources/sample-feed/stop_times.txt +34 -0
  85. package/src/gtfs/__tests__/resources/sample-feed/stops.txt +10 -0
  86. package/src/gtfs/__tests__/resources/sample-feed/trips.txt +13 -0
  87. package/src/gtfs/__tests__/resources/sample-feed.zip +0 -0
  88. package/src/gtfs/__tests__/routes.test.ts +63 -0
  89. package/src/gtfs/__tests__/services.test.ts +209 -0
  90. package/src/gtfs/__tests__/stops.test.ts +177 -0
  91. package/src/gtfs/__tests__/time.test.ts +27 -0
  92. package/src/gtfs/__tests__/transfers.test.ts +117 -0
  93. package/src/gtfs/__tests__/trips.test.ts +463 -0
  94. package/src/gtfs/__tests__/utils.test.ts +13 -0
  95. package/src/gtfs/parser.ts +154 -0
  96. package/src/gtfs/profiles/__tests__/ch.test.ts +43 -0
  97. package/src/gtfs/profiles/ch.ts +70 -0
  98. package/src/gtfs/profiles/standard.ts +39 -0
  99. package/src/gtfs/routes.ts +48 -0
  100. package/src/gtfs/services.ts +98 -0
  101. package/src/gtfs/stops.ts +112 -0
  102. package/src/gtfs/time.ts +33 -0
  103. package/src/gtfs/transfers.ts +102 -0
  104. package/src/gtfs/trips.ts +228 -0
  105. package/src/gtfs/utils.ts +42 -0
  106. package/src/index.ts +28 -0
  107. package/src/routing/__tests__/router.test.ts +760 -0
  108. package/src/routing/plotter.ts +70 -0
  109. package/src/routing/query.ts +74 -0
  110. package/src/routing/result.ts +108 -0
  111. package/src/routing/route.ts +94 -0
  112. package/src/routing/router.ts +262 -0
  113. package/src/stops/__tests__/io.test.ts +43 -0
  114. package/src/stops/__tests__/stopFinder.test.ts +185 -0
  115. package/src/stops/i18n.ts +40 -0
  116. package/src/stops/io.ts +94 -0
  117. package/src/stops/proto/stops.proto +26 -0
  118. package/src/stops/proto/stops.ts +445 -0
  119. package/src/stops/stops.ts +24 -0
  120. package/src/stops/stopsIndex.ts +151 -0
  121. package/src/timetable/__tests__/io.test.ts +175 -0
  122. package/src/timetable/__tests__/timetable.test.ts +180 -0
  123. package/src/timetable/duration.ts +85 -0
  124. package/src/timetable/io.ts +265 -0
  125. package/src/timetable/proto/timetable.proto +76 -0
  126. package/src/timetable/proto/timetable.ts +1304 -0
  127. package/src/timetable/time.ts +192 -0
  128. package/src/timetable/timetable.ts +286 -0
  129. package/src/umdIndex.ts +14 -0
  130. package/tsconfig.build.json +4 -0
  131. package/tsconfig.json +21 -0
@@ -0,0 +1,185 @@
1
+ import assert from 'node:assert';
2
+ import { beforeEach, describe, it } from 'node:test';
3
+
4
+ import { StopsMap } from '../stops.js';
5
+ import { StopsIndex } from '../stopsIndex.js';
6
+ const mockStops: StopsMap = new Map([
7
+ [
8
+ '8587255',
9
+ {
10
+ id: '8587255',
11
+ name: 'Fribourg, Tilleul/Cathédrale',
12
+ lat: 46.8061375857565,
13
+ lon: 7.16145029437328,
14
+ children: [],
15
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
16
+ },
17
+ ],
18
+ [
19
+ '8592383',
20
+ {
21
+ id: '8592383',
22
+ name: 'Fribourg, Neuveville/Court-Ch.',
23
+ lat: 46.8042990960992,
24
+ lon: 7.16060587800609,
25
+ children: [],
26
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
27
+ },
28
+ ],
29
+ [
30
+ '8592386',
31
+ {
32
+ id: '8592386',
33
+ name: 'Fribourg, Petit-St-Jean',
34
+ lat: 46.8035550740648,
35
+ lon: 7.16806189486532,
36
+ children: [],
37
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
38
+ },
39
+ ],
40
+ [
41
+ 'Parent8504100',
42
+ {
43
+ id: 'Parent8504100',
44
+ name: 'Fribourg/Freiburg',
45
+ lat: 46.8031492395272,
46
+ lon: 7.15104780338173,
47
+ children: ['8504100:0:1', '8504100:0:1AB', '8504100:0:2'],
48
+ locationType: 'STATION',
49
+ },
50
+ ],
51
+ [
52
+ '8504100:0:1',
53
+ {
54
+ id: '8504100:0:1',
55
+ name: 'Fribourg/Freiburg',
56
+ lat: 46.8031492395272,
57
+ lon: 7.15104780338173,
58
+ children: [],
59
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
60
+ parent: 'Parent8504100',
61
+ },
62
+ ],
63
+ [
64
+ '8504100:0:1AB',
65
+ {
66
+ id: '8504100:0:1AB',
67
+ name: 'Fribourg/Freiburg',
68
+ lat: 46.8031492395272,
69
+ lon: 7.15104780338173,
70
+ children: [],
71
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
72
+ parent: 'Parent8504100',
73
+ },
74
+ ],
75
+ [
76
+ '8504100:0:2',
77
+ {
78
+ id: '8504100:0:2',
79
+ name: 'Fribourg/Freiburg',
80
+ lat: 46.8031492395272,
81
+ lon: 7.15104780338173,
82
+ children: [],
83
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
84
+ parent: 'Parent8504100',
85
+ },
86
+ ],
87
+ ]);
88
+
89
+ describe('StopFinder', () => {
90
+ let stopFinder: StopsIndex;
91
+
92
+ beforeEach(() => {
93
+ stopFinder = new StopsIndex(mockStops);
94
+ });
95
+
96
+ describe('findStopsByName', () => {
97
+ it('should find stops by exact name', () => {
98
+ const results = stopFinder.findStopsByName(
99
+ 'Fribourg, Tilleul/Cathédrale',
100
+ );
101
+ assert.strictEqual(results[0]?.id, '8587255');
102
+ });
103
+
104
+ it('should not include children stops', () => {
105
+ const results = stopFinder.findStopsByName('Fribourg/Freiburg', 2);
106
+ assert.strictEqual(results[0]?.id, 'Parent8504100');
107
+ assert.strictEqual(results[1]?.id, '8587255');
108
+ });
109
+
110
+ it('should find stops by partial name', () => {
111
+ const results = stopFinder.findStopsByName('Cathédrale');
112
+ assert.strictEqual(results.length, 1);
113
+ assert.strictEqual(results[0]?.id, '8587255');
114
+ });
115
+
116
+ it('should find stops by name with accents', () => {
117
+ const results = stopFinder.findStopsByName('Cathedrale');
118
+ assert.strictEqual(results.length, 1);
119
+ assert.strictEqual(results[0]?.id, '8587255');
120
+ });
121
+
122
+ it('should return an empty array if no stops match the query', () => {
123
+ const results = stopFinder.findStopsByName('Nonexistent Stop');
124
+ assert.strictEqual(results.length, 0);
125
+ });
126
+ });
127
+
128
+ describe('findStopsByLocation', () => {
129
+ it('should find stops by geographic location', () => {
130
+ const results = stopFinder.findStopsByLocation(46.8061, 7.1614, 1);
131
+ assert.strictEqual(results.length, 1);
132
+ assert.strictEqual(results[0]?.id, '8587255');
133
+ });
134
+
135
+ it('should find multiple stops within the radius', () => {
136
+ const results = stopFinder.findStopsByLocation(46.8, 7.16, 10, 0.75);
137
+ assert.strictEqual(results.length, 3);
138
+ assert.strictEqual(results[0]?.id, '8592383');
139
+ assert.strictEqual(results[1]?.id, '8587255');
140
+ assert.strictEqual(results[2]?.id, '8592386');
141
+ });
142
+
143
+ it('should find the N closest stops', () => {
144
+ const results = stopFinder.findStopsByLocation(46.8, 7.16, 2, 10);
145
+ assert.strictEqual(results.length, 2);
146
+ assert.strictEqual(results[0]?.id, '8592383');
147
+ assert.strictEqual(results[1]?.id, '8587255');
148
+ });
149
+
150
+ it('should return an empty array if no stops are within the radius', () => {
151
+ const results = stopFinder.findStopsByLocation(0, 0);
152
+ assert.strictEqual(results.length, 0);
153
+ });
154
+ });
155
+
156
+ describe('fromData', () => {
157
+ it('should deserialize stops data and create a StopFinder instance', () => {
158
+ const serializedData = stopFinder.serialize();
159
+ const deserializedStopFinder = StopsIndex.fromData(serializedData);
160
+ const results = deserializedStopFinder.findStopsByName('Fribourg');
161
+ assert.strictEqual(results.length, 4);
162
+ });
163
+ });
164
+
165
+ describe('equivalentStops', () => {
166
+ it('should find equivalent stops for a given stop ID', () => {
167
+ const equivalentStops = stopFinder.equivalentStops('8504100:0:1');
168
+ assert.deepStrictEqual(equivalentStops, [
169
+ '8504100:0:1',
170
+ '8504100:0:1AB',
171
+ '8504100:0:2',
172
+ ]);
173
+ });
174
+
175
+ it('should return the same stop ID in an array if no equivalents', () => {
176
+ const equivalentStops = stopFinder.equivalentStops('8587255');
177
+ assert.deepStrictEqual(equivalentStops, ['8587255']);
178
+ });
179
+
180
+ it('should return an empty array for non-existent stop ID', () => {
181
+ const equivalentStops = stopFinder.equivalentStops('nonexistent');
182
+ assert.deepStrictEqual(equivalentStops, []);
183
+ });
184
+ });
185
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Generates a list of accent variants for a given term.
3
+ *
4
+ * This function takes a term and generates a list of alternative spellings
5
+ * by replacing characters with their accented variants and vice versa.
6
+ *
7
+ * @param term - The input term for which to generate accent variants.
8
+ * @returns An array of strings containing the original term and its accent variants.
9
+ */
10
+ export const generateAccentVariants = (term: string): string[] => {
11
+ const lowerCaseTerm = term.toLowerCase();
12
+ const alternatives = new Set([lowerCaseTerm]);
13
+
14
+ const accentMap: { [key: string]: string[] } = {
15
+ a: ['à', 'â', 'ä'],
16
+ c: ['ç'],
17
+ e: ['é', 'è', 'ê', 'ë'],
18
+ i: ['î', 'ï'],
19
+ o: ['ô', 'ö'],
20
+ u: ['ù', 'û', 'ü'],
21
+ ae: ['ä'],
22
+ oe: ['ö'],
23
+ ue: ['ü'],
24
+ };
25
+
26
+ for (const [base, accents] of Object.entries(accentMap)) {
27
+ if (lowerCaseTerm.includes(base)) {
28
+ accents.forEach((accent) => {
29
+ alternatives.add(lowerCaseTerm.replace(base, accent));
30
+ });
31
+ }
32
+ accents.forEach((accent) => {
33
+ if (lowerCaseTerm.includes(accent)) {
34
+ alternatives.add(lowerCaseTerm.replace(accent, base));
35
+ }
36
+ });
37
+ }
38
+
39
+ return Array.from(alternatives);
40
+ };
@@ -0,0 +1,94 @@
1
+ import {
2
+ LocationType as ProtoLocationType,
3
+ Stop as ProtoStop,
4
+ StopsMap as ProtoStopsMap,
5
+ } from './proto/stops.js';
6
+ import { LocationType, Stop, StopId, StopsMap } from './stops.js';
7
+
8
+ const CURRENT_VERSION = '0.0.1';
9
+ const serializeStop = (stop: Stop): ProtoStop => {
10
+ return {
11
+ name: stop.name,
12
+ lat: stop.lat,
13
+ lon: stop.lon,
14
+ children: stop.children,
15
+ parent: stop.parent,
16
+ locationType: serializeLocationType(stop.locationType),
17
+ platform: stop.platform,
18
+ };
19
+ };
20
+
21
+ export const serializeStopsMap = (stopsMap: StopsMap): ProtoStopsMap => {
22
+ const protoStopsMap: ProtoStopsMap = {
23
+ version: CURRENT_VERSION,
24
+ stops: {},
25
+ };
26
+
27
+ stopsMap.forEach((value: Stop, key: string) => {
28
+ protoStopsMap.stops[key] = serializeStop(value);
29
+ });
30
+
31
+ return protoStopsMap;
32
+ };
33
+
34
+ const deserializeStop = (stopId: StopId, protoStop: ProtoStop): Stop => {
35
+ return {
36
+ id: stopId,
37
+ name: protoStop.name,
38
+ lat: protoStop.lat,
39
+ lon: protoStop.lon,
40
+ children: protoStop.children,
41
+ parent: protoStop.parent,
42
+ locationType: parseProtoLocationType(protoStop.locationType),
43
+ platform: protoStop.platform,
44
+ };
45
+ };
46
+
47
+ export const deserializeStopsMap = (protoStopsMap: ProtoStopsMap): StopsMap => {
48
+ if (protoStopsMap.version !== CURRENT_VERSION) {
49
+ throw new Error(`Unsupported stopMap version ${protoStopsMap.version}`);
50
+ }
51
+ const stopsMap: StopsMap = new Map();
52
+
53
+ Object.entries(protoStopsMap.stops).forEach(([key, value]) => {
54
+ stopsMap.set(key, deserializeStop(key, value));
55
+ });
56
+
57
+ return stopsMap;
58
+ };
59
+
60
+ const parseProtoLocationType = (
61
+ protoLocationType: ProtoLocationType,
62
+ ): LocationType => {
63
+ switch (protoLocationType) {
64
+ case ProtoLocationType.SIMPLE_STOP_OR_PLATFORM:
65
+ return 'SIMPLE_STOP_OR_PLATFORM';
66
+ case ProtoLocationType.STATION:
67
+ return 'STATION';
68
+ case ProtoLocationType.ENTRANCE_EXIT:
69
+ return 'ENTRANCE_EXIT';
70
+ case ProtoLocationType.GENERIC_NODE:
71
+ return 'GENERIC_NODE';
72
+ case ProtoLocationType.BOARDING_AREA:
73
+ return 'BOARDING_AREA';
74
+ case ProtoLocationType.UNRECOGNIZED:
75
+ throw new Error('Unrecognized protobuf location type.');
76
+ }
77
+ };
78
+
79
+ const serializeLocationType = (
80
+ locationType: LocationType,
81
+ ): ProtoLocationType => {
82
+ switch (locationType) {
83
+ case 'SIMPLE_STOP_OR_PLATFORM':
84
+ return ProtoLocationType.SIMPLE_STOP_OR_PLATFORM;
85
+ case 'STATION':
86
+ return ProtoLocationType.STATION;
87
+ case 'ENTRANCE_EXIT':
88
+ return ProtoLocationType.ENTRANCE_EXIT;
89
+ case 'GENERIC_NODE':
90
+ return ProtoLocationType.GENERIC_NODE;
91
+ case 'BOARDING_AREA':
92
+ return ProtoLocationType.BOARDING_AREA;
93
+ }
94
+ };
@@ -0,0 +1,26 @@
1
+ syntax = "proto3";
2
+
3
+ package minotor.stops;
4
+
5
+ enum LocationType {
6
+ SIMPLE_STOP_OR_PLATFORM = 0;
7
+ STATION = 1;
8
+ ENTRANCE_EXIT = 2;
9
+ GENERIC_NODE = 3;
10
+ BOARDING_AREA = 4;
11
+ }
12
+
13
+ message Stop {
14
+ string name = 1;
15
+ optional double lat = 2;
16
+ optional double lon = 3;
17
+ repeated string children = 4;
18
+ optional string parent = 5;
19
+ LocationType locationType = 6;
20
+ optional string platform = 7;
21
+ }
22
+
23
+ message StopsMap {
24
+ string version = 1;
25
+ map<string, Stop> stops = 2;
26
+ }