roadgraph-router-v2 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +348 -1
- package/README.md +729 -18
- package/dist/chunk-QSIKDM4A.js +2690 -0
- package/dist/index.d.ts +966 -89
- package/dist/index.js +624 -82
- package/dist/worker.js +2 -2
- package/package.json +2 -2
- package/scripts/roadgraph-compile.mjs +307 -76
- package/dist/chunk-4FIOQAFG.js +0 -1333
package/dist/worker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
decodeGraph,
|
|
3
3
|
route
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QSIKDM4A.js";
|
|
5
5
|
|
|
6
6
|
// src/worker.ts
|
|
7
7
|
var graph = null;
|
|
@@ -13,7 +13,7 @@ self.onmessage = ({ data }) => {
|
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
if (!graph) throw new Error("Load a routing graph before requesting a route.");
|
|
16
|
-
self.postMessage({ id: data.id, ok: true, route: route(graph, data.from, data.to, data.preference) });
|
|
16
|
+
self.postMessage({ id: data.id, ok: true, route: route(graph, data.from, data.to, data.preference, { departAt: data.departAt, publicHoliday: data.publicHoliday }) });
|
|
17
17
|
} catch (error) {
|
|
18
18
|
self.postMessage({ id: data.id, ok: false, error: error instanceof Error ? error.message : "Routing failed" });
|
|
19
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadgraph-router-v2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Compile any OpenStreetMap PBF extract into compact, range-addressable routing bundles and route over them with A* in Node.js or the browser.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"homepage": "https://github.com/danieljvsa/roadgraph-router#readme",
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"engines": {
|
|
38
|
-
"node": ">=
|
|
38
|
+
"node": ">=18"
|
|
39
39
|
},
|
|
40
40
|
"exports": {
|
|
41
41
|
".": {
|
|
@@ -16,20 +16,28 @@ import { spawnSync } from "node:child_process";
|
|
|
16
16
|
const EFR2_MAGIC = 0x32524645, EFRB_MAGIC = 0x42524645, SCALE = 10_000_000, HEADER = 32, DIRECTORY = 40;
|
|
17
17
|
let observedPeakRss = 0;
|
|
18
18
|
|
|
19
|
-
async function
|
|
19
|
+
async function loadLibrary() {
|
|
20
20
|
try {
|
|
21
|
-
|
|
22
|
-
return getRoutingProfile(name);
|
|
21
|
+
return await import(new URL("../dist/index.js", import.meta.url).href);
|
|
23
22
|
} catch (error) {
|
|
24
23
|
if (error?.code === "ERR_MODULE_NOT_FOUND") throw new Error("roadgraph-compile needs the package build. Run `npm run build` first.");
|
|
25
24
|
throw error;
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
/** `--profile car,truck` compiles one bundle both vehicles can be routed on. */
|
|
29
|
+
async function loadProfiles(names) {
|
|
30
|
+
const library = await loadLibrary();
|
|
31
|
+
const wanted = names.split(",").map((name) => name.trim()).filter(Boolean);
|
|
32
|
+
if (!wanted.length) throw new Error("--profile needs at least one vehicle.");
|
|
33
|
+
if (new Set(wanted).size !== wanted.length) throw new Error("--profile lists the same vehicle twice.");
|
|
34
|
+
return { library, profiles: wanted.map((name) => library.getRoutingProfile(name)) };
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
/** Reports a problem the way a command-line tool should, without a stack trace. */
|
|
30
38
|
function fail(message) { console.error(`roadgraph-compile: ${message}`); process.exit(1); }
|
|
31
39
|
|
|
32
|
-
function usage() { console.error("Usage: roadgraph-compile input.osm.pbf --output region.efrb [--profile car] [--tile-size-km 120] [--padding-km 2] [--compression gzip|none] [--shape-tolerance-m 2] [--max-memory-mb 2048] [--bbox minLon,minLat,maxLon,maxLat]"); }
|
|
40
|
+
function usage() { console.error("Usage: roadgraph-compile input.osm.pbf --output region.efrb [--profile car,truck,motorcycle] [--default-vehicle articulated] [--tile-size-km 120] [--padding-km 2] [--compression gzip|none] [--shape-tolerance-m 2] [--max-memory-mb 2048] [--bbox minLon,minLat,maxLon,maxLat]"); }
|
|
33
41
|
function opt(name, fallback) { const i = process.argv.indexOf(name); return i < 0 ? fallback : process.argv[i + 1]; }
|
|
34
42
|
function inputArg() { const values = process.argv.slice(2); for (let i = 0; i < values.length; i += 1) { if (values[i].startsWith("--")) { i += 1; continue; } return values[i]; } return undefined; }
|
|
35
43
|
function parseBounds(value) { const p = value?.split(",").map(Number); return p?.length === 4 && p.every(Number.isFinite) && p[0] < p[2] && p[1] < p[3] ? { minLon: p[0], minLat: p[1], maxLon: p[2], maxLat: p[3] } : undefined; }
|
|
@@ -63,16 +71,25 @@ class Column {
|
|
|
63
71
|
view() { return this.values.subarray(0, this.length); }
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
function createTileGraph(tile) {
|
|
74
|
+
function createTileGraph(tile, profileCount) {
|
|
75
|
+
const perProfile = (make) => Array.from({ length: profileCount }, make);
|
|
67
76
|
return {
|
|
68
77
|
tile,
|
|
69
78
|
ids: new Map(),
|
|
70
79
|
nodeLat: new Column(Int32Array), nodeLon: new Column(Int32Array),
|
|
71
80
|
edgeFrom: new Column(Uint32Array), edgeTo: new Column(Uint32Array),
|
|
72
|
-
edgeDistance: new Column(Float64Array),
|
|
81
|
+
edgeDistance: new Column(Float64Array),
|
|
73
82
|
edgeWay: new Column(Float64Array), edgeShapeStart: new Column(Uint32Array),
|
|
74
83
|
shapeLat: new Column(Int32Array), shapeLon: new Column(Int32Array),
|
|
75
|
-
wayEdges: new Map(),
|
|
84
|
+
wayEdges: new Map(),
|
|
85
|
+
// The road network is shared; everything below it belongs to one vehicle.
|
|
86
|
+
edgeDurations: perProfile(() => new Column(Float64Array)),
|
|
87
|
+
edgeUsable: perProfile(() => new Column(Uint8Array)),
|
|
88
|
+
// Sparse: almost no road states a height or a weight.
|
|
89
|
+
edgeLimits: perProfile(() => new Map()),
|
|
90
|
+
edgeConditions: perProfile(() => []),
|
|
91
|
+
restrictions: perProfile(() => []),
|
|
92
|
+
runs: perProfile(() => []),
|
|
76
93
|
};
|
|
77
94
|
}
|
|
78
95
|
|
|
@@ -105,60 +122,164 @@ function scaleFor(values) { let greatest = 0; for (const value of values) greate
|
|
|
105
122
|
* Orders a tile's edges by source node and hands the result to the library
|
|
106
123
|
* encoder, so the compiler and the runtime cannot drift apart on the layout.
|
|
107
124
|
*/
|
|
108
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Orders a tile's edges by source node and hands the result to the library
|
|
127
|
+
* encoder, so the compiler and the runtime cannot drift apart on the layout.
|
|
128
|
+
*
|
|
129
|
+
* One vehicle with no physical limits still writes the single-chunk layout it
|
|
130
|
+
* always has. Anything else writes the road network once and a small chunk per
|
|
131
|
+
* vehicle, because that is the only layout with room for an access mask, a
|
|
132
|
+
* restriction mask and a limit table.
|
|
133
|
+
*/
|
|
134
|
+
function encodeEfr2(graph, library, profiles) {
|
|
109
135
|
const nodes = graph.nodeLat.length, edges = graph.edgeFrom.length;
|
|
110
|
-
const distances = graph.edgeDistance.view()
|
|
111
|
-
const distanceScale = scaleFor(distances)
|
|
136
|
+
const distances = graph.edgeDistance.view();
|
|
137
|
+
const distanceScale = scaleFor(distances);
|
|
112
138
|
const offsets = new Uint32Array(nodes + 1);
|
|
113
139
|
for (let i = 0; i < edges; i += 1) offsets[graph.edgeFrom.at(i) + 1] += 1;
|
|
114
140
|
for (let i = 1; i < offsets.length; i += 1) offsets[i] += offsets[i - 1];
|
|
115
141
|
const order = offsets.slice(), targets = new Uint32Array(edges), placement = new Uint32Array(edges);
|
|
116
|
-
const packedDistances = new Uint16Array(edges)
|
|
142
|
+
const packedDistances = new Uint16Array(edges);
|
|
117
143
|
const shapeOffsets = new Uint32Array(edges + 1);
|
|
118
144
|
for (let i = 0; i < edges; i += 1) {
|
|
119
|
-
const
|
|
120
|
-
placement[i] =
|
|
121
|
-
targets[
|
|
122
|
-
packedDistances[
|
|
123
|
-
|
|
145
|
+
const at = order[graph.edgeFrom.at(i)]++;
|
|
146
|
+
placement[i] = at;
|
|
147
|
+
targets[at] = graph.edgeTo.at(i);
|
|
148
|
+
packedDistances[at] = Math.round(distances[i] / distanceScale);
|
|
149
|
+
const start = graph.edgeShapeStart.at(i);
|
|
150
|
+
const stop = i + 1 < edges ? graph.edgeShapeStart.at(i + 1) : graph.shapeLat.length;
|
|
151
|
+
shapeOffsets[at + 1] = stop - start;
|
|
124
152
|
}
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const sourceLat = graph.shapeLat.view(), sourceLon = graph.shapeLon.view();
|
|
133
|
-
for (let index = 0; index < edges; index += 1) {
|
|
134
|
-
const source = sourceOf[index], from = shapeStart[source], count = lengthOf(source);
|
|
135
|
-
for (let point = 0; point < count; point += 1) { shapeLat[shapeOffsets[index] + point] = sourceLat[from + point]; shapeLon[shapeOffsets[index] + point] = sourceLon[from + point]; }
|
|
153
|
+
for (let i = 1; i <= edges; i += 1) shapeOffsets[i] += shapeOffsets[i - 1];
|
|
154
|
+
const shapeLat = new Int32Array(graph.shapeLat.length), shapeLon = new Int32Array(graph.shapeLon.length);
|
|
155
|
+
for (let i = 0; i < edges; i += 1) {
|
|
156
|
+
const start = graph.edgeShapeStart.at(i);
|
|
157
|
+
const stop = i + 1 < edges ? graph.edgeShapeStart.at(i + 1) : graph.shapeLat.length;
|
|
158
|
+
let out = shapeOffsets[placement[i]];
|
|
159
|
+
for (let point = start; point < stop; point += 1, out += 1) { shapeLat[out] = graph.shapeLat.at(point); shapeLon[out] = graph.shapeLon.at(point); }
|
|
136
160
|
}
|
|
161
|
+
|
|
137
162
|
// Restrictions are collected against pre-grouping edge positions, so they
|
|
138
|
-
// follow each edge into the CSR layout through the same permutation.
|
|
139
|
-
|
|
140
|
-
const
|
|
163
|
+
// follow each edge into the CSR layout through the same permutation. Every
|
|
164
|
+
// vehicle's bans go into one list, with a bit each saying who obeys which.
|
|
165
|
+
const pairKeys = new Map(), runKeys = new Map();
|
|
166
|
+
const pairOwners = [], runOwners = [];
|
|
167
|
+
for (let profileIndex = 0; profileIndex < profiles.length; profileIndex += 1) {
|
|
168
|
+
for (const [from, to] of graph.restrictions[profileIndex]) {
|
|
169
|
+
const key = `${placement[from]}:${placement[to]}`;
|
|
170
|
+
let row = pairKeys.get(key);
|
|
171
|
+
if (row === undefined) { row = pairKeys.size; pairKeys.set(key, row); pairOwners.push(new Set()); }
|
|
172
|
+
pairOwners[row].add(profileIndex);
|
|
173
|
+
}
|
|
174
|
+
for (const run of graph.runs[profileIndex]) {
|
|
175
|
+
const key = run.map((edge) => placement[edge]).join(",");
|
|
176
|
+
let row = runKeys.get(key);
|
|
177
|
+
if (row === undefined) { row = runKeys.size; runKeys.set(key, row); runOwners.push(new Set()); }
|
|
178
|
+
runOwners[row].add(profileIndex);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const pairRows = [...pairKeys.keys()].map((key) => key.split(":").map(Number));
|
|
182
|
+
const runRows = [...runKeys.keys()].map((key) => key.split(",").map(Number)).filter((run) => run.length >= 3);
|
|
183
|
+
const pairOrder = pairRows.map((pair, index) => index).sort((a, b) => pairRows[a][0] - pairRows[b][0] || pairRows[a][1] - pairRows[b][1]);
|
|
184
|
+
const runOrder = runRows.map((run, index) => index).sort((a, b) => runRows[a][0] - runRows[b][0] || runRows[a].length - runRows[b].length || runRows[a][1] - runRows[b][1]);
|
|
185
|
+
const banned = pairOrder.map((index) => pairRows[index]);
|
|
186
|
+
const runs = runOrder.map((index) => runRows[index]);
|
|
141
187
|
const runOffsets = new Uint32Array(runs.length + 1);
|
|
142
188
|
for (let i = 0; i < runs.length; i += 1) runOffsets[i + 1] = runOffsets[i] + runs[i].length;
|
|
143
|
-
|
|
189
|
+
|
|
190
|
+
const durationScales = profiles.map((_, index) => scaleFor(graph.edgeDurations[index].view()));
|
|
191
|
+
const durationColumns = profiles.map((_, index) => {
|
|
192
|
+
const source = graph.edgeDurations[index].view(), packed = new Uint16Array(edges);
|
|
193
|
+
const scale = durationScales[index];
|
|
194
|
+
// Durations are whole units, and rounding one down can leave a short edge
|
|
195
|
+
// costing nothing at all or implying a speed no vehicle of this class can
|
|
196
|
+
// reach. A free edge is a shortcut the search will take, and a speed above
|
|
197
|
+
// the profile's ceiling makes the A* heuristic inadmissible, which lets it
|
|
198
|
+
// settle for a route that is not the best one. So no edge is ever stored
|
|
199
|
+
// as quicker than its own length at the profile's top speed.
|
|
200
|
+
const ceilingMps = (profiles[index].maxSpeedKph * 1000) / 3600;
|
|
201
|
+
for (let i = 0; i < edges; i += 1) {
|
|
202
|
+
const atCeiling = distances[i] / ceilingMps;
|
|
203
|
+
packed[placement[i]] = Math.max(Math.round(source[i] / scale), Math.ceil(atCeiling / scale));
|
|
204
|
+
}
|
|
205
|
+
return packed;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const assembled = {
|
|
144
209
|
nodeLatitudes: graph.nodeLat.view(), nodeLongitudes: graph.nodeLon.view(),
|
|
145
210
|
edgeOffsets: offsets, edgeTargets: targets,
|
|
146
|
-
edgeDistancesMeters: packedDistances, edgeDurationsSeconds:
|
|
147
|
-
coordinateScale: SCALE, edgeDistanceScale: distanceScale, edgeDurationScale:
|
|
211
|
+
edgeDistancesMeters: packedDistances, edgeDurationsSeconds: durationColumns[0],
|
|
212
|
+
coordinateScale: SCALE, edgeDistanceScale: distanceScale, edgeDurationScale: durationScales[0],
|
|
213
|
+
// Without this a bundle compiled for one vehicle reads back as a car,
|
|
214
|
+
// which is the one mistake the profile metadata exists to prevent.
|
|
215
|
+
profileId: profiles[0].id, maxSpeedKph: profiles[0].maxSpeedKph,
|
|
148
216
|
edgeShapeOffsets: shapeOffsets, edgeShapeLatitudes: shapeLat, edgeShapeLongitudes: shapeLon,
|
|
149
217
|
...(banned.length ? { turnRestrictionFromEdges: Uint32Array.from(banned, (pair) => pair[0]), turnRestrictionToEdges: Uint32Array.from(banned, (pair) => pair[1]) } : {}),
|
|
150
218
|
...(runs.length ? { turnRestrictionSequenceOffsets: runOffsets, turnRestrictionSequenceEdges: Uint32Array.from(runs.flat()) } : {}),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const split = profiles.length > 1 || profiles.some((entry) => entry.usesPhysicalLimits);
|
|
222
|
+
if (!split) {
|
|
223
|
+
// One vehicle writes one chunk, and that chunk is its whole graph, so the
|
|
224
|
+
// conditional restrictions belong in it. Without this a bundle compiled
|
|
225
|
+
// for a single vehicle would read as though no road were ever shut.
|
|
226
|
+
const onlyConditions = library.packEdgeConditions(
|
|
227
|
+
graph.edgeConditions[0].map((row) => ({ ...row, edge: placement[row.edge] })),
|
|
228
|
+
);
|
|
229
|
+
const encoded = library.encodeCompactGraph(onlyConditions ? { ...assembled, edgeConditions: onlyConditions } : assembled);
|
|
230
|
+
return { shared: encoded, profiles: [], limitedEdges: 0, conditionalEdges: onlyConditions?.edges.length ?? 0 };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const restrictionCount = banned.length + runs.length;
|
|
234
|
+
let limitedEdges = 0;
|
|
235
|
+
let conditionalEdges = 0;
|
|
236
|
+
const chunks = profiles.map((entry, profileIndex) => {
|
|
237
|
+
const usable = graph.edgeUsable[profileIndex].view();
|
|
238
|
+
// `placement` maps a built edge to its stored position, so the mask is
|
|
239
|
+
// opened for everything and then closed at the stored index of each road
|
|
240
|
+
// this vehicle cannot use.
|
|
241
|
+
const edgeAccess = library.accessMaskFor(edges, () => true);
|
|
242
|
+
for (let i = 0; i < edges; i += 1) if (!usable[i]) edgeAccess[placement[i] >> 3] &= ~(1 << (placement[i] & 7));
|
|
243
|
+
const entries = [];
|
|
244
|
+
for (const [edge, limits] of graph.edgeLimits[profileIndex]) entries.push({ edge: placement[edge], limits });
|
|
245
|
+
const edgeLimits = library.packEdgeLimits(entries);
|
|
246
|
+
limitedEdges += edgeLimits?.edges.length ?? 0;
|
|
247
|
+
const edgeConditions = library.packEdgeConditions(
|
|
248
|
+
graph.edgeConditions[profileIndex].map((row) => ({ ...row, edge: placement[row.edge] })),
|
|
249
|
+
);
|
|
250
|
+
conditionalEdges += edgeConditions?.edges.length ?? 0;
|
|
251
|
+
const restrictionMask = library.accessMaskFor(restrictionCount, (index) => index < banned.length
|
|
252
|
+
? pairOwners[pairOrder[index]].has(profileIndex)
|
|
253
|
+
: runOwners[runOrder[index - banned.length]].has(profileIndex));
|
|
254
|
+
return library.encodeProfileChunk({
|
|
255
|
+
profileId: entry.id,
|
|
256
|
+
maxSpeedKph: entry.maxSpeedKph,
|
|
257
|
+
edgeDurationScale: durationScales[profileIndex],
|
|
258
|
+
edgeDurationsSeconds: durationColumns[profileIndex],
|
|
259
|
+
edgeAccess,
|
|
260
|
+
...(edgeConditions ? { edgeConditions } : {}),
|
|
261
|
+
...(edgeLimits ? { edgeLimits } : {}),
|
|
262
|
+
...(restrictionCount ? { restrictionCount, restrictionMask } : {}),
|
|
263
|
+
});
|
|
151
264
|
});
|
|
265
|
+
return { shared: library.encodeSharedGraph(assembled), profiles: chunks, limitedEdges, conditionalEdges };
|
|
152
266
|
}
|
|
153
267
|
|
|
154
268
|
/** Writes the fixed-size directory, then streams each chunk straight from the
|
|
155
269
|
* spill file. Neither the chunk list nor the finished bundle is ever held in
|
|
156
270
|
* memory, so output size no longer bounds how large a region can be built. */
|
|
157
|
-
async function writeBundle(output, chunks, compression, spillPath) {
|
|
271
|
+
async function writeBundle(output, chunks, compression, spillPath, profiles, split, defaultVehicle, VEHICLE_PRESET_IDS) {
|
|
158
272
|
const directoryBytes = chunks.length * DIRECTORY;
|
|
159
273
|
const head = Buffer.alloc(HEADER + directoryBytes);
|
|
160
|
-
head.writeUInt32LE(EFRB_MAGIC, 0); head.writeUInt16LE(1, 4); head.writeUInt16LE(HEADER, 6);
|
|
274
|
+
head.writeUInt32LE(EFRB_MAGIC, 0); head.writeUInt16LE(split ? 2 : 1, 4); head.writeUInt16LE(HEADER, 6);
|
|
161
275
|
head.writeUInt32LE(chunks.length, 8); head.writeUInt32LE(HEADER, 12); head.writeUInt32LE(directoryBytes, 16);
|
|
276
|
+
// The vehicle, in the header as well as in every chunk, so a reader knows it
|
|
277
|
+
// before downloading a tile and can size its tile budget accordingly.
|
|
278
|
+
head.writeUInt8(profiles[0].id, 20);
|
|
279
|
+
head.writeUInt8(defaultVehicle ? VEHICLE_PRESET_IDS[defaultVehicle] ?? 0 : 0, 21);
|
|
280
|
+
head.writeUInt16LE(Math.round(Math.max(...profiles.map((entry) => entry.maxSpeedKph))), 22);
|
|
281
|
+
head.writeUInt32LE(profiles.reduce((mask, entry) => mask | (1 << (entry.id - 1)), 0) >>> 0, 24);
|
|
282
|
+
head.writeUInt32LE(split ? chunks.length / (profiles.length + 1) : chunks.length, 28);
|
|
162
283
|
let offset = HEADER + directoryBytes;
|
|
163
284
|
for (let i = 0; i < chunks.length; i += 1) {
|
|
164
285
|
const chunk = chunks[i], at = HEADER + i * DIRECTORY, b = chunk.tile.bounds;
|
|
@@ -167,6 +288,7 @@ async function writeBundle(output, chunks, compression, spillPath) {
|
|
|
167
288
|
head.writeInt32LE(fixed(b.maxLat), at + 12); head.writeInt32LE(fixed(b.maxLon), at + 16);
|
|
168
289
|
head.writeUInt32LE(offset, at + 20); head.writeUInt32LE(chunk.byteLength, at + 24);
|
|
169
290
|
head.writeUInt32LE(chunk.rawBytes, at + 28); head.writeUInt16LE(compression === "gzip" ? 1 : 0, at + 32);
|
|
291
|
+
head.writeUInt8(chunk.profileId ?? 0, at + 34);
|
|
170
292
|
offset += chunk.byteLength;
|
|
171
293
|
}
|
|
172
294
|
await mkdir(dirname(output), { recursive: true });
|
|
@@ -205,16 +327,46 @@ function memoryGuard(limitBytes, label, count) { if (count % 1_000 !== 0) return
|
|
|
205
327
|
* restrictions that apply to it. Restriction members are kept as OSM ids and
|
|
206
328
|
* resolved to edges later, because the relation may be read before its ways.
|
|
207
329
|
*/
|
|
208
|
-
async function firstPass(file, roadFile, limitBytes,
|
|
330
|
+
async function firstPass(file, roadFile, limitBytes, profiles) {
|
|
209
331
|
const required = approximateIds(), keeperNodes = approximateIds(), stream = createWriteStream(roadFile), bounds = { minLat: Infinity, minLon: Infinity, maxLat: -Infinity, maxLon: -Infinity };
|
|
210
|
-
const restrictions = [], restrictionWays = new Set();
|
|
211
|
-
let ways = 0, entities = 0, skippedRestrictions = 0;
|
|
212
|
-
|
|
332
|
+
const restrictions = [], restrictionWays = new Set(), barriers = [];
|
|
333
|
+
let ways = 0, entities = 0, skippedRestrictions = 0, barrierCount = 0, conditionalRestrictions = 0;
|
|
334
|
+
// One pass serves every vehicle: the reader is asked for the union of the
|
|
335
|
+
// tags they read, and each way is offered to all of them.
|
|
336
|
+
const wayTags = [...new Set(profiles.flatMap((entry) => entry.wayTagKeys))];
|
|
337
|
+
const relationTags = [...new Set(profiles.flatMap((entry) => entry.relationTagKeys))];
|
|
338
|
+
// Barriers live on nodes, so the reader is asked for their tags too. Only
|
|
339
|
+
// nodes carrying one of these keys arrive with tags, which is a small
|
|
340
|
+
// fraction of a country.
|
|
341
|
+
const nodeTags = [...new Set(profiles.flatMap((entry) => entry.nodeTagKeys ?? []))];
|
|
342
|
+
for await (const entity of createOSMStream(file, { withTags: { node: nodeTags.length ? nodeTags : false, way: wayTags, relation: relationTags } })) {
|
|
213
343
|
memoryGuard(limitBytes, "scanning PBF topology", ++entities);
|
|
214
|
-
if (entity.type === "node") {
|
|
344
|
+
if (entity.type === "node") {
|
|
345
|
+
bounds.minLat = Math.min(bounds.minLat, entity.lat); bounds.minLon = Math.min(bounds.minLon, entity.lon);
|
|
346
|
+
bounds.maxLat = Math.max(bounds.maxLat, entity.lat); bounds.maxLon = Math.max(bounds.maxLon, entity.lon);
|
|
347
|
+
// A node that stops somebody is remembered against the profiles it
|
|
348
|
+
// stops. Most nodes carry no tags at all and cost nothing here.
|
|
349
|
+
const tags = entity.tags;
|
|
350
|
+
if (tags?.barrier) {
|
|
351
|
+
for (let index = 0; index < profiles.length; index += 1) {
|
|
352
|
+
if (profiles[index].evaluateBarrier?.(tags) === false) {
|
|
353
|
+
(barriers[index] ??= new Set()).add(entity.id);
|
|
354
|
+
barrierCount += 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
215
360
|
if (entity.type === "relation") {
|
|
216
|
-
|
|
361
|
+
// A restriction can apply to one vehicle and not another, and can even
|
|
362
|
+
// mean something different to each, so every vehicle's answer is kept.
|
|
363
|
+
const perProfile = profiles.map((entry) => entry.evaluateTurnRestriction(entity.tags ?? {}));
|
|
364
|
+
const parsed = perProfile.find(Boolean) ?? null;
|
|
217
365
|
if (!parsed) continue;
|
|
366
|
+
// A turn banned only at certain hours is banned at every hour here.
|
|
367
|
+
// Counting them says how much of a region that costs rather than
|
|
368
|
+
// leaving it to be discovered: in Portugal it is 1 relation in 26,599.
|
|
369
|
+
if (Object.keys(entity.tags ?? {}).some((key) => key.endsWith(":conditional"))) conditionalRestrictions += 1;
|
|
218
370
|
const members = entity.members ?? [];
|
|
219
371
|
const from = members.find((member) => member.role === "from" && member.type === "way");
|
|
220
372
|
const to = members.find((member) => member.role === "to" && member.type === "way");
|
|
@@ -224,7 +376,7 @@ async function firstPass(file, roadFile, limitBytes, profile) {
|
|
|
224
376
|
if (via.length === 1 && via[0].type === "node") {
|
|
225
377
|
const viaNode = Number(via[0].ref);
|
|
226
378
|
keeperNodes.add(viaNode);
|
|
227
|
-
restrictions.push({
|
|
379
|
+
restrictions.push({ kinds: perProfile.map((entry) => entry?.kind ?? null), from: Number(from.ref), to: Number(to.ref), via: viaNode });
|
|
228
380
|
continue;
|
|
229
381
|
}
|
|
230
382
|
// A `via` way names a whole link road, so the ban covers a run of edges
|
|
@@ -233,25 +385,32 @@ async function firstPass(file, roadFile, limitBytes, profile) {
|
|
|
233
385
|
if (!via.every((member) => member.type === "way")) { skippedRestrictions += 1; continue; }
|
|
234
386
|
const viaWays = via.map((member) => Number(member.ref));
|
|
235
387
|
for (const way of viaWays) restrictionWays.add(way);
|
|
236
|
-
restrictions.push({
|
|
388
|
+
restrictions.push({ kinds: perProfile.map((entry) => entry?.kind ?? null), from: Number(from.ref), to: Number(to.ref), viaWays });
|
|
237
389
|
continue;
|
|
238
390
|
}
|
|
239
391
|
if (entity.type !== "way") continue;
|
|
240
392
|
const tags = entity.tags ?? {}, refs = entity.refs ?? [];
|
|
241
393
|
if (refs.length < 2) continue;
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
394
|
+
const lanes = profiles.map((entry) => entry.evaluateWay(tags));
|
|
395
|
+
if (!lanes.some(Boolean)) continue;
|
|
244
396
|
ways += 1;
|
|
245
397
|
for (const id of refs) { if (required.has(id)) keeperNodes.add(id); else required.add(id); }
|
|
246
398
|
keeperNodes.add(refs[0]); keeperNodes.add(refs[refs.length - 1]);
|
|
247
|
-
await write(stream, {
|
|
399
|
+
await write(stream, {
|
|
400
|
+
id: entity.id, refs,
|
|
401
|
+
forward: lanes.map((lane) => lane?.forward ? { speed: lane.forwardSpeedKph, limits: lane.forwardLimits ?? null, conditions: lane.conditions ?? null } : null),
|
|
402
|
+
reverse: lanes.map((lane) => lane?.backward ? { speed: lane.backwardSpeedKph, limits: lane.backwardLimits ?? null, conditions: lane.conditions ?? null } : null),
|
|
403
|
+
});
|
|
248
404
|
}
|
|
249
405
|
await new Promise((resolve, reject) => stream.end((error) => error ? reject(error) : resolve()));
|
|
250
406
|
if (!Number.isFinite(bounds.minLat)) throw new Error("The input contains no OSM nodes.");
|
|
251
407
|
// Drop restrictions whose ways were filtered out by the profile; they can
|
|
252
408
|
// never produce a banned pair and would only cost memory downstream.
|
|
253
409
|
const usable = restrictions.filter((restriction) => restriction.viaWays || required.has(restriction.via));
|
|
254
|
-
|
|
410
|
+
// A barrier can only matter where a road passes through it, and the node
|
|
411
|
+
// has to survive simplification for the ban to have edges to name.
|
|
412
|
+
for (const blocked of barriers) { if (blocked) for (const id of blocked) if (required.has(id)) keeperNodes.add(id); }
|
|
413
|
+
return { required, keeperNodes, bounds, ways, restrictions: usable, restrictionWays, skippedRestrictions, conditionalRestrictions, barriers, barrierCount };
|
|
255
414
|
}
|
|
256
415
|
|
|
257
416
|
async function secondPass(file, required, limitBytes, databasePath) {
|
|
@@ -305,7 +464,7 @@ function viaChains(graph, outgoing, start, viaWays) {
|
|
|
305
464
|
* link. `only_*` relations are expanded here into bans on every other way out,
|
|
306
465
|
* so the runtime never has to know the difference.
|
|
307
466
|
*/
|
|
308
|
-
function resolveRestrictions(graph, restrictions) {
|
|
467
|
+
function resolveRestrictions(graph, restrictions, profileIndex) {
|
|
309
468
|
if (!graph.wayEdges.size) return { banned: [], runs: [] };
|
|
310
469
|
const outgoing = new Map();
|
|
311
470
|
for (let index = 0; index < graph.edgeFrom.length; index += 1) {
|
|
@@ -314,6 +473,9 @@ function resolveRestrictions(graph, restrictions) {
|
|
|
314
473
|
}
|
|
315
474
|
const banned = [], runs = [];
|
|
316
475
|
for (const restriction of restrictions) {
|
|
476
|
+
// What this relation means to this vehicle, which may be nothing at all.
|
|
477
|
+
const kind = restriction.kinds[profileIndex];
|
|
478
|
+
if (!kind) continue;
|
|
317
479
|
const fromAll = graph.wayEdges.get(restriction.from) ?? [];
|
|
318
480
|
const toAll = graph.wayEdges.get(restriction.to) ?? [];
|
|
319
481
|
if (!fromAll.length || !toAll.length) continue;
|
|
@@ -324,7 +486,7 @@ function resolveRestrictions(graph, restrictions) {
|
|
|
324
486
|
const fromEdges = fromAll.filter((index) => graph.edgeTo.at(index) === via);
|
|
325
487
|
const toEdges = toAll.filter((index) => graph.edgeFrom.at(index) === via);
|
|
326
488
|
if (!fromEdges.length || !toEdges.length) continue;
|
|
327
|
-
if (
|
|
489
|
+
if (kind === "no") { for (const from of fromEdges) for (const to of toEdges) banned.push([from, to]); continue; }
|
|
328
490
|
const mandatory = new Set(toEdges);
|
|
329
491
|
for (const from of fromEdges) for (const to of outgoing.get(via) ?? []) if (!mandatory.has(to)) banned.push([from, to]);
|
|
330
492
|
continue;
|
|
@@ -337,7 +499,7 @@ function resolveRestrictions(graph, restrictions) {
|
|
|
337
499
|
const end = graph.edgeTo.at(chain[chain.length - 1]);
|
|
338
500
|
const toEdges = toAll.filter((index) => graph.edgeFrom.at(index) === end && !chain.includes(index));
|
|
339
501
|
if (!toEdges.length) continue;
|
|
340
|
-
if (
|
|
502
|
+
if (kind === "no") { for (const to of toEdges) runs.push([from, ...chain, to]); continue; }
|
|
341
503
|
const mandatory = new Set(toEdges);
|
|
342
504
|
for (const to of outgoing.get(end) ?? []) if (!mandatory.has(to) && !chain.includes(to)) runs.push([from, ...chain, to]);
|
|
343
505
|
}
|
|
@@ -346,16 +508,21 @@ function resolveRestrictions(graph, restrictions) {
|
|
|
346
508
|
return { banned, runs };
|
|
347
509
|
}
|
|
348
510
|
|
|
349
|
-
async function build(roadFile, coordinates, topology, layout, bounds, tolerance, paddingKm, tileSizeKm, limitBytes) {
|
|
511
|
+
async function build(roadFile, coordinates, topology, layout, bounds, tolerance, paddingKm, tileSizeKm, limitBytes, profileCount) {
|
|
350
512
|
const { tiles, grid } = layout, ring = Math.max(1, Math.ceil(paddingKm / tileSizeKm));
|
|
351
|
-
const graphs = new Map(tiles.map((tile) => [tile.id, createTileGraph(tile)]));
|
|
513
|
+
const graphs = new Map(tiles.map((tile) => [tile.id, createTileGraph(tile, profileCount)]));
|
|
352
514
|
const add = (graph, id, point) => {
|
|
353
515
|
let index = graph.ids.get(id);
|
|
354
516
|
if (index === undefined) { index = graph.nodeLat.push(fixed(point.lat)); graph.nodeLon.push(fixed(point.lon)); graph.ids.set(id, index); }
|
|
355
517
|
return index;
|
|
356
518
|
};
|
|
357
519
|
let routingEdges = 0;
|
|
358
|
-
|
|
520
|
+
/**
|
|
521
|
+
* One directed edge, shared by every vehicle that can drive it. `lanes[i]`
|
|
522
|
+
* is that vehicle's speed and limits for this direction, or null where it
|
|
523
|
+
* cannot use the road at all: the edge still exists, with its bit clear.
|
|
524
|
+
*/
|
|
525
|
+
const addSegment = (wayId, ids, points, lanes) => {
|
|
359
526
|
let distance = 0; for (let i = 1; i < points.length; i += 1) distance += metres(points[i - 1], points[i]);
|
|
360
527
|
if (distance < .2) return;
|
|
361
528
|
routingEdges += 1;
|
|
@@ -366,7 +533,17 @@ async function build(roadFile, coordinates, topology, layout, bounds, tolerance,
|
|
|
366
533
|
const index = graph.edgeFrom.push(from);
|
|
367
534
|
graph.edgeTo.push(to);
|
|
368
535
|
graph.edgeDistance.push(distance);
|
|
369
|
-
|
|
536
|
+
for (let profileIndex = 0; profileIndex < lanes.length; profileIndex += 1) {
|
|
537
|
+
const lane = lanes[profileIndex];
|
|
538
|
+
// An edge this vehicle cannot use still needs a duration, because the
|
|
539
|
+
// column has one slot per edge. Walking pace makes it expensive rather
|
|
540
|
+
// than free, so a lost access bit cannot turn it into a shortcut.
|
|
541
|
+
const speed = lane ? lane.speed : 5;
|
|
542
|
+
graph.edgeDurations[profileIndex].push(distance / (speed * (1000 / 3600)));
|
|
543
|
+
graph.edgeUsable[profileIndex].push(lane ? 1 : 0);
|
|
544
|
+
if (lane?.limits) graph.edgeLimits[profileIndex].set(index, lane.limits);
|
|
545
|
+
if (lane?.conditions) for (const condition of lane.conditions) graph.edgeConditions[profileIndex].push({ edge: index, ...condition });
|
|
546
|
+
}
|
|
370
547
|
graph.edgeWay.push(tracked ? wayId : 0);
|
|
371
548
|
graph.edgeShapeStart.push(graph.shapeLat.length);
|
|
372
549
|
for (const coordinate of shape) { graph.shapeLat.push(fixed(coordinate.lat)); graph.shapeLon.push(fixed(coordinate.lon)); }
|
|
@@ -387,35 +564,80 @@ async function build(roadFile, coordinates, topology, layout, bounds, tolerance,
|
|
|
387
564
|
if (!keep(way.refs[i], points[i], i, way.refs.length - 1)) continue;
|
|
388
565
|
const ids = way.refs.slice(start, i + 1), segment = points.slice(start, i + 1);
|
|
389
566
|
if (inside(segment[0], bounds)) {
|
|
390
|
-
if (way.forward) addSegment(way.id, ids, segment, way.
|
|
391
|
-
if (way.reverse) addSegment(way.id, ids.slice().reverse(), segment.slice().reverse(), way.
|
|
567
|
+
if (way.forward.some(Boolean)) addSegment(way.id, ids, segment, way.forward);
|
|
568
|
+
if (way.reverse.some(Boolean)) addSegment(way.id, ids.slice().reverse(), segment.slice().reverse(), way.reverse);
|
|
392
569
|
}
|
|
393
570
|
start = i;
|
|
394
571
|
}
|
|
395
572
|
}
|
|
396
|
-
let bannedTurns = 0, bannedRuns = 0;
|
|
573
|
+
let bannedTurns = 0, bannedRuns = 0, barrierBans = 0;
|
|
397
574
|
for (const graph of graphs.values()) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
575
|
+
// Incoming and outgoing edges per node, for turning a barrier into bans.
|
|
576
|
+
const incoming = new Map(), outgoing = new Map();
|
|
577
|
+
if (topology.barriers?.some(Boolean)) {
|
|
578
|
+
for (let edge = 0; edge < graph.edgeFrom.length; edge += 1) {
|
|
579
|
+
const from = graph.edgeFrom.at(edge), to = graph.edgeTo.at(edge);
|
|
580
|
+
const out = outgoing.get(from); if (out) out.push(edge); else outgoing.set(from, [edge]);
|
|
581
|
+
const into = incoming.get(to); if (into) into.push(edge); else incoming.set(to, [edge]);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
for (let profileIndex = 0; profileIndex < profileCount; profileIndex += 1) {
|
|
586
|
+
const resolved = resolveRestrictions(graph, topology.restrictions, profileIndex);
|
|
587
|
+
// A barrier stops you passing through a node, which is a ban on every
|
|
588
|
+
// movement across it. Turning back the way you came is still allowed:
|
|
589
|
+
// you can drive up to a locked gate and reverse out again.
|
|
590
|
+
const blocked = topology.barriers?.[profileIndex];
|
|
591
|
+
if (blocked) {
|
|
592
|
+
for (const id of blocked) {
|
|
593
|
+
const node = graph.ids.get(id);
|
|
594
|
+
if (node === undefined) continue;
|
|
595
|
+
for (const into of incoming.get(node) ?? []) {
|
|
596
|
+
for (const out of outgoing.get(node) ?? []) {
|
|
597
|
+
if (graph.edgeTo.at(out) === graph.edgeFrom.at(into)) continue;
|
|
598
|
+
resolved.banned.push([into, out]);
|
|
599
|
+
barrierBans += 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
graph.restrictions[profileIndex] = resolved.banned;
|
|
605
|
+
graph.runs[profileIndex] = resolved.runs;
|
|
606
|
+
bannedTurns += resolved.banned.length;
|
|
607
|
+
bannedRuns += resolved.runs.length;
|
|
608
|
+
}
|
|
403
609
|
}
|
|
404
610
|
const result = [...graphs.values()].filter((graph) => graph.edgeFrom.length), routingNodeIds = new Set();
|
|
405
611
|
for (const graph of result) for (const id of graph.ids.keys()) routingNodeIds.add(id);
|
|
406
612
|
for (const graph of graphs.values()) { graph.ids = new Map(); graph.wayEdges = new Map(); graph.edgeWay = null; }
|
|
407
|
-
return { graphs: result, routingEdges, routingNodes: routingNodeIds.size, bannedTurns, bannedRuns };
|
|
613
|
+
return { graphs: result, routingEdges, routingNodes: routingNodeIds.size, bannedTurns, bannedRuns, barrierBans };
|
|
408
614
|
}
|
|
409
615
|
|
|
410
616
|
const input = inputArg(), outputOption = opt("--output"), profileName = opt("--profile", "car"), compression = opt("--compression", "gzip"), tileSizeKm = Number(opt("--tile-size-km", "120")), paddingKm = Number(opt("--padding-km", "2")), tolerance = Number(opt("--shape-tolerance-m", "2")), requested = parseBounds(opt("--bbox"));
|
|
411
|
-
const maxMemoryMb = Number(opt("--max-memory-mb", "2048"));
|
|
617
|
+
const maxMemoryMb = Number(opt("--max-memory-mb", "2048")), defaultVehicle = opt("--default-vehicle");
|
|
618
|
+
/**
|
|
619
|
+
* The coordinate index needs `node:sqlite`, which arrived in Node 22.5. The
|
|
620
|
+
* import itself sits in the second pass, by which point the first pass has
|
|
621
|
+
* already streamed the whole extract — minutes of work before an older Node
|
|
622
|
+
* is told it cannot finish. Ask here instead, before anything is read.
|
|
623
|
+
*/
|
|
624
|
+
async function assertCompilerRuntime() {
|
|
625
|
+
try { await import("node:sqlite"); } catch {
|
|
626
|
+
fail(`The roadgraph-compile CLI needs Node.js 22.5 or newer for its built-in SQLite coordinate index; this is ${process.version}. The routing library itself runs on Node.js 18 and in the browser.`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
412
630
|
if (!input || !outputOption || !["gzip", "none"].includes(compression) || !(tileSizeKm > 0) || !(paddingKm >= 0) || !(tolerance >= 0) || !(maxMemoryMb > 0 && maxMemoryMb <= 3072) || (opt("--bbox") && !requested)) { usage(); process.exitCode = 1; }
|
|
413
631
|
else if (!process.env.ROADGRAPH_HEAP_LIMITED) {
|
|
632
|
+
await assertCompilerRuntime();
|
|
414
633
|
const result = spawnSync(process.execPath, [`--max-old-space-size=${Math.floor(maxMemoryMb)}`, import.meta.filename, ...process.argv.slice(2)], { stdio: "inherit", env: { ...process.env, ROADGRAPH_HEAP_LIMITED: "1" } });
|
|
415
634
|
process.exitCode = result.status ?? 1;
|
|
416
635
|
}
|
|
417
636
|
else {
|
|
418
|
-
const
|
|
637
|
+
const { library, profiles } = await loadProfiles(profileName);
|
|
638
|
+
const profile = profiles[0];
|
|
639
|
+
if (defaultVehicle && !profiles.some((entry) => entry.usesPhysicalLimits)) fail("--default-vehicle only applies to a profile whose routes depend on the vehicle's size, such as truck.");
|
|
640
|
+
if (defaultVehicle) library.vehiclePreset(defaultVehicle);
|
|
419
641
|
const started = performance.now(), file = resolve(process.cwd(), input), output = resolve(process.cwd(), outputOption);
|
|
420
642
|
if (!file.endsWith(".pbf")) fail("roadgraph-compile accepts .osm.pbf files so compilation stays streaming at country scale.");
|
|
421
643
|
const source = await stat(file).catch(() => fail(`Cannot read "${input}". Check the path and that the file exists.`));
|
|
@@ -431,28 +653,37 @@ else {
|
|
|
431
653
|
const onSignal = (signal) => { sweep(); process.exit(signal === "SIGINT" ? 130 : 143); };
|
|
432
654
|
process.once("SIGINT", onSignal); process.once("SIGTERM", onSignal);
|
|
433
655
|
try {
|
|
434
|
-
const limitBytes = maxMemoryMb * 1024 * 1024, first = await firstPass(file, resolve(workspace, "ways.ndjson"), limitBytes,
|
|
435
|
-
const built = await build(resolve(workspace, "ways.ndjson"), coordinates, first, layout, bounds, tolerance, paddingKm, tileSizeKm, limitBytes); const storedNodes = coordinates.storedNodes; coordinates.close();
|
|
656
|
+
const limitBytes = maxMemoryMb * 1024 * 1024, first = await firstPass(file, resolve(workspace, "ways.ndjson"), limitBytes, profiles), relevantOsmNodes = first.required.count, bounds = requested ?? first.bounds, layout = tilesFor(bounds, tileSizeKm), coordinates = await secondPass(file, first.required, limitBytes, resolve(workspace, "coordinates.sqlite")); first.required.clear();
|
|
657
|
+
const built = await build(resolve(workspace, "ways.ndjson"), coordinates, first, layout, bounds, tolerance, paddingKm, tileSizeKm, limitBytes, profiles.length); const storedNodes = coordinates.storedNodes; coordinates.close();
|
|
436
658
|
// Encode, compress and spill each tile in turn, releasing it immediately,
|
|
437
659
|
// so peak memory tracks the largest single tile rather than the region.
|
|
438
|
-
const { encodeCompactGraph } = await import(new URL("../dist/index.js", import.meta.url).href);
|
|
439
660
|
const spillPath = resolve(workspace, "chunks.bin"), spill = createWriteStream(spillPath), chunks = [];
|
|
661
|
+
let limitedEdges = 0;
|
|
662
|
+
let conditionalEdges = 0;
|
|
440
663
|
for (let index = 0; index < built.graphs.length; index += 1) {
|
|
441
664
|
const graph = built.graphs[index];
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
665
|
+
const encoded = encodeEfr2(graph, library, profiles);
|
|
666
|
+
// The shared half first, then each vehicle's: the reader needs a tile's
|
|
667
|
+
// shared row before the rows that depend on it.
|
|
668
|
+
limitedEdges += encoded.limitedEdges ?? 0;
|
|
669
|
+
conditionalEdges += encoded.conditionalEdges ?? 0;
|
|
670
|
+
for (const [profileId, buffer] of [[0, encoded.shared], ...encoded.profiles.map((chunk, at) => [profiles[at].id, chunk])]) {
|
|
671
|
+
const raw = new Uint8Array(buffer);
|
|
672
|
+
const data = compression === "gzip" ? gzipSync(raw, { level: constants.Z_BEST_COMPRESSION }) : raw;
|
|
673
|
+
chunks.push({ tile: graph.tile, profileId, byteLength: data.byteLength, rawBytes: raw.byteLength });
|
|
674
|
+
if (!spill.write(data)) await new Promise((done) => spill.once("drain", done));
|
|
675
|
+
}
|
|
446
676
|
built.graphs[index] = null;
|
|
447
677
|
memoryGuard(limitBytes, "writing compressed tiles", 1000);
|
|
448
678
|
}
|
|
449
679
|
await new Promise((done, fail) => spill.end((error) => error ? fail(error) : done()));
|
|
450
|
-
const
|
|
451
|
-
await
|
|
680
|
+
const split = profiles.length > 1 || profiles.some((entry) => entry.usesPhysicalLimits);
|
|
681
|
+
const bundleBytes = await writeBundle(output, chunks, compression, spillPath, profiles, split, defaultVehicle, library.VehiclePresetId);
|
|
682
|
+
await verifyBundle(output, built.graphs.length);
|
|
452
683
|
const nodesBefore = storedNodes, nodesAfter = built.routingNodes, edges = built.routingEdges, rawBytes = chunks.reduce((sum, chunk) => sum + chunk.rawBytes, 0);
|
|
453
|
-
const report = { formatVersion: 1, input: file, inputBytes: source.size, profile:
|
|
684
|
+
const report = { formatVersion: 1, input: file, inputBytes: source.size, profile: profiles.map((entry) => entry.name).join(","), compression: `${compression}-per-tile`, tileSizeKm, paddingKm, pbfPasses: 2, tiles: built.graphs.length, relevantOsmWays: first.ways, relevantOsmNodes, relevantOsmNodesExact: false, routingNodesBeforeSimplification: nodesBefore, routingNodesAfterSimplification: nodesAfter, reductionPercent: Number((100 * (1 - nodesAfter / Math.max(1, nodesBefore))).toFixed(2)), routingEdges: edges, turnRestrictionsRead: first.restrictions.length, turnRestrictionsSkipped: first.skippedRestrictions, conditionalTurnRestrictionsIgnored: first.conditionalRestrictions, barriersRead: first.barrierCount, barrierBans: built.barrierBans, bannedTurnPairs: built.bannedTurns, bannedTurnRuns: built.bannedRuns, layout: split ? "shared-plus-profile" : "single-chunk", profiles: profiles.map((entry) => entry.name), defaultVehicle: defaultVehicle ?? null, edgesWithPhysicalLimits: limitedEdges, edgesWithConditionalRestrictions: conditionalEdges, uncompressedGraphBytes: rawBytes, bundleBytes, compressionRatio: Number((bundleBytes / Math.max(1, rawBytes)).toFixed(4)), peakRssBytes: Math.max(observedPeakRss, process.memoryUsage().rss), elapsedMilliseconds: Math.round(performance.now() - started) };
|
|
454
685
|
await writeFile(`${output}.json`, `${JSON.stringify(report, null, 2)}\n`);
|
|
455
|
-
console.table({ input_mb: (source.size / 1e6).toFixed(1), pbf_passes: report.pbfPasses, relevant_ways: first.ways.toLocaleString(), relevant_nodes: relevantOsmNodes.toLocaleString(), nodes_before: nodesBefore.toLocaleString(), nodes_after: nodesAfter.toLocaleString(), reduction_percent: report.reductionPercent, routing_edges: edges.toLocaleString(), turn_restrictions: first.restrictions.length.toLocaleString(), banned_turn_pairs: built.bannedTurns.toLocaleString(), banned_turn_runs: built.bannedRuns.toLocaleString(), tiles:
|
|
686
|
+
console.table({ input_mb: (source.size / 1e6).toFixed(1), pbf_passes: report.pbfPasses, relevant_ways: first.ways.toLocaleString(), relevant_nodes: relevantOsmNodes.toLocaleString(), nodes_before: nodesBefore.toLocaleString(), nodes_after: nodesAfter.toLocaleString(), reduction_percent: report.reductionPercent, routing_edges: edges.toLocaleString(), turn_restrictions: first.restrictions.length.toLocaleString(), barriers: first.barrierCount.toLocaleString(), banned_turn_pairs: built.bannedTurns.toLocaleString(), banned_turn_runs: built.bannedRuns.toLocaleString(), tiles: built.graphs.length, raw_mb: (rawBytes / 1e6).toFixed(1), efrb_mb: (bundleBytes / 1e6).toFixed(1), compression_ratio: report.compressionRatio, peak_rss_mb: (report.peakRssBytes / 1e6).toFixed(1), elapsed_seconds: (report.elapsedMilliseconds / 1000).toFixed(1) });
|
|
456
687
|
} catch (error) {
|
|
457
688
|
// A truncated or non-PBF file surfaces as a parser or stream error deep in
|
|
458
689
|
// the pipeline; report it as a problem with the input, not as a crash.
|