terra-route 0.0.4 → 0.0.6
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 +12 -3
- package/dist/fibonacci-heap.d.ts +11 -0
- package/dist/terra-route.cjs +1 -1
- package/dist/terra-route.cjs.map +1 -1
- package/dist/terra-route.d.ts +1 -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/eslint.config.mjs +54 -0
- package/jest.config.js +1 -1
- package/package.json +14 -28
- package/src/fibonacci-heap.spec.ts +55 -0
- package/src/fibonacci-heap.ts +131 -0
- package/src/terra-route.spec.ts +8 -5
- package/src/terra-route.ts +85 -41
- package/src/test-utils/test-utils.ts +9 -0
- package/src/min-heap.spec.ts +0 -75
- package/src/min-heap.ts +0 -87
package/src/terra-route.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FeatureCollection, LineString, Point, Feature, Position } from "geojson";
|
|
2
|
-
import {
|
|
2
|
+
import { FibonacciHeap } from "./fibonacci-heap";
|
|
3
3
|
import { haversineDistance } from "./distance/haversine";
|
|
4
4
|
import { createCheapRuler } from "./distance/cheap-ruler";
|
|
5
5
|
|
|
@@ -44,11 +44,11 @@ class TerraRoute {
|
|
|
44
44
|
return latMap.get(lat)!;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const index = this.coords.length;
|
|
48
48
|
this.coords.push(coord);
|
|
49
|
-
latMap.set(lat,
|
|
49
|
+
latMap.set(lat, index);
|
|
50
50
|
|
|
51
|
-
return
|
|
51
|
+
return index;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
@@ -68,21 +68,21 @@ class TerraRoute {
|
|
|
68
68
|
for (const feature of this.network.features) {
|
|
69
69
|
const coords = feature.geometry.coordinates;
|
|
70
70
|
for (let i = 0; i < coords.length - 1; i++) {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
71
|
+
const aIndex = this.coordinateIndex(coords[i]);
|
|
72
|
+
const bIndex = this.coordinateIndex(coords[i + 1]);
|
|
73
73
|
const distance = this.distanceMeasurement(coords[i], coords[i + 1]);
|
|
74
74
|
|
|
75
|
-
if (!this.adjacencyList.has(
|
|
76
|
-
if (!this.adjacencyList.has(
|
|
75
|
+
if (!this.adjacencyList.has(aIndex)) this.adjacencyList.set(aIndex, []);
|
|
76
|
+
if (!this.adjacencyList.has(bIndex)) this.adjacencyList.set(bIndex, []);
|
|
77
77
|
|
|
78
|
-
this.adjacencyList.get(
|
|
79
|
-
this.adjacencyList.get(
|
|
78
|
+
this.adjacencyList.get(aIndex)!.push({ node: bIndex, distance });
|
|
79
|
+
this.adjacencyList.get(bIndex)!.push({ node: aIndex, distance });
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Computes the shortest route between two points in the network using A* algorithm.
|
|
85
|
+
* Computes the shortest route between two points in the network using bidirectional A* algorithm.
|
|
86
86
|
*
|
|
87
87
|
* @param start - A GeoJSON Point Feature representing the start location.
|
|
88
88
|
* @param end - A GeoJSON Point Feature representing the end location.
|
|
@@ -92,52 +92,96 @@ class TerraRoute {
|
|
|
92
92
|
*/
|
|
93
93
|
public getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null {
|
|
94
94
|
if (!this.network) {
|
|
95
|
-
throw new Error("Network not built. Please call
|
|
95
|
+
throw new Error("Network not built. Please call buildRouteGraph(network) first.");
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
const
|
|
99
|
-
const
|
|
98
|
+
const startIndex = this.coordinateIndex(start.geometry.coordinates);
|
|
99
|
+
const endIndex = this.coordinateIndex(end.geometry.coordinates);
|
|
100
100
|
|
|
101
|
-
if (
|
|
101
|
+
if (startIndex === endIndex) {
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
const openSetForward = new FibonacciHeap();
|
|
106
|
+
const openSetBackward = new FibonacciHeap();
|
|
107
|
+
openSetForward.insert(0, startIndex);
|
|
108
|
+
openSetBackward.insert(0, endIndex);
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
const cameFromForward = new Map<number, number>();
|
|
111
|
+
const cameFromBackward = new Map<number, number>();
|
|
112
|
+
const gScoreForward = new Map<number, number>([[startIndex, 0]]);
|
|
113
|
+
const gScoreBackward = new Map<number, number>([[endIndex, 0]]);
|
|
112
114
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
const visitedForward = new Set<number>();
|
|
116
|
+
const visitedBackward = new Set<number>();
|
|
117
|
+
|
|
118
|
+
let meetingNode: number | null = null;
|
|
119
|
+
|
|
120
|
+
while (openSetForward.size() > 0 && openSetBackward.size() > 0) {
|
|
121
|
+
const currentForward = openSetForward.extractMin()!;
|
|
122
|
+
visitedForward.add(currentForward);
|
|
123
|
+
|
|
124
|
+
if (visitedBackward.has(currentForward)) {
|
|
125
|
+
meetingNode = currentForward;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const neighbor of this.adjacencyList.get(currentForward) || []) {
|
|
130
|
+
const tentativeG = (gScoreForward.get(currentForward) ?? Infinity) + neighbor.distance;
|
|
131
|
+
if (tentativeG < (gScoreForward.get(neighbor.node) ?? Infinity)) {
|
|
132
|
+
cameFromForward.set(neighbor.node, currentForward);
|
|
133
|
+
gScoreForward.set(neighbor.node, tentativeG);
|
|
134
|
+
const fScore = tentativeG + this.distanceMeasurement(this.coords[neighbor.node], this.coords[endIndex]);
|
|
135
|
+
openSetForward.insert(fScore, neighbor.node);
|
|
119
136
|
}
|
|
120
|
-
return {
|
|
121
|
-
type: "Feature",
|
|
122
|
-
geometry: { type: "LineString", coordinates: path },
|
|
123
|
-
properties: {},
|
|
124
|
-
};
|
|
125
137
|
}
|
|
126
138
|
|
|
127
|
-
const
|
|
139
|
+
const currentBackward = openSetBackward.extractMin()!;
|
|
140
|
+
visitedBackward.add(currentBackward);
|
|
141
|
+
|
|
142
|
+
if (visitedForward.has(currentBackward)) {
|
|
143
|
+
meetingNode = currentBackward;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
128
146
|
|
|
129
|
-
for (const neighbor of
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const fScore =
|
|
135
|
-
|
|
147
|
+
for (const neighbor of this.adjacencyList.get(currentBackward) || []) {
|
|
148
|
+
const tentativeG = (gScoreBackward.get(currentBackward) ?? Infinity) + neighbor.distance;
|
|
149
|
+
if (tentativeG < (gScoreBackward.get(neighbor.node) ?? Infinity)) {
|
|
150
|
+
cameFromBackward.set(neighbor.node, currentBackward);
|
|
151
|
+
gScoreBackward.set(neighbor.node, tentativeG);
|
|
152
|
+
const fScore = tentativeG + this.distanceMeasurement(this.coords[neighbor.node], this.coords[startIndex]);
|
|
153
|
+
openSetBackward.insert(fScore, neighbor.node);
|
|
136
154
|
}
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
157
|
|
|
140
|
-
|
|
158
|
+
if (meetingNode === null) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Reconstruct forward path
|
|
163
|
+
const pathForward: Position[] = [];
|
|
164
|
+
let node = meetingNode;
|
|
165
|
+
while (node !== undefined) {
|
|
166
|
+
pathForward.unshift(this.coords[node]);
|
|
167
|
+
node = cameFromForward.get(node)!;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Reconstruct backward path (omit meeting node to avoid duplication)
|
|
171
|
+
const pathBackward: Position[] = [];
|
|
172
|
+
node = cameFromBackward.get(meetingNode)!;
|
|
173
|
+
while (node !== undefined) {
|
|
174
|
+
pathBackward.push(this.coords[node]);
|
|
175
|
+
node = cameFromBackward.get(node)!;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const fullPath = [...pathForward, ...pathBackward];
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
type: "Feature",
|
|
182
|
+
geometry: { type: "LineString", coordinates: fullPath },
|
|
183
|
+
properties: {},
|
|
184
|
+
};
|
|
141
185
|
}
|
|
142
186
|
}
|
|
143
187
|
|
|
@@ -374,6 +374,8 @@ export function getReasonIfLineStringInvalid(
|
|
|
374
374
|
return `Not enough coordinates: ${coords.length} (${coords})`;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
const seen = new Set<string>();
|
|
378
|
+
|
|
377
379
|
// 5. Validate each coordinate is a valid Position
|
|
378
380
|
// (At minimum, [number, number] or [number, number, number])
|
|
379
381
|
for (const position of coords) {
|
|
@@ -389,5 +391,12 @@ export function getReasonIfLineStringInvalid(
|
|
|
389
391
|
) {
|
|
390
392
|
return 'Not a Position; elements are not a numbers';
|
|
391
393
|
}
|
|
394
|
+
|
|
395
|
+
// 6. Check for duplicates
|
|
396
|
+
const key = `${position[0]},${position[1]}`;
|
|
397
|
+
if (seen.has(key)) {
|
|
398
|
+
return `Duplicate coordinate: ${key}`;
|
|
399
|
+
}
|
|
400
|
+
seen.add(key);
|
|
392
401
|
}
|
|
393
402
|
}
|
package/src/min-heap.spec.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { MinHeap } from './min-heap';
|
|
2
|
-
|
|
3
|
-
describe('MinHeap', () => {
|
|
4
|
-
let heap: MinHeap;
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
heap = new MinHeap();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('should create an empty heap', () => {
|
|
11
|
-
expect(heap.size()).toBe(0);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('should insert a single element', () => {
|
|
15
|
-
heap.insert(5, 100);
|
|
16
|
-
expect(heap.size()).toBe(1);
|
|
17
|
-
expect(heap.extractMin()).toBe(100);
|
|
18
|
-
expect(heap.size()).toBe(0);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should maintain correct heap order when inserting multiple elements', () => {
|
|
22
|
-
heap.insert(10, 200);
|
|
23
|
-
heap.insert(5, 100);
|
|
24
|
-
heap.insert(3, 50);
|
|
25
|
-
heap.insert(7, 150);
|
|
26
|
-
|
|
27
|
-
expect(heap.size()).toBe(4);
|
|
28
|
-
expect(heap.extractMin()).toBe(50);
|
|
29
|
-
expect(heap.extractMin()).toBe(100);
|
|
30
|
-
expect(heap.extractMin()).toBe(150);
|
|
31
|
-
expect(heap.extractMin()).toBe(200);
|
|
32
|
-
expect(heap.size()).toBe(0);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should return null when extracting from an empty heap', () => {
|
|
36
|
-
expect(heap.extractMin()).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should handle duplicate keys properly', () => {
|
|
40
|
-
heap.insert(5, 100);
|
|
41
|
-
heap.insert(5, 200);
|
|
42
|
-
heap.insert(5, 300);
|
|
43
|
-
|
|
44
|
-
expect(heap.size()).toBe(3);
|
|
45
|
-
expect(heap.extractMin()).toBe(100);
|
|
46
|
-
expect(heap.extractMin()).toBe(200);
|
|
47
|
-
expect(heap.extractMin()).toBe(300);
|
|
48
|
-
expect(heap.size()).toBe(0);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should correctly reorder after sequential insert and extract', () => {
|
|
52
|
-
heap.insert(10, 100);
|
|
53
|
-
heap.insert(1, 50);
|
|
54
|
-
expect(heap.extractMin()).toBe(50);
|
|
55
|
-
|
|
56
|
-
heap.insert(2, 60);
|
|
57
|
-
heap.insert(3, 70);
|
|
58
|
-
expect(heap.extractMin()).toBe(60);
|
|
59
|
-
expect(heap.extractMin()).toBe(70);
|
|
60
|
-
expect(heap.extractMin()).toBe(100);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should handle large number of elements correctly', () => {
|
|
64
|
-
const elements = Array.from({ length: 1000 }, (_, i) => ({ key: 1000 - i, value: i }));
|
|
65
|
-
elements.forEach(el => heap.insert(el.key, el.value));
|
|
66
|
-
|
|
67
|
-
expect(heap.size()).toBe(1000);
|
|
68
|
-
|
|
69
|
-
for (let i = 999; i >= 0; i--) {
|
|
70
|
-
expect(heap.extractMin()).toBe(i);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
expect(heap.size()).toBe(0);
|
|
74
|
-
});
|
|
75
|
-
});
|
package/src/min-heap.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
export class MinHeap {
|
|
2
|
-
private heap: Array<{ key: number; value: number; index: number }> = [];
|
|
3
|
-
private insertCounter = 0;
|
|
4
|
-
|
|
5
|
-
insert(key: number, value: number): void {
|
|
6
|
-
const node = { key, value, index: this.insertCounter++ };
|
|
7
|
-
let idx = this.heap.length;
|
|
8
|
-
this.heap.push(node);
|
|
9
|
-
|
|
10
|
-
// Optimized Bubble Up
|
|
11
|
-
while (idx > 0) {
|
|
12
|
-
const parentIdx = (idx - 1) >>> 1; // Fast Math.floor((idx - 1) / 2)
|
|
13
|
-
const parent = this.heap[parentIdx];
|
|
14
|
-
if (node.key > parent.key || (node.key === parent.key && node.index > parent.index)) break;
|
|
15
|
-
this.heap[idx] = parent;
|
|
16
|
-
idx = parentIdx;
|
|
17
|
-
}
|
|
18
|
-
this.heap[idx] = node;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
extractMin(): number | null {
|
|
22
|
-
const length = this.heap.length;
|
|
23
|
-
if (length === 0) return null;
|
|
24
|
-
|
|
25
|
-
const minNode = this.heap[0];
|
|
26
|
-
const endNode = this.heap.pop()!;
|
|
27
|
-
|
|
28
|
-
if (length > 1) {
|
|
29
|
-
this.heap[0] = endNode;
|
|
30
|
-
this.bubbleDown(0);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return minNode.value;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
size(): number {
|
|
37
|
-
return this.heap.length;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
private bubbleDown(idx: number): void {
|
|
41
|
-
const { heap } = this;
|
|
42
|
-
const length = heap.length;
|
|
43
|
-
// Grab the parent node once, then move it down only if needed
|
|
44
|
-
const node = heap[idx];
|
|
45
|
-
const nodeKey = node.key;
|
|
46
|
-
const nodeIndex = node.index;
|
|
47
|
-
|
|
48
|
-
while (true) {
|
|
49
|
-
// Calculate left and right child indexes
|
|
50
|
-
const leftIdx = (idx << 1) + 1;
|
|
51
|
-
if (leftIdx >= length) {
|
|
52
|
-
// No children => we’re already in place
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Assume left child is the smaller one by default
|
|
57
|
-
let smallestIdx = leftIdx;
|
|
58
|
-
let smallestKey = heap[leftIdx].key;
|
|
59
|
-
let smallestIndex = heap[leftIdx].index;
|
|
60
|
-
|
|
61
|
-
const rightIdx = leftIdx + 1;
|
|
62
|
-
if (rightIdx < length) {
|
|
63
|
-
// Compare left child vs. right child
|
|
64
|
-
const rightKey = heap[rightIdx].key;
|
|
65
|
-
const rightIndex = heap[rightIdx].index;
|
|
66
|
-
if (rightKey < smallestKey || (rightKey === smallestKey && rightIndex < smallestIndex)) {
|
|
67
|
-
smallestIdx = rightIdx;
|
|
68
|
-
smallestKey = rightKey;
|
|
69
|
-
smallestIndex = rightIndex;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Compare the smaller child with the parent
|
|
74
|
-
if (smallestKey < nodeKey || (smallestKey === nodeKey && smallestIndex < nodeIndex)) {
|
|
75
|
-
// Swap the smaller child up
|
|
76
|
-
heap[idx] = heap[smallestIdx];
|
|
77
|
-
idx = smallestIdx;
|
|
78
|
-
} else {
|
|
79
|
-
// We’re in the correct position now, so stop
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Place the original node in its final position
|
|
85
|
-
heap[idx] = node;
|
|
86
|
-
}
|
|
87
|
-
}
|