terra-route 0.0.5 → 0.0.7
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 +13 -2
- package/dist/distance/haversine.d.ts +1 -0
- package/dist/fibonacci-heap.d.ts +11 -0
- package/dist/heap/min-heap.d.ts +9 -0
- package/dist/terra-route.cjs +1 -1
- package/dist/terra-route.cjs.map +1 -1
- package/dist/terra-route.d.ts +8 -3
- 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/jest.config.js +2 -2
- package/package.json +6 -3
- package/src/data/network.geojson +822 -0
- package/src/distance/haversine.ts +2 -1
- package/src/heap/fibonacci-heap.spec.ts +98 -0
- package/src/heap/fibonacci-heap.ts +210 -0
- package/src/heap/heap.d.ts +10 -0
- package/src/heap/min-heap.spec.ts +127 -0
- package/src/{min-heap.ts → heap/min-heap.ts} +5 -2
- package/src/heap/pairing-heap.spec.ts +101 -0
- package/src/heap/pairing-heap.ts +109 -0
- package/src/terra-route.compare.spec.ts +89 -0
- package/src/terra-route.spec.ts +500 -445
- package/src/terra-route.ts +49 -64
- package/src/test-utils/test-utils.ts +86 -3
- package/src/min-heap.spec.ts +0 -75
package/src/terra-route.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { FeatureCollection, LineString, Point, Feature, Position } from "geojson";
|
|
2
|
-
import { MinHeap } from "./min-heap";
|
|
3
2
|
import { haversineDistance } from "./distance/haversine";
|
|
4
3
|
import { createCheapRuler } from "./distance/cheap-ruler";
|
|
4
|
+
import { MinHeap } from "./heap/min-heap";
|
|
5
|
+
import { HeapConstructor } from "./heap/heap";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* TerraRoute is a routing utility for finding the shortest path
|
|
@@ -16,16 +17,19 @@ class TerraRoute {
|
|
|
16
17
|
private adjacencyList: Map<number, Array<{ node: number; distance: number }>> = new Map();
|
|
17
18
|
private coords: Position[] = []
|
|
18
19
|
private coordMap: Map<number, Map<number, number>> = new Map();
|
|
20
|
+
private heap: HeapConstructor;
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Creates a new instance of TerraRoute.
|
|
22
24
|
*
|
|
23
25
|
* @param distanceMeasurement - Optional custom distance measurement function (defaults to haversine distance).
|
|
24
26
|
*/
|
|
25
|
-
constructor(
|
|
26
|
-
distanceMeasurement?: (positionA: Position, positionB: Position) => number
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
constructor(options?: {
|
|
28
|
+
distanceMeasurement?: (positionA: Position, positionB: Position) => number,
|
|
29
|
+
heap?: HeapConstructor
|
|
30
|
+
}) {
|
|
31
|
+
this.heap = options?.heap ? options.heap : MinHeap
|
|
32
|
+
this.distanceMeasurement = options?.distanceMeasurement ? options.distanceMeasurement : haversineDistance;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
@@ -53,7 +57,7 @@ class TerraRoute {
|
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
59
|
* Builds the internal graph representation (adjacency list) from the input network.
|
|
56
|
-
* Each LineString segment is translated into
|
|
60
|
+
* Each LineString segment is translated into graph edges with associated distances.
|
|
57
61
|
* Assumes that the network is a connected graph of LineStrings with shared coordinates. Calling this
|
|
58
62
|
* method with a new network overwrite any existing network and reset all internal data structures.
|
|
59
63
|
*
|
|
@@ -82,7 +86,7 @@ class TerraRoute {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
|
-
* Computes the shortest route between two points in the network using
|
|
89
|
+
* Computes the shortest route between two points in the network using the A* algorithm.
|
|
86
90
|
*
|
|
87
91
|
* @param start - A GeoJSON Point Feature representing the start location.
|
|
88
92
|
* @param end - A GeoJSON Point Feature representing the end location.
|
|
@@ -90,7 +94,10 @@ class TerraRoute {
|
|
|
90
94
|
*
|
|
91
95
|
* @throws Error if the network has not been built yet with buildRouteGraph(network).
|
|
92
96
|
*/
|
|
93
|
-
public getRoute(
|
|
97
|
+
public getRoute(
|
|
98
|
+
start: Feature<Point>,
|
|
99
|
+
end: Feature<Point>
|
|
100
|
+
): Feature<LineString> | null {
|
|
94
101
|
if (!this.network) {
|
|
95
102
|
throw new Error("Network not built. Please call buildRouteGraph(network) first.");
|
|
96
103
|
}
|
|
@@ -102,87 +109,65 @@ class TerraRoute {
|
|
|
102
109
|
return null;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
openSetForward.insert(0, startIndex);
|
|
108
|
-
openSetBackward.insert(0, endIndex);
|
|
112
|
+
const openSet = new this.heap();
|
|
113
|
+
openSet.insert(0, startIndex);
|
|
109
114
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const gScoreBackward = new Map<number, number>([[endIndex, 0]]);
|
|
115
|
+
const cameFrom = new Map<number, number>();
|
|
116
|
+
const gScore = new Map<number, number>([[startIndex, 0]]);
|
|
117
|
+
const visited = new Set<number>();
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
while (openSet.size() > 0) {
|
|
120
|
+
// Extract the node with the smallest fScore
|
|
121
|
+
const current = openSet.extractMin()!;
|
|
117
122
|
|
|
118
|
-
|
|
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;
|
|
123
|
+
// If we've reached the end node, we're done
|
|
124
|
+
if (current === endIndex) {
|
|
126
125
|
break;
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
|
|
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);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
128
|
+
visited.add(current);
|
|
138
129
|
|
|
139
|
-
|
|
140
|
-
|
|
130
|
+
// Explore neighbors
|
|
131
|
+
for (const neighbor of this.adjacencyList.get(current) || []) {
|
|
132
|
+
// Tentative cost from start to this neighbor
|
|
133
|
+
const tentativeG = (gScore.get(current) ?? Infinity) + neighbor.distance;
|
|
141
134
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
// If this path to neighbor is better, record it
|
|
136
|
+
if (tentativeG < (gScore.get(neighbor.node) ?? Infinity)) {
|
|
137
|
+
cameFrom.set(neighbor.node, current);
|
|
138
|
+
gScore.set(neighbor.node, tentativeG);
|
|
146
139
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
openSetBackward.insert(fScore, neighbor.node);
|
|
140
|
+
// Calculate fScore: gScore + heuristic distance to the end
|
|
141
|
+
const fScore =
|
|
142
|
+
tentativeG +
|
|
143
|
+
this.distanceMeasurement(this.coords[neighbor.node], this.coords[endIndex]);
|
|
144
|
+
|
|
145
|
+
openSet.insert(fScore, neighbor.node);
|
|
154
146
|
}
|
|
155
147
|
}
|
|
156
148
|
}
|
|
157
149
|
|
|
158
|
-
|
|
150
|
+
// If we never set a path to the end node, there's no route
|
|
151
|
+
if (!cameFrom.has(endIndex)) {
|
|
159
152
|
return null;
|
|
160
153
|
}
|
|
161
154
|
|
|
162
|
-
// Reconstruct
|
|
163
|
-
const
|
|
164
|
-
let node =
|
|
165
|
-
while (node !== undefined) {
|
|
166
|
-
pathForward.unshift(this.coords[node]);
|
|
167
|
-
node = cameFromForward.get(node)!;
|
|
168
|
-
}
|
|
155
|
+
// Reconstruct the path from end node to start node
|
|
156
|
+
const path: Position[] = [];
|
|
157
|
+
let node = endIndex;
|
|
169
158
|
|
|
170
|
-
// Reconstruct backward path (omit meeting node to avoid duplication)
|
|
171
|
-
const pathBackward: Position[] = [];
|
|
172
|
-
node = cameFromBackward.get(meetingNode)!;
|
|
173
159
|
while (node !== undefined) {
|
|
174
|
-
|
|
175
|
-
node =
|
|
160
|
+
path.unshift(this.coords[node]);
|
|
161
|
+
node = cameFrom.get(node)!;
|
|
176
162
|
}
|
|
177
163
|
|
|
178
|
-
const fullPath = [...pathForward, ...pathBackward];
|
|
179
|
-
|
|
180
164
|
return {
|
|
181
165
|
type: "Feature",
|
|
182
|
-
geometry: { type: "LineString", coordinates:
|
|
166
|
+
geometry: { type: "LineString", coordinates: path },
|
|
183
167
|
properties: {},
|
|
184
168
|
};
|
|
185
169
|
}
|
|
170
|
+
|
|
186
171
|
}
|
|
187
172
|
|
|
188
173
|
export { TerraRoute, createCheapRuler, haversineDistance }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Position, Feature, Point, LineString, FeatureCollection } from "geojson";
|
|
2
|
+
import { haversineDistance } from "../terra-route";
|
|
2
3
|
|
|
3
4
|
export const createPointFeature = (coord: Position): Feature<Point> => ({
|
|
4
5
|
type: "Feature",
|
|
@@ -23,6 +24,20 @@ export const createLineStringFeature = (coordinates: Position[]): Feature<LineSt
|
|
|
23
24
|
properties: {},
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
export function routeLength(
|
|
28
|
+
line: Feature<LineString>,
|
|
29
|
+
|
|
30
|
+
) {
|
|
31
|
+
const lineCoords = line.geometry.coordinates;
|
|
32
|
+
|
|
33
|
+
// Calculate the total route distance
|
|
34
|
+
let routeDistance = 0;
|
|
35
|
+
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
36
|
+
routeDistance += haversineDistance(lineCoords[i], lineCoords[i + 1]);
|
|
37
|
+
}
|
|
38
|
+
return routeDistance
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
export function generateGridWithDiagonals(n: number, spacing: number): FeatureCollection<LineString> {
|
|
27
42
|
const features: Feature<LineString>[] = [];
|
|
28
43
|
|
|
@@ -111,9 +126,9 @@ export function generateGridWithDiagonals(n: number, spacing: number): FeatureCo
|
|
|
111
126
|
*/
|
|
112
127
|
export function generateStarPolygon(
|
|
113
128
|
n: number,
|
|
114
|
-
radius
|
|
129
|
+
radius = 0.01,
|
|
115
130
|
center: Position = [0, 0],
|
|
116
|
-
connectAll
|
|
131
|
+
connectAll = true
|
|
117
132
|
): FeatureCollection<LineString> {
|
|
118
133
|
if (n < 3) {
|
|
119
134
|
throw new Error("Star polygon requires at least 3 vertices.");
|
|
@@ -212,7 +227,7 @@ export function generateTreeFeatureCollection(
|
|
|
212
227
|
depth: number,
|
|
213
228
|
branchingFactor: number,
|
|
214
229
|
root: Position = [0, 0],
|
|
215
|
-
length
|
|
230
|
+
length = 0.01
|
|
216
231
|
): FeatureCollection<LineString> {
|
|
217
232
|
if (depth < 1) {
|
|
218
233
|
throw new Error("Tree must have at least depth 1.");
|
|
@@ -374,6 +389,8 @@ export function getReasonIfLineStringInvalid(
|
|
|
374
389
|
return `Not enough coordinates: ${coords.length} (${coords})`;
|
|
375
390
|
}
|
|
376
391
|
|
|
392
|
+
const seen = new Set<string>();
|
|
393
|
+
|
|
377
394
|
// 5. Validate each coordinate is a valid Position
|
|
378
395
|
// (At minimum, [number, number] or [number, number, number])
|
|
379
396
|
for (const position of coords) {
|
|
@@ -389,5 +406,71 @@ export function getReasonIfLineStringInvalid(
|
|
|
389
406
|
) {
|
|
390
407
|
return 'Not a Position; elements are not a numbers';
|
|
391
408
|
}
|
|
409
|
+
|
|
410
|
+
// 6. Check for duplicates
|
|
411
|
+
const key = `${position[0]},${position[1]}`;
|
|
412
|
+
if (seen.has(key)) {
|
|
413
|
+
return `Duplicate coordinate: ${key}`;
|
|
414
|
+
}
|
|
415
|
+
seen.add(key);
|
|
392
416
|
}
|
|
393
417
|
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Checks if the start and end coordinates of a LineString match the given start and end points.
|
|
421
|
+
*
|
|
422
|
+
* @param line - The LineString feature to check
|
|
423
|
+
* @param start - The start point feature
|
|
424
|
+
* @param end - The end point feature
|
|
425
|
+
* @return True if the start and end coordinates match, false otherwise
|
|
426
|
+
* */
|
|
427
|
+
export function startAndEndAreCorrect(line: Feature<LineString>, start: Feature<Point>, end: Feature<Point>) {
|
|
428
|
+
const lineCoords = line.geometry.coordinates;
|
|
429
|
+
const startCoords = start.geometry.coordinates;
|
|
430
|
+
const endCoords = end.geometry.coordinates;
|
|
431
|
+
|
|
432
|
+
// Check if the first coordinate of the LineString matches the start point
|
|
433
|
+
const startMatches = lineCoords[0][0] === startCoords[0] && lineCoords[0][1] === startCoords[1];
|
|
434
|
+
|
|
435
|
+
// Check if the last coordinate of the LineString matches the end point
|
|
436
|
+
const endMatches = lineCoords[lineCoords.length - 1][0] === endCoords[0] && lineCoords[lineCoords.length - 1][1] === endCoords[1];
|
|
437
|
+
|
|
438
|
+
return startMatches && endMatches;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function routeIsLongerThanDirectPath(line: Feature<LineString>, start: Feature<Point>, end: Feature<Point>) {
|
|
442
|
+
const lineCoords = line.geometry.coordinates;
|
|
443
|
+
const startCoords = start.geometry.coordinates;
|
|
444
|
+
const endCoords = end.geometry.coordinates;
|
|
445
|
+
|
|
446
|
+
if (lineCoords.length <= 2) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Calculate the direct distance between the start and end points
|
|
451
|
+
const directDistance = haversineDistance(startCoords, endCoords);
|
|
452
|
+
|
|
453
|
+
// Calculate the route distance
|
|
454
|
+
let routeDistance = 0;
|
|
455
|
+
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
456
|
+
routeDistance += haversineDistance(lineCoords[i], lineCoords[i + 1]);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// If the route distance is 0, it means the start and end points are the same
|
|
460
|
+
if (routeDistance === 0) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (routeDistance < directDistance) {
|
|
465
|
+
|
|
466
|
+
// Check if the route distance is very close to the direct distance
|
|
467
|
+
const absoluteDifference = Math.abs(routeDistance - directDistance);
|
|
468
|
+
if (absoluteDifference < 0.000000000001) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return true
|
|
476
|
+
}
|
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
|
-
});
|