@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @osmix/gtfs
2
+
3
+ ## 0.0.1
4
+
5
+ ### Added
6
+
7
+ - Initial release with GTFS to OSM conversion.
8
+ - Stops parsed as nodes with tags.
9
+ - Routes parsed as ways with shape geometry.
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # @osmix/gtfs
2
+
3
+ Convert GTFS (General Transit Feed Specification) transit feeds to OSM format.
4
+
5
+ **Lazy parsing**: Files are only parsed when accessed, not upfront.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @osmix/gtfs
11
+ # or
12
+ bun add @osmix/gtfs
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { fromGtfs } from "@osmix/gtfs"
19
+
20
+ // Fetch a GTFS zip file
21
+ const response = await fetch("https://example.com/gtfs.zip")
22
+ const zipData = await response.arrayBuffer()
23
+
24
+ // Convert to OSM format
25
+ const osm = await fromGtfs(zipData, { id: "transit" })
26
+
27
+ console.log(`Imported ${osm.nodes.size} stops and ${osm.ways.size} routes`)
28
+ ```
29
+
30
+ ### Using GtfsArchive for Custom Processing
31
+
32
+ For more control, use `GtfsArchive` directly. Files are only parsed when you access them:
33
+
34
+ ```ts
35
+ import { GtfsArchive } from "@osmix/gtfs"
36
+
37
+ const archive = GtfsArchive.fromZip(zipData)
38
+
39
+ // Only stops.txt is parsed - other files remain unread
40
+ for await (const stop of archive.iterStops()) {
41
+ console.log(stop.stop_name, stop.stop_lat, stop.stop_lon)
42
+ }
43
+
44
+ // Access routes later - now routes.txt is parsed
45
+ const routes = await archive.routes()
46
+ ```
47
+
48
+ ## GTFS to OSM Mapping
49
+
50
+ ### Stops → Nodes
51
+
52
+ GTFS stops are converted to OSM nodes with the following tags:
53
+
54
+ | GTFS Field | OSM Tag |
55
+ | -------------------- | -------------------- |
56
+ | `stop_name` | `name` |
57
+ | `stop_id` | `ref` |
58
+ | `stop_code` | `ref:gtfs:stop_code` |
59
+ | `stop_desc` | `description` |
60
+ | `stop_url` | `website` |
61
+ | `platform_code` | `ref:platform` |
62
+ | `wheelchair_boarding`| `wheelchair` |
63
+ | `location_type` | `public_transport` |
64
+
65
+ Location types are mapped as:
66
+ - `0` (stop) → `public_transport=platform`
67
+ - `1` (station) → `public_transport=station`
68
+ - `2` (entrance) → `railway=subway_entrance`
69
+ - `4` (boarding area) → `public_transport=platform`
70
+
71
+ ### Routes → Ways
72
+
73
+ GTFS routes are converted to OSM ways with the following tags:
74
+
75
+ | GTFS Field | OSM Tag |
76
+ | ------------------ | -------------------- |
77
+ | `route_long_name` | `name` |
78
+ | `route_short_name` | `ref` |
79
+ | `route_id` | `ref:gtfs:route_id` |
80
+ | `route_desc` | `description` |
81
+ | `route_url` | `website` |
82
+ | `route_color` | `color` |
83
+ | `route_text_color` | `text_color` |
84
+ | `route_type` | `route`, `gtfs:route_type` |
85
+
86
+ Route types are mapped to OSM route values:
87
+ - `0` → `tram`
88
+ - `1` → `subway`
89
+ - `2` → `train`
90
+ - `3` → `bus`
91
+ - `4` → `ferry`
92
+ - `5` → `tram` (cable tram)
93
+ - `6` → `aerialway`
94
+ - `7` → `funicular`
95
+ - `11` → `trolleybus`
96
+ - `12` → `train` (monorail)
97
+
98
+ ### Geometry
99
+
100
+ Route geometry is derived from:
101
+ 1. **shapes.txt** (preferred) - Uses shape points to create accurate route paths
102
+ 2. **stop_times.txt** (fallback) - Uses stop sequence when shapes are unavailable
103
+
104
+ Files are only parsed when needed for the conversion.
105
+
106
+ ## Options
107
+
108
+ ```ts
109
+ interface GtfsConversionOptions {
110
+ /** Whether to include stops as nodes. Default: true */
111
+ includeStops?: boolean
112
+ /** Filter stops by location_type. Default: include all types. */
113
+ stopTypes?: number[]
114
+ /** Whether to include routes as ways. Default: true */
115
+ includeRoutes?: boolean
116
+ /** Filter routes by route_type. Default: include all types. */
117
+ routeTypes?: number[]
118
+ /** Whether to include shape geometry for routes. Default: true */
119
+ includeShapes?: boolean
120
+ }
121
+ ```
122
+
123
+ ### Example with Options
124
+
125
+ ```ts
126
+ import { fromGtfs } from "@osmix/gtfs"
127
+
128
+ // Only bus routes, with stops and stations
129
+ const osm = await fromGtfs(zipData, { id: "buses-only" }, {
130
+ routeTypes: [3], // Only bus routes
131
+ stopTypes: [0, 1], // Only stops and stations
132
+ })
133
+
134
+ // Routes only (no stops) - useful for just getting route shapes
135
+ const routesOnly = await fromGtfs(zipData, { id: "routes" }, {
136
+ includeStops: false,
137
+ })
138
+
139
+ // Stops only (no routes)
140
+ const stopsOnly = await fromGtfs(zipData, { id: "stops" }, {
141
+ includeRoutes: false,
142
+ })
143
+ ```
144
+
145
+ ## API
146
+
147
+ ### `fromGtfs(zipData, options?, gtfsOptions?, onProgress?)`
148
+
149
+ Main function to convert a GTFS zip file to an Osm index.
150
+
151
+ ### `GtfsArchive`
152
+
153
+ Lazy GTFS archive class. Files are parsed on-demand:
154
+
155
+ ```ts
156
+ const archive = GtfsArchive.fromZip(zipData)
157
+
158
+ // Check what files exist
159
+ archive.listFiles() // ['agency.txt', 'stops.txt', ...]
160
+ archive.hasFile('shapes.txt')
161
+
162
+ // Lazy accessors (parse on first call, cache result)
163
+ await archive.agencies()
164
+ await archive.stops()
165
+ await archive.routes()
166
+ await archive.trips()
167
+ await archive.stopTimes()
168
+ await archive.shapes()
169
+
170
+ // Streaming iterator with automatic type inference
171
+ for await (const stop of archive.iter("stops.txt")) {
172
+ console.log(stop.stop_name) // TypeScript knows this is GtfsStop
173
+ }
174
+
175
+ for await (const route of archive.iter("routes.txt")) {
176
+ console.log(route.route_type) // TypeScript knows this is GtfsRoute
177
+ }
178
+
179
+ for await (const shape of archive.iter("shapes.txt")) {
180
+ console.log(shape.shape_pt_lat) // TypeScript knows this is GtfsShapePoint
181
+ }
182
+ ```
183
+
184
+ The `iter(filename)` method automatically infers the return type based on the filename:
185
+ - `"agency.txt"` → `AsyncGenerator<GtfsAgency>`
186
+ - `"stops.txt"` → `AsyncGenerator<GtfsStop>`
187
+ - `"routes.txt"` → `AsyncGenerator<GtfsRoute>`
188
+ - `"trips.txt"` → `AsyncGenerator<GtfsTrip>`
189
+ - `"stop_times.txt"` → `AsyncGenerator<GtfsStopTime>`
190
+ - `"shapes.txt"` → `AsyncGenerator<GtfsShapePoint>`
191
+
192
+ ### `GtfsOsmBuilder`
193
+
194
+ Class for more fine-grained control over the conversion process.
195
+
196
+ ## Dependencies
197
+
198
+ - [but-unzip](https://github.com/nicolo-ribaudo/but-unzip) - ZIP file parsing
199
+ - [@std/csv](https://jsr.io/@std/csv) - Streaming CSV parsing (from Deno standard library)
200
+ - [@osmix/core](../core) - OSM data structures
@@ -0,0 +1,76 @@
1
+ /**
2
+ * GTFS to OSM conversion utilities.
3
+ *
4
+ * Imports GTFS transit feeds into Osm indexes, mapping:
5
+ * - Stops → Nodes with transit tags
6
+ * - Routes → Ways with shape geometry
7
+ *
8
+ * Uses lazy, on-demand parsing - files are only parsed when needed.
9
+ *
10
+ * @module
11
+ */
12
+ import { Osm, type OsmOptions } from "@osmix/core";
13
+ import { type ProgressEvent } from "@osmix/shared/progress";
14
+ import { GtfsArchive } from "./gtfs-archive";
15
+ import type { GtfsConversionOptions } from "./types";
16
+ /**
17
+ * Create an Osm index from a zipped GTFS file.
18
+ *
19
+ * Parses the GTFS zip lazily - only reading files when needed.
20
+ * Converts stops and routes to OSM entities:
21
+ * - Stops become nodes with transit-related tags
22
+ * - Routes become ways with shape geometry (if available) or stop sequence
23
+ *
24
+ * @param zipData - The GTFS zip file as ArrayBuffer or Uint8Array
25
+ * @param options - Osm index options (id, header)
26
+ * @param gtfsOptions - GTFS conversion options
27
+ * @param onProgress - Progress callback for UI feedback
28
+ * @returns Populated Osm index with built indexes
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { fromGtfs } from "@osmix/gtfs"
33
+ *
34
+ * const response = await fetch("https://example.com/gtfs.zip")
35
+ * const zipData = await response.arrayBuffer()
36
+ * const osm = await fromGtfs(zipData, { id: "transit" })
37
+ *
38
+ * console.log(`Imported ${osm.nodes.size} stops`)
39
+ * ```
40
+ */
41
+ export declare function fromGtfs(zipData: ArrayBuffer | Uint8Array, options?: Partial<OsmOptions>, gtfsOptions?: GtfsConversionOptions, onProgress?: (progress: ProgressEvent) => void): Promise<Osm>;
42
+ /**
43
+ * Builder class for converting GTFS data to OSM entities.
44
+ *
45
+ * Uses lazy parsing - only reads GTFS files when needed.
46
+ */
47
+ export declare class GtfsOsmBuilder {
48
+ private osm;
49
+ private onProgress;
50
+ private nextNodeId;
51
+ private nextWayId;
52
+ private stopIdToNodeId;
53
+ constructor(osmOptions?: Partial<OsmOptions>, onProgress?: (progress: ProgressEvent) => void);
54
+ /**
55
+ * Process GTFS stops into OSM nodes.
56
+ * Uses streaming iteration to avoid loading all stops at once.
57
+ */
58
+ processStops(archive: GtfsArchive): Promise<void>;
59
+ /**
60
+ * Process GTFS routes into OSM ways.
61
+ * Creates one way per unique (shape_id, route_id) pair, so each route gets
62
+ * its own way with correct metadata even when routes share the same shape.
63
+ *
64
+ * @param archive - The GTFS archive
65
+ */
66
+ processRoutes(archive: GtfsArchive): Promise<void>;
67
+ /**
68
+ * Create a way from shape points.
69
+ */
70
+ private createWayFromShape;
71
+ /**
72
+ * Build the OSM index with all entities.
73
+ */
74
+ buildOsm(): Osm;
75
+ }
76
+ //# sourceMappingURL=from-gtfs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"from-gtfs.d.ts","sourceRoot":"","sources":["../src/from-gtfs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,GAAG,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAEN,KAAK,aAAa,EAElB,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,KAAK,EAAE,qBAAqB,EAAkB,MAAM,SAAS,CAAA;AAGpE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,QAAQ,CAC7B,OAAO,EAAE,WAAW,GAAG,UAAU,EACjC,OAAO,GAAE,OAAO,CAAC,UAAU,CAAM,EACjC,WAAW,GAAE,qBAA0B,EACvC,UAAU,GAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAkB,GACzD,OAAO,CAAC,GAAG,CAAC,CAad;AAED;;;;GAIG;AACH,qBAAa,cAAc;IAC1B,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,UAAU,CAAmC;IAErD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,SAAS,CAAK;IAGtB,OAAO,CAAC,cAAc,CAA4B;gBAGjD,UAAU,GAAE,OAAO,CAAC,UAAU,CAAM,EACpC,UAAU,GAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAkB;IAM5D;;;OAGG;IACG,YAAY,CAAC,OAAO,EAAE,WAAW;IAgCvC;;;;;;OAMG;IACG,aAAa,CAAC,OAAO,EAAE,WAAW;IAmFxC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA6B1B;;OAEG;IACH,QAAQ,IAAI,GAAG;CAMf"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * GTFS to OSM conversion utilities.
3
+ *
4
+ * Imports GTFS transit feeds into Osm indexes, mapping:
5
+ * - Stops → Nodes with transit tags
6
+ * - Routes → Ways with shape geometry
7
+ *
8
+ * Uses lazy, on-demand parsing - files are only parsed when needed.
9
+ *
10
+ * @module
11
+ */
12
+ import { Osm } from "@osmix/core";
13
+ import { logProgress, progressEvent, } from "@osmix/shared/progress";
14
+ import { GtfsArchive } from "./gtfs-archive";
15
+ import { routeToTags, stopToTags } from "./utils";
16
+ /**
17
+ * Create an Osm index from a zipped GTFS file.
18
+ *
19
+ * Parses the GTFS zip lazily - only reading files when needed.
20
+ * Converts stops and routes to OSM entities:
21
+ * - Stops become nodes with transit-related tags
22
+ * - Routes become ways with shape geometry (if available) or stop sequence
23
+ *
24
+ * @param zipData - The GTFS zip file as ArrayBuffer or Uint8Array
25
+ * @param options - Osm index options (id, header)
26
+ * @param gtfsOptions - GTFS conversion options
27
+ * @param onProgress - Progress callback for UI feedback
28
+ * @returns Populated Osm index with built indexes
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { fromGtfs } from "@osmix/gtfs"
33
+ *
34
+ * const response = await fetch("https://example.com/gtfs.zip")
35
+ * const zipData = await response.arrayBuffer()
36
+ * const osm = await fromGtfs(zipData, { id: "transit" })
37
+ *
38
+ * console.log(`Imported ${osm.nodes.size} stops`)
39
+ * ```
40
+ */
41
+ export async function fromGtfs(zipData, options = {}, gtfsOptions = {}, onProgress = logProgress) {
42
+ onProgress(progressEvent("Opening GTFS archive..."));
43
+ const archive = GtfsArchive.fromZip(zipData);
44
+ const builder = new GtfsOsmBuilder(options, onProgress);
45
+ if (gtfsOptions.includeStops ?? true) {
46
+ await builder.processStops(archive);
47
+ }
48
+ if (gtfsOptions.includeRoutes ?? true) {
49
+ await builder.processRoutes(archive);
50
+ }
51
+ return builder.buildOsm();
52
+ }
53
+ /**
54
+ * Builder class for converting GTFS data to OSM entities.
55
+ *
56
+ * Uses lazy parsing - only reads GTFS files when needed.
57
+ */
58
+ export class GtfsOsmBuilder {
59
+ osm;
60
+ onProgress;
61
+ nextNodeId = -1;
62
+ nextWayId = -1;
63
+ // Map GTFS stop_id to OSM node ID
64
+ stopIdToNodeId = new Map();
65
+ constructor(osmOptions = {}, onProgress = logProgress) {
66
+ this.osm = new Osm(osmOptions);
67
+ this.onProgress = onProgress;
68
+ }
69
+ /**
70
+ * Process GTFS stops into OSM nodes.
71
+ * Uses streaming iteration to avoid loading all stops at once.
72
+ */
73
+ async processStops(archive) {
74
+ let count = 0;
75
+ this.onProgress(progressEvent("Processing stops..."));
76
+ for await (const stop of archive.iter("stops.txt")) {
77
+ const lat = Number.parseFloat(stop.stop_lat);
78
+ const lon = Number.parseFloat(stop.stop_lon);
79
+ if (Number.isNaN(lat) || Number.isNaN(lon))
80
+ continue;
81
+ const tags = stopToTags(stop);
82
+ const nodeId = this.nextNodeId--;
83
+ this.osm.nodes.addNode({
84
+ id: nodeId,
85
+ lat,
86
+ lon,
87
+ tags,
88
+ });
89
+ this.stopIdToNodeId.set(stop.stop_id, nodeId);
90
+ count++;
91
+ if (count % 1000 === 0) {
92
+ this.onProgress(progressEvent(`Processed ${count} stops...`));
93
+ }
94
+ }
95
+ this.onProgress(progressEvent(`Added ${count} stops as nodes`));
96
+ }
97
+ /**
98
+ * Process GTFS routes into OSM ways.
99
+ * Creates one way per unique (shape_id, route_id) pair, so each route gets
100
+ * its own way with correct metadata even when routes share the same shape.
101
+ *
102
+ * @param archive - The GTFS archive
103
+ */
104
+ async processRoutes(archive) {
105
+ this.onProgress(progressEvent("Processing routes..."));
106
+ // Build shape lookup if shapes exist
107
+ const shapeMap = new Map();
108
+ if (archive.hasFile("shapes.txt")) {
109
+ this.onProgress(progressEvent("Loading shape data..."));
110
+ for await (const point of archive.iter("shapes.txt")) {
111
+ const points = shapeMap.get(point.shape_id) ?? [];
112
+ points.push(point);
113
+ shapeMap.set(point.shape_id, points);
114
+ }
115
+ // Sort each shape by sequence
116
+ for (const points of shapeMap.values()) {
117
+ points.sort((a, b) => Number.parseInt(a.shape_pt_sequence, 10) -
118
+ Number.parseInt(b.shape_pt_sequence, 10));
119
+ }
120
+ }
121
+ else {
122
+ throw Error("No shape data found. Cannot process routes.");
123
+ }
124
+ // Group trips by (shape_id, route_id) to ensure each route gets its own way
125
+ // even when multiple routes share the same shape geometry
126
+ const shapeRouteToTrips = new Map();
127
+ this.onProgress(progressEvent("Loading trip data..."));
128
+ for await (const trip of archive.iter("trips.txt")) {
129
+ if (!trip.shape_id)
130
+ continue;
131
+ const key = `${trip.shape_id}:${trip.route_id}`;
132
+ const existing = shapeRouteToTrips.get(key);
133
+ if (existing) {
134
+ existing.tripIds.push(trip.trip_id);
135
+ }
136
+ else {
137
+ shapeRouteToTrips.set(key, { tripIds: [trip.trip_id] });
138
+ }
139
+ }
140
+ // Load routes into a map for lookup
141
+ const routeMap = new Map();
142
+ for await (const route of archive.iter("routes.txt")) {
143
+ routeMap.set(route.route_id, routeToTags(route));
144
+ }
145
+ // Process unique (shape, route) pairs - one way per combination
146
+ let count = 0;
147
+ for (const [key, { tripIds }] of shapeRouteToTrips) {
148
+ const [shapeId, routeId] = key.split(":");
149
+ const routeTags = routeMap.get(routeId);
150
+ if (!routeTags)
151
+ continue;
152
+ const shapePoints = shapeMap.get(shapeId);
153
+ if (!shapePoints || shapePoints.length < 2) {
154
+ this.onProgress(progressEvent(`No shape data found for shape ${shapeId}`, "error"));
155
+ continue;
156
+ }
157
+ // Build tags with route info and all trip IDs for this route
158
+ const tags = {
159
+ ...routeTags,
160
+ "gtfs:shape_id": shapeId,
161
+ "gtfs:trip_ids": tripIds.join(";"),
162
+ "gtfs:trip_count": tripIds.length,
163
+ };
164
+ // Create way from shape points
165
+ this.createWayFromShape(tags, shapePoints);
166
+ count++;
167
+ if (count % 100 === 0) {
168
+ this.onProgress(progressEvent(`Processed ${count} shape-route pairs...`));
169
+ }
170
+ }
171
+ this.onProgress(progressEvent(`Added ${count} shape-route pairs as ways`));
172
+ }
173
+ /**
174
+ * Create a way from shape points.
175
+ */
176
+ createWayFromShape(tags, shapePoints) {
177
+ const nodeRefs = [];
178
+ for (const point of shapePoints) {
179
+ const lat = Number.parseFloat(point.shape_pt_lat);
180
+ const lon = Number.parseFloat(point.shape_pt_lon);
181
+ if (Number.isNaN(lat) || Number.isNaN(lon))
182
+ continue;
183
+ // Create a node for this shape point
184
+ const nodeId = this.nextNodeId--;
185
+ this.osm.nodes.addNode({
186
+ id: nodeId,
187
+ lat,
188
+ lon,
189
+ });
190
+ nodeRefs.push(nodeId);
191
+ }
192
+ if (nodeRefs.length >= 2) {
193
+ const wayId = this.nextWayId--;
194
+ this.osm.ways.addWay({
195
+ id: wayId,
196
+ refs: nodeRefs,
197
+ tags,
198
+ });
199
+ }
200
+ }
201
+ /**
202
+ * Build the OSM index with all entities.
203
+ */
204
+ buildOsm() {
205
+ this.onProgress(progressEvent("Building indexes..."));
206
+ this.osm.buildIndexes();
207
+ this.osm.buildSpatialIndexes();
208
+ return this.osm;
209
+ }
210
+ }
211
+ //# sourceMappingURL=from-gtfs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"from-gtfs.js","sourceRoot":"","sources":["../src/from-gtfs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Lazy GTFS archive parser with streaming CSV support.
3
+ *
4
+ * Only parses CSV files when they are accessed, not upfront.
5
+ * Uses @std/csv CsvParseStream for true line-by-line streaming.
6
+ *
7
+ * @module
8
+ */
9
+ import type { GtfsAgency, GtfsRoute, GtfsShapePoint, GtfsStop, GtfsStopTime, GtfsTrip } from "./types";
10
+ /**
11
+ * Map of GTFS filenames to their record types.
12
+ */
13
+ export interface GtfsFileTypeMap {
14
+ "agency.txt": GtfsAgency;
15
+ "stops.txt": GtfsStop;
16
+ "routes.txt": GtfsRoute;
17
+ "trips.txt": GtfsTrip;
18
+ "stop_times.txt": GtfsStopTime;
19
+ "shapes.txt": GtfsShapePoint;
20
+ }
21
+ /** Valid GTFS filenames that can be parsed. */
22
+ export type GtfsFileName = keyof GtfsFileTypeMap;
23
+ /**
24
+ * Lazy GTFS archive that only parses files on demand.
25
+ *
26
+ * Files are read from the zip and parsed only when their
27
+ * corresponding getter is called for the first time.
28
+ * Streaming iterators parse CSV line-by-line without loading
29
+ * the entire file into memory.
30
+ */
31
+ export declare class GtfsArchive {
32
+ private entries;
33
+ private constructor();
34
+ /**
35
+ * Create a GtfsArchive from zip data.
36
+ */
37
+ static fromZip(zipData: ArrayBuffer | Uint8Array): GtfsArchive;
38
+ /**
39
+ * Check if a file exists in the archive.
40
+ */
41
+ hasFile(filename: string): boolean;
42
+ /**
43
+ * List all files in the archive.
44
+ */
45
+ listFiles(): string[];
46
+ /**
47
+ * Get a readable stream of bytes for a file.
48
+ */
49
+ private getFileBytes;
50
+ /**
51
+ * Stream parse a CSV file, yielding typed records one at a time.
52
+ *
53
+ * The return type is automatically inferred based on the filename:
54
+ * - `"stops.txt"` → `AsyncGenerator<GtfsStop>`
55
+ * - `"routes.txt"` → `AsyncGenerator<GtfsRoute>`
56
+ * - `"shapes.txt"` → `AsyncGenerator<GtfsShapePoint>`
57
+ * - etc.
58
+ *
59
+ * @param filename - The GTFS filename to parse (e.g., "stops.txt")
60
+ * @returns An async generator yielding typed records
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * for await (const stop of archive.iter("stops.txt")) {
65
+ * console.log(stop.stop_name) // TypeScript knows this is GtfsStop
66
+ * }
67
+ * ```
68
+ */
69
+ iter<F extends GtfsFileName>(filename: F): AsyncGenerator<GtfsFileTypeMap[F], void, unknown>;
70
+ }
71
+ //# sourceMappingURL=gtfs-archive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gtfs-archive.d.ts","sourceRoot":"","sources":["../src/gtfs-archive.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EACX,UAAU,EACV,SAAS,EACT,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,QAAQ,EACR,MAAM,SAAS,CAAA;AAGhB;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,YAAY,EAAE,UAAU,CAAA;IACxB,WAAW,EAAE,QAAQ,CAAA;IACrB,YAAY,EAAE,SAAS,CAAA;IACvB,WAAW,EAAE,QAAQ,CAAA;IACrB,gBAAgB,EAAE,YAAY,CAAA;IAC9B,YAAY,EAAE,cAAc,CAAA;CAC5B;AAED,+CAA+C;AAC/C,MAAM,MAAM,YAAY,GAAG,MAAM,eAAe,CAAA;AAEhD;;;;;;;GAOG;AACH,qBAAa,WAAW;IACvB,OAAO,CAAC,OAAO,CAAsB;IAErC,OAAO;IAIP;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,GAAG,WAAW;IAiB9D;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIlC;;OAEG;IACH,SAAS,IAAI,MAAM,EAAE;IAIrB;;OAEG;YACW,YAAY;IAQ1B;;;;;;;;;;;;;;;;;;OAkBG;IACI,IAAI,CAAC,CAAC,SAAS,YAAY,EACjC,QAAQ,EAAE,CAAC,GACT,cAAc,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC;CAoBpD"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Lazy GTFS archive parser with streaming CSV support.
3
+ *
4
+ * Only parses CSV files when they are accessed, not upfront.
5
+ * Uses @std/csv CsvParseStream for true line-by-line streaming.
6
+ *
7
+ * @module
8
+ */
9
+ import { CsvParseStream } from "@std/csv/parse-stream";
10
+ import { unzip } from "but-unzip";
11
+ import { bytesToTextStream } from "./utils";
12
+ /**
13
+ * Lazy GTFS archive that only parses files on demand.
14
+ *
15
+ * Files are read from the zip and parsed only when their
16
+ * corresponding getter is called for the first time.
17
+ * Streaming iterators parse CSV line-by-line without loading
18
+ * the entire file into memory.
19
+ */
20
+ export class GtfsArchive {
21
+ entries;
22
+ constructor(entries) {
23
+ this.entries = entries;
24
+ }
25
+ /**
26
+ * Create a GtfsArchive from zip data.
27
+ */
28
+ static fromZip(zipData) {
29
+ const bytes = zipData instanceof Uint8Array ? zipData : new Uint8Array(zipData);
30
+ const items = unzip(bytes);
31
+ const entries = new Map();
32
+ for (const item of items) {
33
+ // Remove directory prefix and store by filename
34
+ const name = item.filename.replace(/^.*\//, "");
35
+ if (name.endsWith(".txt")) {
36
+ entries.set(name, item);
37
+ }
38
+ }
39
+ return new GtfsArchive(entries);
40
+ }
41
+ /**
42
+ * Check if a file exists in the archive.
43
+ */
44
+ hasFile(filename) {
45
+ return this.entries.has(filename);
46
+ }
47
+ /**
48
+ * List all files in the archive.
49
+ */
50
+ listFiles() {
51
+ return Array.from(this.entries.keys());
52
+ }
53
+ /**
54
+ * Get a readable stream of bytes for a file.
55
+ */
56
+ async getFileBytes(filename) {
57
+ const entry = this.entries.get(filename);
58
+ if (!entry)
59
+ return null;
60
+ const data = entry.read();
61
+ return data instanceof Promise ? await data : data;
62
+ }
63
+ /**
64
+ * Stream parse a CSV file, yielding typed records one at a time.
65
+ *
66
+ * The return type is automatically inferred based on the filename:
67
+ * - `"stops.txt"` → `AsyncGenerator<GtfsStop>`
68
+ * - `"routes.txt"` → `AsyncGenerator<GtfsRoute>`
69
+ * - `"shapes.txt"` → `AsyncGenerator<GtfsShapePoint>`
70
+ * - etc.
71
+ *
72
+ * @param filename - The GTFS filename to parse (e.g., "stops.txt")
73
+ * @returns An async generator yielding typed records
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * for await (const stop of archive.iter("stops.txt")) {
78
+ * console.log(stop.stop_name) // TypeScript knows this is GtfsStop
79
+ * }
80
+ * ```
81
+ */
82
+ async *iter(filename) {
83
+ const bytes = await this.getFileBytes(filename);
84
+ if (!bytes)
85
+ return;
86
+ const textStream = bytesToTextStream(bytes);
87
+ const csvStream = textStream.pipeThrough(new CsvParseStream({ skipFirstRow: true }));
88
+ const reader = csvStream.getReader();
89
+ try {
90
+ while (true) {
91
+ const { value, done } = await reader.read();
92
+ if (done)
93
+ break;
94
+ yield value;
95
+ }
96
+ }
97
+ finally {
98
+ reader.releaseLock();
99
+ }
100
+ }
101
+ }
102
+ //# sourceMappingURL=gtfs-archive.js.map