terra-route 0.0.7 → 0.0.9
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 +25 -8
- package/dist/graph/graph.d.ts +83 -0
- package/dist/graph/methods/connected.d.ts +9 -0
- package/dist/graph/methods/nodes.d.ts +17 -0
- package/dist/graph/methods/unique-segments.d.ts +5 -0
- package/dist/terra-route.cjs +1 -1
- package/dist/terra-route.cjs.map +1 -1
- package/dist/terra-route.d.ts +15 -27
- package/dist/terra-route.modern.js +1 -1
- package/dist/terra-route.modern.js.map +1 -1
- package/dist/terra-route.module.js +1 -1
- package/dist/terra-route.module.js.map +1 -1
- package/dist/terra-route.umd.js +1 -1
- package/dist/terra-route.umd.js.map +1 -1
- package/dist/test-utils/utils.d.ts +50 -0
- package/jest.config.js +1 -0
- package/package.json +7 -3
- package/src/data/network-5-cc.geojson +822 -0
- package/src/data/network.geojson +21910 -820
- package/src/distance/haversine.ts +1 -0
- package/src/graph/graph.ts +151 -0
- package/src/graph/methods/connected.spec.ts +217 -0
- package/src/graph/methods/connected.ts +168 -0
- package/src/graph/methods/nodes.spec.ts +317 -0
- package/src/graph/methods/nodes.ts +77 -0
- package/src/graph/methods/unique-segments.spec.ts +16 -0
- package/src/graph/methods/unique-segments.ts +69 -0
- package/src/heap/min-heap.ts +50 -46
- package/src/terra-route.compare.spec.ts +3 -1
- package/src/terra-route.spec.ts +12 -1
- package/src/terra-route.ts +122 -99
- package/src/test-utils/create.ts +39 -0
- package/src/test-utils/{test-utils.ts → generate-network.ts} +9 -188
- package/src/test-utils/utils.ts +228 -0
package/src/terra-route.ts
CHANGED
|
@@ -3,33 +3,29 @@ import { haversineDistance } from "./distance/haversine";
|
|
|
3
3
|
import { createCheapRuler } from "./distance/cheap-ruler";
|
|
4
4
|
import { MinHeap } from "./heap/min-heap";
|
|
5
5
|
import { HeapConstructor } from "./heap/heap";
|
|
6
|
+
import { LineStringGraph } from "./graph/graph";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
private
|
|
20
|
-
private
|
|
8
|
+
interface Router {
|
|
9
|
+
buildRouteGraph(network: FeatureCollection<LineString>): void;
|
|
10
|
+
getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class TerraRoute implements Router {
|
|
14
|
+
private network: FeatureCollection<LineString> | null = null;
|
|
15
|
+
private distanceMeasurement: (a: Position, b: Position) => number;
|
|
16
|
+
private heapConstructor: HeapConstructor;
|
|
17
|
+
|
|
18
|
+
// Map from longitude → (map from latitude → index)
|
|
19
|
+
private coordinateIndexMap: Map<number, Map<number, number>> = new Map();
|
|
20
|
+
private coordinates: Position[] = [];
|
|
21
|
+
private adjacencyList: Array<Array<{ node: number; distance: number }>> = [];
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
-
* Creates a new instance of TerraRoute.
|
|
24
|
-
*
|
|
25
|
-
* @param distanceMeasurement - Optional custom distance measurement function (defaults to haversine distance).
|
|
26
|
-
*/
|
|
27
23
|
constructor(options?: {
|
|
28
|
-
distanceMeasurement?: (
|
|
29
|
-
heap?: HeapConstructor
|
|
24
|
+
distanceMeasurement?: (a: Position, b: Position) => number;
|
|
25
|
+
heap?: HeapConstructor;
|
|
30
26
|
}) {
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
27
|
+
this.distanceMeasurement = options?.distanceMeasurement ?? haversineDistance;
|
|
28
|
+
this.heapConstructor = options?.heap ?? MinHeap;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
/**
|
|
@@ -39,48 +35,59 @@ class TerraRoute {
|
|
|
39
35
|
* @param coord - A GeoJSON Position array representing [longitude, latitude].
|
|
40
36
|
* @returns A unique numeric index for the coordinate.
|
|
41
37
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!this.coordMap.has(lng)) this.coordMap.set(lng, new Map());
|
|
45
|
-
|
|
46
|
-
const latMap = this.coordMap.get(lng)!;
|
|
47
|
-
if (latMap.has(lat)) {
|
|
48
|
-
return latMap.get(lat)!;
|
|
49
|
-
}
|
|
38
|
+
public buildRouteGraph(network: FeatureCollection<LineString>): void {
|
|
39
|
+
this.network = network;
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
|
|
41
|
+
// Reset everything
|
|
42
|
+
this.coordinateIndexMap = new Map();
|
|
43
|
+
this.coordinates = [];
|
|
44
|
+
this.adjacencyList = [];
|
|
45
|
+
|
|
46
|
+
// Hoist to locals for speed
|
|
47
|
+
const coordIndexMapLocal = this.coordinateIndexMap;
|
|
48
|
+
const coordsLocal = this.coordinates;
|
|
49
|
+
const adjListLocal = this.adjacencyList;
|
|
50
|
+
const measureDistance = this.distanceMeasurement;
|
|
51
|
+
|
|
52
|
+
for (const feature of network.features) {
|
|
53
|
+
const lineCoords = feature.geometry.coordinates;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
56
|
+
const [lngA, latA] = lineCoords[i];
|
|
57
|
+
const [lngB, latB] = lineCoords[i + 1];
|
|
58
|
+
|
|
59
|
+
// get or assign index for A
|
|
60
|
+
let latMapA = coordIndexMapLocal.get(lngA);
|
|
61
|
+
if (!latMapA) {
|
|
62
|
+
latMapA = new Map<number, number>();
|
|
63
|
+
coordIndexMapLocal.set(lngA, latMapA);
|
|
64
|
+
}
|
|
65
|
+
let indexA = latMapA.get(latA);
|
|
66
|
+
if (indexA === undefined) {
|
|
67
|
+
indexA = coordsLocal.length;
|
|
68
|
+
coordsLocal.push(lineCoords[i]);
|
|
69
|
+
latMapA.set(latA, indexA);
|
|
70
|
+
adjListLocal[indexA] = [];
|
|
71
|
+
}
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
// get or assign index for B
|
|
74
|
+
let latMapB = coordIndexMapLocal.get(lngB);
|
|
75
|
+
if (!latMapB) {
|
|
76
|
+
latMapB = new Map<number, number>();
|
|
77
|
+
coordIndexMapLocal.set(lngB, latMapB);
|
|
78
|
+
}
|
|
79
|
+
let indexB = latMapB.get(latB);
|
|
80
|
+
if (indexB === undefined) {
|
|
81
|
+
indexB = coordsLocal.length;
|
|
82
|
+
coordsLocal.push(lineCoords[i + 1]);
|
|
83
|
+
latMapB.set(latB, indexB);
|
|
84
|
+
adjListLocal[indexB] = [];
|
|
85
|
+
}
|
|
57
86
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* method with a new network overwrite any existing network and reset all internal data structures.
|
|
63
|
-
*
|
|
64
|
-
* @param network - A GeoJSON FeatureCollection of LineStrings representing the road network.
|
|
65
|
-
*/
|
|
66
|
-
public buildRouteGraph(network: FeatureCollection<LineString>): void {
|
|
67
|
-
this.network = network;
|
|
68
|
-
this.adjacencyList = new Map();
|
|
69
|
-
this.coords = [];
|
|
70
|
-
this.coordMap = new Map();
|
|
71
|
-
|
|
72
|
-
for (const feature of this.network.features) {
|
|
73
|
-
const coords = feature.geometry.coordinates;
|
|
74
|
-
for (let i = 0; i < coords.length - 1; i++) {
|
|
75
|
-
const aIndex = this.coordinateIndex(coords[i]);
|
|
76
|
-
const bIndex = this.coordinateIndex(coords[i + 1]);
|
|
77
|
-
const distance = this.distanceMeasurement(coords[i], coords[i + 1]);
|
|
78
|
-
|
|
79
|
-
if (!this.adjacencyList.has(aIndex)) this.adjacencyList.set(aIndex, []);
|
|
80
|
-
if (!this.adjacencyList.has(bIndex)) this.adjacencyList.set(bIndex, []);
|
|
81
|
-
|
|
82
|
-
this.adjacencyList.get(aIndex)!.push({ node: bIndex, distance });
|
|
83
|
-
this.adjacencyList.get(bIndex)!.push({ node: aIndex, distance });
|
|
87
|
+
// record the bidirectional edge
|
|
88
|
+
const segmentDistance = measureDistance(lineCoords[i], lineCoords[i + 1]);
|
|
89
|
+
adjListLocal[indexA].push({ node: indexB, distance: segmentDistance });
|
|
90
|
+
adjListLocal[indexB].push({ node: indexA, distance: segmentDistance });
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
}
|
|
@@ -102,64 +109,60 @@ class TerraRoute {
|
|
|
102
109
|
throw new Error("Network not built. Please call buildRouteGraph(network) first.");
|
|
103
110
|
}
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
const
|
|
112
|
+
// ensure start/end are in the index maps
|
|
113
|
+
const startIndex = this.getOrCreateIndex(start.geometry.coordinates);
|
|
114
|
+
const endIndex = this.getOrCreateIndex(end.geometry.coordinates);
|
|
107
115
|
|
|
108
116
|
if (startIndex === endIndex) {
|
|
109
117
|
return null;
|
|
110
118
|
}
|
|
111
119
|
|
|
112
|
-
const openSet = new this.
|
|
120
|
+
const openSet = new this.heapConstructor();
|
|
113
121
|
openSet.insert(0, startIndex);
|
|
114
122
|
|
|
115
|
-
const
|
|
116
|
-
const gScore = new
|
|
117
|
-
const
|
|
123
|
+
const nodeCount = this.coordinates.length;
|
|
124
|
+
const gScore = new Array<number>(nodeCount).fill(Infinity);
|
|
125
|
+
const cameFrom = new Array<number>(nodeCount).fill(-1);
|
|
126
|
+
const visited = new Array<boolean>(nodeCount).fill(false);
|
|
127
|
+
|
|
128
|
+
gScore[startIndex] = 0;
|
|
118
129
|
|
|
119
130
|
while (openSet.size() > 0) {
|
|
120
|
-
// Extract the node with the smallest fScore
|
|
121
131
|
const current = openSet.extractMin()!;
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
if (visited[current]) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
124
135
|
if (current === endIndex) {
|
|
125
136
|
break;
|
|
126
137
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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);
|
|
138
|
+
visited[current] = true;
|
|
139
|
+
|
|
140
|
+
for (const neighbor of this.adjacencyList[current] || []) {
|
|
141
|
+
const tentativeG = gScore[current] + neighbor.distance;
|
|
142
|
+
if (tentativeG < gScore[neighbor.node]) {
|
|
143
|
+
gScore[neighbor.node] = tentativeG;
|
|
144
|
+
cameFrom[neighbor.node] = current;
|
|
145
|
+
const heuristic = this.distanceMeasurement(
|
|
146
|
+
this.coordinates[neighbor.node],
|
|
147
|
+
this.coordinates[endIndex]
|
|
148
|
+
);
|
|
149
|
+
openSet.insert(tentativeG + heuristic, neighbor.node);
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
if (!cameFrom.has(endIndex)) {
|
|
154
|
+
if (cameFrom[endIndex] < 0) {
|
|
152
155
|
return null;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
// Reconstruct
|
|
158
|
+
// Reconstruct path
|
|
156
159
|
const path: Position[] = [];
|
|
157
|
-
let
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
node = cameFrom.get(node)!;
|
|
160
|
+
let current = endIndex;
|
|
161
|
+
while (current !== startIndex) {
|
|
162
|
+
path.unshift(this.coordinates[current]);
|
|
163
|
+
current = cameFrom[current];
|
|
162
164
|
}
|
|
165
|
+
path.unshift(this.coordinates[startIndex]);
|
|
163
166
|
|
|
164
167
|
return {
|
|
165
168
|
type: "Feature",
|
|
@@ -168,6 +171,26 @@ class TerraRoute {
|
|
|
168
171
|
};
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Helper to index start/end in getRoute.
|
|
176
|
+
*/
|
|
177
|
+
private getOrCreateIndex(coord: Position): number {
|
|
178
|
+
const [lng, lat] = coord;
|
|
179
|
+
let latMap = this.coordinateIndexMap.get(lng);
|
|
180
|
+
if (!latMap) {
|
|
181
|
+
latMap = new Map<number, number>();
|
|
182
|
+
this.coordinateIndexMap.set(lng, latMap);
|
|
183
|
+
}
|
|
184
|
+
let index = latMap.get(lat);
|
|
185
|
+
if (index === undefined) {
|
|
186
|
+
index = this.coordinates.length;
|
|
187
|
+
this.coordinates.push(coord);
|
|
188
|
+
latMap.set(lat, index);
|
|
189
|
+
// ensure adjacencyList covers this new node
|
|
190
|
+
this.adjacencyList[index] = [];
|
|
191
|
+
}
|
|
192
|
+
return index;
|
|
193
|
+
}
|
|
171
194
|
}
|
|
172
195
|
|
|
173
|
-
export { TerraRoute, createCheapRuler, haversineDistance }
|
|
196
|
+
export { TerraRoute, createCheapRuler, haversineDistance, LineStringGraph }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Position, Feature, Point, LineString, FeatureCollection } from "geojson";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a GeoJSON Point feature from a coordinate.
|
|
5
|
+
* @param coord - A coordinate in the form of [longitude, latitude]
|
|
6
|
+
* @returns A GeoJSON Feature<Point> object
|
|
7
|
+
*/
|
|
8
|
+
export const createPointFeature = (coord: Position): Feature<Point> => ({
|
|
9
|
+
type: "Feature",
|
|
10
|
+
geometry: {
|
|
11
|
+
type: "Point",
|
|
12
|
+
coordinates: coord,
|
|
13
|
+
},
|
|
14
|
+
properties: {},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a GeoJSON LineString feature from an array of coordinates.
|
|
19
|
+
* @param coordinates - An array of coordinates in the form of [longitude, latitude]
|
|
20
|
+
* @returns A GeoJSON Feature<LineString> object
|
|
21
|
+
*/
|
|
22
|
+
export const createLineStringFeature = (coordinates: Position[]): Feature<LineString> => ({
|
|
23
|
+
type: "Feature",
|
|
24
|
+
geometry: {
|
|
25
|
+
type: "LineString",
|
|
26
|
+
coordinates,
|
|
27
|
+
},
|
|
28
|
+
properties: {},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a GeoJSON FeatureCollection from an array of features.
|
|
33
|
+
* @param features - An array of GeoJSON Feature<LineString> objects
|
|
34
|
+
* @returns A GeoJSON FeatureCollection object
|
|
35
|
+
*/
|
|
36
|
+
export const createFeatureCollection = (features: Feature<LineString>[]): FeatureCollection<LineString> => ({
|
|
37
|
+
type: "FeatureCollection",
|
|
38
|
+
features,
|
|
39
|
+
});
|
|
@@ -1,43 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { haversineDistance } from "../terra-route";
|
|
3
|
-
|
|
4
|
-
export const createPointFeature = (coord: Position): Feature<Point> => ({
|
|
5
|
-
type: "Feature",
|
|
6
|
-
geometry: {
|
|
7
|
-
type: "Point",
|
|
8
|
-
coordinates: coord,
|
|
9
|
-
},
|
|
10
|
-
properties: {},
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
export const createFeatureCollection = (features: Feature<LineString>[]): FeatureCollection<LineString> => ({
|
|
14
|
-
type: "FeatureCollection",
|
|
15
|
-
features,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export const createLineStringFeature = (coordinates: Position[]): Feature<LineString> => ({
|
|
19
|
-
type: "Feature",
|
|
20
|
-
geometry: {
|
|
21
|
-
type: "LineString",
|
|
22
|
-
coordinates,
|
|
23
|
-
},
|
|
24
|
-
properties: {},
|
|
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
|
-
}
|
|
1
|
+
import { FeatureCollection, LineString, Feature, Position } from "geojson";
|
|
40
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Generates a grid of LineStrings with n x n nodes, spaced by the given spacing.
|
|
5
|
+
* Each node is connected to its right and upward neighbors, and diagonals are included.
|
|
6
|
+
*
|
|
7
|
+
* @param n - Number of nodes in each dimension (n x n grid)
|
|
8
|
+
* @param spacing - Distance between consecutive nodes
|
|
9
|
+
* @returns FeatureCollection of LineStrings representing the grid
|
|
10
|
+
*/
|
|
41
11
|
export function generateGridWithDiagonals(n: number, spacing: number): FeatureCollection<LineString> {
|
|
42
12
|
const features: Feature<LineString>[] = [];
|
|
43
13
|
|
|
@@ -182,38 +152,6 @@ export function generateStarPolygon(
|
|
|
182
152
|
};
|
|
183
153
|
}
|
|
184
154
|
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Extracts unique coordinates from a FeatureCollection of LineStrings.
|
|
188
|
-
*
|
|
189
|
-
* @param collection - A GeoJSON FeatureCollection of LineStrings
|
|
190
|
-
* @returns An array of unique Position coordinates
|
|
191
|
-
*/
|
|
192
|
-
export function getUniqueCoordinatesFromLineStrings(
|
|
193
|
-
collection: FeatureCollection<LineString>
|
|
194
|
-
): Position[] {
|
|
195
|
-
const seen = new Set<string>();
|
|
196
|
-
const unique: Position[] = [];
|
|
197
|
-
|
|
198
|
-
for (const feature of collection.features) {
|
|
199
|
-
if (feature.geometry.type !== "LineString") {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
for (const coord of feature.geometry.coordinates) {
|
|
204
|
-
const key = `${coord[0]},${coord[1]}`;
|
|
205
|
-
|
|
206
|
-
if (!seen.has(key)) {
|
|
207
|
-
seen.add(key);
|
|
208
|
-
unique.push(coord);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return unique;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
155
|
/**
|
|
218
156
|
* Generate a spatial n-depth tree as a FeatureCollection<LineString>.
|
|
219
157
|
*
|
|
@@ -357,120 +295,3 @@ export function generateConcentricRings(
|
|
|
357
295
|
features,
|
|
358
296
|
};
|
|
359
297
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Validates a GeoJSON Feature<LineString> route.
|
|
364
|
-
*
|
|
365
|
-
* @param route - The GeoJSON feature to validate
|
|
366
|
-
* @returns A boolean indicating if it is a valid LineString route
|
|
367
|
-
*/
|
|
368
|
-
export function getReasonIfLineStringInvalid(
|
|
369
|
-
route: Feature<LineString> | null | undefined
|
|
370
|
-
): string | undefined {
|
|
371
|
-
// 1. Must exist
|
|
372
|
-
if (!route) {
|
|
373
|
-
return 'No feature';
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// 2. Must be a Feature
|
|
377
|
-
if (route.type !== "Feature") {
|
|
378
|
-
return 'Not a Feature';
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// 3. Must have a geometry of type LineString
|
|
382
|
-
if (!route.geometry || route.geometry.type !== "LineString") {
|
|
383
|
-
return 'Not a LineString';
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// 4. Coordinates must be an array with length >= 2
|
|
387
|
-
const coords = route.geometry.coordinates;
|
|
388
|
-
if (!Array.isArray(coords) || coords.length < 2) {
|
|
389
|
-
return `Not enough coordinates: ${coords.length} (${coords})`;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const seen = new Set<string>();
|
|
393
|
-
|
|
394
|
-
// 5. Validate each coordinate is a valid Position
|
|
395
|
-
// (At minimum, [number, number] or [number, number, number])
|
|
396
|
-
for (const position of coords) {
|
|
397
|
-
if (!Array.isArray(position)) {
|
|
398
|
-
return 'Not a Position; not an array';
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Check numeric values, ignoring optional altitude
|
|
402
|
-
if (
|
|
403
|
-
position.length < 2 ||
|
|
404
|
-
typeof position[0] !== "number" ||
|
|
405
|
-
typeof position[1] !== "number"
|
|
406
|
-
) {
|
|
407
|
-
return 'Not a Position; elements are not a numbers';
|
|
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);
|
|
416
|
-
}
|
|
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
|
-
}
|