terra-route 0.0.4 → 0.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Terra Route
2
2
 
3
- Terra Route aims to be a fast library for routing on GeoJSON LineStrings networks, where LineStrings share identical coordinates. Terra Routes main aim is currently performance.
3
+ Terra Route aims to be a fast library for routing on GeoJSON LineStrings networks, where LineStrings share identical coordinates. Terra Routes main aim is currently performance - it uses bidirectional A* to help achieve this.
4
4
 
5
5
  ## Install
6
6
 
@@ -89,7 +89,7 @@ You can run the benchmarks yourself using:
89
89
  npm run benchmark
90
90
  ```
91
91
 
92
- Using default Haversine distance, Terra Route is approximately 1.6x faster than GeoJSON Path Finder with Haversine distance. If you pass in the CheapRuler distance metric (you can use the exposed `createCheapRuler` function), it is about 3x faster.
92
+ Using default Haversine distance, Terra Route is approximately 3x faster than GeoJSON Path Finder with Haversine distance. If you pass in the CheapRuler distance metric (you can use the exposed `createCheapRuler` function), it is about 5x faster.
93
93
 
94
94
  ## Limitations
95
95
 
@@ -0,0 +1,54 @@
1
+ import typescriptEslint from "@typescript-eslint/eslint-plugin";
2
+ import tsParser from "@typescript-eslint/parser";
3
+ import eslintPluginPrettier from "eslint-plugin-prettier";
4
+ import prettierConfig from "eslint-config-prettier";
5
+ import json from "@eslint/json";
6
+ import markdown from "@eslint/markdown";
7
+
8
+ const ignores = ["**/node_modules", "**/dist", "**/docs", ".github", ".husky", ".vscode", "**/public", "**/coverage", "packages/e2e/playwright-report"];
9
+
10
+ export default [
11
+ {
12
+ ignores
13
+ },
14
+ {
15
+ files: ["**/*.json"],
16
+ language: "json/json",
17
+ plugins: {
18
+ json,
19
+ }
20
+ },
21
+ {
22
+ files: ["**/*.md"],
23
+ plugins: {
24
+ markdown
25
+ },
26
+ language: "markdown/commonmark",
27
+ },
28
+ {
29
+ name: "prettier", // Configuration name
30
+ files: ["**/*.{js,jsx,ts,tsx,json,md,yml,yaml,html,css}"],
31
+ ignores: ['**.json'],
32
+ plugins: {
33
+ prettier: eslintPluginPrettier, // Include Prettier plugin
34
+ },
35
+ rules: {
36
+ ...prettierConfig.rules, // Disable ESLint rules that conflict with Prettier
37
+ },
38
+ },
39
+ {
40
+ name: "typescript", // Configuration name
41
+ files: ["**/*.ts"], // TypeScript-specific configuration
42
+ plugins: {
43
+ "@typescript-eslint": typescriptEslint, // Include TypeScript ESLint plugin
44
+ },
45
+ languageOptions: {
46
+ parser: tsParser,
47
+ },
48
+ rules: {
49
+ "@typescript-eslint/no-empty-function": "warn",
50
+ "@typescript-eslint/no-explicit-any": "warn",
51
+ "no-console": process.env.CI ? "error" : "warn",
52
+ },
53
+ },
54
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terra-route",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "A library for routing along GeoJSON LineString networks",
5
5
  "scripts": {
6
6
  "docs": "typedoc",
@@ -10,10 +10,12 @@
10
10
  "build": "microbundle",
11
11
  "watch": "microbundle --watch --format modern",
12
12
  "unused": "knip",
13
- "lint": "eslint --ext .ts src/",
14
- "lint:quiet": "eslint --ext .ts --quiet src/",
15
- "lint:fix": "eslint --fix --ext .ts src/",
16
- "lint:fix:quiet": "eslint --fix --quiet --ext .ts src/"
13
+ "lint": "eslint --config eslint.config.mjs",
14
+ "lint:quiet": "eslint --quiet --config eslint.config.mjs",
15
+ "lint:fix": "eslint --fix --config eslint.config.mjs",
16
+ "lint:fix:quiet": "eslint --fix --quiet --config eslint.config.mjs",
17
+ "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"",
18
+ "format:quiet": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\" --log-level=silent"
17
19
  },
18
20
  "type": "module",
19
21
  "source": "src/terra-route.ts",
@@ -29,6 +31,8 @@
29
31
  "author": "James Milner",
30
32
  "license": "MIT",
31
33
  "devDependencies": {
34
+ "@eslint/json": "^0.11.0",
35
+ "@eslint/markdown": "^6.3.0",
32
36
  "@types/jest": "^29.5.14",
33
37
  "@types/lodash": "^4.17.13",
34
38
  "@typescript-eslint/eslint-plugin": "8.16.0",
@@ -47,9 +51,7 @@
47
51
  "typedoc": "^0.28.1",
48
52
  "typescript": "^5.8.3"
49
53
  },
50
- "dependencies": {
51
- "@turf/turf": "^7.1.0"
52
- },
54
+ "dependencies": {},
53
55
  "keywords": [
54
56
  "geojson",
55
57
  "linestring",
@@ -60,25 +62,6 @@
60
62
  "astar",
61
63
  "djikstra"
62
64
  ],
63
- "eslintConfig": {
64
- "parser": "@typescript-eslint/parser",
65
- "plugins": [
66
- "@typescript-eslint"
67
- ],
68
- "rules": {
69
- "@typescript-eslint/no-empty-function": "warn",
70
- "@typescript-eslint/no-explicit-any": "warn"
71
- },
72
- "extends": [
73
- "plugin:@typescript-eslint/recommended",
74
- "prettier"
75
- ]
76
- },
77
- "prettier": {
78
- "printWidth": 80,
79
- "semi": true,
80
- "useTabs": true
81
- },
82
65
  "knip": {
83
66
  "$schema": "https://unpkg.com/knip@5/schema.json",
84
67
  "entry": [
@@ -378,7 +378,7 @@ describe("TerraRoute", () => {
378
378
  it('is able to route through a interconnected hexagonal star polygon ignoring other routes than the direct line', () => {
379
379
  const network = generateStarPolygon(5, 1, [0, 0]);
380
380
  const points = getUniqueCoordinatesFromLineStrings(network);
381
-
381
+ routeFinder.buildRouteGraph(network);
382
382
 
383
383
  for (let i = 0; i < points.length - 1; i++) {
384
384
  const start = createPointFeature(points[i]);
@@ -393,6 +393,7 @@ describe("TerraRoute", () => {
393
393
  it('is able to route through a interconnected hexadecagon star polygon ignoring other routes than the direct line', () => {
394
394
  const network = generateStarPolygon(16, 1, [0, 0]);
395
395
  const points = getUniqueCoordinatesFromLineStrings(network);
396
+ routeFinder.buildRouteGraph(network);
396
397
 
397
398
  for (let i = 0; i < points.length - 1; i++) {
398
399
  const start = createPointFeature(points[i]);
@@ -407,6 +408,7 @@ describe("TerraRoute", () => {
407
408
  it('is able to route around a non-interconnected hexagon star polygon', () => {
408
409
  const network = generateStarPolygon(5, 1, [0, 0], false);
409
410
  const points = getUniqueCoordinatesFromLineStrings(network)
411
+ routeFinder.buildRouteGraph(network);
410
412
 
411
413
  for (let i = 1; i < points.length; i++) {
412
414
  const start = createPointFeature(points[0]);
@@ -425,6 +427,7 @@ describe("TerraRoute", () => {
425
427
  it('is able to route around a non-interconnected hexadecagon star polygon', () => {
426
428
  const network = generateStarPolygon(16, 1, [0, 0], false);
427
429
  const points = getUniqueCoordinatesFromLineStrings(network)
430
+ routeFinder.buildRouteGraph(network);
428
431
 
429
432
  for (let i = 1; i < points.length; i++) {
430
433
  const start = createPointFeature(points[0]);
@@ -445,6 +448,7 @@ describe("TerraRoute", () => {
445
448
  const depth = 5;
446
449
  const network = generateTreeFeatureCollection(depth, 2)
447
450
  const points = getUniqueCoordinatesFromLineStrings(network)
451
+ routeFinder.buildRouteGraph(network);
448
452
 
449
453
  const start = createPointFeature(points[0]);
450
454
  const end = createPointFeature(points[points.length - 1]);
@@ -457,8 +461,8 @@ describe("TerraRoute", () => {
457
461
 
458
462
  it('is able to traverse a concentric graph with 3 rings', () => {
459
463
  const network = generateConcentricRings(3, 10, 1, [0, 0])
460
-
461
464
  const points = getUniqueCoordinatesFromLineStrings(network)
465
+ routeFinder.buildRouteGraph(network);
462
466
 
463
467
  for (let i = 1; i < points.length; i++) {
464
468
  const start = createPointFeature(points[0]);
@@ -475,9 +479,7 @@ describe("TerraRoute", () => {
475
479
 
476
480
  it('is able to traverse a concentric graph with 5 rings', () => {
477
481
  const network = generateConcentricRings(5, 10, 1, [0, 0])
478
-
479
482
  const points = getUniqueCoordinatesFromLineStrings(network)
480
-
481
483
  routeFinder.buildRouteGraph(network);
482
484
 
483
485
  for (let i = 1; i < points.length; i++) {
@@ -44,11 +44,11 @@ class TerraRoute {
44
44
  return latMap.get(lat)!;
45
45
  }
46
46
 
47
- const idx = this.coords.length;
47
+ const index = this.coords.length;
48
48
  this.coords.push(coord);
49
- latMap.set(lat, idx);
49
+ latMap.set(lat, index);
50
50
 
51
- return idx;
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 aIdx = this.coordinateIndex(coords[i]);
72
- const bIdx = this.coordinateIndex(coords[i + 1]);
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(aIdx)) this.adjacencyList.set(aIdx, []);
76
- if (!this.adjacencyList.has(bIdx)) this.adjacencyList.set(bIdx, []);
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(aIdx)!.push({ node: bIdx, distance });
79
- this.adjacencyList.get(bIdx)!.push({ node: aIdx, distance });
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 buildNetworkGraph(network) first.");
95
+ throw new Error("Network not built. Please call buildRouteGraph(network) first.");
96
96
  }
97
97
 
98
- const startIdx = this.coordinateIndex(start.geometry.coordinates);
99
- const endIdx = this.coordinateIndex(end.geometry.coordinates);
98
+ const startIndex = this.coordinateIndex(start.geometry.coordinates);
99
+ const endIndex = this.coordinateIndex(end.geometry.coordinates);
100
100
 
101
- if (startIdx === endIdx) {
101
+ if (startIndex === endIndex) {
102
102
  return null;
103
103
  }
104
104
 
105
- const openSet = new MinHeap();
106
- openSet.insert(0, startIdx);
107
- const cameFrom = new Map<number, number>();
108
- const gScore = new Map<number, number>([[startIdx, 0]]);
105
+ const openSetForward = new MinHeap();
106
+ const openSetBackward = new MinHeap();
107
+ openSetForward.insert(0, startIndex);
108
+ openSetBackward.insert(0, endIndex);
109
109
 
110
- while (openSet.size() > 0) {
111
- const current = openSet.extractMin()!;
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
- if (current === endIdx) {
114
- const path: Position[] = [];
115
- let currNode: number | undefined = current;
116
- while (currNode !== undefined) {
117
- path.unshift(this.coords[currNode]);
118
- currNode = cameFrom.get(currNode);
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 neighbors = this.adjacencyList.get(current) || [];
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 neighbors) {
130
- const tentativeGScore = (gScore.get(current) ?? Infinity) + neighbor.distance;
131
- if (tentativeGScore < (gScore.get(neighbor.node) ?? Infinity)) {
132
- cameFrom.set(neighbor.node, current);
133
- gScore.set(neighbor.node, tentativeGScore);
134
- const fScore = tentativeGScore + this.distanceMeasurement(this.coords[neighbor.node], this.coords[endIdx]);
135
- openSet.insert(fScore, neighbor.node);
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
- return null;
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