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/index.js
CHANGED
|
@@ -1,32 +1,77 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ConditionKind,
|
|
3
|
+
MAX_STORED_VALUES,
|
|
4
|
+
ProfileId,
|
|
2
5
|
RouterError,
|
|
3
6
|
VarintReader,
|
|
4
7
|
VarintWriter,
|
|
8
|
+
accessAtTime,
|
|
9
|
+
accessAvoiding,
|
|
10
|
+
accessForVehicle,
|
|
11
|
+
accessMaskFor,
|
|
12
|
+
allowsEverything,
|
|
5
13
|
buildTurnRestrictionIndex,
|
|
6
14
|
buildTurnSequenceIndex,
|
|
7
15
|
carRoutingProfile,
|
|
16
|
+
combineProfiledGraph,
|
|
17
|
+
componentSizes,
|
|
18
|
+
componentsFor,
|
|
19
|
+
conditionHolds,
|
|
20
|
+
conditionalDurations,
|
|
21
|
+
conditionalValue,
|
|
22
|
+
conditionsOf,
|
|
23
|
+
conditionsOnEdge,
|
|
24
|
+
countAvoided,
|
|
25
|
+
countBlocked,
|
|
26
|
+
countClosedAt,
|
|
8
27
|
createEdgeSpatialIndex,
|
|
9
28
|
createSpatialIndex,
|
|
10
29
|
decodeGraph,
|
|
30
|
+
decodeProfileChunk,
|
|
31
|
+
decodeSharedGraph,
|
|
32
|
+
edgeAllowed,
|
|
11
33
|
edgePolyline,
|
|
12
34
|
edgeSource,
|
|
13
35
|
edgeSpatialIndexFor,
|
|
14
36
|
encodeCompactGraph,
|
|
15
37
|
encodeGraph,
|
|
38
|
+
encodeProfileChunk,
|
|
39
|
+
encodeSharedGraph,
|
|
16
40
|
findNearestNode,
|
|
41
|
+
fromDecimetres,
|
|
42
|
+
fromHalfTonnes,
|
|
17
43
|
getRoutingProfile,
|
|
44
|
+
getRoutingProfileById,
|
|
45
|
+
guardedJunctionsFor,
|
|
18
46
|
isTurnBanned,
|
|
47
|
+
maxSpeedMpsFor,
|
|
48
|
+
motorcycleRoutingProfile,
|
|
19
49
|
nearbyEdgeIds,
|
|
50
|
+
packEdgeConditions,
|
|
51
|
+
packEdgeLimits,
|
|
20
52
|
packTurnRestrictions,
|
|
21
53
|
packTurnSequences,
|
|
54
|
+
parseConditional,
|
|
55
|
+
parseEdgeConditions,
|
|
56
|
+
parseLengthMeters,
|
|
22
57
|
parseMaxSpeedKph,
|
|
58
|
+
parseWeightTonnes,
|
|
59
|
+
profileOf,
|
|
23
60
|
route,
|
|
24
61
|
routingProfiles,
|
|
62
|
+
searchBoundsFor,
|
|
25
63
|
snapToGraph,
|
|
64
|
+
straightLineAtMost,
|
|
65
|
+
toDecimetres,
|
|
66
|
+
toHalfTonnes,
|
|
67
|
+
touchesArea,
|
|
68
|
+
travelTimeFrom,
|
|
69
|
+
truckRoutingProfile,
|
|
70
|
+
unreadableConditions,
|
|
26
71
|
unzigzag,
|
|
27
72
|
withSpatialIndex,
|
|
28
73
|
zigzag
|
|
29
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-QSIKDM4A.js";
|
|
30
75
|
|
|
31
76
|
// src/compact.ts
|
|
32
77
|
function scaleFor(values, maximum) {
|
|
@@ -54,7 +99,8 @@ function compactGraph(graph, options = {}) {
|
|
|
54
99
|
const edgeDurationsSeconds = new Uint16Array(durations.length);
|
|
55
100
|
for (let index = 0; index < distances.length; index += 1) {
|
|
56
101
|
edgeDistancesMeters[index] = Math.round(distances[index] / edgeDistanceScale);
|
|
57
|
-
|
|
102
|
+
const quantised = Math.round(durations[index] / edgeDurationScale);
|
|
103
|
+
edgeDurationsSeconds[index] = quantised || (durations[index] > 0 || distances[index] > 0 ? 1 : 0);
|
|
58
104
|
}
|
|
59
105
|
const edgeShapeOffsets = graph.edgeShapeOffsets?.slice();
|
|
60
106
|
const edgeShapeLatitudes = graph.edgeShapeLatitudes && Int32Array.from(graph.edgeShapeLatitudes, (value) => Math.round(value / inputCoordinateScale * coordinateScale));
|
|
@@ -69,12 +115,113 @@ function compactGraph(graph, options = {}) {
|
|
|
69
115
|
coordinateScale,
|
|
70
116
|
edgeDistanceScale,
|
|
71
117
|
edgeDurationScale,
|
|
118
|
+
// Quantising the weights does not change which vehicle the graph is for.
|
|
119
|
+
...graph.profileId ? { profileId: graph.profileId } : {},
|
|
120
|
+
...graph.maxSpeedKph ? { maxSpeedKph: graph.maxSpeedKph } : {},
|
|
72
121
|
...edgeShapeOffsets && edgeShapeLatitudes && edgeShapeLongitudes ? { edgeShapeOffsets, edgeShapeLatitudes, edgeShapeLongitudes } : {},
|
|
73
122
|
...graph.turnRestrictionFromEdges && graph.turnRestrictionToEdges ? { turnRestrictionFromEdges: graph.turnRestrictionFromEdges.slice(), turnRestrictionToEdges: graph.turnRestrictionToEdges.slice() } : {},
|
|
74
123
|
...graph.turnRestrictionSequenceOffsets && graph.turnRestrictionSequenceEdges ? { turnRestrictionSequenceOffsets: graph.turnRestrictionSequenceOffsets.slice(), turnRestrictionSequenceEdges: graph.turnRestrictionSequenceEdges.slice() } : {}
|
|
75
124
|
};
|
|
76
125
|
}
|
|
77
126
|
|
|
127
|
+
// src/alternatives.ts
|
|
128
|
+
var DEFAULT_MAX_STRETCH = 1.4;
|
|
129
|
+
var DEFAULTS = { count: 3, maxStretch: DEFAULT_MAX_STRETCH, maxSharing: 0.75, penaltyFactor: 2 };
|
|
130
|
+
var MOST_ROUTES = 10;
|
|
131
|
+
function checkAlternativeOptions(options) {
|
|
132
|
+
const whole = (name, value) => {
|
|
133
|
+
if (!Number.isInteger(value)) throw new RangeError(`routeAlternatives: ${name} must be a whole number, not ${value}.`);
|
|
134
|
+
};
|
|
135
|
+
const finite = (name, value, least, most) => {
|
|
136
|
+
if (!Number.isFinite(value) || value < least || value > most) {
|
|
137
|
+
throw new RangeError(`routeAlternatives: ${name} must be between ${least} and ${most}, not ${value}.`);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
if (options.count !== void 0) {
|
|
141
|
+
whole("count", options.count);
|
|
142
|
+
finite("count", options.count, 1, MOST_ROUTES);
|
|
143
|
+
}
|
|
144
|
+
if (options.maxStretch !== void 0) finite("maxStretch", options.maxStretch, 1, Number.MAX_SAFE_INTEGER);
|
|
145
|
+
if (options.maxSharing !== void 0) finite("maxSharing", options.maxSharing, 0, 1);
|
|
146
|
+
if (options.penaltyFactor !== void 0) finite("penaltyFactor", options.penaltyFactor, 1, Number.MAX_SAFE_INTEGER);
|
|
147
|
+
}
|
|
148
|
+
var costOfRoute = (result, preference) => preference === "fastest" ? result.durationSeconds : result.distanceMeters;
|
|
149
|
+
function roadOf(graph, edge) {
|
|
150
|
+
const source = edgeSource(graph, edge);
|
|
151
|
+
const target = graph.edgeTargets[edge];
|
|
152
|
+
const ends = source < target ? `${source}:${target}` : `${target}:${source}`;
|
|
153
|
+
const polyline = edgePolyline(graph, edge, source);
|
|
154
|
+
const points = source < target ? polyline : [...polyline].reverse();
|
|
155
|
+
const scale = graph.coordinateScale ?? 1e7;
|
|
156
|
+
let first = 2166136261;
|
|
157
|
+
let second = 16777619;
|
|
158
|
+
for (const point of points) {
|
|
159
|
+
for (const value of [Math.round(point.lat * scale), Math.round(point.lng * scale)]) {
|
|
160
|
+
first = Math.imul(first ^ value & 65535, 16777619) >>> 0;
|
|
161
|
+
first = Math.imul(first ^ value >>> 16, 16777619) >>> 0;
|
|
162
|
+
second = Math.imul(second ^ value & 65535, 2246822507) >>> 0;
|
|
163
|
+
second = Math.imul(second ^ value >>> 16, 3266489909) >>> 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return `${ends}:${first.toString(36)}${second.toString(36)}`;
|
|
167
|
+
}
|
|
168
|
+
function roadsOf(graph, route2) {
|
|
169
|
+
const roads = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const edge of route2.edgeIds) roads.add(roadOf(graph, edge));
|
|
171
|
+
for (const snap of [route2.originSnap, route2.destinationSnap]) {
|
|
172
|
+
if (snap.edge >= 0) roads.add(roadOf(graph, snap.edge));
|
|
173
|
+
}
|
|
174
|
+
return roads;
|
|
175
|
+
}
|
|
176
|
+
function sharedFraction(graph, candidate, used) {
|
|
177
|
+
const scale = graph.edgeDistanceScale ?? 1;
|
|
178
|
+
let shared = 0;
|
|
179
|
+
let total = 0;
|
|
180
|
+
for (const edge of candidate.edgeIds) {
|
|
181
|
+
const length = graph.edgeDistancesMeters[edge] * scale;
|
|
182
|
+
total += length;
|
|
183
|
+
if (used.has(roadOf(graph, edge))) shared += length;
|
|
184
|
+
}
|
|
185
|
+
for (const snap of [candidate.originSnap, candidate.destinationSnap]) {
|
|
186
|
+
if (snap.edge < 0) continue;
|
|
187
|
+
const travelled = snap.travelledMeters ?? 0;
|
|
188
|
+
total += travelled;
|
|
189
|
+
if (used.has(roadOf(graph, snap.edge))) shared += travelled;
|
|
190
|
+
}
|
|
191
|
+
return total > 0 ? shared / total : 1;
|
|
192
|
+
}
|
|
193
|
+
function routeAlternatives(graph, from, to, preference = "fastest", options = {}) {
|
|
194
|
+
checkAlternativeOptions(options);
|
|
195
|
+
const { count, maxStretch, maxSharing, penaltyFactor } = { ...DEFAULTS, ...options };
|
|
196
|
+
const best = route(graph, from, to, preference, options);
|
|
197
|
+
if (count <= 1) return [best];
|
|
198
|
+
const chosen = [best];
|
|
199
|
+
const ceiling = costOfRoute(best, preference) * maxStretch;
|
|
200
|
+
const used = roadsOf(graph, best);
|
|
201
|
+
const penalties = /* @__PURE__ */ new Map();
|
|
202
|
+
const penalise = (edges) => {
|
|
203
|
+
for (const edge of edges) penalties.set(edge, (penalties.get(edge) ?? 1) * penaltyFactor);
|
|
204
|
+
};
|
|
205
|
+
penalise(best.edgeIds);
|
|
206
|
+
const attempts = (count - 1) * 3;
|
|
207
|
+
for (let attempt = 0; attempt < attempts && chosen.length < count; attempt += 1) {
|
|
208
|
+
let candidate;
|
|
209
|
+
try {
|
|
210
|
+
candidate = route(graph, from, to, preference, { ...options, costPenalties: penalties });
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error instanceof RouterError && error.code === "NO_ROUTE") break;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
penalise(candidate.edgeIds);
|
|
216
|
+
if (costOfRoute(candidate, preference) > ceiling) continue;
|
|
217
|
+
if (sharedFraction(graph, candidate, used) > maxSharing) continue;
|
|
218
|
+
chosen.push(candidate);
|
|
219
|
+
for (const road of roadsOf(graph, candidate)) used.add(road);
|
|
220
|
+
}
|
|
221
|
+
const rest = chosen.slice(1).sort((left, right) => costOfRoute(left, preference) - costOfRoute(right, preference));
|
|
222
|
+
return [best, ...rest];
|
|
223
|
+
}
|
|
224
|
+
|
|
78
225
|
// src/merge.ts
|
|
79
226
|
var COORDINATE_SCALE = 1e7;
|
|
80
227
|
var LENGTH_EPSILON_METERS = 0.01;
|
|
@@ -84,24 +231,52 @@ function nodeKey(latitude, longitude) {
|
|
|
84
231
|
function mergeRoadGraphs(graphs) {
|
|
85
232
|
if (!graphs.length) throw new Error("At least one road graph is required.");
|
|
86
233
|
if (graphs.length === 1) return graphs[0];
|
|
234
|
+
const profileId = graphs[0].profileId;
|
|
235
|
+
for (const graph of graphs) {
|
|
236
|
+
if (graph.profileId !== profileId) throw new Error("Cannot merge road graphs compiled for different routing profiles.");
|
|
237
|
+
}
|
|
238
|
+
const scale = graphs[0].coordinateScale ?? 1;
|
|
239
|
+
const uniformScale = graphs.every((graph) => (graph.coordinateScale ?? 1) === scale);
|
|
240
|
+
let minLatitude = Infinity;
|
|
241
|
+
let minLongitude = Infinity;
|
|
242
|
+
let span = Infinity;
|
|
243
|
+
if (uniformScale) {
|
|
244
|
+
let maxLatitude = -Infinity;
|
|
245
|
+
let maxLongitude = -Infinity;
|
|
246
|
+
for (const graph of graphs) {
|
|
247
|
+
for (let index = 0; index < graph.nodeLatitudes.length; index += 1) {
|
|
248
|
+
const latitude = graph.nodeLatitudes[index];
|
|
249
|
+
const longitude = graph.nodeLongitudes[index];
|
|
250
|
+
if (latitude < minLatitude) minLatitude = latitude;
|
|
251
|
+
if (latitude > maxLatitude) maxLatitude = latitude;
|
|
252
|
+
if (longitude < minLongitude) minLongitude = longitude;
|
|
253
|
+
if (longitude > maxLongitude) maxLongitude = longitude;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
span = maxLongitude - minLongitude + 1;
|
|
257
|
+
if ((maxLatitude - minLatitude + 1) * span > Number.MAX_SAFE_INTEGER) span = Infinity;
|
|
258
|
+
}
|
|
259
|
+
const packed = Number.isFinite(span);
|
|
260
|
+
const totalNodes = graphs.reduce((sum, graph) => sum + graph.nodeLatitudes.length, 0);
|
|
261
|
+
const mergedLatitudes = new Int32Array(totalNodes);
|
|
262
|
+
const mergedLongitudes = new Int32Array(totalNodes);
|
|
263
|
+
let nodeCount = 0;
|
|
87
264
|
const nodeIds = /* @__PURE__ */ new Map();
|
|
88
|
-
const latitudes = [];
|
|
89
|
-
const longitudes = [];
|
|
90
265
|
const nodeMaps = [];
|
|
91
266
|
let candidateEdges = 0;
|
|
92
267
|
for (const graph of graphs) {
|
|
93
|
-
const
|
|
268
|
+
const graphScale = graph.coordinateScale ?? 1;
|
|
94
269
|
const nodeMap = new Uint32Array(graph.nodeLatitudes.length);
|
|
95
270
|
for (let index = 0; index < graph.nodeLatitudes.length; index += 1) {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const key = nodeKey(
|
|
271
|
+
const rawLatitude = graph.nodeLatitudes[index];
|
|
272
|
+
const rawLongitude = graph.nodeLongitudes[index];
|
|
273
|
+
const key = packed ? (rawLatitude - minLatitude) * span + (rawLongitude - minLongitude) : nodeKey(rawLatitude / graphScale, rawLongitude / graphScale);
|
|
99
274
|
let mergedIndex = nodeIds.get(key);
|
|
100
275
|
if (mergedIndex === void 0) {
|
|
101
|
-
mergedIndex =
|
|
276
|
+
mergedIndex = nodeCount++;
|
|
102
277
|
nodeIds.set(key, mergedIndex);
|
|
103
|
-
|
|
104
|
-
|
|
278
|
+
mergedLatitudes[mergedIndex] = graphScale === COORDINATE_SCALE ? rawLatitude : Math.round(rawLatitude / graphScale * COORDINATE_SCALE);
|
|
279
|
+
mergedLongitudes[mergedIndex] = graphScale === COORDINATE_SCALE ? rawLongitude : Math.round(rawLongitude / graphScale * COORDINATE_SCALE);
|
|
105
280
|
}
|
|
106
281
|
nodeMap[index] = mergedIndex;
|
|
107
282
|
}
|
|
@@ -109,7 +284,6 @@ function mergeRoadGraphs(graphs) {
|
|
|
109
284
|
candidateEdges += graph.edgeTargets.length;
|
|
110
285
|
}
|
|
111
286
|
nodeIds.clear();
|
|
112
|
-
const nodeCount = latitudes.length;
|
|
113
287
|
const slotStart = new Uint32Array(nodeCount + 1);
|
|
114
288
|
for (let index = 0; index < graphs.length; index += 1) {
|
|
115
289
|
const graph = graphs[index];
|
|
@@ -190,22 +364,61 @@ function mergeRoadGraphs(graphs) {
|
|
|
190
364
|
for (let edge = 0; edge < edgeCount; edge += 1) {
|
|
191
365
|
const graph = graphs[sourceGraph[edge]];
|
|
192
366
|
if (!graph.edgeShapeOffsets || !graph.edgeShapeLatitudes || !graph.edgeShapeLongitudes) continue;
|
|
193
|
-
const
|
|
367
|
+
const scale2 = graph.coordinateScale ?? 1;
|
|
194
368
|
let target = edgeShapeOffsets[edge];
|
|
195
369
|
for (let point = graph.edgeShapeOffsets[sourceEdge[edge]]; point < graph.edgeShapeOffsets[sourceEdge[edge] + 1]; point += 1, target += 1) {
|
|
196
|
-
edgeShapeLatitudes[target] = Math.round(graph.edgeShapeLatitudes[point] /
|
|
197
|
-
edgeShapeLongitudes[target] = Math.round(graph.edgeShapeLongitudes[point] /
|
|
370
|
+
edgeShapeLatitudes[target] = Math.round(graph.edgeShapeLatitudes[point] / scale2 * COORDINATE_SCALE);
|
|
371
|
+
edgeShapeLongitudes[target] = Math.round(graph.edgeShapeLongitudes[point] / scale2 * COORDINATE_SCALE);
|
|
198
372
|
}
|
|
199
373
|
}
|
|
200
374
|
const bannedPairs = [];
|
|
201
375
|
const bannedSequences = [];
|
|
376
|
+
const hasAccess = graphs.some((graph) => graph.edgeAccess);
|
|
377
|
+
const mergedAccess = hasAccess ? accessMaskFor(edgeCount, () => true) : void 0;
|
|
378
|
+
const limitRows = /* @__PURE__ */ new Map();
|
|
379
|
+
const conditionRows = [];
|
|
380
|
+
const seenConditions = /* @__PURE__ */ new Set();
|
|
202
381
|
for (let index = 0; index < graphs.length; index += 1) {
|
|
203
382
|
const graph = graphs[index];
|
|
204
383
|
const edgeMap = edgeMaps[index];
|
|
205
384
|
const rebase = (edge) => slotToEdge[edgeMap[edge]];
|
|
385
|
+
if (mergedAccess) {
|
|
386
|
+
for (let edge = 0; edge < graph.edgeTargets.length; edge += 1) {
|
|
387
|
+
if (edgeAllowed(graph.edgeAccess, edge)) continue;
|
|
388
|
+
const merged = rebase(edge);
|
|
389
|
+
if (merged >= 0) mergedAccess[merged >> 3] &= ~(1 << (merged & 7));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const limits = graph.edgeLimits;
|
|
393
|
+
if (limits) {
|
|
394
|
+
for (let row = 0; row < limits.edges.length; row += 1) {
|
|
395
|
+
const merged = rebase(limits.edges[row]);
|
|
396
|
+
if (merged < 0) continue;
|
|
397
|
+
const existing = limitRows.get(merged) ?? {};
|
|
398
|
+
const stricter = (left, right) => left === void 0 ? right : Math.min(left, right);
|
|
399
|
+
if (limits.heightDecimetres[row]) existing.heightMeters = stricter(existing.heightMeters, fromDecimetres(limits.heightDecimetres[row]));
|
|
400
|
+
if (limits.widthDecimetres[row]) existing.widthMeters = stricter(existing.widthMeters, fromDecimetres(limits.widthDecimetres[row]));
|
|
401
|
+
if (limits.lengthDecimetres[row]) existing.lengthMeters = stricter(existing.lengthMeters, fromDecimetres(limits.lengthDecimetres[row]));
|
|
402
|
+
if (limits.weightHalfTonnes[row]) existing.weightTonnes = stricter(existing.weightTonnes, fromHalfTonnes(limits.weightHalfTonnes[row]));
|
|
403
|
+
limitRows.set(merged, existing);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const conditions = graph.edgeConditions;
|
|
407
|
+
if (conditions) {
|
|
408
|
+
for (let row = 0; row < conditions.edges.length; row += 1) {
|
|
409
|
+
const merged = rebase(conditions.edges[row]);
|
|
410
|
+
if (merged < 0) continue;
|
|
411
|
+
const value = conditions.values[conditions.rules[row]];
|
|
412
|
+
const key = `${merged}:${conditions.kinds[row]}:${value}`;
|
|
413
|
+
if (seenConditions.has(key)) continue;
|
|
414
|
+
seenConditions.add(key);
|
|
415
|
+
conditionRows.push({ edge: merged, kind: conditions.kinds[row], value });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
206
418
|
const { turnRestrictionFromEdges: fromEdges, turnRestrictionToEdges: toEdges } = graph;
|
|
207
419
|
if (fromEdges && toEdges) {
|
|
208
420
|
for (let pair = 0; pair < fromEdges.length; pair += 1) {
|
|
421
|
+
if (!edgeAllowed(graph.restrictionMask, pair)) continue;
|
|
209
422
|
const fromEdge = rebase(fromEdges[pair]);
|
|
210
423
|
const toEdge = rebase(toEdges[pair]);
|
|
211
424
|
if (fromEdge >= 0 && toEdge >= 0) bannedPairs.push([fromEdge, toEdge]);
|
|
@@ -214,20 +427,29 @@ function mergeRoadGraphs(graphs) {
|
|
|
214
427
|
const offsets = graph.turnRestrictionSequenceOffsets;
|
|
215
428
|
const sequenceEdges = graph.turnRestrictionSequenceEdges;
|
|
216
429
|
if (!offsets || !sequenceEdges) continue;
|
|
430
|
+
const pairCount = fromEdges?.length ?? 0;
|
|
217
431
|
for (let sequence = 0; sequence + 1 < offsets.length; sequence += 1) {
|
|
432
|
+
if (!edgeAllowed(graph.restrictionMask, pairCount + sequence)) continue;
|
|
218
433
|
const rebased = [];
|
|
219
434
|
for (let at = offsets[sequence]; at < offsets[sequence + 1]; at += 1) rebased.push(rebase(sequenceEdges[at]));
|
|
220
435
|
if (rebased.every((edge) => edge >= 0)) bannedSequences.push(rebased);
|
|
221
436
|
}
|
|
222
437
|
}
|
|
438
|
+
const mergedLimits = packEdgeLimits([...limitRows].map(([edge, limits]) => ({ edge, limits })));
|
|
439
|
+
const mergedConditions = packEdgeConditions(conditionRows);
|
|
223
440
|
return withSpatialIndex({
|
|
224
|
-
nodeLatitudes:
|
|
225
|
-
nodeLongitudes:
|
|
441
|
+
nodeLatitudes: mergedLatitudes.slice(0, nodeCount),
|
|
442
|
+
nodeLongitudes: mergedLongitudes.slice(0, nodeCount),
|
|
226
443
|
coordinateScale: COORDINATE_SCALE,
|
|
227
444
|
edgeOffsets,
|
|
228
445
|
edgeTargets,
|
|
229
446
|
edgeDistancesMeters,
|
|
230
447
|
edgeDurationsSeconds,
|
|
448
|
+
...profileId ? { profileId } : {},
|
|
449
|
+
...graphs[0].maxSpeedKph ? { maxSpeedKph: graphs[0].maxSpeedKph } : {},
|
|
450
|
+
...mergedAccess ? { edgeAccess: mergedAccess } : {},
|
|
451
|
+
...mergedLimits ? { edgeLimits: mergedLimits } : {},
|
|
452
|
+
...mergedConditions ? { edgeConditions: mergedConditions } : {},
|
|
231
453
|
...pointCount ? { edgeShapeOffsets, edgeShapeLatitudes, edgeShapeLongitudes } : {},
|
|
232
454
|
...bannedPairs.length ? packTurnRestrictions(bannedPairs) : {},
|
|
233
455
|
...bannedSequences.length ? packTurnSequences(bannedSequences) : {}
|
|
@@ -289,12 +511,55 @@ async function createRouteGraphLoader(manifestUrl, fetcher = fetch) {
|
|
|
289
511
|
};
|
|
290
512
|
}
|
|
291
513
|
|
|
514
|
+
// src/vehicle.ts
|
|
515
|
+
var VehiclePresetId = {
|
|
516
|
+
van: 1,
|
|
517
|
+
rigid: 2,
|
|
518
|
+
articulated: 3,
|
|
519
|
+
"road-train": 4,
|
|
520
|
+
"us-semi": 5
|
|
521
|
+
};
|
|
522
|
+
function vehiclePresetName(id) {
|
|
523
|
+
return Object.keys(VehiclePresetId).find((name) => VehiclePresetId[name] === id) ?? null;
|
|
524
|
+
}
|
|
525
|
+
var VEHICLE_PRESETS = {
|
|
526
|
+
van: { heightMeters: 2.6, widthMeters: 2.1, lengthMeters: 6, weightTonnes: 3.5 },
|
|
527
|
+
rigid: { heightMeters: 3.8, widthMeters: 2.55, lengthMeters: 10, weightTonnes: 18 },
|
|
528
|
+
articulated: { heightMeters: 4, widthMeters: 2.55, lengthMeters: 16.5, weightTonnes: 40 },
|
|
529
|
+
"road-train": { heightMeters: 4, widthMeters: 2.55, lengthMeters: 18.75, weightTonnes: 44 },
|
|
530
|
+
"us-semi": { heightMeters: 4.11, widthMeters: 2.59, lengthMeters: 22, weightTonnes: 36.3 }
|
|
531
|
+
};
|
|
532
|
+
var DEFAULT_VEHICLE_PRESET = "articulated";
|
|
533
|
+
function vehiclePreset(name) {
|
|
534
|
+
const preset = VEHICLE_PRESETS[name];
|
|
535
|
+
if (!preset) throw new Error(`Unknown vehicle preset "${name}". Available: ${Object.keys(VEHICLE_PRESETS).join(", ")}.`);
|
|
536
|
+
return { ...preset };
|
|
537
|
+
}
|
|
538
|
+
function resolveVehicle(input, fallbackPreset = DEFAULT_VEHICLE_PRESET) {
|
|
539
|
+
if (typeof input === "string") return vehiclePreset(input);
|
|
540
|
+
const base = vehiclePreset(input?.preset ?? fallbackPreset);
|
|
541
|
+
if (!input) return base;
|
|
542
|
+
const resolved = {
|
|
543
|
+
heightMeters: input.heightMeters ?? base.heightMeters,
|
|
544
|
+
widthMeters: input.widthMeters ?? base.widthMeters,
|
|
545
|
+
lengthMeters: input.lengthMeters ?? base.lengthMeters,
|
|
546
|
+
weightTonnes: input.weightTonnes ?? base.weightTonnes
|
|
547
|
+
};
|
|
548
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
549
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`Vehicle ${name} must be a positive number.`);
|
|
550
|
+
}
|
|
551
|
+
return resolved;
|
|
552
|
+
}
|
|
553
|
+
|
|
292
554
|
// src/bundle.ts
|
|
293
555
|
var MAGIC = 1112688197;
|
|
294
556
|
var VERSION = 1;
|
|
557
|
+
var PROFILED_VERSION = 2;
|
|
558
|
+
var SHARED_CHUNK = 0;
|
|
295
559
|
var HEADER_BYTES = 32;
|
|
296
560
|
var DIRECTORY_ENTRY_BYTES = 40;
|
|
297
561
|
var COORDINATE_SCALE2 = 1e7;
|
|
562
|
+
var DEFAULT_MERGED_CACHE_SIZE = 2;
|
|
298
563
|
var BundleCompression = {
|
|
299
564
|
none: 0,
|
|
300
565
|
gzip: 1
|
|
@@ -334,56 +599,75 @@ async function fetchRange(url, start, end, fetcher) {
|
|
|
334
599
|
if (response.status === 200 && buffer.byteLength >= end + 1) return buffer.slice(start, end + 1);
|
|
335
600
|
throw new Error("Routing bundle server returned an invalid byte range.");
|
|
336
601
|
}
|
|
337
|
-
async function
|
|
338
|
-
if (compression === BundleCompression.none) return
|
|
602
|
+
async function decompressChunk(buffer, compression) {
|
|
603
|
+
if (compression === BundleCompression.none) return buffer;
|
|
339
604
|
if (compression !== BundleCompression.gzip || !("DecompressionStream" in globalThis)) {
|
|
340
605
|
throw new Error("This browser cannot decompress the routing bundle chunk.");
|
|
341
606
|
}
|
|
342
607
|
const stream = new Blob([buffer]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
343
|
-
return
|
|
608
|
+
return new Response(stream).arrayBuffer();
|
|
609
|
+
}
|
|
610
|
+
async function decodeChunk(buffer, compression) {
|
|
611
|
+
return withSpatialIndex(decodeGraph(await decompressChunk(buffer, compression)));
|
|
344
612
|
}
|
|
345
613
|
async function gzipChunk(buffer) {
|
|
346
614
|
if (!("CompressionStream" in globalThis)) throw new Error("This runtime cannot create gzip-compressed routing bundles.");
|
|
347
615
|
const stream = new Blob([buffer]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
348
616
|
return new Response(stream).arrayBuffer();
|
|
349
617
|
}
|
|
350
|
-
function
|
|
351
|
-
const
|
|
618
|
+
function profileOfTiles(tiles) {
|
|
619
|
+
const profileId = tiles[0].graph.profileId ?? 0;
|
|
620
|
+
const maxSpeedKph = tiles[0].graph.maxSpeedKph ?? 0;
|
|
621
|
+
for (const tile of tiles) {
|
|
622
|
+
if ((tile.graph.profileId ?? 0) !== profileId) throw new Error("A routing bundle cannot mix tiles compiled for different profiles.");
|
|
623
|
+
}
|
|
624
|
+
return { profileId, maxSpeedKph };
|
|
625
|
+
}
|
|
626
|
+
function createBundle(rows, version, header) {
|
|
627
|
+
const directoryBytes = rows.length * DIRECTORY_ENTRY_BYTES;
|
|
352
628
|
let offset = HEADER_BYTES + directoryBytes;
|
|
353
|
-
const byteLength = offset +
|
|
629
|
+
const byteLength = offset + rows.reduce((sum, row) => sum + row.chunk.data.byteLength, 0);
|
|
354
630
|
const buffer = new ArrayBuffer(byteLength);
|
|
355
631
|
const view = new DataView(buffer);
|
|
356
632
|
view.setUint32(0, MAGIC, true);
|
|
357
|
-
view.setUint16(4,
|
|
633
|
+
view.setUint16(4, version, true);
|
|
358
634
|
view.setUint16(6, HEADER_BYTES, true);
|
|
359
|
-
view.setUint32(8,
|
|
635
|
+
view.setUint32(8, rows.length, true);
|
|
360
636
|
view.setUint32(12, HEADER_BYTES, true);
|
|
361
637
|
view.setUint32(16, directoryBytes, true);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
638
|
+
view.setUint8(20, header.defaultProfileId);
|
|
639
|
+
view.setUint16(22, Math.round(header.maxSpeedKph), true);
|
|
640
|
+
view.setUint32(24, header.profileMask, true);
|
|
641
|
+
view.setUint32(28, header.tileCount, true);
|
|
642
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
643
|
+
const row = rows[index];
|
|
365
644
|
const entry = HEADER_BYTES + index * DIRECTORY_ENTRY_BYTES;
|
|
366
|
-
view.setUint32(entry,
|
|
367
|
-
view.setInt32(entry + 4, toFixed(
|
|
368
|
-
view.setInt32(entry + 8, toFixed(
|
|
369
|
-
view.setInt32(entry + 12, toFixed(
|
|
370
|
-
view.setInt32(entry + 16, toFixed(
|
|
645
|
+
view.setUint32(entry, row.tileId, true);
|
|
646
|
+
view.setInt32(entry + 4, toFixed(row.bounds.minLat), true);
|
|
647
|
+
view.setInt32(entry + 8, toFixed(row.bounds.minLng), true);
|
|
648
|
+
view.setInt32(entry + 12, toFixed(row.bounds.maxLat), true);
|
|
649
|
+
view.setInt32(entry + 16, toFixed(row.bounds.maxLng), true);
|
|
371
650
|
view.setUint32(entry + 20, offset, true);
|
|
372
|
-
view.setUint32(entry + 24, chunk.data.byteLength, true);
|
|
373
|
-
view.setUint32(entry + 28, chunk.rawByteLength, true);
|
|
374
|
-
view.setUint16(entry + 32, chunk.compression, true);
|
|
375
|
-
|
|
376
|
-
offset
|
|
651
|
+
view.setUint32(entry + 24, row.chunk.data.byteLength, true);
|
|
652
|
+
view.setUint32(entry + 28, row.chunk.rawByteLength, true);
|
|
653
|
+
view.setUint16(entry + 32, row.chunk.compression, true);
|
|
654
|
+
view.setUint8(entry + 34, row.profileId);
|
|
655
|
+
new Uint8Array(buffer, offset, row.chunk.data.byteLength).set(new Uint8Array(row.chunk.data));
|
|
656
|
+
offset += row.chunk.data.byteLength;
|
|
377
657
|
}
|
|
378
658
|
return buffer;
|
|
379
659
|
}
|
|
660
|
+
function singleProfileRows(tiles, chunks) {
|
|
661
|
+
return tiles.map((tile, index) => ({ tileId: tile.id, bounds: tile.bounds, profileId: SHARED_CHUNK, chunk: chunks[index] }));
|
|
662
|
+
}
|
|
380
663
|
function encodeRoadGraphBundle(tiles) {
|
|
381
664
|
if (!tiles.length) throw new Error("A routing bundle requires at least one tile.");
|
|
382
665
|
const chunks = tiles.map((tile) => {
|
|
383
666
|
const data = encodeCompactGraph(compactGraph(tile.graph));
|
|
384
667
|
return { data, rawByteLength: data.byteLength, compression: BundleCompression.none };
|
|
385
668
|
});
|
|
386
|
-
|
|
669
|
+
const profile = profileOfTiles(tiles);
|
|
670
|
+
return createBundle(singleProfileRows(tiles, chunks), VERSION, { ...profile, defaultProfileId: profile.profileId, profileMask: maskFor([profile.profileId]), tileCount: tiles.length });
|
|
387
671
|
}
|
|
388
672
|
async function encodeCompressedRoadGraphBundle(tiles, options = {}) {
|
|
389
673
|
if (!tiles.length) throw new Error("A routing bundle requires at least one tile.");
|
|
@@ -392,46 +676,136 @@ async function encodeCompressedRoadGraphBundle(tiles, options = {}) {
|
|
|
392
676
|
const raw = encodeCompactGraph(compactGraph(tile.graph));
|
|
393
677
|
return { data: await gzipChunk(raw), rawByteLength: raw.byteLength, compression: BundleCompression.gzip };
|
|
394
678
|
}));
|
|
395
|
-
|
|
679
|
+
const profile = profileOfTiles(tiles);
|
|
680
|
+
return createBundle(singleProfileRows(tiles, chunks), VERSION, { ...profile, defaultProfileId: profile.profileId, profileMask: maskFor([profile.profileId]), tileCount: tiles.length });
|
|
681
|
+
}
|
|
682
|
+
function maskFor(profileIds) {
|
|
683
|
+
let mask = 0;
|
|
684
|
+
for (const id of profileIds) if (id > 0 && id <= 32) mask |= 1 << id - 1;
|
|
685
|
+
return mask >>> 0;
|
|
686
|
+
}
|
|
687
|
+
function profilesIn(mask) {
|
|
688
|
+
const ids = [];
|
|
689
|
+
for (let bit = 0; bit < 32; bit += 1) if (mask & 1 << bit) ids.push(bit + 1);
|
|
690
|
+
return ids;
|
|
691
|
+
}
|
|
692
|
+
async function encodeProfiledRoadGraphBundle(tiles, options = {}) {
|
|
693
|
+
if (!tiles.length) throw new Error("A routing bundle requires at least one tile.");
|
|
694
|
+
const wanted = tiles[0].profiles.map((profile) => profile.profileId);
|
|
695
|
+
if (!wanted.length) throw new Error("A multi-profile bundle requires at least one profile.");
|
|
696
|
+
if (new Set(wanted).size !== wanted.length) throw new Error("A multi-profile bundle cannot list the same profile twice.");
|
|
697
|
+
const gzip = (options.compression ?? "gzip") !== "none";
|
|
698
|
+
const rows = [];
|
|
699
|
+
for (const tile of tiles) {
|
|
700
|
+
const present = tile.profiles.map((profile) => profile.profileId);
|
|
701
|
+
if (present.length !== wanted.length || present.some((id, index) => id !== wanted[index])) {
|
|
702
|
+
throw new Error("Every tile in a multi-profile bundle must carry the same profiles, in the same order.");
|
|
703
|
+
}
|
|
704
|
+
const pack = async (raw) => gzip ? { data: await gzipChunk(raw), rawByteLength: raw.byteLength, compression: BundleCompression.gzip } : { data: raw, rawByteLength: raw.byteLength, compression: BundleCompression.none };
|
|
705
|
+
const shared = compactGraph(tile.graph);
|
|
706
|
+
rows.push({ tileId: tile.id, bounds: tile.bounds, profileId: SHARED_CHUNK, chunk: await pack(encodeSharedGraph(shared)) });
|
|
707
|
+
for (const profile of tile.profiles) {
|
|
708
|
+
rows.push({ tileId: tile.id, bounds: tile.bounds, profileId: profile.profileId, chunk: await pack(encodeProfileChunk(profile)) });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const fastest = Math.max(...tiles[0].profiles.map((profile) => profile.maxSpeedKph));
|
|
712
|
+
return createBundle(rows, PROFILED_VERSION, { defaultProfileId: wanted[0], maxSpeedKph: fastest, profileMask: maskFor(wanted), tileCount: tiles.length });
|
|
396
713
|
}
|
|
397
|
-
async function openRoadGraphBundle(url, fetcher = fetch) {
|
|
714
|
+
async function openRoadGraphBundle(url, fetcher = fetch, options = {}) {
|
|
398
715
|
const header = await fetchRange(url, 0, HEADER_BYTES - 1, fetcher);
|
|
399
716
|
const headerView = new DataView(header);
|
|
400
|
-
|
|
717
|
+
const bundleVersion = headerView.getUint16(4, true);
|
|
718
|
+
if (headerView.getUint32(0, true) !== MAGIC || bundleVersion !== VERSION && bundleVersion !== PROFILED_VERSION || headerView.getUint16(6, true) !== HEADER_BYTES) {
|
|
401
719
|
throw new Error("Unsupported routing bundle format.");
|
|
402
720
|
}
|
|
403
|
-
const
|
|
721
|
+
const profiled = bundleVersion === PROFILED_VERSION;
|
|
722
|
+
const defaultProfileId = headerView.getUint8(20);
|
|
723
|
+
const defaultVehicle = vehiclePresetName(headerView.getUint8(21)) ?? void 0;
|
|
724
|
+
const headerSpeedKph = headerView.getUint16(22, true);
|
|
725
|
+
const available = profiled ? profilesIn(headerView.getUint32(24, true)) : [defaultProfileId || ProfileId.car];
|
|
726
|
+
for (const id of available) {
|
|
727
|
+
if (id && !getRoutingProfileById(id)) throw new Error(`This routing bundle was compiled for a profile this build does not support (id ${id}).`);
|
|
728
|
+
}
|
|
729
|
+
const chosenId = options.profile ? getRoutingProfile(options.profile).id : defaultProfileId;
|
|
730
|
+
if (!profiled && getRoutingProfileById(chosenId)?.usesPhysicalLimits) {
|
|
731
|
+
throw new Error(`A "${getRoutingProfileById(chosenId).name}" bundle must carry its physical limits, which this one cannot: it was written in the single-chunk layout. Recompile it.`);
|
|
732
|
+
}
|
|
733
|
+
if (options.profile && !available.includes(chosenId)) {
|
|
734
|
+
const names = available.map((id) => getRoutingProfileById(id)?.name ?? `id ${id}`).join(", ");
|
|
735
|
+
throw new Error(`This routing bundle does not hold the "${options.profile}" profile. It holds: ${names}.`);
|
|
736
|
+
}
|
|
737
|
+
const entryCount = headerView.getUint32(8, true);
|
|
404
738
|
const directoryOffset = headerView.getUint32(12, true);
|
|
405
739
|
const directoryLength = headerView.getUint32(16, true);
|
|
406
|
-
if (!
|
|
740
|
+
if (!entryCount || directoryLength !== entryCount * DIRECTORY_ENTRY_BYTES) throw new Error("Invalid routing bundle directory.");
|
|
407
741
|
const directory = await fetchRange(url, directoryOffset, directoryOffset + directoryLength - 1, fetcher);
|
|
408
742
|
const directoryView = new DataView(directory);
|
|
409
743
|
const tiles = [];
|
|
410
|
-
|
|
744
|
+
const byTileId = /* @__PURE__ */ new Map();
|
|
745
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
411
746
|
const entry = index * DIRECTORY_ENTRY_BYTES;
|
|
412
747
|
const compression = directoryView.getUint16(entry + 32, true);
|
|
413
748
|
if (compression !== BundleCompression.none && compression !== BundleCompression.gzip) throw new Error("Unsupported routing bundle chunk compression.");
|
|
414
|
-
|
|
415
|
-
|
|
749
|
+
const chunk = {
|
|
750
|
+
offset: directoryView.getUint32(entry + 20, true),
|
|
751
|
+
byteLength: directoryView.getUint32(entry + 24, true),
|
|
752
|
+
rawByteLength: directoryView.getUint32(entry + 28, true),
|
|
753
|
+
compression
|
|
754
|
+
};
|
|
755
|
+
const id = directoryView.getUint32(entry, true);
|
|
756
|
+
const rowProfile = profiled ? directoryView.getUint8(entry + 34) : SHARED_CHUNK;
|
|
757
|
+
if (rowProfile !== SHARED_CHUNK) {
|
|
758
|
+
const tile2 = byTileId.get(id);
|
|
759
|
+
if (!tile2) throw new Error("Invalid routing bundle directory: a profile chunk arrives before its tile.");
|
|
760
|
+
(tile2.profileChunks ??= /* @__PURE__ */ new Map()).set(rowProfile, chunk);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const tile = {
|
|
764
|
+
id,
|
|
416
765
|
bounds: {
|
|
417
766
|
minLat: fromFixed(directoryView.getInt32(entry + 4, true)),
|
|
418
767
|
minLng: fromFixed(directoryView.getInt32(entry + 8, true)),
|
|
419
768
|
maxLat: fromFixed(directoryView.getInt32(entry + 12, true)),
|
|
420
769
|
maxLng: fromFixed(directoryView.getInt32(entry + 16, true))
|
|
421
770
|
},
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
771
|
+
...chunk,
|
|
772
|
+
...profiled ? { profileChunks: /* @__PURE__ */ new Map() } : {}
|
|
773
|
+
};
|
|
774
|
+
tiles.push(tile);
|
|
775
|
+
byTileId.set(id, tile);
|
|
776
|
+
}
|
|
777
|
+
if (profiled) {
|
|
778
|
+
for (const tile of tiles) {
|
|
779
|
+
if (!tile.profileChunks?.has(chosenId)) throw new Error(`Invalid routing bundle: tile ${tile.id} has no chunk for the selected profile.`);
|
|
780
|
+
}
|
|
427
781
|
}
|
|
782
|
+
const fetchChunk = async (chunk) => decompressChunk(await fetchRange(url, chunk.offset, chunk.offset + chunk.byteLength - 1, fetcher), chunk.compression);
|
|
783
|
+
const sharedCache = /* @__PURE__ */ new Map();
|
|
784
|
+
const profileCache = /* @__PURE__ */ new Map();
|
|
428
785
|
const cache = /* @__PURE__ */ new Map();
|
|
429
|
-
const loadTile = (tile) => {
|
|
430
|
-
|
|
431
|
-
|
|
786
|
+
const loadTile = (tile, profileId) => {
|
|
787
|
+
const key = `${tile.id}:${profileId}`;
|
|
788
|
+
let graph = cache.get(key);
|
|
789
|
+
if (graph) return graph;
|
|
790
|
+
if (!tile.profileChunks) {
|
|
432
791
|
graph = fetchRange(url, tile.offset, tile.offset + tile.byteLength - 1, fetcher).then((buffer) => decodeChunk(buffer, tile.compression));
|
|
433
|
-
|
|
792
|
+
} else {
|
|
793
|
+
let shared = sharedCache.get(tile.id);
|
|
794
|
+
if (!shared) {
|
|
795
|
+
shared = fetchChunk(tile).then(decodeSharedGraph);
|
|
796
|
+
sharedCache.set(tile.id, shared);
|
|
797
|
+
shared.catch(() => sharedCache.delete(tile.id));
|
|
798
|
+
}
|
|
799
|
+
let columns = profileCache.get(key);
|
|
800
|
+
if (!columns) {
|
|
801
|
+
columns = fetchChunk(tile.profileChunks.get(profileId)).then(decodeProfileChunk);
|
|
802
|
+
profileCache.set(key, columns);
|
|
803
|
+
columns.catch(() => profileCache.delete(key));
|
|
804
|
+
}
|
|
805
|
+
graph = Promise.all([shared, columns]).then(([half, vehicle]) => withSpatialIndex(combineProfiledGraph(half, vehicle)));
|
|
434
806
|
}
|
|
807
|
+
cache.set(key, graph);
|
|
808
|
+
graph.catch(() => cache.delete(key));
|
|
435
809
|
return graph;
|
|
436
810
|
};
|
|
437
811
|
const routeTiles = (origin, destination, ring) => {
|
|
@@ -452,25 +826,47 @@ async function openRoadGraphBundle(url, fetcher = fetch) {
|
|
|
452
826
|
maxLng: corridor.maxLng + longitudePadding
|
|
453
827
|
}));
|
|
454
828
|
};
|
|
455
|
-
const
|
|
829
|
+
const mergedLimit = Math.max(0, Math.trunc(options.mergedCacheSize ?? Math.max(DEFAULT_MERGED_CACHE_SIZE, available.length)));
|
|
830
|
+
const merged = /* @__PURE__ */ new Map();
|
|
831
|
+
const loadTiles = async (routeTiles2, profileId = chosenId) => {
|
|
456
832
|
if (!routeTiles2.length) throw new RouterError("ROUTE_ACROSS_TILES", "No local graph tiles cover the route corridor.");
|
|
457
|
-
if (routeTiles2.length === 1) return loadTile(routeTiles2[0]);
|
|
458
|
-
|
|
833
|
+
if (routeTiles2.length === 1) return loadTile(routeTiles2[0], profileId);
|
|
834
|
+
const key = `${profileId}/${routeTiles2.map((tile) => tile.id).sort((a, b) => a - b).join(",")}`;
|
|
835
|
+
const held = merged.get(key);
|
|
836
|
+
if (held) {
|
|
837
|
+
merged.delete(key);
|
|
838
|
+
merged.set(key, held);
|
|
839
|
+
return held;
|
|
840
|
+
}
|
|
841
|
+
const graph = Promise.all(routeTiles2.map((tile) => loadTile(tile, profileId))).then(mergeRoadGraphs);
|
|
842
|
+
if (mergedLimit) {
|
|
843
|
+
merged.set(key, graph);
|
|
844
|
+
for (const oldest of merged.keys()) {
|
|
845
|
+
if (merged.size <= mergedLimit) break;
|
|
846
|
+
merged.delete(oldest);
|
|
847
|
+
}
|
|
848
|
+
graph.catch(() => merged.delete(key));
|
|
849
|
+
}
|
|
850
|
+
return graph;
|
|
459
851
|
};
|
|
460
852
|
return {
|
|
461
853
|
url,
|
|
462
854
|
tiles,
|
|
855
|
+
profileId: chosenId,
|
|
856
|
+
maxSpeedKph: headerSpeedKph,
|
|
857
|
+
profiles: available,
|
|
858
|
+
...defaultVehicle ? { defaultVehicle } : {},
|
|
463
859
|
tilesWithinBudget(origin, destination, budgetMeters) {
|
|
464
860
|
return tiles.filter((tile) => contains2(tile.bounds, origin) || contains2(tile.bounds, destination) || distanceToBounds(origin, tile.bounds) + distanceToBounds(destination, tile.bounds) <= budgetMeters);
|
|
465
861
|
},
|
|
466
|
-
loadTiles(requested) {
|
|
467
|
-
return loadTiles(requested);
|
|
862
|
+
loadTiles(requested, profileId) {
|
|
863
|
+
return loadTiles(requested, profileId ?? chosenId);
|
|
468
864
|
},
|
|
469
865
|
async loadForRoute(origin, destination) {
|
|
470
866
|
const originTile = tiles.find((tile) => contains2(tile.bounds, origin));
|
|
471
867
|
const destinationTile = tiles.find((tile) => contains2(tile.bounds, destination));
|
|
472
868
|
if (!originTile || !destinationTile) throw new RouterError("ROUTE_OUTSIDE_COVERAGE", "The route is outside the available bundle coverage.");
|
|
473
|
-
if (originTile.id === destinationTile.id) return loadTile(originTile);
|
|
869
|
+
if (originTile.id === destinationTile.id) return loadTile(originTile, chosenId);
|
|
474
870
|
return loadTiles(routeTiles(origin, destination, 0));
|
|
475
871
|
},
|
|
476
872
|
async loadExpandedForRoute(origin, destination) {
|
|
@@ -480,7 +876,6 @@ async function openRoadGraphBundle(url, fetcher = fetch) {
|
|
|
480
876
|
}
|
|
481
877
|
|
|
482
878
|
// src/road-router.ts
|
|
483
|
-
var MAX_SPEED_MPS = carRoutingProfile.maxSpeedKph * 1e3 / 3600;
|
|
484
879
|
var INITIAL_DETOUR_FACTOR = 1.4;
|
|
485
880
|
var MINIMUM_BUDGET_METERS = 5e3;
|
|
486
881
|
var SEARCH_WIDENING_FACTORS = [3, 8, Infinity];
|
|
@@ -496,26 +891,85 @@ function haversineMeters2(a, b) {
|
|
|
496
891
|
return 6371e3 * 2 * Math.atan2(Math.sqrt(value), Math.sqrt(1 - value));
|
|
497
892
|
}
|
|
498
893
|
var RoadRouter = class _RoadRouter {
|
|
499
|
-
constructor(graph, loader, options = {}) {
|
|
894
|
+
constructor(graph, loader, options = {}, profileId, bundleVehicle) {
|
|
500
895
|
this.graph = graph;
|
|
501
896
|
this.loader = loader;
|
|
502
897
|
this.options = options;
|
|
898
|
+
this.profileId = profileId;
|
|
899
|
+
this.bundleVehicle = bundleVehicle;
|
|
900
|
+
const profile = getRoutingProfileById(profileId ?? 0);
|
|
901
|
+
this.vehicle = profile?.usesPhysicalLimits && options.vehicle !== null ? resolveVehicle(options.vehicle ?? void 0, bundleVehicle ?? DEFAULT_VEHICLE_PRESET) : void 0;
|
|
503
902
|
}
|
|
504
903
|
graph;
|
|
505
904
|
loader;
|
|
506
905
|
options;
|
|
906
|
+
profileId;
|
|
907
|
+
bundleVehicle;
|
|
908
|
+
/** The lorry these routes are for, where the profile depends on one. */
|
|
909
|
+
vehicle;
|
|
507
910
|
static fromGraph(graph) {
|
|
508
911
|
return new _RoadRouter(graph);
|
|
509
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* Asking for dimensions a profile cannot act on is a mistake worth saying
|
|
915
|
+
* out loud: accepting them silently would look like they had been applied.
|
|
916
|
+
* It is checked where the caller made the pairing, not when switching
|
|
917
|
+
* profiles, where carrying them over is deliberate.
|
|
918
|
+
*/
|
|
919
|
+
static assertVehicleApplies(profileId, options) {
|
|
920
|
+
if (options.vehicle === void 0 || options.vehicle === null) return;
|
|
921
|
+
const profile = getRoutingProfileById(profileId);
|
|
922
|
+
if (profile && !profile.usesPhysicalLimits) {
|
|
923
|
+
throw new Error(`The "${profile.name}" profile does not use vehicle dimensions; only a profile that stores physical limits does.`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
510
926
|
static async open(source, fetcher, options = {}) {
|
|
511
|
-
if (typeof source !== "string")
|
|
512
|
-
|
|
927
|
+
if (typeof source !== "string") {
|
|
928
|
+
_RoadRouter.assertVehicleApplies(source.profileId ?? 0, options);
|
|
929
|
+
return new _RoadRouter(source, void 0, options, source.profileId);
|
|
930
|
+
}
|
|
931
|
+
if (source.endsWith(".efrb")) {
|
|
932
|
+
const bundle = await openRoadGraphBundle(source, fetcher, { mergedCacheSize: options.mergedCacheSize, profile: options.profile });
|
|
933
|
+
_RoadRouter.assertVehicleApplies(bundle.profileId, options);
|
|
934
|
+
return new _RoadRouter(void 0, bundle, options, bundle.profileId, bundle.defaultVehicle);
|
|
935
|
+
}
|
|
513
936
|
return new _RoadRouter(void 0, await createRouteGraphLoader(source, fetcher), options);
|
|
514
937
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
938
|
+
/** The vehicle this router is routing as, when the source names one. */
|
|
939
|
+
get profile() {
|
|
940
|
+
return getRoutingProfileById(this.profileId ?? 0)?.name;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Every vehicle this source can be read as, so a caller can offer a choice
|
|
944
|
+
* without knowing how the bundle was compiled. A bundle holding one vehicle
|
|
945
|
+
* reports that one, and a plain graph reports none.
|
|
946
|
+
*/
|
|
947
|
+
get profiles() {
|
|
948
|
+
if (!isBundle(this.loader)) return this.profile ? [this.profile] : [];
|
|
949
|
+
return this.loader.profiles.map((id) => getRoutingProfileById(id)?.name).filter((name) => Boolean(name));
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* The same bundle read as another vehicle. The shared half of every tile is
|
|
953
|
+
* already downloaded and stays so, which is the point of storing the road
|
|
954
|
+
* network once: switching vehicle costs only the small per-vehicle chunks.
|
|
955
|
+
*/
|
|
956
|
+
withProfile(name) {
|
|
957
|
+
const wanted = getRoutingProfile(name);
|
|
958
|
+
if (!isBundle(this.loader)) throw new Error("Only a multi-profile bundle can be read as another vehicle.");
|
|
959
|
+
if (!this.loader.profiles.includes(wanted.id)) {
|
|
960
|
+
throw new Error(`This routing bundle does not hold the "${name}" profile. It holds: ${this.loader.profiles.map((id) => getRoutingProfileById(id)?.name ?? `id ${id}`).join(", ")}.`);
|
|
961
|
+
}
|
|
962
|
+
return new _RoadRouter(void 0, this.loader, this.options, wanted.id, this.bundleVehicle);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* `when` says when the journey starts, so conditional restrictions apply.
|
|
966
|
+
* Leaving it out ignores them, which is how this library behaved before it
|
|
967
|
+
* could read them.
|
|
968
|
+
*/
|
|
969
|
+
async route(from, to, preference = "fastest", when = {}) {
|
|
970
|
+
if (this.graph) return route(this.graph, from, to, preference, { vehicle: this.vehicle, ...when });
|
|
971
|
+
if (isBundle(this.loader)) return this.routeOverBundle(this.loader, from, to, preference, when);
|
|
972
|
+
return this.routeOverLoader(this.loader, from, to, preference, when);
|
|
519
973
|
}
|
|
520
974
|
/**
|
|
521
975
|
* Downloads a corridor, routes, then asks whether any tile left out could
|
|
@@ -526,10 +980,44 @@ var RoadRouter = class _RoadRouter {
|
|
|
526
980
|
* graph contains. Because the test measures ground distance rather than
|
|
527
981
|
* counting neighbours, the answer does not depend on the tile size.
|
|
528
982
|
*/
|
|
529
|
-
|
|
983
|
+
/**
|
|
984
|
+
* Several ways there, best first, with up to `count - 1` alternatives after
|
|
985
|
+
* it. Fewer than asked for means the network held nothing else worth
|
|
986
|
+
* offering, which is the usual answer where there is one obvious way.
|
|
987
|
+
*
|
|
988
|
+
* Over a bundle this downloads a wider corridor than a single route needs:
|
|
989
|
+
* an alternative may be worse than the best by as much as `maxStretch`, and
|
|
990
|
+
* the tiles holding it lie outside the corridor that proved the best route.
|
|
991
|
+
*/
|
|
992
|
+
async routeAlternatives(from, to, preference = "fastest", options = {}) {
|
|
993
|
+
checkAlternativeOptions(options);
|
|
994
|
+
const withVehicle = { vehicle: this.vehicle, ...options };
|
|
995
|
+
if (this.graph) return routeAlternatives(this.graph, from, to, preference, withVehicle);
|
|
996
|
+
if (isBundle(this.loader)) {
|
|
997
|
+
const { best, graphNow, widen, reachFor } = await this.bestOverBundle(this.loader, from, to, preference, options);
|
|
998
|
+
if (options.count !== void 0 && options.count <= 1) return [best];
|
|
999
|
+
if (options.searchWiderCorridor === false) return routeAlternatives(graphNow(), from, to, preference, withVehicle);
|
|
1000
|
+
await widen(reachFor(best) * (options.maxStretch ?? DEFAULT_MAX_STRETCH));
|
|
1001
|
+
return routeAlternatives(graphNow(), from, to, preference, withVehicle);
|
|
1002
|
+
}
|
|
1003
|
+
const loader = this.loader;
|
|
1004
|
+
return routeAlternatives(await loader.loadForRoute(from, to), from, to, preference, withVehicle);
|
|
1005
|
+
}
|
|
1006
|
+
async routeOverBundle(bundle, from, to, preference, when = {}) {
|
|
1007
|
+
return (await this.bestOverBundle(bundle, from, to, preference, when)).best;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* The same corridor walk, stopping with the graph it settled on as well as
|
|
1011
|
+
* the route. Alternatives need the graph, and need to be able to widen it
|
|
1012
|
+
* once more: a second route may be worse than the best by more than the
|
|
1013
|
+
* corridor was ever sized to hold.
|
|
1014
|
+
*/
|
|
1015
|
+
async bestOverBundle(bundle, from, to, preference, when = {}) {
|
|
530
1016
|
const straightLine = haversineMeters2(from, to);
|
|
531
1017
|
const ceiling = straightLine * (this.options.maxDetourFactor ?? Infinity);
|
|
532
|
-
const
|
|
1018
|
+
const selected = this.profileId ?? bundle.profileId;
|
|
1019
|
+
const maxSpeedMps = maxSpeedMpsFor({ profileId: selected, maxSpeedKph: bundle.profiles.length > 1 ? void 0 : bundle.maxSpeedKph || void 0 });
|
|
1020
|
+
const reachFor = (result) => Math.min(ceiling, preference === "fastest" ? result.durationSeconds * maxSpeedMps : result.distanceMeters);
|
|
533
1021
|
let budget = Math.max(MINIMUM_BUDGET_METERS, Math.min(ceiling, straightLine * INITIAL_DETOUR_FACTOR));
|
|
534
1022
|
let loaded = [];
|
|
535
1023
|
let graph = null;
|
|
@@ -539,7 +1027,7 @@ var RoadRouter = class _RoadRouter {
|
|
|
539
1027
|
const wanted = bundle.tilesWithinBudget(from, to, nextBudget);
|
|
540
1028
|
if (wanted.length <= loaded.length && wanted.every((tile) => loaded.some((held) => held.id === tile.id))) return false;
|
|
541
1029
|
loaded = wanted;
|
|
542
|
-
graph = await bundle.loadTiles(wanted);
|
|
1030
|
+
graph = await bundle.loadTiles(wanted, selected);
|
|
543
1031
|
return true;
|
|
544
1032
|
};
|
|
545
1033
|
await widen(budget);
|
|
@@ -547,70 +1035,124 @@ var RoadRouter = class _RoadRouter {
|
|
|
547
1035
|
for (const factor of [1, ...SEARCH_WIDENING_FACTORS]) {
|
|
548
1036
|
if (factor !== 1 && !await widen(factor === Infinity ? Infinity : budget * factor)) break;
|
|
549
1037
|
try {
|
|
550
|
-
best = route(graph, from, to, preference);
|
|
1038
|
+
best = route(graph, from, to, preference, { vehicle: this.vehicle, ...when });
|
|
551
1039
|
break;
|
|
552
1040
|
} catch (error) {
|
|
553
1041
|
failure = error;
|
|
554
1042
|
if (error.code !== "NO_ROUTE") throw error;
|
|
555
1043
|
}
|
|
556
1044
|
}
|
|
557
|
-
if (!best) throw failure ?? new RouterError("NO_ROUTE", "No
|
|
1045
|
+
if (!best) throw failure ?? new RouterError("NO_ROUTE", "No route connects the selected points.");
|
|
558
1046
|
for (let pass = 0; pass < MAX_CONFIRMATION_PASSES; pass += 1) {
|
|
559
|
-
if (!await widen(reachFor(best))) return best;
|
|
1047
|
+
if (!await widen(reachFor(best))) return { best, graphNow: () => graph, widen, reachFor };
|
|
560
1048
|
try {
|
|
561
|
-
best = route(graph, from, to, preference, { costBound: preference === "fastest" ? best.durationSeconds : best.distanceMeters });
|
|
1049
|
+
best = route(graph, from, to, preference, { costBound: preference === "fastest" ? best.durationSeconds : best.distanceMeters, vehicle: this.vehicle, ...when });
|
|
562
1050
|
} catch (error) {
|
|
563
1051
|
if (error.code !== "NO_ROUTE") throw error;
|
|
564
|
-
return best;
|
|
1052
|
+
return { best, graphNow: () => graph, widen, reachFor };
|
|
565
1053
|
}
|
|
566
1054
|
}
|
|
567
|
-
return best;
|
|
1055
|
+
return { best, graphNow: () => graph, widen, reachFor };
|
|
568
1056
|
}
|
|
569
|
-
async routeOverLoader(loader, from, to, preference) {
|
|
1057
|
+
async routeOverLoader(loader, from, to, preference, when = {}) {
|
|
570
1058
|
const graph = await loader.loadForRoute(from, to);
|
|
571
1059
|
try {
|
|
572
|
-
return route(graph, from, to, preference);
|
|
1060
|
+
return route(graph, from, to, preference, { vehicle: this.vehicle, ...when });
|
|
573
1061
|
} catch (error) {
|
|
574
1062
|
if (!(error instanceof Error) || error.code !== "NO_ROUTE" || !loader.loadExpandedForRoute) throw error;
|
|
575
|
-
return route(await loader.loadExpandedForRoute(from, to), from, to, preference);
|
|
1063
|
+
return route(await loader.loadExpandedForRoute(from, to), from, to, preference, { vehicle: this.vehicle, ...when });
|
|
576
1064
|
}
|
|
577
1065
|
}
|
|
578
1066
|
};
|
|
579
1067
|
export {
|
|
580
1068
|
BundleCompression,
|
|
1069
|
+
ConditionKind,
|
|
1070
|
+
DEFAULT_VEHICLE_PRESET,
|
|
1071
|
+
MAX_STORED_VALUES,
|
|
1072
|
+
ProfileId,
|
|
581
1073
|
RoadRouter,
|
|
582
1074
|
RouterError,
|
|
1075
|
+
VEHICLE_PRESETS,
|
|
583
1076
|
VarintReader,
|
|
584
1077
|
VarintWriter,
|
|
1078
|
+
VehiclePresetId,
|
|
1079
|
+
accessAtTime,
|
|
1080
|
+
accessAvoiding,
|
|
1081
|
+
accessForVehicle,
|
|
1082
|
+
accessMaskFor,
|
|
1083
|
+
allowsEverything,
|
|
585
1084
|
buildTurnRestrictionIndex,
|
|
586
1085
|
buildTurnSequenceIndex,
|
|
587
1086
|
carRoutingProfile,
|
|
1087
|
+
checkAlternativeOptions,
|
|
1088
|
+
combineProfiledGraph,
|
|
588
1089
|
compactGraph,
|
|
1090
|
+
componentSizes,
|
|
1091
|
+
componentsFor,
|
|
1092
|
+
conditionHolds,
|
|
1093
|
+
conditionalDurations,
|
|
1094
|
+
conditionalValue,
|
|
1095
|
+
conditionsOf,
|
|
1096
|
+
conditionsOnEdge,
|
|
1097
|
+
countAvoided,
|
|
1098
|
+
countBlocked,
|
|
1099
|
+
countClosedAt,
|
|
589
1100
|
createEdgeSpatialIndex,
|
|
590
1101
|
createRouteGraphLoader,
|
|
591
1102
|
createSpatialIndex,
|
|
592
1103
|
decodeGraph,
|
|
1104
|
+
decodeProfileChunk,
|
|
1105
|
+
decodeSharedGraph,
|
|
593
1106
|
distanceToBounds,
|
|
1107
|
+
edgeAllowed,
|
|
594
1108
|
edgePolyline,
|
|
595
1109
|
edgeSource,
|
|
596
1110
|
edgeSpatialIndexFor,
|
|
597
1111
|
encodeCompactGraph,
|
|
598
1112
|
encodeCompressedRoadGraphBundle,
|
|
599
1113
|
encodeGraph,
|
|
1114
|
+
encodeProfileChunk,
|
|
1115
|
+
encodeProfiledRoadGraphBundle,
|
|
600
1116
|
encodeRoadGraphBundle,
|
|
1117
|
+
encodeSharedGraph,
|
|
601
1118
|
findNearestNode,
|
|
1119
|
+
fromDecimetres,
|
|
1120
|
+
fromHalfTonnes,
|
|
602
1121
|
getRoutingProfile,
|
|
1122
|
+
getRoutingProfileById,
|
|
1123
|
+
guardedJunctionsFor,
|
|
603
1124
|
isTurnBanned,
|
|
1125
|
+
maxSpeedMpsFor,
|
|
604
1126
|
mergeRoadGraphs,
|
|
1127
|
+
motorcycleRoutingProfile,
|
|
605
1128
|
nearbyEdgeIds,
|
|
606
1129
|
openRoadGraphBundle,
|
|
1130
|
+
packEdgeConditions,
|
|
1131
|
+
packEdgeLimits,
|
|
607
1132
|
packTurnRestrictions,
|
|
608
1133
|
packTurnSequences,
|
|
1134
|
+
parseConditional,
|
|
1135
|
+
parseEdgeConditions,
|
|
1136
|
+
parseLengthMeters,
|
|
609
1137
|
parseMaxSpeedKph,
|
|
1138
|
+
parseWeightTonnes,
|
|
1139
|
+
profileOf,
|
|
1140
|
+
resolveVehicle,
|
|
610
1141
|
route,
|
|
1142
|
+
routeAlternatives,
|
|
611
1143
|
routingProfiles,
|
|
1144
|
+
searchBoundsFor,
|
|
612
1145
|
snapToGraph,
|
|
1146
|
+
straightLineAtMost,
|
|
1147
|
+
toDecimetres,
|
|
1148
|
+
toHalfTonnes,
|
|
1149
|
+
touchesArea,
|
|
1150
|
+
travelTimeFrom,
|
|
1151
|
+
truckRoutingProfile,
|
|
1152
|
+
unreadableConditions,
|
|
613
1153
|
unzigzag,
|
|
1154
|
+
vehiclePreset,
|
|
1155
|
+
vehiclePresetName,
|
|
614
1156
|
withSpatialIndex,
|
|
615
1157
|
zigzag
|
|
616
1158
|
};
|