@osmix/gtfs 0.0.1

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 (60) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +200 -0
  3. package/dist/from-gtfs.d.ts +76 -0
  4. package/dist/from-gtfs.d.ts.map +1 -0
  5. package/dist/from-gtfs.js +211 -0
  6. package/dist/from-gtfs.js.map +1 -0
  7. package/dist/gtfs-archive.d.ts +71 -0
  8. package/dist/gtfs-archive.d.ts.map +1 -0
  9. package/dist/gtfs-archive.js +102 -0
  10. package/dist/gtfs-archive.js.map +1 -0
  11. package/dist/index.d.ts +39 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +39 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/src/from-gtfs.d.ts +76 -0
  16. package/dist/src/from-gtfs.d.ts.map +1 -0
  17. package/dist/src/from-gtfs.js +211 -0
  18. package/dist/src/from-gtfs.js.map +1 -0
  19. package/dist/src/gtfs-archive.d.ts +71 -0
  20. package/dist/src/gtfs-archive.d.ts.map +1 -0
  21. package/dist/src/gtfs-archive.js +102 -0
  22. package/dist/src/gtfs-archive.js.map +1 -0
  23. package/dist/src/index.d.ts +39 -0
  24. package/dist/src/index.d.ts.map +1 -0
  25. package/dist/src/index.js +39 -0
  26. package/dist/src/index.js.map +1 -0
  27. package/dist/src/types.d.ts +139 -0
  28. package/dist/src/types.d.ts.map +1 -0
  29. package/dist/src/types.js +48 -0
  30. package/dist/src/types.js.map +1 -0
  31. package/dist/src/utils.d.ts +25 -0
  32. package/dist/src/utils.d.ts.map +1 -0
  33. package/dist/src/utils.js +210 -0
  34. package/dist/src/utils.js.map +1 -0
  35. package/dist/test/from-gtfs.test.d.ts +2 -0
  36. package/dist/test/from-gtfs.test.d.ts.map +1 -0
  37. package/dist/test/from-gtfs.test.js +389 -0
  38. package/dist/test/from-gtfs.test.js.map +1 -0
  39. package/dist/test/helpers.d.ts +14 -0
  40. package/dist/test/helpers.d.ts.map +1 -0
  41. package/dist/test/helpers.js +84 -0
  42. package/dist/test/helpers.js.map +1 -0
  43. package/dist/types.d.ts +139 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +48 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/utils.d.ts +25 -0
  48. package/dist/utils.d.ts.map +1 -0
  49. package/dist/utils.js +211 -0
  50. package/dist/utils.js.map +1 -0
  51. package/package.json +53 -0
  52. package/src/from-gtfs.ts +259 -0
  53. package/src/gtfs-archive.ts +138 -0
  54. package/src/index.ts +54 -0
  55. package/src/types.ts +184 -0
  56. package/src/utils.ts +226 -0
  57. package/test/from-gtfs.test.ts +501 -0
  58. package/test/helpers.ts +118 -0
  59. package/tsconfig.build.json +5 -0
  60. package/tsconfig.json +9 -0
@@ -0,0 +1,501 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import {
3
+ fromGtfs,
4
+ GtfsArchive,
5
+ GtfsOsmBuilder,
6
+ isGtfsZip,
7
+ routeTypeToOsmRoute,
8
+ wheelchairBoardingToOsm,
9
+ } from "../src"
10
+ import { routeToTags, stopToTags } from "../src/utils"
11
+ import { createSharedShapeGtfsZip, createTestGtfsZip } from "./helpers"
12
+
13
+ // Path to the Monaco GTFS fixture
14
+ const MONACO_GTFS_PATH = new URL(
15
+ "../../../fixtures/monaco-gtfs.zip",
16
+ import.meta.url,
17
+ )
18
+
19
+ describe("routeTypeToOsmRoute", () => {
20
+ test("maps GTFS route types to OSM route values", () => {
21
+ expect(routeTypeToOsmRoute("0")).toBe("tram")
22
+ expect(routeTypeToOsmRoute("1")).toBe("subway")
23
+ expect(routeTypeToOsmRoute("2")).toBe("train")
24
+ expect(routeTypeToOsmRoute("3")).toBe("bus")
25
+ expect(routeTypeToOsmRoute("4")).toBe("ferry")
26
+ expect(routeTypeToOsmRoute("5")).toBe("tram")
27
+ expect(routeTypeToOsmRoute("6")).toBe("aerialway")
28
+ expect(routeTypeToOsmRoute("7")).toBe("funicular")
29
+ expect(routeTypeToOsmRoute("11")).toBe("trolleybus")
30
+ expect(routeTypeToOsmRoute("12")).toBe("train")
31
+ })
32
+
33
+ test("defaults to bus for unknown types", () => {
34
+ expect(routeTypeToOsmRoute("99")).toBe("bus")
35
+ expect(routeTypeToOsmRoute("")).toBe("bus")
36
+ })
37
+ })
38
+
39
+ describe("wheelchairBoardingToOsm", () => {
40
+ test("maps wheelchair boarding values", () => {
41
+ expect(wheelchairBoardingToOsm("1")).toBe("yes")
42
+ expect(wheelchairBoardingToOsm("2")).toBe("no")
43
+ expect(wheelchairBoardingToOsm("0")).toBeUndefined()
44
+ expect(wheelchairBoardingToOsm(undefined)).toBeUndefined()
45
+ })
46
+ })
47
+
48
+ describe("stopToTags", () => {
49
+ test("tags regular stops as platforms", () => {
50
+ const tags = stopToTags({
51
+ stop_id: "stop1",
52
+ stop_name: "Main St",
53
+ stop_lat: "40.7128",
54
+ stop_lon: "-74.0060",
55
+ location_type: "0",
56
+ })
57
+
58
+ expect(tags["public_transport"]).toBe("platform")
59
+ expect(tags["name"]).toBe("Main St")
60
+ })
61
+
62
+ test("tags stations correctly", () => {
63
+ const tags = stopToTags({
64
+ stop_id: "station1",
65
+ stop_name: "Central Station",
66
+ stop_lat: "40.7128",
67
+ stop_lon: "-74.0060",
68
+ location_type: "1",
69
+ })
70
+
71
+ expect(tags["public_transport"]).toBe("station")
72
+ })
73
+
74
+ test("tags entrances without public_transport tag", () => {
75
+ const tags = stopToTags({
76
+ stop_id: "entrance1",
77
+ stop_name: "Station Entrance A",
78
+ stop_lat: "40.7128",
79
+ stop_lon: "-74.0060",
80
+ location_type: "2",
81
+ })
82
+
83
+ // Entrances should have railway=subway_entrance but NOT public_transport
84
+ expect(tags["railway"]).toBe("subway_entrance")
85
+ expect(tags["public_transport"]).toBeUndefined()
86
+ expect(tags["name"]).toBe("Station Entrance A")
87
+ })
88
+
89
+ test("tags boarding areas as platforms", () => {
90
+ const tags = stopToTags({
91
+ stop_id: "boarding1",
92
+ stop_name: "Platform A",
93
+ stop_lat: "40.7128",
94
+ stop_lon: "-74.0060",
95
+ location_type: "4",
96
+ })
97
+
98
+ expect(tags["public_transport"]).toBe("platform")
99
+ })
100
+ })
101
+
102
+ describe("routeToTags", () => {
103
+ test("normalizes valid hex colors", () => {
104
+ const tags = routeToTags({
105
+ route_id: "route1",
106
+ route_short_name: "1",
107
+ route_type: "3",
108
+ route_color: "ff0000",
109
+ route_text_color: "FFFFFF",
110
+ })
111
+
112
+ expect(tags["color"]).toBe("#FF0000")
113
+ expect(tags["text_color"]).toBe("#FFFFFF")
114
+ })
115
+
116
+ test("accepts colors with # prefix", () => {
117
+ const tags = routeToTags({
118
+ route_id: "route1",
119
+ route_short_name: "1",
120
+ route_type: "3",
121
+ route_color: "#00FF00",
122
+ })
123
+
124
+ expect(tags["color"]).toBe("#00FF00")
125
+ })
126
+
127
+ test("accepts 3-character shorthand colors", () => {
128
+ const tags = routeToTags({
129
+ route_id: "route1",
130
+ route_short_name: "1",
131
+ route_type: "3",
132
+ route_color: "F00",
133
+ })
134
+
135
+ expect(tags["color"]).toBe("#FF0000")
136
+ })
137
+
138
+ test("rejects invalid hex colors", () => {
139
+ const tags = routeToTags({
140
+ route_id: "route1",
141
+ route_short_name: "1",
142
+ route_type: "3",
143
+ route_color: "ZZZZZZ",
144
+ route_text_color: "not-a-color",
145
+ })
146
+
147
+ // Invalid colors should not be added to tags
148
+ expect(tags["color"]).toBeUndefined()
149
+ expect(tags["text_color"]).toBeUndefined()
150
+ })
151
+
152
+ test("handles missing colors", () => {
153
+ const tags = routeToTags({
154
+ route_id: "route1",
155
+ route_short_name: "1",
156
+ route_type: "3",
157
+ })
158
+
159
+ expect(tags["color"]).toBeUndefined()
160
+ expect(tags["text_color"]).toBeUndefined()
161
+ })
162
+
163
+ test("sets route type and name correctly", () => {
164
+ const tags = routeToTags({
165
+ route_id: "route1",
166
+ route_short_name: "R1",
167
+ route_long_name: "Red Line",
168
+ route_type: "1",
169
+ })
170
+
171
+ expect(tags["route"]).toBe("subway")
172
+ expect(tags["name"]).toBe("Red Line")
173
+ expect(tags["ref"]).toBe("R1")
174
+ })
175
+ })
176
+
177
+ describe("GtfsArchive", () => {
178
+ test("parses a GTFS zip file lazily", async () => {
179
+ const zipData = await createTestGtfsZip()
180
+ const archive = GtfsArchive.fromZip(zipData)
181
+
182
+ // Check files are listed
183
+ const files = archive.listFiles()
184
+ expect(files).toContain("agency.txt")
185
+ expect(files).toContain("stops.txt")
186
+ expect(files).toContain("routes.txt")
187
+
188
+ // Parse agencies on demand
189
+ const agencies = await Array.fromAsync(archive.iter("agency.txt"))
190
+ expect(agencies.length).toBe(1)
191
+ expect(agencies[0]?.agency_name).toBe("Test Transit")
192
+
193
+ // Parse stops on demand
194
+ const stops = await Array.fromAsync(archive.iter("stops.txt"))
195
+ expect(stops.length).toBe(3)
196
+ expect(stops[0]?.stop_name).toBe("Main St Station")
197
+
198
+ // Parse routes on demand
199
+ const routes = await Array.fromAsync(archive.iter("routes.txt"))
200
+ expect(routes.length).toBe(1)
201
+ expect(routes[0]?.route_short_name).toBe("1")
202
+ })
203
+
204
+ test("iterates stops without loading all at once", async () => {
205
+ const zipData = await createTestGtfsZip()
206
+ const archive = GtfsArchive.fromZip(zipData)
207
+
208
+ const stops = []
209
+ for await (const stop of archive.iter("stops.txt")) {
210
+ stops.push(stop)
211
+ }
212
+
213
+ expect(stops.length).toBe(3)
214
+ expect(stops[0]?.stop_name).toBe("Main St Station")
215
+ })
216
+
217
+ test("iter() returns correctly typed records", async () => {
218
+ const zipData = await createTestGtfsZip()
219
+ const archive = GtfsArchive.fromZip(zipData)
220
+
221
+ // TypeScript should infer the correct type for each file
222
+ for await (const stop of archive.iter("stops.txt")) {
223
+ // stop is GtfsStop
224
+ expect(stop.stop_id).toBeDefined()
225
+ expect(stop.stop_lat).toBeDefined()
226
+ break
227
+ }
228
+
229
+ for await (const route of archive.iter("routes.txt")) {
230
+ // route is GtfsRoute
231
+ expect(route.route_id).toBeDefined()
232
+ expect(route.route_type).toBeDefined()
233
+ break
234
+ }
235
+
236
+ for await (const shape of archive.iter("shapes.txt")) {
237
+ // shape is GtfsShapePoint
238
+ expect(shape.shape_id).toBeDefined()
239
+ expect(shape.shape_pt_lat).toBeDefined()
240
+ break
241
+ }
242
+ })
243
+ })
244
+
245
+ describe("isGtfsZip", () => {
246
+ test("detects GTFS zip created in tests", async () => {
247
+ const zipData = await createTestGtfsZip()
248
+ expect(isGtfsZip(zipData)).toBe(true)
249
+ })
250
+
251
+ test("detects Monaco GTFS fixture as GTFS", async () => {
252
+ const file = Bun.file(MONACO_GTFS_PATH)
253
+ const zipData = new Uint8Array(await file.arrayBuffer())
254
+ expect(isGtfsZip(zipData)).toBe(true)
255
+ })
256
+
257
+ test("returns false for non-GTFS zip", async () => {
258
+ const encoder = new TextEncoder()
259
+ const { zipSync } = await import("fflate")
260
+
261
+ const bytes = zipSync({
262
+ "readme.txt": encoder.encode("not a gtfs feed"),
263
+ "data.csv": encoder.encode("col1,col2\n1,2\n"),
264
+ })
265
+
266
+ expect(isGtfsZip(bytes)).toBe(false)
267
+ })
268
+ })
269
+
270
+ describe("fromGtfs", () => {
271
+ test("converts GTFS to OSM with shapes", async () => {
272
+ const zipData = await createTestGtfsZip()
273
+ const osm = await fromGtfs(zipData, { id: "test-transit" })
274
+
275
+ // Should have stops as nodes
276
+ expect(osm.nodes.size).toBeGreaterThanOrEqual(3)
277
+
278
+ // Should have route as way
279
+ expect(osm.ways.size).toBe(1)
280
+
281
+ // Check node tags
282
+ const stopNodes = []
283
+ for (let i = 0; i < osm.nodes.size; i++) {
284
+ const tags = osm.nodes.tags.getTags(i)
285
+ if (tags?.["public_transport"]) {
286
+ stopNodes.push({ index: i, tags })
287
+ }
288
+ }
289
+ expect(stopNodes.length).toBe(3)
290
+
291
+ // First stop should have correct tags
292
+ const firstStop = stopNodes.find(
293
+ (n) => n.tags?.["name"] === "Main St Station",
294
+ )
295
+ expect(firstStop).toBeDefined()
296
+ expect(firstStop?.tags?.["ref"]).toBe("stop1")
297
+ expect(firstStop?.tags?.["wheelchair"]).toBe("yes")
298
+
299
+ // Check way tags
300
+ const wayTags = osm.ways.tags.getTags(0)
301
+ expect(wayTags?.["route"]).toBe("bus")
302
+ expect(wayTags?.["ref"]).toBe("1")
303
+ expect(wayTags?.["name"]).toBe("Downtown Express")
304
+ })
305
+
306
+ test("can exclude stops entirely", async () => {
307
+ const zipData = await createTestGtfsZip()
308
+ const osm = await fromGtfs(
309
+ zipData,
310
+ { id: "routes-only" },
311
+ { includeStops: false },
312
+ )
313
+
314
+ // Should have only shape nodes, no stop nodes
315
+ let stopCount = 0
316
+ for (let i = 0; i < osm.nodes.size; i++) {
317
+ const tags = osm.nodes.tags.getTags(i)
318
+ if (tags?.["public_transport"]) {
319
+ stopCount++
320
+ }
321
+ }
322
+ expect(stopCount).toBe(0)
323
+
324
+ // Should still have the route
325
+ expect(osm.ways.size).toBe(1)
326
+ })
327
+
328
+ test("can exclude routes entirely", async () => {
329
+ const zipData = await createTestGtfsZip()
330
+ const osm = await fromGtfs(
331
+ zipData,
332
+ { id: "stops-only" },
333
+ { includeRoutes: false },
334
+ )
335
+
336
+ // Should have stops
337
+ let stopCount = 0
338
+ for (let i = 0; i < osm.nodes.size; i++) {
339
+ const tags = osm.nodes.tags.getTags(i)
340
+ if (tags?.["public_transport"]) {
341
+ stopCount++
342
+ }
343
+ }
344
+ expect(stopCount).toBe(3)
345
+
346
+ // Should have no routes
347
+ expect(osm.ways.size).toBe(0)
348
+ })
349
+
350
+ test("creates separate ways for routes sharing the same shape", async () => {
351
+ const zipData = await createSharedShapeGtfsZip()
352
+ const osm = await fromGtfs(
353
+ zipData,
354
+ { id: "shared-shape" },
355
+ { includeStops: false },
356
+ )
357
+
358
+ // Should have 2 ways - one for each route, even though they share a shape
359
+ expect(osm.ways.size).toBe(2)
360
+
361
+ // Collect way tags
362
+ const wayTagsList: Record<string, unknown>[] = []
363
+ for (let i = 0; i < osm.ways.size; i++) {
364
+ const tags = osm.ways.tags.getTags(i)
365
+ if (tags) wayTagsList.push(tags)
366
+ }
367
+
368
+ // Find the Red Line (route1) way
369
+ const redLineWay = wayTagsList.find((tags) => tags["ref"] === "R1")
370
+ expect(redLineWay).toBeDefined()
371
+ expect(redLineWay?.["name"]).toBe("Red Line")
372
+ expect(redLineWay?.["route"]).toBe("subway")
373
+ expect(redLineWay?.["color"]).toBe("#FF0000")
374
+ // Should have 2 trips (trip1 and trip2)
375
+ expect(Number(redLineWay?.["gtfs:trip_count"])).toBe(2)
376
+ expect(redLineWay?.["gtfs:trip_ids"]).toBe("trip1;trip2")
377
+
378
+ // Find the Blue Express (route2) way
379
+ const blueExpressWay = wayTagsList.find((tags) => tags["ref"] === "B2")
380
+ expect(blueExpressWay).toBeDefined()
381
+ expect(blueExpressWay?.["name"]).toBe("Blue Express")
382
+ expect(blueExpressWay?.["route"]).toBe("bus")
383
+ expect(blueExpressWay?.["color"]).toBe("#0000FF")
384
+ // Should have 1 trip (trip3)
385
+ expect(Number(blueExpressWay?.["gtfs:trip_count"])).toBe(1)
386
+ expect(blueExpressWay?.["gtfs:trip_ids"]).toBe("trip3")
387
+
388
+ // Both should reference the same shape
389
+ expect(redLineWay?.["gtfs:shape_id"]).toBe("shared_shape")
390
+ expect(blueExpressWay?.["gtfs:shape_id"]).toBe("shared_shape")
391
+ })
392
+ })
393
+
394
+ describe("GtfsOsmBuilder", () => {
395
+ test("can be used for step-by-step conversion", async () => {
396
+ const zipData = await createTestGtfsZip()
397
+ const archive = GtfsArchive.fromZip(zipData)
398
+
399
+ const stops = await Array.fromAsync(archive.iter("stops.txt"))
400
+ const routes = await Array.fromAsync(archive.iter("routes.txt"))
401
+
402
+ expect(stops.length).toBe(3)
403
+ expect(routes.length).toBe(1)
404
+
405
+ const builder = new GtfsOsmBuilder({ id: "manual-test" })
406
+ await builder.processStops(archive)
407
+ await builder.processRoutes(archive)
408
+ const osm = builder.buildOsm()
409
+
410
+ expect(osm.nodes.size).toBeGreaterThanOrEqual(3)
411
+ expect(osm.ways.size).toBe(1)
412
+ })
413
+ })
414
+
415
+ describe("Monaco GTFS fixture", () => {
416
+ test("parses Monaco GTFS archive", async () => {
417
+ const file = Bun.file(MONACO_GTFS_PATH)
418
+ const zipData = await file.arrayBuffer()
419
+ const archive = GtfsArchive.fromZip(zipData)
420
+
421
+ // Check expected files exist
422
+ expect(archive.hasFile("agency.txt")).toBe(true)
423
+ expect(archive.hasFile("stops.txt")).toBe(true)
424
+ expect(archive.hasFile("routes.txt")).toBe(true)
425
+ expect(archive.hasFile("shapes.txt")).toBe(true)
426
+ expect(archive.hasFile("trips.txt")).toBe(true)
427
+ expect(archive.hasFile("stop_times.txt")).toBe(true)
428
+
429
+ // Parse agency
430
+ const agencies = await Array.fromAsync(archive.iter("agency.txt"))
431
+ expect(agencies.length).toBe(1)
432
+
433
+ // Parse stops
434
+ const stops = await Array.fromAsync(archive.iter("stops.txt"))
435
+ expect(stops.length).toBe(98)
436
+
437
+ // Parse routes
438
+ const routes = await Array.fromAsync(archive.iter("routes.txt"))
439
+ expect(routes.length).toBe(15)
440
+ })
441
+
442
+ test("converts Monaco GTFS to OSM with routes only", async () => {
443
+ const zipData = await Bun.file(MONACO_GTFS_PATH).arrayBuffer()
444
+
445
+ // Only include routes (no stops) to test shapes parsing
446
+ const osm = await fromGtfs(
447
+ zipData,
448
+ { id: "monaco-routes" },
449
+ { includeStops: false },
450
+ )
451
+
452
+ // Should have routes as ways (one per shape+route pair, not per trip)
453
+ // Previously 271 when grouping by shape only, now 315 with shape+route pairs
454
+ expect(osm.ways.size).toBe(315)
455
+
456
+ // Check a route has proper tags
457
+ const wayTags = osm.ways.tags.getTags(0)
458
+ expect(wayTags?.["route"]).toBeDefined()
459
+ })
460
+
461
+ test("converts Monaco GTFS to OSM with stops only", async () => {
462
+ const zipData = await Bun.file(MONACO_GTFS_PATH).arrayBuffer()
463
+
464
+ // Only include stops (no routes)
465
+ const osm = await fromGtfs(
466
+ zipData,
467
+ { id: "monaco-stops" },
468
+ { includeRoutes: false },
469
+ )
470
+
471
+ // Should have stops as nodes
472
+ expect(osm.nodes.size).toBeGreaterThan(0)
473
+
474
+ // No routes
475
+ expect(osm.ways.size).toBe(0)
476
+
477
+ // Check a stop has proper tags
478
+ let foundStop = false
479
+ for (let i = 0; i < osm.nodes.size; i++) {
480
+ const tags = osm.nodes.tags.getTags(i)
481
+ if (tags?.["public_transport"]) {
482
+ foundStop = true
483
+ expect(tags["name"]).toBeDefined()
484
+ break
485
+ }
486
+ }
487
+ expect(foundStop).toBe(true)
488
+ })
489
+
490
+ test("converts full Monaco GTFS to OSM", async () => {
491
+ const zipData = await Bun.file(MONACO_GTFS_PATH).arrayBuffer()
492
+
493
+ const osm = await fromGtfs(zipData, { id: "monaco-full" })
494
+
495
+ // Should have both stops and routes
496
+ expect(osm.nodes.size).toBeGreaterThan(0)
497
+ expect(osm.ways.size).toBeGreaterThan(0)
498
+
499
+ console.log(`Monaco GTFS: ${osm.nodes.size} nodes, ${osm.ways.size} ways`)
500
+ })
501
+ })
@@ -0,0 +1,118 @@
1
+ import { zipSync } from "fflate"
2
+
3
+ /**
4
+ * Create a test GTFS zip file with sample data.
5
+ */
6
+ export async function createTestGtfsZip(): Promise<Uint8Array> {
7
+ const encoder = new TextEncoder()
8
+
9
+ const files: Record<string, Uint8Array> = {
10
+ "agency.txt":
11
+ encoder.encode(`agency_id,agency_name,agency_url,agency_timezone
12
+ agency1,Test Transit,https://example.com,America/New_York`),
13
+
14
+ "stops.txt":
15
+ encoder.encode(`stop_id,stop_name,stop_lat,stop_lon,location_type,wheelchair_boarding
16
+ stop1,Main St Station,40.7128,-74.0060,0,1
17
+ stop2,Broadway Station,40.7580,-73.9855,1,2
18
+ stop3,Park Ave Stop,40.7614,-73.9776,0,0`),
19
+
20
+ "routes.txt":
21
+ encoder.encode(`route_id,route_short_name,route_long_name,route_type,route_color,route_text_color
22
+ route1,1,Downtown Express,3,FF0000,FFFFFF`),
23
+
24
+ "trips.txt": encoder.encode(`trip_id,route_id,service_id,shape_id
25
+ trip1,route1,weekday,shape1`),
26
+
27
+ "stop_times.txt":
28
+ encoder.encode(`trip_id,stop_id,stop_sequence,arrival_time,departure_time
29
+ trip1,stop1,1,08:00:00,08:00:00
30
+ trip1,stop2,2,08:10:00,08:10:00
31
+ trip1,stop3,3,08:20:00,08:20:00`),
32
+
33
+ "shapes.txt":
34
+ encoder.encode(`shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence
35
+ shape1,40.7128,-74.0060,1
36
+ shape1,40.7400,-73.9900,2
37
+ shape1,40.7614,-73.9776,3`),
38
+
39
+ "calendar.txt":
40
+ encoder.encode(`service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date
41
+ weekday,1,1,1,1,1,0,0,20240101,20241231`),
42
+ }
43
+
44
+ return zipSync(files)
45
+ }
46
+
47
+ /**
48
+ * Create a GTFS zip where two routes share the same shape.
49
+ * Used to test that each route gets its own way with correct metadata.
50
+ */
51
+ export async function createSharedShapeGtfsZip(): Promise<Uint8Array> {
52
+ const encoder = new TextEncoder()
53
+
54
+ const files: Record<string, Uint8Array> = {
55
+ "agency.txt":
56
+ encoder.encode(`agency_id,agency_name,agency_url,agency_timezone
57
+ agency1,Test Transit,https://example.com,America/New_York`),
58
+
59
+ "stops.txt": encoder.encode(`stop_id,stop_name,stop_lat,stop_lon
60
+ stop1,Stop A,40.7128,-74.0060
61
+ stop2,Stop B,40.7614,-73.9776`),
62
+
63
+ "routes.txt":
64
+ encoder.encode(`route_id,route_short_name,route_long_name,route_type,route_color
65
+ route1,R1,Red Line,1,FF0000
66
+ route2,B2,Blue Express,3,0000FF`),
67
+
68
+ // Both trips use the same shape but different routes
69
+ "trips.txt": encoder.encode(`trip_id,route_id,service_id,shape_id
70
+ trip1,route1,daily,shared_shape
71
+ trip2,route1,daily,shared_shape
72
+ trip3,route2,daily,shared_shape`),
73
+
74
+ "stop_times.txt": encoder.encode(`trip_id,stop_id,stop_sequence
75
+ trip1,stop1,1
76
+ trip1,stop2,2
77
+ trip2,stop1,1
78
+ trip2,stop2,2
79
+ trip3,stop1,1
80
+ trip3,stop2,2`),
81
+
82
+ "shapes.txt":
83
+ encoder.encode(`shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence
84
+ shared_shape,40.7128,-74.0060,1
85
+ shared_shape,40.7400,-73.9900,2
86
+ shared_shape,40.7614,-73.9776,3`),
87
+ }
88
+
89
+ return zipSync(files)
90
+ }
91
+
92
+ /**
93
+ * Create a minimal GTFS zip with just stops (no routes or shapes).
94
+ */
95
+ export async function createMinimalGtfsZip(): Promise<Uint8Array> {
96
+ const encoder = new TextEncoder()
97
+
98
+ const files: Record<string, Uint8Array> = {
99
+ "agency.txt":
100
+ encoder.encode(`agency_id,agency_name,agency_url,agency_timezone
101
+ agency1,Minimal Transit,https://example.com,America/New_York`),
102
+
103
+ "stops.txt": encoder.encode(`stop_id,stop_name,stop_lat,stop_lon
104
+ stop1,Test Stop,40.7128,-74.0060`),
105
+
106
+ "routes.txt":
107
+ encoder.encode(`route_id,route_short_name,route_long_name,route_type
108
+ route1,M,Metro Line,1`),
109
+
110
+ "trips.txt": encoder.encode(`trip_id,route_id,service_id
111
+ trip1,route1,daily`),
112
+
113
+ "stop_times.txt": encoder.encode(`trip_id,stop_id,stop_sequence
114
+ trip1,stop1,1`),
115
+ }
116
+
117
+ return zipSync(files)
118
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "./tsconfig.json",
4
+ "include": ["src"]
5
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@osmix/shared/tsconfig/base.json",
4
+ "exclude": ["node_modules", "dist"],
5
+ "include": ["src", "test"],
6
+ "compilerOptions": {
7
+ "outDir": "./dist"
8
+ }
9
+ }