@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.
- package/CHANGELOG.md +9 -0
- package/README.md +200 -0
- package/dist/from-gtfs.d.ts +76 -0
- package/dist/from-gtfs.d.ts.map +1 -0
- package/dist/from-gtfs.js +211 -0
- package/dist/from-gtfs.js.map +1 -0
- package/dist/gtfs-archive.d.ts +71 -0
- package/dist/gtfs-archive.d.ts.map +1 -0
- package/dist/gtfs-archive.js +102 -0
- package/dist/gtfs-archive.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/src/from-gtfs.d.ts +76 -0
- package/dist/src/from-gtfs.d.ts.map +1 -0
- package/dist/src/from-gtfs.js +211 -0
- package/dist/src/from-gtfs.js.map +1 -0
- package/dist/src/gtfs-archive.d.ts +71 -0
- package/dist/src/gtfs-archive.d.ts.map +1 -0
- package/dist/src/gtfs-archive.js +102 -0
- package/dist/src/gtfs-archive.js.map +1 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/types.d.ts +139 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +48 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +25 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +210 -0
- package/dist/src/utils.js.map +1 -0
- package/dist/test/from-gtfs.test.d.ts +2 -0
- package/dist/test/from-gtfs.test.d.ts.map +1 -0
- package/dist/test/from-gtfs.test.js +389 -0
- package/dist/test/from-gtfs.test.js.map +1 -0
- package/dist/test/helpers.d.ts +14 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/helpers.js +84 -0
- package/dist/test/helpers.js.map +1 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +48 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +25 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +211 -0
- package/dist/utils.js.map +1 -0
- package/package.json +53 -0
- package/src/from-gtfs.ts +259 -0
- package/src/gtfs-archive.ts +138 -0
- package/src/index.ts +54 -0
- package/src/types.ts +184 -0
- package/src/utils.ts +226 -0
- package/test/from-gtfs.test.ts +501 -0
- package/test/helpers.ts +118 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +9 -0
package/CHANGELOG.md
ADDED
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
|