terra-route 0.0.6 → 0.0.8

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,31 +1,30 @@
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
- * TerraRoute is a routing utility for finding the shortest path
8
- * between two geographic points over a given GeoJSON LineString network.
9
- *
10
- * The class builds an internal graph structure based on the provided network,
11
- * then applies A* algorithm to compute the shortest route.
12
- */
13
- class TerraRoute {
14
- private network: FeatureCollection<LineString> | undefined;
15
- private distanceMeasurement: (positionA: Position, positionB: Position) => number;
16
- private adjacencyList: Map<number, Array<{ node: number; distance: number }>> = new Map();
17
- private coords: Position[] = []
18
- private coordMap: Map<number, Map<number, number>> = new Map();
7
+ interface Router {
8
+ buildRouteGraph(network: FeatureCollection<LineString>): void;
9
+ getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null;
10
+ }
19
11
 
20
- /**
21
- * Creates a new instance of TerraRoute.
22
- *
23
- * @param distanceMeasurement - Optional custom distance measurement function (defaults to haversine distance).
24
- */
25
- constructor(
26
- distanceMeasurement?: (positionA: Position, positionB: Position) => number
27
- ) {
28
- this.distanceMeasurement = distanceMeasurement ? distanceMeasurement : haversineDistance;
12
+ class TerraRoute implements Router {
13
+ private network: FeatureCollection<LineString> | null = null;
14
+ private distanceMeasurement: (a: Position, b: Position) => number;
15
+ private heapConstructor: HeapConstructor;
16
+
17
+ // Map from longitude → (map from latitude → index)
18
+ private coordinateIndexMap: Map<number, Map<number, number>> = new Map();
19
+ private coordinates: Position[] = [];
20
+ private adjacencyList: Array<Array<{ node: number; distance: number }>> = [];
21
+
22
+ constructor(options?: {
23
+ distanceMeasurement?: (a: Position, b: Position) => number;
24
+ heap?: HeapConstructor;
25
+ }) {
26
+ this.distanceMeasurement = options?.distanceMeasurement ?? haversineDistance;
27
+ this.heapConstructor = options?.heap ?? MinHeap;
29
28
  }
30
29
 
31
30
  /**
@@ -35,54 +34,65 @@ class TerraRoute {
35
34
  * @param coord - A GeoJSON Position array representing [longitude, latitude].
36
35
  * @returns A unique numeric index for the coordinate.
37
36
  */
38
- private coordinateIndex(coord: Position): number {
39
- const [lng, lat] = coord;
40
- if (!this.coordMap.has(lng)) this.coordMap.set(lng, new Map());
41
-
42
- const latMap = this.coordMap.get(lng)!;
43
- if (latMap.has(lat)) {
44
- return latMap.get(lat)!;
45
- }
37
+ public buildRouteGraph(network: FeatureCollection<LineString>): void {
38
+ this.network = network;
46
39
 
47
- const index = this.coords.length;
48
- this.coords.push(coord);
49
- latMap.set(lat, index);
40
+ // Reset everything
41
+ this.coordinateIndexMap = new Map();
42
+ this.coordinates = [];
43
+ this.adjacencyList = [];
44
+
45
+ // Hoist to locals for speed
46
+ const coordIndexMapLocal = this.coordinateIndexMap;
47
+ const coordsLocal = this.coordinates;
48
+ const adjListLocal = this.adjacencyList;
49
+ const measureDistance = this.distanceMeasurement;
50
+
51
+ for (const feature of network.features) {
52
+ const lineCoords = feature.geometry.coordinates;
53
+
54
+ for (let i = 0; i < lineCoords.length - 1; i++) {
55
+ const [lngA, latA] = lineCoords[i];
56
+ const [lngB, latB] = lineCoords[i + 1];
57
+
58
+ // get or assign index for A
59
+ let latMapA = coordIndexMapLocal.get(lngA);
60
+ if (!latMapA) {
61
+ latMapA = new Map<number, number>();
62
+ coordIndexMapLocal.set(lngA, latMapA);
63
+ }
64
+ let indexA = latMapA.get(latA);
65
+ if (indexA === undefined) {
66
+ indexA = coordsLocal.length;
67
+ coordsLocal.push(lineCoords[i]);
68
+ latMapA.set(latA, indexA);
69
+ adjListLocal[indexA] = [];
70
+ }
50
71
 
51
- return index;
52
- }
72
+ // get or assign index for B
73
+ let latMapB = coordIndexMapLocal.get(lngB);
74
+ if (!latMapB) {
75
+ latMapB = new Map<number, number>();
76
+ coordIndexMapLocal.set(lngB, latMapB);
77
+ }
78
+ let indexB = latMapB.get(latB);
79
+ if (indexB === undefined) {
80
+ indexB = coordsLocal.length;
81
+ coordsLocal.push(lineCoords[i + 1]);
82
+ latMapB.set(latB, indexB);
83
+ adjListLocal[indexB] = [];
84
+ }
53
85
 
54
- /**
55
- * Builds the internal graph representation (adjacency list) from the input network.
56
- * Each LineString segment is translated into bidirectional graph edges with associated distances.
57
- * Assumes that the network is a connected graph of LineStrings with shared coordinates. Calling this
58
- * method with a new network overwrite any existing network and reset all internal data structures.
59
- *
60
- * @param network - A GeoJSON FeatureCollection of LineStrings representing the road network.
61
- */
62
- public buildRouteGraph(network: FeatureCollection<LineString>): void {
63
- this.network = network;
64
- this.adjacencyList = new Map();
65
- this.coords = [];
66
- this.coordMap = new Map();
67
-
68
- for (const feature of this.network.features) {
69
- const coords = feature.geometry.coordinates;
70
- for (let i = 0; i < coords.length - 1; i++) {
71
- const aIndex = this.coordinateIndex(coords[i]);
72
- const bIndex = this.coordinateIndex(coords[i + 1]);
73
- const distance = this.distanceMeasurement(coords[i], coords[i + 1]);
74
-
75
- if (!this.adjacencyList.has(aIndex)) this.adjacencyList.set(aIndex, []);
76
- if (!this.adjacencyList.has(bIndex)) this.adjacencyList.set(bIndex, []);
77
-
78
- this.adjacencyList.get(aIndex)!.push({ node: bIndex, distance });
79
- this.adjacencyList.get(bIndex)!.push({ node: aIndex, distance });
86
+ // record the bidirectional edge
87
+ const segmentDistance = measureDistance(lineCoords[i], lineCoords[i + 1]);
88
+ adjListLocal[indexA].push({ node: indexB, distance: segmentDistance });
89
+ adjListLocal[indexB].push({ node: indexA, distance: segmentDistance });
80
90
  }
81
91
  }
82
92
  }
83
93
 
84
94
  /**
85
- * Computes the shortest route between two points in the network using bidirectional A* algorithm.
95
+ * Computes the shortest route between two points in the network using the A* algorithm.
86
96
  *
87
97
  * @param start - A GeoJSON Point Feature representing the start location.
88
98
  * @param end - A GeoJSON Point Feature representing the end location.
@@ -90,99 +100,96 @@ class TerraRoute {
90
100
  *
91
101
  * @throws Error if the network has not been built yet with buildRouteGraph(network).
92
102
  */
93
- public getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null {
103
+ public getRoute(
104
+ start: Feature<Point>,
105
+ end: Feature<Point>
106
+ ): Feature<LineString> | null {
94
107
  if (!this.network) {
95
108
  throw new Error("Network not built. Please call buildRouteGraph(network) first.");
96
109
  }
97
110
 
98
- const startIndex = this.coordinateIndex(start.geometry.coordinates);
99
- const endIndex = this.coordinateIndex(end.geometry.coordinates);
111
+ // ensure start/end are in the index maps
112
+ const startIndex = this.getOrCreateIndex(start.geometry.coordinates);
113
+ const endIndex = this.getOrCreateIndex(end.geometry.coordinates);
100
114
 
101
115
  if (startIndex === endIndex) {
102
116
  return null;
103
117
  }
104
118
 
105
- const openSetForward = new FibonacciHeap();
106
- const openSetBackward = new FibonacciHeap();
107
- openSetForward.insert(0, startIndex);
108
- openSetBackward.insert(0, endIndex);
119
+ const openSet = new this.heapConstructor();
120
+ openSet.insert(0, startIndex);
109
121
 
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]]);
122
+ const nodeCount = this.coordinates.length;
123
+ const gScore = new Array<number>(nodeCount).fill(Infinity);
124
+ const cameFrom = new Array<number>(nodeCount).fill(-1);
125
+ const visited = new Array<boolean>(nodeCount).fill(false);
114
126
 
115
- const visitedForward = new Set<number>();
116
- const visitedBackward = new Set<number>();
127
+ gScore[startIndex] = 0;
117
128
 
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);
136
- }
129
+ while (openSet.size() > 0) {
130
+ const current = openSet.extractMin()!;
131
+ if (visited[current]) {
132
+ continue;
137
133
  }
138
-
139
- const currentBackward = openSetBackward.extractMin()!;
140
- visitedBackward.add(currentBackward);
141
-
142
- if (visitedForward.has(currentBackward)) {
143
- meetingNode = currentBackward;
134
+ if (current === endIndex) {
144
135
  break;
145
136
  }
146
-
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);
137
+ visited[current] = true;
138
+
139
+ for (const neighbor of this.adjacencyList[current] || []) {
140
+ const tentativeG = gScore[current] + neighbor.distance;
141
+ if (tentativeG < gScore[neighbor.node]) {
142
+ gScore[neighbor.node] = tentativeG;
143
+ cameFrom[neighbor.node] = current;
144
+ const heuristic = this.distanceMeasurement(
145
+ this.coordinates[neighbor.node],
146
+ this.coordinates[endIndex]
147
+ );
148
+ openSet.insert(tentativeG + heuristic, neighbor.node);
154
149
  }
155
150
  }
156
151
  }
157
152
 
158
- if (meetingNode === null) {
153
+ if (cameFrom[endIndex] < 0) {
159
154
  return null;
160
155
  }
161
156
 
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)!;
157
+ // Reconstruct path
158
+ const path: Position[] = [];
159
+ let current = endIndex;
160
+ while (current !== startIndex) {
161
+ path.unshift(this.coordinates[current]);
162
+ current = cameFrom[current];
176
163
  }
177
-
178
- const fullPath = [...pathForward, ...pathBackward];
164
+ path.unshift(this.coordinates[startIndex]);
179
165
 
180
166
  return {
181
167
  type: "Feature",
182
- geometry: { type: "LineString", coordinates: fullPath },
168
+ geometry: { type: "LineString", coordinates: path },
183
169
  properties: {},
184
170
  };
185
171
  }
172
+
173
+ /**
174
+ * Helper to index start/end in getRoute.
175
+ */
176
+ private getOrCreateIndex(coord: Position): number {
177
+ const [lng, lat] = coord;
178
+ let latMap = this.coordinateIndexMap.get(lng);
179
+ if (!latMap) {
180
+ latMap = new Map<number, number>();
181
+ this.coordinateIndexMap.set(lng, latMap);
182
+ }
183
+ let index = latMap.get(lat);
184
+ if (index === undefined) {
185
+ index = this.coordinates.length;
186
+ this.coordinates.push(coord);
187
+ latMap.set(lat, index);
188
+ // ensure adjacencyList covers this new node
189
+ this.adjacencyList[index] = [];
190
+ }
191
+ return index;
192
+ }
186
193
  }
187
194
 
188
195
  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
- }