terra-route 0.0.8 → 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 +24 -7
- 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 +2 -1
- 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/terra-route.compare.spec.ts +3 -1
- package/src/terra-route.spec.ts +12 -1
- package/src/terra-route.ts +2 -1
- 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
|
@@ -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
|
-
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Position, Feature, Point, LineString, FeatureCollection } from "geojson";
|
|
2
|
+
import { haversineDistance } from "../terra-route";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calculates the total length of a LineString route in meters.
|
|
6
|
+
*
|
|
7
|
+
* @param line - A GeoJSON Feature<LineString> representing the route
|
|
8
|
+
* @returns The total length of the route in meters
|
|
9
|
+
*/
|
|
10
|
+
export function routeLength(
|
|
11
|
+
line: Feature<LineString>,
|
|
12
|
+
) {
|
|
13
|
+
const lineCoords = line.geometry.coordinates;
|
|
14
|
+
|
|
15
|
+
// Calculate the total route distance
|
|
16
|
+
let routeDistance = 0;
|
|
17
|
+
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
18
|
+
routeDistance += haversineDistance(lineCoords[i], lineCoords[i + 1]);
|
|
19
|
+
}
|
|
20
|
+
return routeDistance
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extracts unique coordinates from a FeatureCollection of LineStrings.
|
|
25
|
+
*
|
|
26
|
+
* @param collection - A GeoJSON FeatureCollection of LineStrings
|
|
27
|
+
* @returns An array of unique Position coordinates
|
|
28
|
+
*/
|
|
29
|
+
export function getUniqueCoordinatesFromLineStrings(
|
|
30
|
+
collection: FeatureCollection<LineString>
|
|
31
|
+
): Position[] {
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
const unique: Position[] = [];
|
|
34
|
+
|
|
35
|
+
for (const feature of collection.features) {
|
|
36
|
+
if (feature.geometry.type !== "LineString") {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const coord of feature.geometry.coordinates) {
|
|
41
|
+
const key = `${coord[0]},${coord[1]}`;
|
|
42
|
+
|
|
43
|
+
if (!seen.has(key)) {
|
|
44
|
+
seen.add(key);
|
|
45
|
+
unique.push(coord);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return unique;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates a GeoJSON Feature<LineString> route.
|
|
55
|
+
*
|
|
56
|
+
* @param route - The GeoJSON feature to validate
|
|
57
|
+
* @returns A boolean indicating if it is a valid LineString route
|
|
58
|
+
*/
|
|
59
|
+
export function getReasonIfLineStringInvalid(
|
|
60
|
+
route: Feature<LineString> | null | undefined
|
|
61
|
+
): string | undefined {
|
|
62
|
+
// 1. Must exist
|
|
63
|
+
if (!route) {
|
|
64
|
+
return 'No feature';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Must be a Feature
|
|
68
|
+
if (route.type !== "Feature") {
|
|
69
|
+
return 'Not a Feature';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Must have a geometry of type LineString
|
|
73
|
+
if (!route.geometry || route.geometry.type !== "LineString") {
|
|
74
|
+
return 'Not a LineString';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. Coordinates must be an array with length >= 2
|
|
78
|
+
const coords = route.geometry.coordinates;
|
|
79
|
+
if (!Array.isArray(coords) || coords.length < 2) {
|
|
80
|
+
return `Not enough coordinates: ${coords.length} (${coords})`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const seen = new Set<string>();
|
|
84
|
+
|
|
85
|
+
// 5. Validate each coordinate is a valid Position
|
|
86
|
+
// (At minimum, [number, number] or [number, number, number])
|
|
87
|
+
for (const position of coords) {
|
|
88
|
+
if (!Array.isArray(position)) {
|
|
89
|
+
return 'Not a Position; not an array';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check numeric values, ignoring optional altitude
|
|
93
|
+
if (
|
|
94
|
+
position.length < 2 ||
|
|
95
|
+
typeof position[0] !== "number" ||
|
|
96
|
+
typeof position[1] !== "number"
|
|
97
|
+
) {
|
|
98
|
+
return 'Not a Position; elements are not a numbers';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 6. Check for duplicates
|
|
102
|
+
const key = `${position[0]},${position[1]}`;
|
|
103
|
+
if (seen.has(key)) {
|
|
104
|
+
return `Duplicate coordinate: ${key}`;
|
|
105
|
+
}
|
|
106
|
+
seen.add(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Checks if the start and end coordinates of a LineString match the given start and end points.
|
|
112
|
+
*
|
|
113
|
+
* @param line - The LineString feature to check
|
|
114
|
+
* @param start - The start point feature
|
|
115
|
+
* @param end - The end point feature
|
|
116
|
+
* @return True if the start and end coordinates match, false otherwise
|
|
117
|
+
* */
|
|
118
|
+
export function startAndEndAreCorrect(line: Feature<LineString>, start: Feature<Point>, end: Feature<Point>): boolean {
|
|
119
|
+
const lineCoords = line.geometry.coordinates;
|
|
120
|
+
const startCoords = start.geometry.coordinates;
|
|
121
|
+
const endCoords = end.geometry.coordinates;
|
|
122
|
+
|
|
123
|
+
// Check if the first coordinate of the LineString matches the start point
|
|
124
|
+
const startMatches = lineCoords[0][0] === startCoords[0] && lineCoords[0][1] === startCoords[1];
|
|
125
|
+
|
|
126
|
+
// Check if the last coordinate of the LineString matches the end point
|
|
127
|
+
const endMatches = lineCoords[lineCoords.length - 1][0] === endCoords[0] && lineCoords[lineCoords.length - 1][1] === endCoords[1];
|
|
128
|
+
|
|
129
|
+
return startMatches && endMatches;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Checks if the route represented by a LineString is longer than the direct path.
|
|
134
|
+
* In theory, a route should always longer than the direct path if it has more than two points.
|
|
135
|
+
* @param line - The LineString feature representing the route
|
|
136
|
+
* @param start - The start point feature
|
|
137
|
+
* @param end - The end point feature
|
|
138
|
+
* @returns - True if the route is longer than the direct path, false otherwise
|
|
139
|
+
*/
|
|
140
|
+
export function routeIsLongerThanDirectPath(line: Feature<LineString>, start: Feature<Point>, end: Feature<Point>): boolean {
|
|
141
|
+
const lineCoords = line.geometry.coordinates;
|
|
142
|
+
const startCoords = start.geometry.coordinates;
|
|
143
|
+
const endCoords = end.geometry.coordinates;
|
|
144
|
+
|
|
145
|
+
if (lineCoords.length <= 2) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Calculate the direct distance between the start and end points
|
|
150
|
+
const directDistance = haversineDistance(startCoords, endCoords);
|
|
151
|
+
|
|
152
|
+
// Calculate the route distance
|
|
153
|
+
let routeDistance = 0;
|
|
154
|
+
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
155
|
+
routeDistance += haversineDistance(lineCoords[i], lineCoords[i + 1]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If the route distance is 0, it means the start and end points are the same
|
|
159
|
+
if (routeDistance === 0) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (routeDistance < directDistance) {
|
|
164
|
+
|
|
165
|
+
// Check if the route distance is very close to the direct distance
|
|
166
|
+
const absoluteDifference = Math.abs(routeDistance - directDistance);
|
|
167
|
+
if (absoluteDifference < 0.000000000001) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Modifies a FeatureCollection of LineStrings to break connections
|
|
179
|
+
* between lines that share coordinates, by adjusting one of the shared
|
|
180
|
+
* coordinates within a given tolerance.
|
|
181
|
+
*
|
|
182
|
+
* @param collection - The input FeatureCollection of LineStrings
|
|
183
|
+
* @param tolerance - The amount by which to offset shared coordinates (in degrees)
|
|
184
|
+
* @returns A new FeatureCollection with modified coordinates
|
|
185
|
+
*/
|
|
186
|
+
export function disconnectLineStrings(
|
|
187
|
+
collection: FeatureCollection<LineString>,
|
|
188
|
+
tolerance: number
|
|
189
|
+
): FeatureCollection<LineString> {
|
|
190
|
+
const seenCoordinates = new Map<string, number>()
|
|
191
|
+
|
|
192
|
+
function getCoordinateKey(coordinate: Position): string {
|
|
193
|
+
return `${coordinate[0]},${coordinate[1]}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function offsetCoordinate(coordinate: Position, count: number): Position {
|
|
197
|
+
const offset = count * tolerance
|
|
198
|
+
return [coordinate[0] + offset, coordinate[1] + offset]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const updatedFeatures = collection.features.map((feature) => {
|
|
202
|
+
const updatedCoordinates: Position[] = feature.geometry.coordinates.map((coordinate) => {
|
|
203
|
+
const key = getCoordinateKey(coordinate)
|
|
204
|
+
|
|
205
|
+
if (seenCoordinates.has(key)) {
|
|
206
|
+
const count = seenCoordinates.get(key)!
|
|
207
|
+
seenCoordinates.set(key, count + 1)
|
|
208
|
+
return offsetCoordinate(coordinate, count + 1)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
seenCoordinates.set(key, 0)
|
|
212
|
+
return coordinate
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
...feature,
|
|
217
|
+
geometry: {
|
|
218
|
+
...feature.geometry,
|
|
219
|
+
coordinates: updatedCoordinates
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
...collection,
|
|
226
|
+
features: updatedFeatures
|
|
227
|
+
}
|
|
228
|
+
}
|