terra-route 0.0.13 → 0.0.17
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 +105 -0
- package/README.md +6 -6
- package/dist/heap/four-ary-heap.d.ts +2 -0
- package/dist/terra-route.cjs +1 -1
- package/dist/terra-route.cjs.map +1 -1
- package/dist/terra-route.d.ts +25 -1
- package/dist/terra-route.modern.js +1 -1
- package/dist/terra-route.modern.js.map +1 -1
- package/dist/terra-route.module.js +1 -1
- package/dist/terra-route.module.js.map +1 -1
- package/dist/terra-route.umd.js +1 -1
- package/dist/terra-route.umd.js.map +1 -1
- package/fta.config.json +9 -0
- package/jest.config.js +1 -1
- package/package.json +35 -8
- package/scripts/bump.mjs +57 -0
- package/scripts/release.mjs +30 -0
- package/scripts/versionrc.cjs +26 -0
- package/src/distance/haversine.ts +13 -15
- package/src/heap/fibonacci-heap.ts +9 -0
- package/src/heap/four-ary-heap.ts +10 -0
- package/src/heap/heap.d.ts +5 -0
- package/src/heap/min-heap.ts +9 -0
- package/src/heap/pairing-heap.ts +5 -0
- package/src/terra-route.compare.spec.ts +4 -5
- package/src/terra-route.spec.ts +81 -0
- package/src/terra-route.ts +282 -196
- package/dist/fibonacci-heap.d.ts +0 -11
- package/dist/graph/graph.d.ts +0 -114
- package/dist/graph/methods/bounding-box.d.ts +0 -13
- package/dist/graph/methods/connected.d.ts +0 -9
- package/dist/graph/methods/duplicates.d.ts +0 -7
- package/dist/graph/methods/leaf.d.ts +0 -12
- package/dist/graph/methods/nodes.d.ts +0 -17
- package/dist/graph/methods/spatial-index/geokdbush.d.ts +0 -3
- package/dist/graph/methods/spatial-index/kdbush.d.ts +0 -16
- package/dist/graph/methods/spatial-index/tinyqueue.d.ts +0 -11
- package/dist/graph/methods/unify.d.ts +0 -2
- package/dist/graph/methods/unique-segments.d.ts +0 -5
- package/dist/graph/methods/unique.d.ts +0 -5
- package/dist/heap/min-heap.d.ts +0 -9
- package/dist/min-heap.d.ts +0 -8
- package/dist/test-utils/utils.d.ts +0 -50
- package/instructions.md +0 -13
package/src/terra-route.spec.ts
CHANGED
|
@@ -836,6 +836,87 @@ describe("TerraRoute", () => {
|
|
|
836
836
|
expect(result!.geometry.coordinates.at(-1)).toEqual([2, 0]);
|
|
837
837
|
});
|
|
838
838
|
|
|
839
|
+
|
|
840
|
+
it("fails gracefully when a custom heap returns null unexpectedly", () => {
|
|
841
|
+
class NullingHeap {
|
|
842
|
+
private items: Array<{ key: number; value: number }> = [];
|
|
843
|
+
|
|
844
|
+
insert(key: number, value: number) {
|
|
845
|
+
this.items.push({ key, value });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
extractMin() {
|
|
849
|
+
if (this.items.length === 0) {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
size() {
|
|
856
|
+
return this.items.length;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const heapConstructor = NullingHeap as unknown as new () => {
|
|
861
|
+
insert: (key: number, value: number) => void;
|
|
862
|
+
extractMin: () => number | null;
|
|
863
|
+
size: () => number;
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
const network = createFeatureCollection([
|
|
867
|
+
createLineStringFeature([
|
|
868
|
+
[0, 0],
|
|
869
|
+
[1, 0],
|
|
870
|
+
[2, 0],
|
|
871
|
+
]),
|
|
872
|
+
]);
|
|
873
|
+
|
|
874
|
+
const rf = new TerraRoute({ heap: heapConstructor });
|
|
875
|
+
|
|
876
|
+
expect(() => rf.buildRouteGraph(network)).not.toThrow();
|
|
877
|
+
|
|
878
|
+
const result = rf.getRoute(
|
|
879
|
+
createPointFeature([0, 0]),
|
|
880
|
+
createPointFeature([2, 0]),
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
expect(result).toBeNull();
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
it("routes correctly when ALT landmarks are in another disconnected component", () => {
|
|
888
|
+
// Component A is listed first so ALT landmark selection starts here.
|
|
889
|
+
// Query is in component B, where landmark distances become Infinity and
|
|
890
|
+
// the ALT lower bound should safely degrade to 0 (Dijkstra behavior).
|
|
891
|
+
const network = createFeatureCollection([
|
|
892
|
+
createLineStringFeature([
|
|
893
|
+
[0, 0],
|
|
894
|
+
[1, 0],
|
|
895
|
+
[2, 0],
|
|
896
|
+
]),
|
|
897
|
+
createLineStringFeature([
|
|
898
|
+
[100, 0],
|
|
899
|
+
[101, 0],
|
|
900
|
+
[102, 0],
|
|
901
|
+
]),
|
|
902
|
+
]);
|
|
903
|
+
|
|
904
|
+
routeFinder.buildRouteGraph(network);
|
|
905
|
+
|
|
906
|
+
const start = createPointFeature([100, 0]);
|
|
907
|
+
const end = createPointFeature([102, 0]);
|
|
908
|
+
|
|
909
|
+
const result = routeFinder.getRoute(start, end);
|
|
910
|
+
|
|
911
|
+
expect(result).not.toBeNull();
|
|
912
|
+
expect(result!.geometry.coordinates).toEqual([
|
|
913
|
+
[100, 0],
|
|
914
|
+
[101, 0],
|
|
915
|
+
[102, 0],
|
|
916
|
+
]);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
|
|
839
920
|
describe('returns null if', () => {
|
|
840
921
|
it("start and end are the same", () => {
|
|
841
922
|
const network = createFeatureCollection([
|
package/src/terra-route.ts
CHANGED
|
@@ -26,19 +26,26 @@ class TerraRoute implements Router {
|
|
|
26
26
|
private csrIndices: Int32Array | null = null; // Column indices: neighbor node IDs, length = totalEdges
|
|
27
27
|
private csrDistances: Float64Array | null = null; // Edge weights aligned to csrIndices, length = totalEdges
|
|
28
28
|
private csrNodeCount = 0; // Number of nodes captured in the CSR arrays
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// ALT-style landmark heuristic data (query-time lower bounds via triangle inequality)
|
|
30
|
+
private landmarkNodeCount = 0;
|
|
31
|
+
private landmarkCount = 0;
|
|
32
|
+
private landmarkDistancesFlat: Float64Array | null = null; // Layout: [landmark0 distances..., landmark1 distances...]
|
|
33
|
+
private readonly maxLandmarks = 4;
|
|
34
|
+
private landmarksDirty = true;
|
|
35
|
+
|
|
36
|
+
// Reusable typed scratch buffers for shortest-path search
|
|
31
37
|
private gScoreScratch: Float64Array | null = null; // gScore per node (cost from start)
|
|
32
38
|
private cameFromScratch: Int32Array | null = null; // Predecessor per node for path reconstruction
|
|
33
39
|
private visitedScratch: Uint8Array | null = null; // Visited set to avoid reprocessing
|
|
34
|
-
private
|
|
35
|
-
//
|
|
36
|
-
private
|
|
37
|
-
private cameFromRevScratch: Int32Array | null = null; // Successor per node (next step toward the end)
|
|
38
|
-
private visitedRevScratch: Uint8Array | null = null; // Visited set for reverse search
|
|
39
|
-
private hRevScratch: Float64Array | null = null; // Heuristic cache for reverse direction per query
|
|
40
|
+
private heuristicScratch: Float64Array | null = null; // Cached h(node) per query for A*
|
|
41
|
+
private heuristicStampScratch: Uint32Array | null = null; // Query-stamp per node for heuristic cache validity
|
|
42
|
+
private heuristicQueryStamp = 1; // Monotonic stamp to avoid clearing heuristic cache each query
|
|
40
43
|
private scratchCapacity = 0; // Current capacity of scratch arrays
|
|
41
44
|
|
|
45
|
+
// Reused open set to avoid heap allocations during repeated getRoute calls.
|
|
46
|
+
// (If a custom heap doesn't implement `clear()`, we fall back to constructing anew.)
|
|
47
|
+
private openForward: InstanceType<HeapConstructor> | null = null;
|
|
48
|
+
|
|
42
49
|
constructor(options?: {
|
|
43
50
|
distanceMeasurement?: (a: Position, b: Position) => number; // Optional distance function override
|
|
44
51
|
heap?: HeapConstructor; // Optional heap implementation override
|
|
@@ -63,21 +70,40 @@ class TerraRoute implements Router {
|
|
|
63
70
|
this.csrIndices = null;
|
|
64
71
|
this.csrDistances = null;
|
|
65
72
|
this.csrNodeCount = 0;
|
|
73
|
+
this.landmarkNodeCount = 0;
|
|
74
|
+
this.landmarkCount = 0;
|
|
75
|
+
this.landmarkDistancesFlat = null;
|
|
76
|
+
this.landmarksDirty = true;
|
|
66
77
|
|
|
67
78
|
// Hoist to locals for speed (avoid repeated property lookups in hot loops)
|
|
68
79
|
const coordIndexMapLocal = this.coordinateIndexMap; // Local alias for coord map
|
|
69
80
|
const coordsLocal = this.coordinates; // Local alias for coordinates array
|
|
70
81
|
const measureDistance = this.distanceMeasurement; // Local alias for distance function
|
|
71
82
|
|
|
72
|
-
//
|
|
83
|
+
// Assign indices, count degrees and capture edges in one pass
|
|
73
84
|
const degree: number[] = []; // Dynamic degree array; grows as nodes are discovered
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
const edgeFrom: number[] = [];
|
|
86
|
+
const edgeTo: number[] = [];
|
|
87
|
+
const edgeDistance: number[] = [];
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
const features = network.features;
|
|
90
|
+
for (let f = 0, featureLength = features.length; f < featureLength; f++) {
|
|
91
|
+
const lineCoords = features[f].geometry.coordinates;
|
|
92
|
+
for (let i = 0, segmentLength = lineCoords.length - 1; i < segmentLength; i++) {
|
|
93
|
+
const from = lineCoords[i] as Position;
|
|
94
|
+
const to = lineCoords[i + 1] as Position;
|
|
95
|
+
|
|
96
|
+
const indexA = this.indexCoordinate(from, coordIndexMapLocal, coordsLocal);
|
|
97
|
+
const indexB = this.indexCoordinate(to, coordIndexMapLocal, coordsLocal);
|
|
98
|
+
|
|
99
|
+
degree[indexA] = (degree[indexA] ?? 0) + 1;
|
|
100
|
+
degree[indexB] = (degree[indexB] ?? 0) + 1;
|
|
101
|
+
|
|
102
|
+
edgeFrom.push(indexA);
|
|
103
|
+
edgeTo.push(indexB);
|
|
104
|
+
edgeDistance.push(measureDistance(from, to));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
81
107
|
|
|
82
108
|
// Build CSR arrays from degree counts
|
|
83
109
|
const nodeCount = this.coordinates.length; // Total nodes discovered
|
|
@@ -91,14 +117,12 @@ class TerraRoute implements Router {
|
|
|
91
117
|
const indices = new Int32Array(totalEdges); // Neighbor indices array
|
|
92
118
|
const distances = new Float64Array(totalEdges); // Distances array aligned to indices
|
|
93
119
|
|
|
94
|
-
//
|
|
120
|
+
// Fill CSR arrays using a write cursor per node
|
|
95
121
|
const cursor = offsets.slice(); // Current write positions per node
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const segmentDistance = measureDistance(a, b); // Edge weight once
|
|
122
|
+
for (let i = 0, edgeLength = edgeFrom.length; i < edgeLength; i++) {
|
|
123
|
+
const indexA = edgeFrom[i];
|
|
124
|
+
const indexB = edgeTo[i];
|
|
125
|
+
const segmentDistance = edgeDistance[i];
|
|
102
126
|
|
|
103
127
|
let pos = cursor[indexA]++;
|
|
104
128
|
indices[pos] = indexB;
|
|
@@ -106,7 +130,7 @@ class TerraRoute implements Router {
|
|
|
106
130
|
pos = cursor[indexB]++;
|
|
107
131
|
indices[pos] = indexA;
|
|
108
132
|
distances[pos] = segmentDistance;
|
|
109
|
-
}
|
|
133
|
+
}
|
|
110
134
|
|
|
111
135
|
// Commit CSR to instance
|
|
112
136
|
this.csrOffsets = offsets;
|
|
@@ -228,6 +252,10 @@ class TerraRoute implements Router {
|
|
|
228
252
|
this.csrIndices = indices;
|
|
229
253
|
this.csrDistances = distances;
|
|
230
254
|
this.csrNodeCount = nodeCount;
|
|
255
|
+
this.landmarksDirty = true;
|
|
256
|
+
this.landmarkNodeCount = 0;
|
|
257
|
+
this.landmarkCount = 0;
|
|
258
|
+
this.landmarkDistancesFlat = null;
|
|
231
259
|
|
|
232
260
|
// Keep adjacency list for *future* dynamic additions, but clear existing edges to avoid duplication.
|
|
233
261
|
this.adjacencyList = new Array(nodeCount);
|
|
@@ -260,204 +288,154 @@ class TerraRoute implements Router {
|
|
|
260
288
|
}
|
|
261
289
|
|
|
262
290
|
// Local aliases
|
|
263
|
-
const
|
|
264
|
-
const
|
|
291
|
+
const coordinates = this.coordinates; // Alias to coordinates array
|
|
292
|
+
const adjacency = this.adjacencyList; // Alias to sparse adjacency list (for dynamic nodes)
|
|
293
|
+
const csrOffsets = this.csrOffsets;
|
|
294
|
+
const csrIndices = this.csrIndices;
|
|
295
|
+
const csrDistances = this.csrDistances;
|
|
296
|
+
const csrNodeCount = this.csrNodeCount;
|
|
297
|
+
const hasCsr = !!csrOffsets; // indices/distances should exist whenever offsets exist
|
|
298
|
+
const PositiveInfinity = Number.POSITIVE_INFINITY;
|
|
299
|
+
const measureDistance = this.distanceMeasurement;
|
|
300
|
+
const endCoordinates = end.geometry.coordinates;
|
|
301
|
+
|
|
302
|
+
this.ensureLandmarkHeuristicData();
|
|
303
|
+
const landmarkDistancesFlat = this.landmarkDistancesFlat;
|
|
304
|
+
const landmarkNodeCount = this.landmarkNodeCount;
|
|
305
|
+
const landmarkCount = this.landmarkCount;
|
|
265
306
|
|
|
266
307
|
// Ensure and init scratch buffers
|
|
267
|
-
const nodeCount =
|
|
308
|
+
const nodeCount = coordinates.length; // Current number of nodes (may be >= csrNodeCount if new nodes added)
|
|
268
309
|
this.ensureScratch(nodeCount); // Allocate scratch arrays if needed
|
|
269
310
|
|
|
270
311
|
// Non-null after ensure
|
|
271
|
-
const gF = this.gScoreScratch!; //
|
|
272
|
-
const
|
|
273
|
-
const prevF = this.cameFromScratch!; // predecessor in forward search
|
|
274
|
-
const nextR = this.cameFromRevScratch!; // successor in reverse search (toward end)
|
|
312
|
+
const gF = this.gScoreScratch!; // gScore from start
|
|
313
|
+
const prevF = this.cameFromScratch!; // predecessor for reconstruction
|
|
275
314
|
const visF = this.visitedScratch!;
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
const hR = this.hRevScratch!;
|
|
315
|
+
const heuristic = this.heuristicScratch!;
|
|
316
|
+
const heuristicStamp = this.heuristicStampScratch!;
|
|
279
317
|
|
|
280
|
-
gF.fill(
|
|
281
|
-
gR.fill(Number.POSITIVE_INFINITY, 0, nodeCount);
|
|
318
|
+
gF.fill(PositiveInfinity, 0, nodeCount);
|
|
282
319
|
prevF.fill(-1, 0, nodeCount);
|
|
283
|
-
nextR.fill(-1, 0, nodeCount);
|
|
284
320
|
visF.fill(0, 0, nodeCount);
|
|
285
|
-
visR.fill(0, 0, nodeCount);
|
|
286
|
-
hF.fill(-1, 0, nodeCount);
|
|
287
|
-
hR.fill(-1, 0, nodeCount);
|
|
288
321
|
|
|
289
|
-
|
|
290
|
-
|
|
322
|
+
// Increment query stamp for heuristic cache validity; handle wraparound.
|
|
323
|
+
let queryStamp = (this.heuristicQueryStamp + 1) >>> 0;
|
|
324
|
+
if (queryStamp === 0) {
|
|
325
|
+
heuristicStamp.fill(0, 0, nodeCount);
|
|
326
|
+
queryStamp = 1;
|
|
327
|
+
}
|
|
328
|
+
this.heuristicQueryStamp = queryStamp;
|
|
291
329
|
|
|
292
|
-
const
|
|
293
|
-
|
|
330
|
+
const getHeuristic = (node: number): number => {
|
|
331
|
+
if (heuristicStamp[node] !== queryStamp) {
|
|
332
|
+
heuristicStamp[node] = queryStamp;
|
|
294
333
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
openF.insert(0, startIndex);
|
|
301
|
-
openR.insert(0, endIndex);
|
|
302
|
-
|
|
303
|
-
// Best meeting point found so far
|
|
304
|
-
let bestPathCost = Number.POSITIVE_INFINITY;
|
|
305
|
-
let meetingNode = -1;
|
|
306
|
-
|
|
307
|
-
// Main bidirectional loop: expand alternately.
|
|
308
|
-
// Without a heap peek, a safe and effective stopping rule is based on the last extracted keys:
|
|
309
|
-
// once min_g_forward + min_g_reverse >= bestPathCost, no shorter path can still be found.
|
|
310
|
-
let lastExtractedGForward = 0;
|
|
311
|
-
let lastExtractedGReverse = 0;
|
|
312
|
-
while (openF.size() > 0 && openR.size() > 0) {
|
|
313
|
-
if (meetingNode >= 0 && (lastExtractedGForward + lastExtractedGReverse) >= bestPathCost) {
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
334
|
+
if (landmarkDistancesFlat && landmarkCount > 0 && node < landmarkNodeCount && endIndex < landmarkNodeCount) {
|
|
335
|
+
let lowerBound = 0;
|
|
336
|
+
for (let l = 0, offset = 0; l < landmarkCount; l++, offset += landmarkNodeCount) {
|
|
337
|
+
const distanceToNode = landmarkDistancesFlat[offset + node];
|
|
338
|
+
const distanceToEnd = landmarkDistancesFlat[offset + endIndex];
|
|
316
339
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (expandForward) {
|
|
321
|
-
const current = openF.extractMin()!;
|
|
322
|
-
if (visF[current] !== 0) continue;
|
|
323
|
-
lastExtractedGForward = gF[current];
|
|
324
|
-
visF[current] = 1;
|
|
325
|
-
|
|
326
|
-
// If reverse has finalized this node, we have a candidate meeting.
|
|
327
|
-
if (visR[current] !== 0) {
|
|
328
|
-
const total = gF[current] + gR[current];
|
|
329
|
-
if (total < bestPathCost) {
|
|
330
|
-
bestPathCost = total;
|
|
331
|
-
meetingNode = current;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
340
|
+
if (!Number.isFinite(distanceToNode) || !Number.isFinite(distanceToEnd)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
334
343
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const nbNode = csrIndices[i];
|
|
342
|
-
const tentativeG = gF[current] + csrDistances[i];
|
|
343
|
-
if (tentativeG < gF[nbNode]) {
|
|
344
|
-
gF[nbNode] = tentativeG;
|
|
345
|
-
prevF[nbNode] = current;
|
|
346
|
-
const otherG = gR[nbNode];
|
|
347
|
-
if (otherG !== Number.POSITIVE_INFINITY) {
|
|
348
|
-
const total = tentativeG + otherG;
|
|
349
|
-
if (total < bestPathCost) { bestPathCost = total; meetingNode = nbNode; }
|
|
350
|
-
}
|
|
351
|
-
openF.insert(tentativeG, nbNode);
|
|
344
|
+
const landmarkLowerBound = distanceToEnd >= distanceToNode
|
|
345
|
+
? distanceToEnd - distanceToNode
|
|
346
|
+
: distanceToNode - distanceToEnd;
|
|
347
|
+
|
|
348
|
+
if (landmarkLowerBound > lowerBound) {
|
|
349
|
+
lowerBound = landmarkLowerBound;
|
|
352
350
|
}
|
|
353
351
|
}
|
|
352
|
+
heuristic[node] = lowerBound;
|
|
354
353
|
} else {
|
|
355
|
-
|
|
356
|
-
if (neighbors && neighbors.length) {
|
|
357
|
-
for (let i = 0, n = neighbors.length; i < n; i++) {
|
|
358
|
-
const nb = neighbors[i];
|
|
359
|
-
const nbNode = nb.node;
|
|
360
|
-
const tentativeG = gF[current] + nb.distance;
|
|
361
|
-
if (tentativeG < gF[nbNode]) {
|
|
362
|
-
gF[nbNode] = tentativeG;
|
|
363
|
-
prevF[nbNode] = current;
|
|
364
|
-
const otherG = gR[nbNode];
|
|
365
|
-
if (otherG !== Number.POSITIVE_INFINITY) {
|
|
366
|
-
const total = tentativeG + otherG;
|
|
367
|
-
if (total < bestPathCost) { bestPathCost = total; meetingNode = nbNode; }
|
|
368
|
-
}
|
|
369
|
-
openF.insert(tentativeG, nbNode);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
354
|
+
heuristic[node] = measureDistance(coordinates[node], endCoordinates);
|
|
373
355
|
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
356
|
+
}
|
|
357
|
+
return heuristic[node];
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Prefer reusing heaps if supported.
|
|
361
|
+
const openFReuse = this.openForward ?? (this.openForward = new this.heapConstructor());
|
|
362
|
+
const openFAny = openFReuse as unknown as { clear?: () => void };
|
|
363
|
+
const canReuse = !!openFAny.clear;
|
|
364
|
+
|
|
365
|
+
const openF2 = canReuse ? openFReuse : new this.heapConstructor();
|
|
366
|
+
|
|
367
|
+
if (canReuse) {
|
|
368
|
+
openFAny.clear!();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
gF[startIndex] = 0;
|
|
372
|
+
openF2.insert(getHeuristic(startIndex), startIndex);
|
|
373
|
+
|
|
374
|
+
while (openF2.size() > 0) {
|
|
375
|
+
const current = openF2.extractMin();
|
|
376
|
+
if (current === null) {
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
if (visF[current] !== 0) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (current === endIndex) {
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
visF[current] = 1;
|
|
387
|
+
const currentDistance = gF[current];
|
|
388
|
+
|
|
389
|
+
const isCsrNode = hasCsr && current < csrNodeCount;
|
|
390
|
+
if (!isCsrNode) {
|
|
391
|
+
const neighbors = adjacency[current];
|
|
392
|
+
if (!neighbors || neighbors.length === 0) {
|
|
393
|
+
continue;
|
|
386
394
|
}
|
|
387
395
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
for (let i = csrOffsets[current], endOff = csrOffsets[current + 1]; i < endOff; i++) {
|
|
395
|
-
const nbNode = csrIndices[i];
|
|
396
|
-
const tentativeG = gR[current] + csrDistances[i];
|
|
397
|
-
if (tentativeG < gR[nbNode]) {
|
|
398
|
-
gR[nbNode] = tentativeG;
|
|
399
|
-
nextR[nbNode] = current;
|
|
400
|
-
const otherG = gF[nbNode];
|
|
401
|
-
if (otherG !== Number.POSITIVE_INFINITY) {
|
|
402
|
-
const total = tentativeG + otherG;
|
|
403
|
-
if (total < bestPathCost) { bestPathCost = total; meetingNode = nbNode; }
|
|
404
|
-
}
|
|
405
|
-
openR.insert(tentativeG, nbNode);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
const neighbors = adj[current];
|
|
410
|
-
if (neighbors && neighbors.length) {
|
|
411
|
-
for (let i = 0, n = neighbors.length; i < n; i++) {
|
|
412
|
-
const nb = neighbors[i];
|
|
413
|
-
const nbNode = nb.node;
|
|
414
|
-
const tentativeG = gR[current] + nb.distance;
|
|
415
|
-
if (tentativeG < gR[nbNode]) {
|
|
416
|
-
gR[nbNode] = tentativeG;
|
|
417
|
-
nextR[nbNode] = current;
|
|
418
|
-
const otherG = gF[nbNode];
|
|
419
|
-
if (otherG !== Number.POSITIVE_INFINITY) {
|
|
420
|
-
const total = tentativeG + otherG;
|
|
421
|
-
if (total < bestPathCost) { bestPathCost = total; meetingNode = nbNode; }
|
|
422
|
-
}
|
|
423
|
-
openR.insert(tentativeG, nbNode);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
396
|
+
for (let i = 0, n = neighbors.length; i < n; i++) {
|
|
397
|
+
const nb = neighbors[i];
|
|
398
|
+
const nbNode = nb.node;
|
|
399
|
+
const tentativeG = currentDistance + nb.distance;
|
|
400
|
+
if (tentativeG >= gF[nbNode]) {
|
|
401
|
+
continue;
|
|
426
402
|
}
|
|
403
|
+
|
|
404
|
+
gF[nbNode] = tentativeG;
|
|
405
|
+
prevF[nbNode] = current;
|
|
406
|
+
openF2.insert(tentativeG + getHeuristic(nbNode), nbNode);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (let i = csrOffsets![current], endOff = csrOffsets![current + 1]; i < endOff; i++) {
|
|
412
|
+
const nbNode = csrIndices![i];
|
|
413
|
+
const tentativeG = currentDistance + csrDistances![i];
|
|
414
|
+
if (tentativeG >= gF[nbNode]) {
|
|
415
|
+
continue;
|
|
427
416
|
}
|
|
417
|
+
|
|
418
|
+
gF[nbNode] = tentativeG;
|
|
419
|
+
prevF[nbNode] = current;
|
|
420
|
+
openF2.insert(tentativeG + getHeuristic(nbNode), nbNode);
|
|
428
421
|
}
|
|
429
422
|
}
|
|
430
423
|
|
|
431
|
-
if (
|
|
424
|
+
if (gF[endIndex] === PositiveInfinity) {
|
|
432
425
|
return null;
|
|
433
426
|
}
|
|
434
427
|
|
|
435
|
-
// Reconstruct path
|
|
428
|
+
// Reconstruct path from end back to start through predecessor links.
|
|
436
429
|
const path: Position[] = [];
|
|
437
430
|
|
|
438
|
-
|
|
439
|
-
let cur = meetingNode;
|
|
431
|
+
let cur = endIndex;
|
|
440
432
|
while (cur !== startIndex && cur >= 0) {
|
|
441
|
-
path.push(
|
|
433
|
+
path.push(coordinates[cur]);
|
|
442
434
|
cur = prevF[cur];
|
|
443
435
|
}
|
|
444
|
-
|
|
445
|
-
// Forward tree doesn't connect start to meeting (shouldn't happen if meeting is valid)
|
|
446
|
-
return null;
|
|
447
|
-
}
|
|
448
|
-
path.push(coords[startIndex]);
|
|
436
|
+
path.push(coordinates[startIndex]);
|
|
449
437
|
path.reverse();
|
|
450
438
|
|
|
451
|
-
// Walk from meeting to end (skip meeting node because it's already included)
|
|
452
|
-
cur = meetingNode;
|
|
453
|
-
while (cur !== endIndex) {
|
|
454
|
-
cur = nextR[cur];
|
|
455
|
-
if (cur < 0) {
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
path.push(coords[cur]);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
439
|
return {
|
|
462
440
|
type: "Feature",
|
|
463
441
|
geometry: { type: "LineString", coordinates: path },
|
|
@@ -465,6 +443,120 @@ class TerraRoute implements Router {
|
|
|
465
443
|
};
|
|
466
444
|
}
|
|
467
445
|
|
|
446
|
+
// Build ALT heuristic tables by running shortest-path trees from selected landmarks.
|
|
447
|
+
private buildLandmarkHeuristicData(): void {
|
|
448
|
+
const offsets = this.csrOffsets;
|
|
449
|
+
const indices = this.csrIndices;
|
|
450
|
+
const distances = this.csrDistances;
|
|
451
|
+
const nodeCount = this.csrNodeCount;
|
|
452
|
+
|
|
453
|
+
if (!offsets || !indices || !distances || nodeCount === 0) {
|
|
454
|
+
this.landmarkNodeCount = 0;
|
|
455
|
+
this.landmarkCount = 0;
|
|
456
|
+
this.landmarkDistancesFlat = null;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const targetLandmarkCount = Math.min(this.maxLandmarks, nodeCount);
|
|
461
|
+
const selected = new Uint8Array(nodeCount);
|
|
462
|
+
const allDistances: Float64Array[] = [];
|
|
463
|
+
|
|
464
|
+
let source = 0;
|
|
465
|
+
for (let landmarkIndex = 0; landmarkIndex < targetLandmarkCount; landmarkIndex++) {
|
|
466
|
+
selected[source] = 1;
|
|
467
|
+
|
|
468
|
+
const computedDistances = this.computeShortestDistancesFrom(source, nodeCount, offsets, indices, distances);
|
|
469
|
+
allDistances.push(computedDistances);
|
|
470
|
+
|
|
471
|
+
let farthestDistance = -1;
|
|
472
|
+
let farthestIndex = -1;
|
|
473
|
+
for (let node = 0; node < nodeCount; node++) {
|
|
474
|
+
if (selected[node] !== 0) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const distanceAtNode = computedDistances[node];
|
|
479
|
+
if (!Number.isFinite(distanceAtNode)) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (distanceAtNode > farthestDistance) {
|
|
484
|
+
farthestDistance = distanceAtNode;
|
|
485
|
+
farthestIndex = node;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (farthestIndex < 0) {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
source = farthestIndex;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const landmarkCount = allDistances.length;
|
|
496
|
+
|
|
497
|
+
const flat = new Float64Array(landmarkCount * nodeCount);
|
|
498
|
+
for (let i = 0; i < landmarkCount; i++) {
|
|
499
|
+
flat.set(allDistances[i], i * nodeCount);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
this.landmarkNodeCount = nodeCount;
|
|
503
|
+
this.landmarkCount = landmarkCount;
|
|
504
|
+
this.landmarkDistancesFlat = flat;
|
|
505
|
+
this.landmarksDirty = false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Build landmark tables only when needed by getRoute.
|
|
509
|
+
private ensureLandmarkHeuristicData(): void {
|
|
510
|
+
if (!this.landmarksDirty) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.buildLandmarkHeuristicData();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Dijkstra over CSR to compute all-pairs distances from one source node.
|
|
517
|
+
private computeShortestDistancesFrom(
|
|
518
|
+
source: number,
|
|
519
|
+
nodeCount: number,
|
|
520
|
+
offsets: Int32Array,
|
|
521
|
+
indices: Int32Array,
|
|
522
|
+
distances: Float64Array,
|
|
523
|
+
): Float64Array {
|
|
524
|
+
const PositiveInfinity = Number.POSITIVE_INFINITY;
|
|
525
|
+
const bestDistance = new Float64Array(nodeCount);
|
|
526
|
+
const visited = new Uint8Array(nodeCount);
|
|
527
|
+
bestDistance.fill(PositiveInfinity);
|
|
528
|
+
bestDistance[source] = 0;
|
|
529
|
+
|
|
530
|
+
const openSet = new this.heapConstructor();
|
|
531
|
+
openSet.insert(0, source);
|
|
532
|
+
|
|
533
|
+
while (openSet.size() > 0) {
|
|
534
|
+
const current = openSet.extractMin();
|
|
535
|
+
if (current === null) {
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
if (visited[current] !== 0) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
visited[current] = 1;
|
|
543
|
+
const currentDistance = bestDistance[current];
|
|
544
|
+
|
|
545
|
+
for (let i = offsets[current], endOffset = offsets[current + 1]; i < endOffset; i++) {
|
|
546
|
+
const neighbor = indices[i];
|
|
547
|
+
const tentativeDistance = currentDistance + distances[i];
|
|
548
|
+
if (tentativeDistance >= bestDistance[neighbor]) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
bestDistance[neighbor] = tentativeDistance;
|
|
553
|
+
openSet.insert(tentativeDistance, neighbor);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return bestDistance;
|
|
558
|
+
}
|
|
559
|
+
|
|
468
560
|
/**
|
|
469
561
|
* Helper to index start/end in getRoute.
|
|
470
562
|
*/
|
|
@@ -516,11 +608,8 @@ class TerraRoute implements Router {
|
|
|
516
608
|
&& this.gScoreScratch
|
|
517
609
|
&& this.cameFromScratch
|
|
518
610
|
&& this.visitedScratch
|
|
519
|
-
&& this.
|
|
520
|
-
&& this.
|
|
521
|
-
&& this.cameFromRevScratch
|
|
522
|
-
&& this.visitedRevScratch
|
|
523
|
-
&& this.hRevScratch;
|
|
611
|
+
&& this.heuristicScratch
|
|
612
|
+
&& this.heuristicStampScratch;
|
|
524
613
|
|
|
525
614
|
if (ifAlreadyBigEnough) {
|
|
526
615
|
return; // Nothing to do
|
|
@@ -529,11 +618,8 @@ class TerraRoute implements Router {
|
|
|
529
618
|
this.gScoreScratch = new Float64Array(capacity);
|
|
530
619
|
this.cameFromScratch = new Int32Array(capacity);
|
|
531
620
|
this.visitedScratch = new Uint8Array(capacity);
|
|
532
|
-
this.
|
|
533
|
-
this.
|
|
534
|
-
this.cameFromRevScratch = new Int32Array(capacity);
|
|
535
|
-
this.visitedRevScratch = new Uint8Array(capacity);
|
|
536
|
-
this.hRevScratch = new Float64Array(capacity);
|
|
621
|
+
this.heuristicScratch = new Float64Array(capacity);
|
|
622
|
+
this.heuristicStampScratch = new Uint32Array(capacity);
|
|
537
623
|
this.scratchCapacity = capacity;
|
|
538
624
|
}
|
|
539
625
|
|