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.
@@ -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
- 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 MinHeap();
106
- const openSetBackward = new MinHeap();
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.");
@@ -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
+ }
@@ -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
- });