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 +2 -2
- package/eslint.config.mjs +54 -0
- package/package.json +10 -27
- package/src/terra-route.spec.ts +6 -4
- package/src/terra-route.ts +84 -40
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
|
|
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.
|
|
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 --
|
|
14
|
-
"lint:quiet": "eslint --
|
|
15
|
-
"lint:fix": "eslint --fix --
|
|
16
|
-
"lint:fix:quiet": "eslint --fix --quiet --
|
|
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": [
|
package/src/terra-route.spec.ts
CHANGED
|
@@ -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++) {
|
package/src/terra-route.ts
CHANGED
|
@@ -44,11 +44,11 @@ class TerraRoute {
|
|
|
44
44
|
return latMap.get(lat)!;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const index = this.coords.length;
|
|
48
48
|
this.coords.push(coord);
|
|
49
|
-
latMap.set(lat,
|
|
49
|
+
latMap.set(lat, index);
|
|
50
50
|
|
|
51
|
-
return
|
|
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
|
|
72
|
-
const
|
|
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(
|
|
76
|
-
if (!this.adjacencyList.has(
|
|
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(
|
|
79
|
-
this.adjacencyList.get(
|
|
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
|
|
95
|
+
throw new Error("Network not built. Please call buildRouteGraph(network) first.");
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
const
|
|
99
|
-
const
|
|
98
|
+
const startIndex = this.coordinateIndex(start.geometry.coordinates);
|
|
99
|
+
const endIndex = this.coordinateIndex(end.geometry.coordinates);
|
|
100
100
|
|
|
101
|
-
if (
|
|
101
|
+
if (startIndex === endIndex) {
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
const openSetForward = new MinHeap();
|
|
106
|
+
const openSetBackward = new MinHeap();
|
|
107
|
+
openSetForward.insert(0, startIndex);
|
|
108
|
+
openSetBackward.insert(0, endIndex);
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const fScore =
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|