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.
@@ -1,43 +1,13 @@
1
- import { Position, Feature, Point, LineString, FeatureCollection } from "geojson";
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
+ }