terra-route 0.0.6 → 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.
@@ -1,7 +1,8 @@
1
1
  import { FeatureCollection, LineString, Point, Feature, Position } from "geojson";
2
- import { FibonacciHeap } from "./fibonacci-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
- this.distanceMeasurement = distanceMeasurement ? distanceMeasurement : haversineDistance;
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 bidirectional graph edges with associated distances.
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 bidirectional A* algorithm.
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(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null {
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 openSetForward = new FibonacciHeap();
106
- const openSetBackward = new FibonacciHeap();
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 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]]);
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
- const visitedForward = new Set<number>();
116
- const visitedBackward = new Set<number>();
119
+ while (openSet.size() > 0) {
120
+ // Extract the node with the smallest fScore
121
+ const current = openSet.extractMin()!;
117
122
 
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;
123
+ // If we've reached the end node, we're done
124
+ if (current === endIndex) {
126
125
  break;
127
126
  }
128
127
 
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);
136
- }
137
- }
128
+ visited.add(current);
138
129
 
139
- const currentBackward = openSetBackward.extractMin()!;
140
- visitedBackward.add(currentBackward);
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
- if (visitedForward.has(currentBackward)) {
143
- meetingNode = currentBackward;
144
- break;
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
- 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);
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
- if (meetingNode === null) {
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 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
- }
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
- pathBackward.push(this.coords[node]);
175
- node = cameFromBackward.get(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: fullPath },
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: number = 0.01,
129
+ radius = 0.01,
115
130
  center: Position = [0, 0],
116
- connectAll: boolean = true
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: number = 0.01
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.");
@@ -400,3 +415,62 @@ export function getReasonIfLineStringInvalid(
400
415
  seen.add(key);
401
416
  }
402
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
+ }
@@ -1,55 +0,0 @@
1
- import { FibonacciHeap } from './fibonacci-heap';
2
-
3
- describe('FibonacciHeap', () => {
4
- let heap: FibonacciHeap;
5
-
6
- beforeEach(() => {
7
- heap = new FibonacciHeap();
8
- });
9
-
10
- test('insert() and size()', () => {
11
- expect(heap.size()).toBe(0);
12
- heap.insert(10, 1);
13
- expect(heap.size()).toBe(1);
14
- heap.insert(5, 2);
15
- expect(heap.size()).toBe(2);
16
- });
17
-
18
- test('extractMin() returns the correct value', () => {
19
- heap.insert(20, 101);
20
- heap.insert(5, 102);
21
- heap.insert(15, 103);
22
-
23
- expect(heap.extractMin()).toBe(102); // key 5
24
- expect(heap.size()).toBe(2);
25
- expect(heap.extractMin()).toBe(103); // key 15
26
- expect(heap.extractMin()).toBe(101); // key 20
27
- expect(heap.extractMin()).toBeNull();
28
- });
29
-
30
- test('extractMin() on empty heap returns null', () => {
31
- expect(heap.extractMin()).toBeNull();
32
- });
33
-
34
- test('insert() handles duplicate keys', () => {
35
- heap.insert(10, 201);
36
- heap.insert(10, 202);
37
- heap.insert(10, 203);
38
-
39
- const values = [heap.extractMin(), heap.extractMin(), heap.extractMin()];
40
- expect(values).toContain(201);
41
- expect(values).toContain(202);
42
- expect(values).toContain(203);
43
- expect(heap.size()).toBe(0);
44
- });
45
-
46
- test('interleaved insert and extractMin()', () => {
47
- heap.insert(30, 301);
48
- heap.insert(10, 302);
49
- expect(heap.extractMin()).toBe(302);
50
- heap.insert(20, 303);
51
- expect(heap.extractMin()).toBe(303);
52
- expect(heap.extractMin()).toBe(301);
53
- expect(heap.extractMin()).toBeNull();
54
- });
55
- });
@@ -1,131 +0,0 @@
1
- type FibNode = {
2
- key: number;
3
- value: number;
4
- degree: number;
5
- mark: boolean;
6
- parent: FibNode | null;
7
- child: FibNode | null;
8
- left: FibNode;
9
- right: FibNode;
10
- };
11
-
12
- export class FibonacciHeap {
13
- private nodeCount = 0;
14
- private minNode: FibNode | null = null;
15
-
16
- insert(key: number, value: number): void {
17
- const node: FibNode = {
18
- key,
19
- value,
20
- degree: 0,
21
- mark: false,
22
- parent: null,
23
- child: null,
24
- left: null as any,
25
- right: null as any,
26
- };
27
- node.left = node;
28
- node.right = node;
29
-
30
- this.minNode = this.mergeLists(this.minNode, node);
31
- this.nodeCount++;
32
- }
33
-
34
- extractMin(): number | null {
35
- const z = this.minNode;
36
- if (!z) return null;
37
-
38
- // Add children to root list
39
- if (z.child) {
40
- let child = z.child;
41
- do {
42
- child.parent = null;
43
- child = child.right!;
44
- } while (child !== z.child);
45
- this.minNode = this.mergeLists(this.minNode, z.child);
46
- }
47
-
48
- this.removeFromList(z);
49
-
50
- if (z === z.right) {
51
- this.minNode = null;
52
- } else {
53
- this.minNode = z.right;
54
- this.consolidate();
55
- }
56
-
57
- this.nodeCount--;
58
- return z.value;
59
- }
60
-
61
- size(): number {
62
- return this.nodeCount;
63
- }
64
-
65
- // ========== Internal Methods ==========
66
-
67
- private consolidate(): void {
68
- const maxDegree = Math.floor(Math.log2(this.nodeCount)) + 1;
69
- const A = new Array<FibNode | null>(maxDegree).fill(null);
70
-
71
- const rootList: FibNode[] = [];
72
- let curr = this.minNode!;
73
- do {
74
- rootList.push(curr);
75
- curr = curr.right!;
76
- } while (curr !== this.minNode);
77
-
78
- for (const w of rootList) {
79
- let x = w;
80
- let d = x.degree;
81
- while (A[d]) {
82
- let y = A[d]!;
83
- if (x.key > y.key) {
84
- const temp = x;
85
- x = y;
86
- y = temp;
87
- }
88
- this.link(y, x);
89
- A[d] = null;
90
- d++;
91
- }
92
- A[d] = x;
93
- }
94
-
95
- this.minNode = null;
96
- for (const node of A) {
97
- if (node) {
98
- this.minNode = this.mergeLists(this.minNode, node);
99
- }
100
- }
101
- }
102
-
103
- private link(y: FibNode, x: FibNode): void {
104
- this.removeFromList(y);
105
- y.left = y.right = y;
106
- x.child = this.mergeLists(x.child, y);
107
- y.parent = x;
108
- x.degree++;
109
- y.mark = false;
110
- }
111
-
112
- private mergeLists(a: FibNode | null, b: FibNode | null): FibNode {
113
- if (!a) return b!;
114
- if (!b) return a;
115
-
116
- const aRight = a.right!;
117
- const bRight = b.right!;
118
-
119
- a.right = bRight;
120
- bRight.left = a;
121
- b.right = aRight;
122
- aRight.left = b;
123
-
124
- return a.key < b.key ? a : b;
125
- }
126
-
127
- private removeFromList(node: FibNode): void {
128
- node.left.right = node.right;
129
- node.right.left = node.left;
130
- }
131
- }