roadgraph-router-v2 0.2.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 +217 -2
- package/README.md +408 -24
- package/dist/{chunk-PXL65P3L.js → chunk-QSIKDM4A.js} +942 -316
- package/dist/index.d.ts +480 -66
- package/dist/index.js +201 -14
- package/dist/worker.js +2 -2
- package/package.json +1 -1
- package/scripts/roadgraph-compile.mjs +30 -7
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ConditionKind,
|
|
3
|
+
MAX_STORED_VALUES,
|
|
2
4
|
ProfileId,
|
|
3
5
|
RouterError,
|
|
4
6
|
VarintReader,
|
|
5
7
|
VarintWriter,
|
|
8
|
+
accessAtTime,
|
|
9
|
+
accessAvoiding,
|
|
6
10
|
accessForVehicle,
|
|
7
11
|
accessMaskFor,
|
|
8
12
|
allowsEverything,
|
|
@@ -12,7 +16,14 @@ import {
|
|
|
12
16
|
combineProfiledGraph,
|
|
13
17
|
componentSizes,
|
|
14
18
|
componentsFor,
|
|
19
|
+
conditionHolds,
|
|
20
|
+
conditionalDurations,
|
|
21
|
+
conditionalValue,
|
|
22
|
+
conditionsOf,
|
|
23
|
+
conditionsOnEdge,
|
|
24
|
+
countAvoided,
|
|
15
25
|
countBlocked,
|
|
26
|
+
countClosedAt,
|
|
16
27
|
createEdgeSpatialIndex,
|
|
17
28
|
createSpatialIndex,
|
|
18
29
|
decodeGraph,
|
|
@@ -36,9 +47,12 @@ import {
|
|
|
36
47
|
maxSpeedMpsFor,
|
|
37
48
|
motorcycleRoutingProfile,
|
|
38
49
|
nearbyEdgeIds,
|
|
50
|
+
packEdgeConditions,
|
|
39
51
|
packEdgeLimits,
|
|
40
52
|
packTurnRestrictions,
|
|
41
53
|
packTurnSequences,
|
|
54
|
+
parseConditional,
|
|
55
|
+
parseEdgeConditions,
|
|
42
56
|
parseLengthMeters,
|
|
43
57
|
parseMaxSpeedKph,
|
|
44
58
|
parseWeightTonnes,
|
|
@@ -50,11 +64,14 @@ import {
|
|
|
50
64
|
straightLineAtMost,
|
|
51
65
|
toDecimetres,
|
|
52
66
|
toHalfTonnes,
|
|
67
|
+
touchesArea,
|
|
68
|
+
travelTimeFrom,
|
|
53
69
|
truckRoutingProfile,
|
|
70
|
+
unreadableConditions,
|
|
54
71
|
unzigzag,
|
|
55
72
|
withSpatialIndex,
|
|
56
73
|
zigzag
|
|
57
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-QSIKDM4A.js";
|
|
58
75
|
|
|
59
76
|
// src/compact.ts
|
|
60
77
|
function scaleFor(values, maximum) {
|
|
@@ -107,6 +124,104 @@ function compactGraph(graph, options = {}) {
|
|
|
107
124
|
};
|
|
108
125
|
}
|
|
109
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
|
+
|
|
110
225
|
// src/merge.ts
|
|
111
226
|
var COORDINATE_SCALE = 1e7;
|
|
112
227
|
var LENGTH_EPSILON_METERS = 0.01;
|
|
@@ -261,6 +376,8 @@ function mergeRoadGraphs(graphs) {
|
|
|
261
376
|
const hasAccess = graphs.some((graph) => graph.edgeAccess);
|
|
262
377
|
const mergedAccess = hasAccess ? accessMaskFor(edgeCount, () => true) : void 0;
|
|
263
378
|
const limitRows = /* @__PURE__ */ new Map();
|
|
379
|
+
const conditionRows = [];
|
|
380
|
+
const seenConditions = /* @__PURE__ */ new Set();
|
|
264
381
|
for (let index = 0; index < graphs.length; index += 1) {
|
|
265
382
|
const graph = graphs[index];
|
|
266
383
|
const edgeMap = edgeMaps[index];
|
|
@@ -286,6 +403,18 @@ function mergeRoadGraphs(graphs) {
|
|
|
286
403
|
limitRows.set(merged, existing);
|
|
287
404
|
}
|
|
288
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
|
+
}
|
|
289
418
|
const { turnRestrictionFromEdges: fromEdges, turnRestrictionToEdges: toEdges } = graph;
|
|
290
419
|
if (fromEdges && toEdges) {
|
|
291
420
|
for (let pair = 0; pair < fromEdges.length; pair += 1) {
|
|
@@ -307,6 +436,7 @@ function mergeRoadGraphs(graphs) {
|
|
|
307
436
|
}
|
|
308
437
|
}
|
|
309
438
|
const mergedLimits = packEdgeLimits([...limitRows].map(([edge, limits]) => ({ edge, limits })));
|
|
439
|
+
const mergedConditions = packEdgeConditions(conditionRows);
|
|
310
440
|
return withSpatialIndex({
|
|
311
441
|
nodeLatitudes: mergedLatitudes.slice(0, nodeCount),
|
|
312
442
|
nodeLongitudes: mergedLongitudes.slice(0, nodeCount),
|
|
@@ -319,6 +449,7 @@ function mergeRoadGraphs(graphs) {
|
|
|
319
449
|
...graphs[0].maxSpeedKph ? { maxSpeedKph: graphs[0].maxSpeedKph } : {},
|
|
320
450
|
...mergedAccess ? { edgeAccess: mergedAccess } : {},
|
|
321
451
|
...mergedLimits ? { edgeLimits: mergedLimits } : {},
|
|
452
|
+
...mergedConditions ? { edgeConditions: mergedConditions } : {},
|
|
322
453
|
...pointCount ? { edgeShapeOffsets, edgeShapeLatitudes, edgeShapeLongitudes } : {},
|
|
323
454
|
...bannedPairs.length ? packTurnRestrictions(bannedPairs) : {},
|
|
324
455
|
...bannedSequences.length ? packTurnSequences(bannedSequences) : {}
|
|
@@ -830,10 +961,15 @@ var RoadRouter = class _RoadRouter {
|
|
|
830
961
|
}
|
|
831
962
|
return new _RoadRouter(void 0, this.loader, this.options, wanted.id, this.bundleVehicle);
|
|
832
963
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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);
|
|
837
973
|
}
|
|
838
974
|
/**
|
|
839
975
|
* Downloads a corridor, routes, then asks whether any tile left out could
|
|
@@ -844,7 +980,39 @@ var RoadRouter = class _RoadRouter {
|
|
|
844
980
|
* graph contains. Because the test measures ground distance rather than
|
|
845
981
|
* counting neighbours, the answer does not depend on the tile size.
|
|
846
982
|
*/
|
|
847
|
-
|
|
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 = {}) {
|
|
848
1016
|
const straightLine = haversineMeters2(from, to);
|
|
849
1017
|
const ceiling = straightLine * (this.options.maxDetourFactor ?? Infinity);
|
|
850
1018
|
const selected = this.profileId ?? bundle.profileId;
|
|
@@ -867,7 +1035,7 @@ var RoadRouter = class _RoadRouter {
|
|
|
867
1035
|
for (const factor of [1, ...SEARCH_WIDENING_FACTORS]) {
|
|
868
1036
|
if (factor !== 1 && !await widen(factor === Infinity ? Infinity : budget * factor)) break;
|
|
869
1037
|
try {
|
|
870
|
-
best = route(graph, from, to, preference, { vehicle: this.vehicle });
|
|
1038
|
+
best = route(graph, from, to, preference, { vehicle: this.vehicle, ...when });
|
|
871
1039
|
break;
|
|
872
1040
|
} catch (error) {
|
|
873
1041
|
failure = error;
|
|
@@ -876,29 +1044,31 @@ var RoadRouter = class _RoadRouter {
|
|
|
876
1044
|
}
|
|
877
1045
|
if (!best) throw failure ?? new RouterError("NO_ROUTE", "No route connects the selected points.");
|
|
878
1046
|
for (let pass = 0; pass < MAX_CONFIRMATION_PASSES; pass += 1) {
|
|
879
|
-
if (!await widen(reachFor(best))) return best;
|
|
1047
|
+
if (!await widen(reachFor(best))) return { best, graphNow: () => graph, widen, reachFor };
|
|
880
1048
|
try {
|
|
881
|
-
best = route(graph, from, to, preference, { costBound: preference === "fastest" ? best.durationSeconds : best.distanceMeters, vehicle: this.vehicle });
|
|
1049
|
+
best = route(graph, from, to, preference, { costBound: preference === "fastest" ? best.durationSeconds : best.distanceMeters, vehicle: this.vehicle, ...when });
|
|
882
1050
|
} catch (error) {
|
|
883
1051
|
if (error.code !== "NO_ROUTE") throw error;
|
|
884
|
-
return best;
|
|
1052
|
+
return { best, graphNow: () => graph, widen, reachFor };
|
|
885
1053
|
}
|
|
886
1054
|
}
|
|
887
|
-
return best;
|
|
1055
|
+
return { best, graphNow: () => graph, widen, reachFor };
|
|
888
1056
|
}
|
|
889
|
-
async routeOverLoader(loader, from, to, preference) {
|
|
1057
|
+
async routeOverLoader(loader, from, to, preference, when = {}) {
|
|
890
1058
|
const graph = await loader.loadForRoute(from, to);
|
|
891
1059
|
try {
|
|
892
|
-
return route(graph, from, to, preference, { vehicle: this.vehicle });
|
|
1060
|
+
return route(graph, from, to, preference, { vehicle: this.vehicle, ...when });
|
|
893
1061
|
} catch (error) {
|
|
894
1062
|
if (!(error instanceof Error) || error.code !== "NO_ROUTE" || !loader.loadExpandedForRoute) throw error;
|
|
895
|
-
return route(await loader.loadExpandedForRoute(from, to), from, to, preference, { vehicle: this.vehicle });
|
|
1063
|
+
return route(await loader.loadExpandedForRoute(from, to), from, to, preference, { vehicle: this.vehicle, ...when });
|
|
896
1064
|
}
|
|
897
1065
|
}
|
|
898
1066
|
};
|
|
899
1067
|
export {
|
|
900
1068
|
BundleCompression,
|
|
1069
|
+
ConditionKind,
|
|
901
1070
|
DEFAULT_VEHICLE_PRESET,
|
|
1071
|
+
MAX_STORED_VALUES,
|
|
902
1072
|
ProfileId,
|
|
903
1073
|
RoadRouter,
|
|
904
1074
|
RouterError,
|
|
@@ -906,17 +1076,27 @@ export {
|
|
|
906
1076
|
VarintReader,
|
|
907
1077
|
VarintWriter,
|
|
908
1078
|
VehiclePresetId,
|
|
1079
|
+
accessAtTime,
|
|
1080
|
+
accessAvoiding,
|
|
909
1081
|
accessForVehicle,
|
|
910
1082
|
accessMaskFor,
|
|
911
1083
|
allowsEverything,
|
|
912
1084
|
buildTurnRestrictionIndex,
|
|
913
1085
|
buildTurnSequenceIndex,
|
|
914
1086
|
carRoutingProfile,
|
|
1087
|
+
checkAlternativeOptions,
|
|
915
1088
|
combineProfiledGraph,
|
|
916
1089
|
compactGraph,
|
|
917
1090
|
componentSizes,
|
|
918
1091
|
componentsFor,
|
|
1092
|
+
conditionHolds,
|
|
1093
|
+
conditionalDurations,
|
|
1094
|
+
conditionalValue,
|
|
1095
|
+
conditionsOf,
|
|
1096
|
+
conditionsOnEdge,
|
|
1097
|
+
countAvoided,
|
|
919
1098
|
countBlocked,
|
|
1099
|
+
countClosedAt,
|
|
920
1100
|
createEdgeSpatialIndex,
|
|
921
1101
|
createRouteGraphLoader,
|
|
922
1102
|
createSpatialIndex,
|
|
@@ -947,22 +1127,29 @@ export {
|
|
|
947
1127
|
motorcycleRoutingProfile,
|
|
948
1128
|
nearbyEdgeIds,
|
|
949
1129
|
openRoadGraphBundle,
|
|
1130
|
+
packEdgeConditions,
|
|
950
1131
|
packEdgeLimits,
|
|
951
1132
|
packTurnRestrictions,
|
|
952
1133
|
packTurnSequences,
|
|
1134
|
+
parseConditional,
|
|
1135
|
+
parseEdgeConditions,
|
|
953
1136
|
parseLengthMeters,
|
|
954
1137
|
parseMaxSpeedKph,
|
|
955
1138
|
parseWeightTonnes,
|
|
956
1139
|
profileOf,
|
|
957
1140
|
resolveVehicle,
|
|
958
1141
|
route,
|
|
1142
|
+
routeAlternatives,
|
|
959
1143
|
routingProfiles,
|
|
960
1144
|
searchBoundsFor,
|
|
961
1145
|
snapToGraph,
|
|
962
1146
|
straightLineAtMost,
|
|
963
1147
|
toDecimetres,
|
|
964
1148
|
toHalfTonnes,
|
|
1149
|
+
touchesArea,
|
|
1150
|
+
travelTimeFrom,
|
|
965
1151
|
truckRoutingProfile,
|
|
1152
|
+
unreadableConditions,
|
|
966
1153
|
unzigzag,
|
|
967
1154
|
vehiclePreset,
|
|
968
1155
|
vehiclePresetName,
|
package/dist/worker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
decodeGraph,
|
|
3
3
|
route
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QSIKDM4A.js";
|
|
5
5
|
|
|
6
6
|
// src/worker.ts
|
|
7
7
|
var graph = null;
|
|
@@ -13,7 +13,7 @@ self.onmessage = ({ data }) => {
|
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
if (!graph) throw new Error("Load a routing graph before requesting a route.");
|
|
16
|
-
self.postMessage({ id: data.id, ok: true, route: route(graph, data.from, data.to, data.preference) });
|
|
16
|
+
self.postMessage({ id: data.id, ok: true, route: route(graph, data.from, data.to, data.preference, { departAt: data.departAt, publicHoliday: data.publicHoliday }) });
|
|
17
17
|
} catch (error) {
|
|
18
18
|
self.postMessage({ id: data.id, ok: false, error: error instanceof Error ? error.message : "Routing failed" });
|
|
19
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadgraph-router-v2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Compile any OpenStreetMap PBF extract into compact, range-addressable routing bundles and route over them with A* in Node.js or the browser.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -87,6 +87,7 @@ function createTileGraph(tile, profileCount) {
|
|
|
87
87
|
edgeUsable: perProfile(() => new Column(Uint8Array)),
|
|
88
88
|
// Sparse: almost no road states a height or a weight.
|
|
89
89
|
edgeLimits: perProfile(() => new Map()),
|
|
90
|
+
edgeConditions: perProfile(() => []),
|
|
90
91
|
restrictions: perProfile(() => []),
|
|
91
92
|
runs: perProfile(() => []),
|
|
92
93
|
};
|
|
@@ -218,10 +219,20 @@ function encodeEfr2(graph, library, profiles) {
|
|
|
218
219
|
};
|
|
219
220
|
|
|
220
221
|
const split = profiles.length > 1 || profiles.some((entry) => entry.usesPhysicalLimits);
|
|
221
|
-
if (!split)
|
|
222
|
+
if (!split) {
|
|
223
|
+
// One vehicle writes one chunk, and that chunk is its whole graph, so the
|
|
224
|
+
// conditional restrictions belong in it. Without this a bundle compiled
|
|
225
|
+
// for a single vehicle would read as though no road were ever shut.
|
|
226
|
+
const onlyConditions = library.packEdgeConditions(
|
|
227
|
+
graph.edgeConditions[0].map((row) => ({ ...row, edge: placement[row.edge] })),
|
|
228
|
+
);
|
|
229
|
+
const encoded = library.encodeCompactGraph(onlyConditions ? { ...assembled, edgeConditions: onlyConditions } : assembled);
|
|
230
|
+
return { shared: encoded, profiles: [], limitedEdges: 0, conditionalEdges: onlyConditions?.edges.length ?? 0 };
|
|
231
|
+
}
|
|
222
232
|
|
|
223
233
|
const restrictionCount = banned.length + runs.length;
|
|
224
234
|
let limitedEdges = 0;
|
|
235
|
+
let conditionalEdges = 0;
|
|
225
236
|
const chunks = profiles.map((entry, profileIndex) => {
|
|
226
237
|
const usable = graph.edgeUsable[profileIndex].view();
|
|
227
238
|
// `placement` maps a built edge to its stored position, so the mask is
|
|
@@ -233,6 +244,10 @@ function encodeEfr2(graph, library, profiles) {
|
|
|
233
244
|
for (const [edge, limits] of graph.edgeLimits[profileIndex]) entries.push({ edge: placement[edge], limits });
|
|
234
245
|
const edgeLimits = library.packEdgeLimits(entries);
|
|
235
246
|
limitedEdges += edgeLimits?.edges.length ?? 0;
|
|
247
|
+
const edgeConditions = library.packEdgeConditions(
|
|
248
|
+
graph.edgeConditions[profileIndex].map((row) => ({ ...row, edge: placement[row.edge] })),
|
|
249
|
+
);
|
|
250
|
+
conditionalEdges += edgeConditions?.edges.length ?? 0;
|
|
236
251
|
const restrictionMask = library.accessMaskFor(restrictionCount, (index) => index < banned.length
|
|
237
252
|
? pairOwners[pairOrder[index]].has(profileIndex)
|
|
238
253
|
: runOwners[runOrder[index - banned.length]].has(profileIndex));
|
|
@@ -242,11 +257,12 @@ function encodeEfr2(graph, library, profiles) {
|
|
|
242
257
|
edgeDurationScale: durationScales[profileIndex],
|
|
243
258
|
edgeDurationsSeconds: durationColumns[profileIndex],
|
|
244
259
|
edgeAccess,
|
|
260
|
+
...(edgeConditions ? { edgeConditions } : {}),
|
|
245
261
|
...(edgeLimits ? { edgeLimits } : {}),
|
|
246
262
|
...(restrictionCount ? { restrictionCount, restrictionMask } : {}),
|
|
247
263
|
});
|
|
248
264
|
});
|
|
249
|
-
return { shared: library.encodeSharedGraph(assembled), profiles: chunks, limitedEdges };
|
|
265
|
+
return { shared: library.encodeSharedGraph(assembled), profiles: chunks, limitedEdges, conditionalEdges };
|
|
250
266
|
}
|
|
251
267
|
|
|
252
268
|
/** Writes the fixed-size directory, then streams each chunk straight from the
|
|
@@ -314,7 +330,7 @@ function memoryGuard(limitBytes, label, count) { if (count % 1_000 !== 0) return
|
|
|
314
330
|
async function firstPass(file, roadFile, limitBytes, profiles) {
|
|
315
331
|
const required = approximateIds(), keeperNodes = approximateIds(), stream = createWriteStream(roadFile), bounds = { minLat: Infinity, minLon: Infinity, maxLat: -Infinity, maxLon: -Infinity };
|
|
316
332
|
const restrictions = [], restrictionWays = new Set(), barriers = [];
|
|
317
|
-
let ways = 0, entities = 0, skippedRestrictions = 0, barrierCount = 0;
|
|
333
|
+
let ways = 0, entities = 0, skippedRestrictions = 0, barrierCount = 0, conditionalRestrictions = 0;
|
|
318
334
|
// One pass serves every vehicle: the reader is asked for the union of the
|
|
319
335
|
// tags they read, and each way is offered to all of them.
|
|
320
336
|
const wayTags = [...new Set(profiles.flatMap((entry) => entry.wayTagKeys))];
|
|
@@ -347,6 +363,10 @@ async function firstPass(file, roadFile, limitBytes, profiles) {
|
|
|
347
363
|
const perProfile = profiles.map((entry) => entry.evaluateTurnRestriction(entity.tags ?? {}));
|
|
348
364
|
const parsed = perProfile.find(Boolean) ?? null;
|
|
349
365
|
if (!parsed) continue;
|
|
366
|
+
// A turn banned only at certain hours is banned at every hour here.
|
|
367
|
+
// Counting them says how much of a region that costs rather than
|
|
368
|
+
// leaving it to be discovered: in Portugal it is 1 relation in 26,599.
|
|
369
|
+
if (Object.keys(entity.tags ?? {}).some((key) => key.endsWith(":conditional"))) conditionalRestrictions += 1;
|
|
350
370
|
const members = entity.members ?? [];
|
|
351
371
|
const from = members.find((member) => member.role === "from" && member.type === "way");
|
|
352
372
|
const to = members.find((member) => member.role === "to" && member.type === "way");
|
|
@@ -378,8 +398,8 @@ async function firstPass(file, roadFile, limitBytes, profiles) {
|
|
|
378
398
|
keeperNodes.add(refs[0]); keeperNodes.add(refs[refs.length - 1]);
|
|
379
399
|
await write(stream, {
|
|
380
400
|
id: entity.id, refs,
|
|
381
|
-
forward: lanes.map((lane) => lane?.forward ? { speed: lane.forwardSpeedKph, limits: lane.forwardLimits ?? null } : null),
|
|
382
|
-
reverse: lanes.map((lane) => lane?.backward ? { speed: lane.backwardSpeedKph, limits: lane.backwardLimits ?? null } : null),
|
|
401
|
+
forward: lanes.map((lane) => lane?.forward ? { speed: lane.forwardSpeedKph, limits: lane.forwardLimits ?? null, conditions: lane.conditions ?? null } : null),
|
|
402
|
+
reverse: lanes.map((lane) => lane?.backward ? { speed: lane.backwardSpeedKph, limits: lane.backwardLimits ?? null, conditions: lane.conditions ?? null } : null),
|
|
383
403
|
});
|
|
384
404
|
}
|
|
385
405
|
await new Promise((resolve, reject) => stream.end((error) => error ? reject(error) : resolve()));
|
|
@@ -390,7 +410,7 @@ async function firstPass(file, roadFile, limitBytes, profiles) {
|
|
|
390
410
|
// A barrier can only matter where a road passes through it, and the node
|
|
391
411
|
// has to survive simplification for the ban to have edges to name.
|
|
392
412
|
for (const blocked of barriers) { if (blocked) for (const id of blocked) if (required.has(id)) keeperNodes.add(id); }
|
|
393
|
-
return { required, keeperNodes, bounds, ways, restrictions: usable, restrictionWays, skippedRestrictions, barriers, barrierCount };
|
|
413
|
+
return { required, keeperNodes, bounds, ways, restrictions: usable, restrictionWays, skippedRestrictions, conditionalRestrictions, barriers, barrierCount };
|
|
394
414
|
}
|
|
395
415
|
|
|
396
416
|
async function secondPass(file, required, limitBytes, databasePath) {
|
|
@@ -522,6 +542,7 @@ async function build(roadFile, coordinates, topology, layout, bounds, tolerance,
|
|
|
522
542
|
graph.edgeDurations[profileIndex].push(distance / (speed * (1000 / 3600)));
|
|
523
543
|
graph.edgeUsable[profileIndex].push(lane ? 1 : 0);
|
|
524
544
|
if (lane?.limits) graph.edgeLimits[profileIndex].set(index, lane.limits);
|
|
545
|
+
if (lane?.conditions) for (const condition of lane.conditions) graph.edgeConditions[profileIndex].push({ edge: index, ...condition });
|
|
525
546
|
}
|
|
526
547
|
graph.edgeWay.push(tracked ? wayId : 0);
|
|
527
548
|
graph.edgeShapeStart.push(graph.shapeLat.length);
|
|
@@ -638,12 +659,14 @@ else {
|
|
|
638
659
|
// so peak memory tracks the largest single tile rather than the region.
|
|
639
660
|
const spillPath = resolve(workspace, "chunks.bin"), spill = createWriteStream(spillPath), chunks = [];
|
|
640
661
|
let limitedEdges = 0;
|
|
662
|
+
let conditionalEdges = 0;
|
|
641
663
|
for (let index = 0; index < built.graphs.length; index += 1) {
|
|
642
664
|
const graph = built.graphs[index];
|
|
643
665
|
const encoded = encodeEfr2(graph, library, profiles);
|
|
644
666
|
// The shared half first, then each vehicle's: the reader needs a tile's
|
|
645
667
|
// shared row before the rows that depend on it.
|
|
646
668
|
limitedEdges += encoded.limitedEdges ?? 0;
|
|
669
|
+
conditionalEdges += encoded.conditionalEdges ?? 0;
|
|
647
670
|
for (const [profileId, buffer] of [[0, encoded.shared], ...encoded.profiles.map((chunk, at) => [profiles[at].id, chunk])]) {
|
|
648
671
|
const raw = new Uint8Array(buffer);
|
|
649
672
|
const data = compression === "gzip" ? gzipSync(raw, { level: constants.Z_BEST_COMPRESSION }) : raw;
|
|
@@ -658,7 +681,7 @@ else {
|
|
|
658
681
|
const bundleBytes = await writeBundle(output, chunks, compression, spillPath, profiles, split, defaultVehicle, library.VehiclePresetId);
|
|
659
682
|
await verifyBundle(output, built.graphs.length);
|
|
660
683
|
const nodesBefore = storedNodes, nodesAfter = built.routingNodes, edges = built.routingEdges, rawBytes = chunks.reduce((sum, chunk) => sum + chunk.rawBytes, 0);
|
|
661
|
-
const report = { formatVersion: 1, input: file, inputBytes: source.size, profile: profiles.map((entry) => entry.name).join(","), compression: `${compression}-per-tile`, tileSizeKm, paddingKm, pbfPasses: 2, tiles: built.graphs.length, relevantOsmWays: first.ways, relevantOsmNodes, relevantOsmNodesExact: false, routingNodesBeforeSimplification: nodesBefore, routingNodesAfterSimplification: nodesAfter, reductionPercent: Number((100 * (1 - nodesAfter / Math.max(1, nodesBefore))).toFixed(2)), routingEdges: edges, turnRestrictionsRead: first.restrictions.length, turnRestrictionsSkipped: first.skippedRestrictions, barriersRead: first.barrierCount, barrierBans: built.barrierBans, bannedTurnPairs: built.bannedTurns, bannedTurnRuns: built.bannedRuns, layout: split ? "shared-plus-profile" : "single-chunk", profiles: profiles.map((entry) => entry.name), defaultVehicle: defaultVehicle ?? null, edgesWithPhysicalLimits: limitedEdges, uncompressedGraphBytes: rawBytes, bundleBytes, compressionRatio: Number((bundleBytes / Math.max(1, rawBytes)).toFixed(4)), peakRssBytes: Math.max(observedPeakRss, process.memoryUsage().rss), elapsedMilliseconds: Math.round(performance.now() - started) };
|
|
684
|
+
const report = { formatVersion: 1, input: file, inputBytes: source.size, profile: profiles.map((entry) => entry.name).join(","), compression: `${compression}-per-tile`, tileSizeKm, paddingKm, pbfPasses: 2, tiles: built.graphs.length, relevantOsmWays: first.ways, relevantOsmNodes, relevantOsmNodesExact: false, routingNodesBeforeSimplification: nodesBefore, routingNodesAfterSimplification: nodesAfter, reductionPercent: Number((100 * (1 - nodesAfter / Math.max(1, nodesBefore))).toFixed(2)), routingEdges: edges, turnRestrictionsRead: first.restrictions.length, turnRestrictionsSkipped: first.skippedRestrictions, conditionalTurnRestrictionsIgnored: first.conditionalRestrictions, barriersRead: first.barrierCount, barrierBans: built.barrierBans, bannedTurnPairs: built.bannedTurns, bannedTurnRuns: built.bannedRuns, layout: split ? "shared-plus-profile" : "single-chunk", profiles: profiles.map((entry) => entry.name), defaultVehicle: defaultVehicle ?? null, edgesWithPhysicalLimits: limitedEdges, edgesWithConditionalRestrictions: conditionalEdges, uncompressedGraphBytes: rawBytes, bundleBytes, compressionRatio: Number((bundleBytes / Math.max(1, rawBytes)).toFixed(4)), peakRssBytes: Math.max(observedPeakRss, process.memoryUsage().rss), elapsedMilliseconds: Math.round(performance.now() - started) };
|
|
662
685
|
await writeFile(`${output}.json`, `${JSON.stringify(report, null, 2)}\n`);
|
|
663
686
|
console.table({ input_mb: (source.size / 1e6).toFixed(1), pbf_passes: report.pbfPasses, relevant_ways: first.ways.toLocaleString(), relevant_nodes: relevantOsmNodes.toLocaleString(), nodes_before: nodesBefore.toLocaleString(), nodes_after: nodesAfter.toLocaleString(), reduction_percent: report.reductionPercent, routing_edges: edges.toLocaleString(), turn_restrictions: first.restrictions.length.toLocaleString(), barriers: first.barrierCount.toLocaleString(), banned_turn_pairs: built.bannedTurns.toLocaleString(), banned_turn_runs: built.bannedRuns.toLocaleString(), tiles: built.graphs.length, raw_mb: (rawBytes / 1e6).toFixed(1), efrb_mb: (bundleBytes / 1e6).toFixed(1), compression_ratio: report.compressionRatio, peak_rss_mb: (report.peakRssBytes / 1e6).toFixed(1), elapsed_seconds: (report.elapsedMilliseconds / 1000).toFixed(1) });
|
|
664
687
|
} catch (error) {
|