@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GTFS (General Transit Feed Specification) type definitions.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Map GTFS route_type to OSM route tag value.
|
|
8
|
+
*/
|
|
9
|
+
export function routeTypeToOsmRoute(routeType) {
|
|
10
|
+
switch (routeType) {
|
|
11
|
+
case "0":
|
|
12
|
+
return "tram";
|
|
13
|
+
case "1":
|
|
14
|
+
return "subway";
|
|
15
|
+
case "2":
|
|
16
|
+
return "train";
|
|
17
|
+
case "3":
|
|
18
|
+
return "bus";
|
|
19
|
+
case "4":
|
|
20
|
+
return "ferry";
|
|
21
|
+
case "5":
|
|
22
|
+
return "tram"; // Cable tram
|
|
23
|
+
case "6":
|
|
24
|
+
return "aerialway";
|
|
25
|
+
case "7":
|
|
26
|
+
return "funicular";
|
|
27
|
+
case "11":
|
|
28
|
+
return "trolleybus";
|
|
29
|
+
case "12":
|
|
30
|
+
return "train"; // Monorail
|
|
31
|
+
default:
|
|
32
|
+
return "bus";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Map GTFS wheelchair_boarding to OSM wheelchair tag value.
|
|
37
|
+
*/
|
|
38
|
+
export function wheelchairBoardingToOsm(value) {
|
|
39
|
+
switch (value) {
|
|
40
|
+
case "1":
|
|
41
|
+
return "yes";
|
|
42
|
+
case "2":
|
|
43
|
+
return "no";
|
|
44
|
+
default:
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAuIH;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACpD,QAAQ,SAAS,EAAE,CAAC;QACnB,KAAK,GAAG;YACP,OAAO,MAAM,CAAA;QACd,KAAK,GAAG;YACP,OAAO,QAAQ,CAAA;QAChB,KAAK,GAAG;YACP,OAAO,OAAO,CAAA;QACf,KAAK,GAAG;YACP,OAAO,KAAK,CAAA;QACb,KAAK,GAAG;YACP,OAAO,OAAO,CAAA;QACf,KAAK,GAAG;YACP,OAAO,MAAM,CAAA,CAAC,aAAa;QAC5B,KAAK,GAAG;YACP,OAAO,WAAW,CAAA;QACnB,KAAK,GAAG;YACP,OAAO,WAAW,CAAA;QACnB,KAAK,IAAI;YACR,OAAO,YAAY,CAAA;QACpB,KAAK,IAAI;YACR,OAAO,OAAO,CAAA,CAAC,WAAW;QAC3B;YACC,OAAO,KAAK,CAAA;IACd,CAAC;AACF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CACtC,KAAyB;IAEzB,QAAQ,KAAK,EAAE,CAAC;QACf,KAAK,GAAG;YACP,OAAO,KAAK,CAAA;QACb,KAAK,GAAG;YACP,OAAO,IAAI,CAAA;QACZ;YACC,OAAO,SAAS,CAAA;IAClB,CAAC;AACF,CAAC"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OsmTags } from "@osmix/shared/types";
|
|
2
|
+
import { type GtfsRoute, type GtfsStop, type GtfsTrip } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Check if a ZIP file (as bytes) is a GTFS archive by looking for characteristic GTFS files.
|
|
5
|
+
* GTFS archives must contain at least agency.txt, stops.txt, routes.txt, trips.txt,
|
|
6
|
+
* and stop_times.txt according to the GTFS specification.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isGtfsZip(bytes: Uint8Array): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Convert a GTFS stop to OSM tags.
|
|
11
|
+
*/
|
|
12
|
+
export declare function stopToTags(stop: GtfsStop): OsmTags;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a GTFS route to OSM tags.
|
|
15
|
+
*/
|
|
16
|
+
export declare function routeToTags(route: GtfsRoute): OsmTags;
|
|
17
|
+
/**
|
|
18
|
+
* Convert a GTFS trip to OSM tags.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tripToTags(trip: GtfsTrip): OsmTags;
|
|
21
|
+
/**
|
|
22
|
+
* Create a ReadableStream of text from file bytes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function bytesToTextStream(bytes: Uint8Array): ReadableStream<string>;
|
|
25
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EACN,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,QAAQ,EAGb,MAAM,SAAS,CAAA;AAEhB;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAsFpD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAoClD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAoCrD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAelD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,CAAC,MAAM,CAAC,CAqB3E"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { routeTypeToOsmRoute, wheelchairBoardingToOsm, } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Check if a ZIP file (as bytes) is a GTFS archive by looking for characteristic GTFS files.
|
|
4
|
+
* GTFS archives must contain at least agency.txt, stops.txt, routes.txt, trips.txt,
|
|
5
|
+
* and stop_times.txt according to the GTFS specification.
|
|
6
|
+
*/
|
|
7
|
+
export function isGtfsZip(bytes) {
|
|
8
|
+
// GTFS required files per the spec
|
|
9
|
+
const requiredGtfsFiles = [
|
|
10
|
+
"agency.txt",
|
|
11
|
+
"stops.txt",
|
|
12
|
+
"routes.txt",
|
|
13
|
+
"trips.txt",
|
|
14
|
+
"stop_times.txt",
|
|
15
|
+
];
|
|
16
|
+
// Find filenames in the ZIP by scanning for the local file headers
|
|
17
|
+
// ZIP local file header signature: 0x04034b50 (little-endian: 50 4b 03 04)
|
|
18
|
+
const foundFiles = new Set();
|
|
19
|
+
const decoder = new TextDecoder();
|
|
20
|
+
let pos = 0;
|
|
21
|
+
while (pos < bytes.length - 30) {
|
|
22
|
+
// Check for ZIP local file header signature
|
|
23
|
+
const isLocalHeader = bytes[pos] === 0x50 &&
|
|
24
|
+
bytes[pos + 1] === 0x4b &&
|
|
25
|
+
bytes[pos + 2] === 0x03 &&
|
|
26
|
+
bytes[pos + 3] === 0x04;
|
|
27
|
+
if (isLocalHeader) {
|
|
28
|
+
// Read filename length at offset 26-27 (little-endian)
|
|
29
|
+
const nameLen = (bytes[pos + 26] ?? 0) | ((bytes[pos + 27] ?? 0) << 8);
|
|
30
|
+
// Read extra field length at offset 28-29 (little-endian)
|
|
31
|
+
const extraLen = (bytes[pos + 28] ?? 0) | ((bytes[pos + 29] ?? 0) << 8);
|
|
32
|
+
// Defensive bounds check
|
|
33
|
+
if (nameLen < 0 || extraLen < 0)
|
|
34
|
+
break;
|
|
35
|
+
if (pos + 30 + nameLen + extraLen > bytes.length)
|
|
36
|
+
break;
|
|
37
|
+
// General purpose bit flag at offset 6-7 (little-endian)
|
|
38
|
+
// Bit 3 indicates the presence of a data descriptor, in which case
|
|
39
|
+
// the compressed size fields in the local header are zero and the
|
|
40
|
+
// actual sizes follow the compressed data.
|
|
41
|
+
const flags = (bytes[pos + 6] ?? 0) | ((bytes[pos + 7] ?? 0) << 8);
|
|
42
|
+
const hasDataDescriptor = (flags & 0x0008) !== 0;
|
|
43
|
+
// Extract filename (starts at offset 30)
|
|
44
|
+
const nameBytes = bytes.slice(pos + 30, pos + 30 + nameLen);
|
|
45
|
+
const filename = decoder.decode(nameBytes);
|
|
46
|
+
// Normalize path - extract just the filename part
|
|
47
|
+
const basename = filename.replace(/^.*\//, "").toLowerCase();
|
|
48
|
+
if (basename) {
|
|
49
|
+
foundFiles.add(basename);
|
|
50
|
+
}
|
|
51
|
+
if (!hasDataDescriptor) {
|
|
52
|
+
// Read compressed size at offset 18-21 (little-endian)
|
|
53
|
+
const compSize = (bytes[pos + 18] ?? 0) |
|
|
54
|
+
((bytes[pos + 19] ?? 0) << 8) |
|
|
55
|
+
((bytes[pos + 20] ?? 0) << 16) |
|
|
56
|
+
((bytes[pos + 21] ?? 0) << 24);
|
|
57
|
+
// Move to next entry using the known compressed size
|
|
58
|
+
pos += 30 + nameLen + extraLen + compSize;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// When a data descriptor is present, the compressed size is not
|
|
62
|
+
// available in the local header. Skip past the header, filename,
|
|
63
|
+
// and extra fields, then scan forward for the next local header
|
|
64
|
+
// signature. This avoids getting stuck at the same position when
|
|
65
|
+
// the compressed size field is zero.
|
|
66
|
+
pos += 30 + nameLen + extraLen;
|
|
67
|
+
while (pos < bytes.length - 3) {
|
|
68
|
+
const nextIsHeader = bytes[pos] === 0x50 &&
|
|
69
|
+
bytes[pos + 1] === 0x4b &&
|
|
70
|
+
bytes[pos + 2] === 0x03 &&
|
|
71
|
+
bytes[pos + 3] === 0x04;
|
|
72
|
+
if (nextIsHeader)
|
|
73
|
+
break;
|
|
74
|
+
pos++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
pos++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Check if all required GTFS files are present
|
|
83
|
+
return requiredGtfsFiles.every((f) => foundFiles.has(f));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Convert a GTFS stop to OSM tags.
|
|
87
|
+
*/
|
|
88
|
+
export function stopToTags(stop) {
|
|
89
|
+
const tags = {
|
|
90
|
+
public_transport: "platform",
|
|
91
|
+
};
|
|
92
|
+
if (stop.stop_name)
|
|
93
|
+
tags["name"] = stop.stop_name;
|
|
94
|
+
if (stop.stop_id)
|
|
95
|
+
tags["ref"] = stop.stop_id;
|
|
96
|
+
if (stop.stop_code)
|
|
97
|
+
tags["ref:gtfs:stop_code"] = stop.stop_code;
|
|
98
|
+
if (stop.stop_desc)
|
|
99
|
+
tags["description"] = stop.stop_desc;
|
|
100
|
+
if (stop.stop_url)
|
|
101
|
+
tags["website"] = stop.stop_url;
|
|
102
|
+
if (stop.platform_code)
|
|
103
|
+
tags["ref:platform"] = stop.platform_code;
|
|
104
|
+
// Location type determines more specific tagging
|
|
105
|
+
const locationType = stop.location_type ?? "0";
|
|
106
|
+
switch (locationType) {
|
|
107
|
+
case "1":
|
|
108
|
+
tags["public_transport"] = "station";
|
|
109
|
+
break;
|
|
110
|
+
case "2":
|
|
111
|
+
// Entrances are not platforms - remove the default and use railway tag
|
|
112
|
+
delete tags["public_transport"];
|
|
113
|
+
tags["railway"] = "subway_entrance";
|
|
114
|
+
break;
|
|
115
|
+
case "3":
|
|
116
|
+
// Generic node - keep as platform
|
|
117
|
+
break;
|
|
118
|
+
case "4":
|
|
119
|
+
tags["public_transport"] = "platform";
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
// Wheelchair accessibility
|
|
123
|
+
const wheelchair = wheelchairBoardingToOsm(stop.wheelchair_boarding);
|
|
124
|
+
if (wheelchair)
|
|
125
|
+
tags["wheelchair"] = wheelchair;
|
|
126
|
+
return tags;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Convert a GTFS route to OSM tags.
|
|
130
|
+
*/
|
|
131
|
+
export function routeToTags(route) {
|
|
132
|
+
const tags = {
|
|
133
|
+
route: routeTypeToOsmRoute(route.route_type),
|
|
134
|
+
};
|
|
135
|
+
// Use long name if available, otherwise short name
|
|
136
|
+
if (route.route_long_name) {
|
|
137
|
+
tags["name"] = route.route_long_name;
|
|
138
|
+
}
|
|
139
|
+
else if (route.route_short_name) {
|
|
140
|
+
tags["name"] = route.route_short_name;
|
|
141
|
+
}
|
|
142
|
+
if (route.route_short_name)
|
|
143
|
+
tags["ref"] = route.route_short_name;
|
|
144
|
+
if (route.route_id)
|
|
145
|
+
tags["ref:gtfs:route_id"] = route.route_id;
|
|
146
|
+
if (route.route_desc)
|
|
147
|
+
tags["description"] = route.route_desc;
|
|
148
|
+
if (route.route_url)
|
|
149
|
+
tags["website"] = route.route_url;
|
|
150
|
+
// Route color (normalize to include # prefix)
|
|
151
|
+
if (route.route_color) {
|
|
152
|
+
const color = route.route_color.startsWith("#")
|
|
153
|
+
? route.route_color
|
|
154
|
+
: `#${route.route_color}`;
|
|
155
|
+
tags["color"] = color;
|
|
156
|
+
}
|
|
157
|
+
if (route.route_text_color) {
|
|
158
|
+
const textColor = route.route_text_color.startsWith("#")
|
|
159
|
+
? route.route_text_color
|
|
160
|
+
: `#${route.route_text_color}`;
|
|
161
|
+
tags["text_color"] = textColor;
|
|
162
|
+
}
|
|
163
|
+
// Route type as additional tag
|
|
164
|
+
tags["gtfs:route_type"] = route.route_type;
|
|
165
|
+
return tags;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Convert a GTFS trip to OSM tags.
|
|
169
|
+
*/
|
|
170
|
+
export function tripToTags(trip) {
|
|
171
|
+
const tags = {};
|
|
172
|
+
tags["ref:gtfs:trip_id"] = trip.trip_id;
|
|
173
|
+
if (trip.service_id)
|
|
174
|
+
tags["ref:gtfs:service_id"] = trip.service_id;
|
|
175
|
+
if (trip.trip_headsign)
|
|
176
|
+
tags["ref:gtfs:trip_headsign"] = trip.trip_headsign;
|
|
177
|
+
if (trip.trip_short_name)
|
|
178
|
+
tags["ref:gtfs:trip_short_name"] = trip.trip_short_name;
|
|
179
|
+
if (trip.direction_id)
|
|
180
|
+
tags["ref:gtfs:direction_id"] = trip.direction_id;
|
|
181
|
+
if (trip.block_id)
|
|
182
|
+
tags["ref:gtfs:block_id"] = trip.block_id;
|
|
183
|
+
if (trip.shape_id)
|
|
184
|
+
tags["ref:gtfs:shape_id"] = trip.shape_id;
|
|
185
|
+
if (trip.wheelchair_accessible)
|
|
186
|
+
tags["ref:gtfs:wheelchair_accessible"] = trip.wheelchair_accessible;
|
|
187
|
+
if (trip.bikes_allowed)
|
|
188
|
+
tags["ref:gtfs:bikes_allowed"] = trip.bikes_allowed;
|
|
189
|
+
return tags;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create a ReadableStream of text from file bytes.
|
|
193
|
+
*/
|
|
194
|
+
export function bytesToTextStream(bytes) {
|
|
195
|
+
const decoder = new TextDecoder();
|
|
196
|
+
let offset = 0;
|
|
197
|
+
const chunkSize = 64 * 1024; // 64KB chunks
|
|
198
|
+
return new ReadableStream({
|
|
199
|
+
pull(controller) {
|
|
200
|
+
if (offset >= bytes.length) {
|
|
201
|
+
controller.close();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const end = Math.min(offset + chunkSize, bytes.length);
|
|
205
|
+
const chunk = bytes.subarray(offset, end);
|
|
206
|
+
offset = end;
|
|
207
|
+
controller.enqueue(decoder.decode(chunk, { stream: offset < bytes.length }));
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
3
|
+
"name": "@osmix/gtfs",
|
|
4
|
+
"description": "Convert GTFS transit feeds to OSM format.",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/conveyal/osmix.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/conveyal/osmix#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/conveyal/osmix/issues"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"bench": "bun test --bench",
|
|
38
|
+
"prepublishOnly": "tsc",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"typecheck": "tsgo --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/bun": "catalog:",
|
|
44
|
+
"fflate": "^0.8.2",
|
|
45
|
+
"typescript": "catalog:"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@osmix/core": "workspace:*",
|
|
49
|
+
"@osmix/shared": "workspace:*",
|
|
50
|
+
"@std/csv": "npm:@jsr/std__csv",
|
|
51
|
+
"but-unzip": "^0.1.7"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/from-gtfs.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
|
|
13
|
+
import { Osm, type OsmOptions } from "@osmix/core"
|
|
14
|
+
import {
|
|
15
|
+
logProgress,
|
|
16
|
+
type ProgressEvent,
|
|
17
|
+
progressEvent,
|
|
18
|
+
} from "@osmix/shared/progress"
|
|
19
|
+
import type { OsmTags } from "@osmix/shared/types"
|
|
20
|
+
import { GtfsArchive } from "./gtfs-archive"
|
|
21
|
+
import type { GtfsConversionOptions, GtfsShapePoint } from "./types"
|
|
22
|
+
import { routeToTags, stopToTags } from "./utils"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create an Osm index from a zipped GTFS file.
|
|
26
|
+
*
|
|
27
|
+
* Parses the GTFS zip lazily - only reading files when needed.
|
|
28
|
+
* Converts stops and routes to OSM entities:
|
|
29
|
+
* - Stops become nodes with transit-related tags
|
|
30
|
+
* - Routes become ways with shape geometry (if available) or stop sequence
|
|
31
|
+
*
|
|
32
|
+
* @param zipData - The GTFS zip file as ArrayBuffer or Uint8Array
|
|
33
|
+
* @param options - Osm index options (id, header)
|
|
34
|
+
* @param gtfsOptions - GTFS conversion options
|
|
35
|
+
* @param onProgress - Progress callback for UI feedback
|
|
36
|
+
* @returns Populated Osm index with built indexes
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { fromGtfs } from "@osmix/gtfs"
|
|
41
|
+
*
|
|
42
|
+
* const response = await fetch("https://example.com/gtfs.zip")
|
|
43
|
+
* const zipData = await response.arrayBuffer()
|
|
44
|
+
* const osm = await fromGtfs(zipData, { id: "transit" })
|
|
45
|
+
*
|
|
46
|
+
* console.log(`Imported ${osm.nodes.size} stops`)
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export async function fromGtfs(
|
|
50
|
+
zipData: ArrayBuffer | Uint8Array,
|
|
51
|
+
options: Partial<OsmOptions> = {},
|
|
52
|
+
gtfsOptions: GtfsConversionOptions = {},
|
|
53
|
+
onProgress: (progress: ProgressEvent) => void = logProgress,
|
|
54
|
+
): Promise<Osm> {
|
|
55
|
+
onProgress(progressEvent("Opening GTFS archive..."))
|
|
56
|
+
const archive = GtfsArchive.fromZip(zipData)
|
|
57
|
+
const builder = new GtfsOsmBuilder(options, onProgress)
|
|
58
|
+
if (gtfsOptions.includeStops ?? true) {
|
|
59
|
+
await builder.processStops(archive)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (gtfsOptions.includeRoutes ?? true) {
|
|
63
|
+
await builder.processRoutes(archive)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return builder.buildOsm()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Builder class for converting GTFS data to OSM entities.
|
|
71
|
+
*
|
|
72
|
+
* Uses lazy parsing - only reads GTFS files when needed.
|
|
73
|
+
*/
|
|
74
|
+
export class GtfsOsmBuilder {
|
|
75
|
+
private osm: Osm
|
|
76
|
+
private onProgress: (progress: ProgressEvent) => void
|
|
77
|
+
|
|
78
|
+
private nextNodeId = -1
|
|
79
|
+
private nextWayId = -1
|
|
80
|
+
|
|
81
|
+
// Map GTFS stop_id to OSM node ID
|
|
82
|
+
private stopIdToNodeId = new Map<string, number>()
|
|
83
|
+
|
|
84
|
+
constructor(
|
|
85
|
+
osmOptions: Partial<OsmOptions> = {},
|
|
86
|
+
onProgress: (progress: ProgressEvent) => void = logProgress,
|
|
87
|
+
) {
|
|
88
|
+
this.osm = new Osm(osmOptions)
|
|
89
|
+
this.onProgress = onProgress
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Process GTFS stops into OSM nodes.
|
|
94
|
+
* Uses streaming iteration to avoid loading all stops at once.
|
|
95
|
+
*/
|
|
96
|
+
async processStops(archive: GtfsArchive) {
|
|
97
|
+
let count = 0
|
|
98
|
+
|
|
99
|
+
this.onProgress(progressEvent("Processing stops..."))
|
|
100
|
+
|
|
101
|
+
for await (const stop of archive.iter("stops.txt")) {
|
|
102
|
+
const lat = Number.parseFloat(stop.stop_lat)
|
|
103
|
+
const lon = Number.parseFloat(stop.stop_lon)
|
|
104
|
+
|
|
105
|
+
if (Number.isNaN(lat) || Number.isNaN(lon)) continue
|
|
106
|
+
|
|
107
|
+
const tags = stopToTags(stop)
|
|
108
|
+
const nodeId = this.nextNodeId--
|
|
109
|
+
|
|
110
|
+
this.osm.nodes.addNode({
|
|
111
|
+
id: nodeId,
|
|
112
|
+
lat,
|
|
113
|
+
lon,
|
|
114
|
+
tags,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
this.stopIdToNodeId.set(stop.stop_id, nodeId)
|
|
118
|
+
count++
|
|
119
|
+
|
|
120
|
+
if (count % 1000 === 0) {
|
|
121
|
+
this.onProgress(progressEvent(`Processed ${count} stops...`))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.onProgress(progressEvent(`Added ${count} stops as nodes`))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Process GTFS routes into OSM ways.
|
|
130
|
+
* Creates one way per unique (shape_id, route_id) pair, so each route gets
|
|
131
|
+
* its own way with correct metadata even when routes share the same shape.
|
|
132
|
+
*
|
|
133
|
+
* @param archive - The GTFS archive
|
|
134
|
+
*/
|
|
135
|
+
async processRoutes(archive: GtfsArchive) {
|
|
136
|
+
this.onProgress(progressEvent("Processing routes..."))
|
|
137
|
+
|
|
138
|
+
// Build shape lookup if shapes exist
|
|
139
|
+
const shapeMap = new Map<string, GtfsShapePoint[]>()
|
|
140
|
+
if (archive.hasFile("shapes.txt")) {
|
|
141
|
+
this.onProgress(progressEvent("Loading shape data..."))
|
|
142
|
+
for await (const point of archive.iter("shapes.txt")) {
|
|
143
|
+
const points = shapeMap.get(point.shape_id) ?? []
|
|
144
|
+
points.push(point)
|
|
145
|
+
shapeMap.set(point.shape_id, points)
|
|
146
|
+
}
|
|
147
|
+
// Sort each shape by sequence
|
|
148
|
+
for (const points of shapeMap.values()) {
|
|
149
|
+
points.sort(
|
|
150
|
+
(a, b) =>
|
|
151
|
+
Number.parseInt(a.shape_pt_sequence, 10) -
|
|
152
|
+
Number.parseInt(b.shape_pt_sequence, 10),
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
throw Error("No shape data found. Cannot process routes.")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Group trips by (shape_id, route_id) to ensure each route gets its own way
|
|
160
|
+
// even when multiple routes share the same shape geometry
|
|
161
|
+
const shapeRouteToTrips = new Map<string, { tripIds: string[] }>()
|
|
162
|
+
this.onProgress(progressEvent("Loading trip data..."))
|
|
163
|
+
for await (const trip of archive.iter("trips.txt")) {
|
|
164
|
+
if (!trip.shape_id) continue
|
|
165
|
+
|
|
166
|
+
const key = `${trip.shape_id}:${trip.route_id}`
|
|
167
|
+
const existing = shapeRouteToTrips.get(key)
|
|
168
|
+
if (existing) {
|
|
169
|
+
existing.tripIds.push(trip.trip_id)
|
|
170
|
+
} else {
|
|
171
|
+
shapeRouteToTrips.set(key, { tripIds: [trip.trip_id] })
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Load routes into a map for lookup
|
|
176
|
+
const routeMap = new Map<string, Awaited<ReturnType<typeof routeToTags>>>()
|
|
177
|
+
for await (const route of archive.iter("routes.txt")) {
|
|
178
|
+
routeMap.set(route.route_id, routeToTags(route))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Process unique (shape, route) pairs - one way per combination
|
|
182
|
+
let count = 0
|
|
183
|
+
for (const [key, { tripIds }] of shapeRouteToTrips) {
|
|
184
|
+
const [shapeId, routeId] = key.split(":")
|
|
185
|
+
const routeTags = routeMap.get(routeId!)
|
|
186
|
+
if (!routeTags) continue
|
|
187
|
+
|
|
188
|
+
const shapePoints = shapeMap.get(shapeId!)
|
|
189
|
+
if (!shapePoints || shapePoints.length < 2) {
|
|
190
|
+
this.onProgress(
|
|
191
|
+
progressEvent(`No shape data found for shape ${shapeId}`, "error"),
|
|
192
|
+
)
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Build tags with route info and all trip IDs for this route
|
|
197
|
+
const tags: OsmTags = {
|
|
198
|
+
...routeTags,
|
|
199
|
+
"gtfs:shape_id": shapeId!,
|
|
200
|
+
"gtfs:trip_ids": tripIds.join(";"),
|
|
201
|
+
"gtfs:trip_count": tripIds.length,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Create way from shape points
|
|
205
|
+
this.createWayFromShape(tags, shapePoints)
|
|
206
|
+
count++
|
|
207
|
+
|
|
208
|
+
if (count % 100 === 0) {
|
|
209
|
+
this.onProgress(
|
|
210
|
+
progressEvent(`Processed ${count} shape-route pairs...`),
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.onProgress(progressEvent(`Added ${count} shape-route pairs as ways`))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Create a way from shape points.
|
|
220
|
+
*/
|
|
221
|
+
private createWayFromShape(tags: OsmTags, shapePoints: GtfsShapePoint[]) {
|
|
222
|
+
const nodeRefs: number[] = []
|
|
223
|
+
|
|
224
|
+
for (const point of shapePoints) {
|
|
225
|
+
const lat = Number.parseFloat(point.shape_pt_lat)
|
|
226
|
+
const lon = Number.parseFloat(point.shape_pt_lon)
|
|
227
|
+
|
|
228
|
+
if (Number.isNaN(lat) || Number.isNaN(lon)) continue
|
|
229
|
+
|
|
230
|
+
// Create a node for this shape point
|
|
231
|
+
const nodeId = this.nextNodeId--
|
|
232
|
+
this.osm.nodes.addNode({
|
|
233
|
+
id: nodeId,
|
|
234
|
+
lat,
|
|
235
|
+
lon,
|
|
236
|
+
})
|
|
237
|
+
nodeRefs.push(nodeId)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (nodeRefs.length >= 2) {
|
|
241
|
+
const wayId = this.nextWayId--
|
|
242
|
+
this.osm.ways.addWay({
|
|
243
|
+
id: wayId,
|
|
244
|
+
refs: nodeRefs,
|
|
245
|
+
tags,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Build the OSM index with all entities.
|
|
252
|
+
*/
|
|
253
|
+
buildOsm(): Osm {
|
|
254
|
+
this.onProgress(progressEvent("Building indexes..."))
|
|
255
|
+
this.osm.buildIndexes()
|
|
256
|
+
this.osm.buildSpatialIndexes()
|
|
257
|
+
return this.osm
|
|
258
|
+
}
|
|
259
|
+
}
|