terra-route 0.0.12 → 0.0.14
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/README.md +5 -5
- package/dist/terra-route.cjs +1 -1
- package/dist/terra-route.cjs.map +1 -1
- package/dist/terra-route.d.ts +17 -0
- 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/instructions.md +13 -0
- package/package.json +2 -1
- package/src/terra-route.compare.spec.ts +81 -0
- package/src/terra-route.spec.ts +576 -0
- package/src/terra-route.ts +369 -152
package/src/terra-route.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { FourAryHeap } from "./heap/four-ary-heap";
|
|
|
6
6
|
|
|
7
7
|
interface Router {
|
|
8
8
|
buildRouteGraph(network: FeatureCollection<LineString>): void;
|
|
9
|
+
expandRouteGraph(network: FeatureCollection<LineString>): void;
|
|
9
10
|
getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null;
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -31,6 +32,11 @@ class TerraRoute implements Router {
|
|
|
31
32
|
private cameFromScratch: Int32Array | null = null; // Predecessor per node for path reconstruction
|
|
32
33
|
private visitedScratch: Uint8Array | null = null; // Visited set to avoid reprocessing
|
|
33
34
|
private hScratch: Float64Array | null = null; // Per-node heuristic cache for the current query (lazy compute)
|
|
35
|
+
// Reverse-direction scratch for bidirectional search
|
|
36
|
+
private gScoreRevScratch: Float64Array | null = null; // gScore per node from the end
|
|
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
|
|
34
40
|
private scratchCapacity = 0; // Current capacity of scratch arrays
|
|
35
41
|
|
|
36
42
|
constructor(options?: {
|
|
@@ -63,52 +69,15 @@ class TerraRoute implements Router {
|
|
|
63
69
|
const coordsLocal = this.coordinates; // Local alias for coordinates array
|
|
64
70
|
const measureDistance = this.distanceMeasurement; // Local alias for distance function
|
|
65
71
|
|
|
66
|
-
const features = network.features; // All LineString features
|
|
67
|
-
|
|
68
72
|
// Pass 1: assign indices and count degrees per node
|
|
69
73
|
const degree: number[] = []; // Dynamic degree array; grows as nodes are discovered
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
for (let i = 0, len = lineCoords.length - 1; i < len; i++) { // Iterate segment pairs
|
|
75
|
-
const a = lineCoords[i]; // Segment start coord
|
|
76
|
-
const b = lineCoords[i + 1]; // Segment end coord
|
|
77
|
-
|
|
78
|
-
const lngA = a[0], latA = a[1]; // Keys for A
|
|
79
|
-
const lngB = b[0], latB = b[1]; // Keys for B
|
|
80
|
-
|
|
81
|
-
// Index A
|
|
82
|
-
let latMapA = coordIndexMapLocal.get(lngA);
|
|
83
|
-
if (latMapA === undefined) {
|
|
84
|
-
latMapA = new Map<number, number>();
|
|
85
|
-
coordIndexMapLocal.set(lngA, latMapA);
|
|
86
|
-
}
|
|
87
|
-
let indexA = latMapA.get(latA);
|
|
88
|
-
if (indexA === undefined) {
|
|
89
|
-
indexA = coordsLocal.length;
|
|
90
|
-
coordsLocal.push(a);
|
|
91
|
-
latMapA.set(latA, indexA);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Index B
|
|
95
|
-
let latMapB = coordIndexMapLocal.get(lngB);
|
|
96
|
-
if (latMapB === undefined) {
|
|
97
|
-
latMapB = new Map<number, number>();
|
|
98
|
-
coordIndexMapLocal.set(lngB, latMapB);
|
|
99
|
-
}
|
|
100
|
-
let indexB = latMapB.get(latB);
|
|
101
|
-
if (indexB === undefined) {
|
|
102
|
-
indexB = coordsLocal.length;
|
|
103
|
-
coordsLocal.push(b);
|
|
104
|
-
latMapB.set(latB, indexB);
|
|
105
|
-
}
|
|
74
|
+
this.forEachSegment(network, (a, b) => {
|
|
75
|
+
const indexA = this.indexCoordinate(a, coordIndexMapLocal, coordsLocal);
|
|
76
|
+
const indexB = this.indexCoordinate(b, coordIndexMapLocal, coordsLocal);
|
|
106
77
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
}
|
|
78
|
+
degree[indexA] = (degree[indexA] ?? 0) + 1;
|
|
79
|
+
degree[indexB] = (degree[indexB] ?? 0) + 1;
|
|
80
|
+
});
|
|
112
81
|
|
|
113
82
|
// Build CSR arrays from degree counts
|
|
114
83
|
const nodeCount = this.coordinates.length; // Total nodes discovered
|
|
@@ -124,32 +93,20 @@ class TerraRoute implements Router {
|
|
|
124
93
|
|
|
125
94
|
// Pass 2: fill CSR arrays using a write cursor per node
|
|
126
95
|
const cursor = offsets.slice(); // Current write positions per node
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const segmentDistance = measureDistance(a, b); // Edge weight once
|
|
142
|
-
|
|
143
|
-
// Write A → B
|
|
144
|
-
let pos = cursor[indexA]++;
|
|
145
|
-
indices[pos] = indexB;
|
|
146
|
-
distances[pos] = segmentDistance;
|
|
147
|
-
// Write B → A
|
|
148
|
-
pos = cursor[indexB]++;
|
|
149
|
-
indices[pos] = indexA;
|
|
150
|
-
distances[pos] = segmentDistance;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
96
|
+
this.forEachSegment(network, (a, b) => {
|
|
97
|
+
// Read back indices (guaranteed to exist from pass 1)
|
|
98
|
+
const indexA = this.coordinateIndexMap.get(a[0])!.get(a[1])!;
|
|
99
|
+
const indexB = this.coordinateIndexMap.get(b[0])!.get(b[1])!;
|
|
100
|
+
|
|
101
|
+
const segmentDistance = measureDistance(a, b); // Edge weight once
|
|
102
|
+
|
|
103
|
+
let pos = cursor[indexA]++;
|
|
104
|
+
indices[pos] = indexB;
|
|
105
|
+
distances[pos] = segmentDistance;
|
|
106
|
+
pos = cursor[indexB]++;
|
|
107
|
+
indices[pos] = indexA;
|
|
108
|
+
distances[pos] = segmentDistance;
|
|
109
|
+
});
|
|
153
110
|
|
|
154
111
|
// Commit CSR to instance
|
|
155
112
|
this.csrOffsets = offsets;
|
|
@@ -160,6 +117,122 @@ class TerraRoute implements Router {
|
|
|
160
117
|
this.adjacencyList = new Array(nodeCount);
|
|
161
118
|
}
|
|
162
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Expands (merges) the existing graph with an additional LineString FeatureCollection.
|
|
122
|
+
*/
|
|
123
|
+
public expandRouteGraph(network: FeatureCollection<LineString>): void {
|
|
124
|
+
if (this.network === null) {
|
|
125
|
+
throw new Error("Network not built. Please call buildRouteGraph(network) first.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Merge the feature arrays for reference/debugging. We avoid copying properties deeply.
|
|
129
|
+
this.network = {
|
|
130
|
+
type: "FeatureCollection",
|
|
131
|
+
features: [...this.network.features, ...network.features],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const coordIndexMapLocal = this.coordinateIndexMap;
|
|
135
|
+
const coordsLocal = this.coordinates;
|
|
136
|
+
const measureDistance = this.distanceMeasurement;
|
|
137
|
+
const adj = this.adjacencyList;
|
|
138
|
+
|
|
139
|
+
// Ensure we have adjacency arrays for any existing CSR-only nodes.
|
|
140
|
+
// (During buildRouteGraph, adjacencyList is sized but entries are undefined.)
|
|
141
|
+
for (let i = 0; i < adj.length; i++) {
|
|
142
|
+
if (adj[i] === undefined) adj[i] = [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Add new edges into the adjacency list (sparse), then rebuild CSR from adjacency.
|
|
146
|
+
this.forEachSegment(network, (a, b) => {
|
|
147
|
+
const indexA = this.indexCoordinate(a, coordIndexMapLocal, coordsLocal, (idx) => { adj[idx] = []; });
|
|
148
|
+
const indexB = this.indexCoordinate(b, coordIndexMapLocal, coordsLocal, (idx) => { adj[idx] = []; });
|
|
149
|
+
|
|
150
|
+
const segmentDistance = measureDistance(a, b);
|
|
151
|
+
|
|
152
|
+
adj[indexA].push({ node: indexB, distance: segmentDistance });
|
|
153
|
+
adj[indexB].push({ node: indexA, distance: segmentDistance });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this.rebuildCsrFromAdjacency();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Rebuild CSR arrays for the full node set, using:
|
|
161
|
+
* - Existing CSR edges (from the last build/expand)
|
|
162
|
+
* - Any additional edges stored in `adjacencyList`
|
|
163
|
+
*/
|
|
164
|
+
private rebuildCsrFromAdjacency(): void {
|
|
165
|
+
const nodeCount = this.coordinates.length;
|
|
166
|
+
const adj = this.adjacencyList;
|
|
167
|
+
|
|
168
|
+
// Compute degree using CSR degree + adjacency degree
|
|
169
|
+
const degree = new Int32Array(nodeCount);
|
|
170
|
+
|
|
171
|
+
if (this.csrOffsets && this.csrIndices && this.csrDistances) {
|
|
172
|
+
const csrOffsets = this.csrOffsets;
|
|
173
|
+
const covered = Math.min(this.csrNodeCount, nodeCount);
|
|
174
|
+
for (let i = 0; i < covered; i++) {
|
|
175
|
+
degree[i] += (csrOffsets[i + 1] - csrOffsets[i]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
180
|
+
const neighbors = adj[i];
|
|
181
|
+
if (neighbors && neighbors.length) degree[i] += neighbors.length;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const offsets = new Int32Array(nodeCount + 1);
|
|
185
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
186
|
+
offsets[i + 1] = offsets[i] + degree[i];
|
|
187
|
+
}
|
|
188
|
+
const totalEdges = offsets[nodeCount];
|
|
189
|
+
const indices = new Int32Array(totalEdges);
|
|
190
|
+
const distances = new Float64Array(totalEdges);
|
|
191
|
+
const cursor = offsets.slice();
|
|
192
|
+
|
|
193
|
+
// Copy existing CSR edges first
|
|
194
|
+
if (this.csrOffsets && this.csrIndices && this.csrDistances) {
|
|
195
|
+
const csrOffsets = this.csrOffsets;
|
|
196
|
+
const csrIndices = this.csrIndices;
|
|
197
|
+
const csrDistances = this.csrDistances;
|
|
198
|
+
const covered = Math.min(this.csrNodeCount, nodeCount);
|
|
199
|
+
for (let n = 0; n < covered; n++) {
|
|
200
|
+
const startOff = csrOffsets[n];
|
|
201
|
+
const endOff = csrOffsets[n + 1];
|
|
202
|
+
let pos = cursor[n];
|
|
203
|
+
for (let i = startOff; i < endOff; i++) {
|
|
204
|
+
indices[pos] = csrIndices[i];
|
|
205
|
+
distances[pos] = csrDistances[i];
|
|
206
|
+
pos++;
|
|
207
|
+
}
|
|
208
|
+
cursor[n] = pos;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Append adjacency edges
|
|
213
|
+
for (let n = 0; n < nodeCount; n++) {
|
|
214
|
+
const neighbors = adj[n];
|
|
215
|
+
if (!neighbors || neighbors.length === 0) continue;
|
|
216
|
+
let pos = cursor[n];
|
|
217
|
+
for (let i = 0, len = neighbors.length; i < len; i++) {
|
|
218
|
+
const nb = neighbors[i];
|
|
219
|
+
indices[pos] = nb.node;
|
|
220
|
+
distances[pos] = nb.distance;
|
|
221
|
+
pos++;
|
|
222
|
+
}
|
|
223
|
+
cursor[n] = pos;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Commit and reset adjacency (we've absorbed edges into CSR)
|
|
227
|
+
this.csrOffsets = offsets;
|
|
228
|
+
this.csrIndices = indices;
|
|
229
|
+
this.csrDistances = distances;
|
|
230
|
+
this.csrNodeCount = nodeCount;
|
|
231
|
+
|
|
232
|
+
// Keep adjacency list for *future* dynamic additions, but clear existing edges to avoid duplication.
|
|
233
|
+
this.adjacencyList = new Array(nodeCount);
|
|
234
|
+
}
|
|
235
|
+
|
|
163
236
|
/**
|
|
164
237
|
* Computes the shortest route between two points in the network using the A* algorithm.
|
|
165
238
|
*
|
|
@@ -189,112 +262,202 @@ class TerraRoute implements Router {
|
|
|
189
262
|
// Local aliases
|
|
190
263
|
const coords = this.coordinates; // Alias to coordinates array
|
|
191
264
|
const adj = this.adjacencyList; // Alias to sparse adjacency list (for dynamic nodes)
|
|
192
|
-
const measureDistance = this.distanceMeasurement; // Alias to distance function
|
|
193
265
|
|
|
194
266
|
// Ensure and init scratch buffers
|
|
195
267
|
const nodeCount = coords.length; // Current number of nodes (may be >= csrNodeCount if new nodes added)
|
|
196
268
|
this.ensureScratch(nodeCount); // Allocate scratch arrays if needed
|
|
197
269
|
|
|
198
270
|
// Non-null after ensure
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
271
|
+
const gF = this.gScoreScratch!; // forward gScore (from start)
|
|
272
|
+
const gR = this.gScoreRevScratch!; // reverse gScore (from end)
|
|
273
|
+
const prevF = this.cameFromScratch!; // predecessor in forward search
|
|
274
|
+
const nextR = this.cameFromRevScratch!; // successor in reverse search (toward end)
|
|
275
|
+
const visF = this.visitedScratch!;
|
|
276
|
+
const visR = this.visitedRevScratch!;
|
|
277
|
+
const hF = this.hScratch!;
|
|
278
|
+
const hR = this.hRevScratch!;
|
|
279
|
+
|
|
280
|
+
gF.fill(Number.POSITIVE_INFINITY, 0, nodeCount);
|
|
281
|
+
gR.fill(Number.POSITIVE_INFINITY, 0, nodeCount);
|
|
282
|
+
prevF.fill(-1, 0, nodeCount);
|
|
283
|
+
nextR.fill(-1, 0, nodeCount);
|
|
284
|
+
visF.fill(0, 0, nodeCount);
|
|
285
|
+
visR.fill(0, 0, nodeCount);
|
|
286
|
+
hF.fill(-1, 0, nodeCount);
|
|
287
|
+
hR.fill(-1, 0, nodeCount);
|
|
288
|
+
|
|
289
|
+
const openF = new this.heapConstructor();
|
|
290
|
+
const openR = new this.heapConstructor();
|
|
291
|
+
|
|
292
|
+
const startCoord = coords[startIndex];
|
|
293
|
+
const endCoord = coords[endIndex];
|
|
294
|
+
|
|
295
|
+
gF[startIndex] = 0;
|
|
296
|
+
gR[endIndex] = 0;
|
|
297
|
+
|
|
298
|
+
// Bidirectional Dijkstra (A* with zero heuristic). This keeps correctness simple and matches the
|
|
299
|
+
// reference pathfinder while still saving work by meeting in the middle.
|
|
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) {
|
|
231
314
|
break;
|
|
232
315
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
cameFrom[nbNode] = current; // Track predecessor
|
|
250
|
-
// A* priority = g + h (cache h per node for this query)
|
|
251
|
-
let hVal = hCache[nbNode];
|
|
252
|
-
if (hVal < 0) { hVal = measureDistance(coords[nbNode], endCoord); hCache[nbNode] = hVal; }
|
|
253
|
-
openSet.insert(tentativeG + hVal, nbNode); // Push/update neighbor into open set
|
|
316
|
+
|
|
317
|
+
// Expand one step from each side, prioritizing the smaller frontier.
|
|
318
|
+
const expandForward = openF.size() <= openR.size();
|
|
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;
|
|
254
332
|
}
|
|
255
333
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
334
|
+
|
|
335
|
+
// Relax neighbors and push newly improved ones
|
|
336
|
+
if (this.csrOffsets && current < this.csrNodeCount) {
|
|
337
|
+
const csrOffsets = this.csrOffsets!;
|
|
338
|
+
const csrIndices = this.csrIndices!;
|
|
339
|
+
const csrDistances = this.csrDistances!;
|
|
340
|
+
for (let i = csrOffsets[current], endOff = csrOffsets[current + 1]; i < endOff; i++) {
|
|
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);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
const neighbors = adj[current];
|
|
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
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
const current = openR.extractMin()!;
|
|
376
|
+
if (visR[current] !== 0) continue;
|
|
377
|
+
lastExtractedGReverse = gR[current];
|
|
378
|
+
visR[current] = 1;
|
|
379
|
+
|
|
380
|
+
if (visF[current] !== 0) {
|
|
381
|
+
const total = gF[current] + gR[current];
|
|
382
|
+
if (total < bestPathCost) {
|
|
383
|
+
bestPathCost = total;
|
|
384
|
+
meetingNode = current;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Reverse direction: same neighbor iteration because graph is undirected.
|
|
389
|
+
// Store successor pointer (next step toward end) i.e. nextR[neighbor] = current.
|
|
390
|
+
if (this.csrOffsets && current < this.csrNodeCount) {
|
|
391
|
+
const csrOffsets = this.csrOffsets!;
|
|
392
|
+
const csrIndices = this.csrIndices!;
|
|
393
|
+
const csrDistances = this.csrDistances!;
|
|
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
|
+
}
|
|
275
426
|
}
|
|
276
427
|
}
|
|
277
428
|
}
|
|
278
429
|
}
|
|
279
430
|
|
|
280
|
-
|
|
281
|
-
if (cameFrom[endIndex] < 0) {
|
|
431
|
+
if (meetingNode < 0) {
|
|
282
432
|
return null;
|
|
283
433
|
}
|
|
284
434
|
|
|
285
|
-
// Reconstruct path
|
|
435
|
+
// Reconstruct path: start -> meeting using prevF, then meeting -> end using nextR
|
|
286
436
|
const path: Position[] = [];
|
|
287
|
-
|
|
288
|
-
|
|
437
|
+
|
|
438
|
+
// Walk back from meeting to start, collecting nodes
|
|
439
|
+
let cur = meetingNode;
|
|
440
|
+
while (cur !== startIndex && cur >= 0) {
|
|
289
441
|
path.push(coords[cur]);
|
|
290
|
-
cur =
|
|
442
|
+
cur = prevF[cur];
|
|
443
|
+
}
|
|
444
|
+
if (cur !== startIndex) {
|
|
445
|
+
// Forward tree doesn't connect start to meeting (shouldn't happen if meeting is valid)
|
|
446
|
+
return null;
|
|
291
447
|
}
|
|
292
|
-
// Include start coordinate
|
|
293
448
|
path.push(coords[startIndex]);
|
|
294
|
-
|
|
295
|
-
// Reverse to get start→end order
|
|
296
449
|
path.reverse();
|
|
297
450
|
|
|
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
|
+
|
|
298
461
|
return {
|
|
299
462
|
type: "Feature",
|
|
300
463
|
geometry: { type: "LineString", coordinates: path },
|
|
@@ -353,7 +516,11 @@ class TerraRoute implements Router {
|
|
|
353
516
|
&& this.gScoreScratch
|
|
354
517
|
&& this.cameFromScratch
|
|
355
518
|
&& this.visitedScratch
|
|
356
|
-
&& this.hScratch
|
|
519
|
+
&& this.hScratch
|
|
520
|
+
&& this.gScoreRevScratch
|
|
521
|
+
&& this.cameFromRevScratch
|
|
522
|
+
&& this.visitedRevScratch
|
|
523
|
+
&& this.hRevScratch;
|
|
357
524
|
|
|
358
525
|
if (ifAlreadyBigEnough) {
|
|
359
526
|
return; // Nothing to do
|
|
@@ -363,8 +530,58 @@ class TerraRoute implements Router {
|
|
|
363
530
|
this.cameFromScratch = new Int32Array(capacity);
|
|
364
531
|
this.visitedScratch = new Uint8Array(capacity);
|
|
365
532
|
this.hScratch = new Float64Array(capacity);
|
|
533
|
+
this.gScoreRevScratch = new Float64Array(capacity);
|
|
534
|
+
this.cameFromRevScratch = new Int32Array(capacity);
|
|
535
|
+
this.visitedRevScratch = new Uint8Array(capacity);
|
|
536
|
+
this.hRevScratch = new Float64Array(capacity);
|
|
366
537
|
this.scratchCapacity = capacity;
|
|
367
538
|
}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
// Iterate all consecutive segment pairs in a LineString FeatureCollection.
|
|
542
|
+
// Kept as a simple loop helper to avoid repeating nested iteration logic.
|
|
543
|
+
private forEachSegment(
|
|
544
|
+
network: FeatureCollection<LineString>,
|
|
545
|
+
fn: (a: Position, b: Position) => void,
|
|
546
|
+
): void {
|
|
547
|
+
const features = network.features;
|
|
548
|
+
for (let f = 0, fLen = features.length; f < fLen; f++) {
|
|
549
|
+
const lineCoords = features[f].geometry.coordinates;
|
|
550
|
+
for (let i = 0, len = lineCoords.length - 1; i < len; i++) {
|
|
551
|
+
// GeoJSON coordinates are compatible with Position (number[]), and the project assumes [lng,lat]
|
|
552
|
+
fn(lineCoords[i] as Position, lineCoords[i + 1] as Position);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Hot-path coordinate indexer used by both build/expand.
|
|
558
|
+
// Accepts explicit maps/arrays so callers can hoist them once.
|
|
559
|
+
private indexCoordinate(
|
|
560
|
+
coord: Position,
|
|
561
|
+
coordIndexMapLocal: Map<number, Map<number, number>>,
|
|
562
|
+
coordsLocal: Position[],
|
|
563
|
+
onNewIndex?: (index: number) => void,
|
|
564
|
+
): number {
|
|
565
|
+
const lng = coord[0];
|
|
566
|
+
const lat = coord[1];
|
|
567
|
+
|
|
568
|
+
let latMap = coordIndexMapLocal.get(lng);
|
|
569
|
+
if (latMap === undefined) {
|
|
570
|
+
latMap = new Map<number, number>();
|
|
571
|
+
coordIndexMapLocal.set(lng, latMap);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let idx = latMap.get(lat);
|
|
575
|
+
if (idx === undefined) {
|
|
576
|
+
idx = coordsLocal.length;
|
|
577
|
+
coordsLocal.push(coord);
|
|
578
|
+
latMap.set(lat, idx);
|
|
579
|
+
if (onNewIndex) onNewIndex(idx);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return idx;
|
|
583
|
+
}
|
|
584
|
+
|
|
368
585
|
}
|
|
369
586
|
|
|
370
587
|
export { TerraRoute, createCheapRuler, haversineDistance }
|