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/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-4FIOQAFG.js";
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
- edgeDurationsSeconds[index] = Math.round(durations[index] / edgeDurationScale);
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 coordinateScale = graph.coordinateScale ?? 1;
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 lat = graph.nodeLatitudes[index] / coordinateScale;
97
- const lng = graph.nodeLongitudes[index] / coordinateScale;
98
- const key = nodeKey(lat, lng);
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 = latitudes.length;
276
+ mergedIndex = nodeCount++;
102
277
  nodeIds.set(key, mergedIndex);
103
- latitudes.push(lat);
104
- longitudes.push(lng);
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 scale = graph.coordinateScale ?? 1;
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] / scale * COORDINATE_SCALE);
197
- edgeShapeLongitudes[target] = Math.round(graph.edgeShapeLongitudes[point] / scale * COORDINATE_SCALE);
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: Int32Array.from(latitudes, (value) => Math.round(value * COORDINATE_SCALE)),
225
- nodeLongitudes: Int32Array.from(longitudes, (value) => Math.round(value * COORDINATE_SCALE)),
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 decodeChunk(buffer, compression) {
338
- if (compression === BundleCompression.none) return withSpatialIndex(decodeGraph(buffer));
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 withSpatialIndex(decodeGraph(await new Response(stream).arrayBuffer()));
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 createBundle(tiles, chunks) {
351
- const directoryBytes = tiles.length * DIRECTORY_ENTRY_BYTES;
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 + chunks.reduce((sum, chunk) => sum + chunk.data.byteLength, 0);
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, VERSION, true);
633
+ view.setUint16(4, version, true);
358
634
  view.setUint16(6, HEADER_BYTES, true);
359
- view.setUint32(8, tiles.length, true);
635
+ view.setUint32(8, rows.length, true);
360
636
  view.setUint32(12, HEADER_BYTES, true);
361
637
  view.setUint32(16, directoryBytes, true);
362
- for (let index = 0; index < tiles.length; index += 1) {
363
- const tile = tiles[index];
364
- const chunk = chunks[index];
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, tile.id, true);
367
- view.setInt32(entry + 4, toFixed(tile.bounds.minLat), true);
368
- view.setInt32(entry + 8, toFixed(tile.bounds.minLng), true);
369
- view.setInt32(entry + 12, toFixed(tile.bounds.maxLat), true);
370
- view.setInt32(entry + 16, toFixed(tile.bounds.maxLng), true);
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
- new Uint8Array(buffer, offset, chunk.data.byteLength).set(new Uint8Array(chunk.data));
376
- offset += chunk.data.byteLength;
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
- return createBundle(tiles, chunks);
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
- return createBundle(tiles, chunks);
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
- if (headerView.getUint32(0, true) !== MAGIC || headerView.getUint16(4, true) !== VERSION || headerView.getUint16(6, true) !== HEADER_BYTES) {
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 tileCount = headerView.getUint32(8, true);
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 (!tileCount || directoryLength !== tileCount * DIRECTORY_ENTRY_BYTES) throw new Error("Invalid routing bundle directory.");
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
- for (let index = 0; index < tileCount; index += 1) {
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
- tiles.push({
415
- id: directoryView.getUint32(entry, true),
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
- offset: directoryView.getUint32(entry + 20, true),
423
- byteLength: directoryView.getUint32(entry + 24, true),
424
- rawByteLength: directoryView.getUint32(entry + 28, true),
425
- compression
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
- let graph = cache.get(tile.id);
431
- if (!graph) {
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
- cache.set(tile.id, graph);
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 loadTiles = async (routeTiles2) => {
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
- return mergeRoadGraphs(await Promise.all(routeTiles2.map(loadTile)));
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") return _RoadRouter.fromGraph(source);
512
- if (source.endsWith(".efrb")) return new _RoadRouter(void 0, await openRoadGraphBundle(source, fetcher), options);
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
- async route(from, to, preference = "fastest") {
516
- if (this.graph) return route(this.graph, from, to, preference);
517
- if (isBundle(this.loader)) return this.routeOverBundle(this.loader, from, to, preference);
518
- return this.routeOverLoader(this.loader, from, to, preference);
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
- async routeOverBundle(bundle, from, to, preference) {
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 reachFor = (result) => Math.min(ceiling, preference === "fastest" ? result.durationSeconds * MAX_SPEED_MPS : result.distanceMeters);
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 car route connects the selected points.");
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
  };