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.
@@ -0,0 +1,317 @@
1
+ import { FeatureCollection, LineString } from 'geojson';
2
+ import { graphGetNodesAsPoints, graphGetNodeAndEdgeCount } from './nodes';
3
+ import { createFeatureCollection, createLineStringFeature } from '../../test-utils/create';
4
+
5
+ describe('graphGetNodeAndEdgeCount', () => {
6
+ describe('for an empty feature collection', () => {
7
+ it('returns 0 nodes and edges', () => {
8
+ const input: FeatureCollection<LineString> = createFeatureCollection([]);
9
+ const output = graphGetNodeAndEdgeCount(input);
10
+
11
+ expect(output).toEqual({
12
+ nodeCount: 0,
13
+ edgeCount: 0
14
+ });
15
+ });
16
+ });
17
+
18
+ describe('for feature collection with 1 linestring', () => {
19
+ it('returns 1', () => {
20
+ const input: FeatureCollection<LineString> = createFeatureCollection([
21
+ createLineStringFeature([[0, 0], [1, 1]])
22
+ ]);
23
+ const output = graphGetNodeAndEdgeCount(input);
24
+
25
+ expect(output).toEqual({
26
+ nodeCount: 2,
27
+ edgeCount: 1
28
+ });
29
+ });
30
+ });
31
+
32
+ describe('for feature collection with 2 linestring', () => {
33
+ it('returns 3 nodes and 2 edges if line is connected', () => {
34
+ const input: FeatureCollection<LineString> = createFeatureCollection([
35
+ createLineStringFeature([[0, 0], [1, 1]]),
36
+ createLineStringFeature([[1, 1], [2, 2]]),
37
+
38
+ ]);
39
+ const output = graphGetNodeAndEdgeCount(input);
40
+
41
+ expect(output).toEqual({
42
+ nodeCount: 3,
43
+ edgeCount: 2
44
+ });
45
+ });
46
+
47
+ it('returns 4 nodes and 2 if unconnected', () => {
48
+ const input: FeatureCollection<LineString> = createFeatureCollection([
49
+ createLineStringFeature([[0, 0], [1, 1]]),
50
+ createLineStringFeature([[10, 10], [11, 11]]),
51
+
52
+ ]);
53
+ const output = graphGetNodeAndEdgeCount(input);
54
+
55
+ expect(output).toEqual({
56
+ nodeCount: 4,
57
+ edgeCount: 2
58
+ });
59
+ });
60
+ });
61
+
62
+ describe('for feature collection with 3 linestring', () => {
63
+ it('returns 1 if connected', () => {
64
+ const input: FeatureCollection<LineString> = createFeatureCollection([
65
+ createLineStringFeature([[0, 0], [1, 1]]),
66
+ createLineStringFeature([[1, 1], [2, 2]]),
67
+ createLineStringFeature([[2, 2], [3, 3]]),
68
+
69
+ ]);
70
+ const output = graphGetNodeAndEdgeCount(input);
71
+
72
+ expect(output).toEqual({
73
+ nodeCount: 4,
74
+ edgeCount: 3
75
+ });
76
+ });
77
+
78
+ it('returns 3 if unconnected', () => {
79
+ const input: FeatureCollection<LineString> = createFeatureCollection([
80
+ createLineStringFeature([[0, 0], [1, 1]]),
81
+ createLineStringFeature([[10, 10], [11, 11]]),
82
+ createLineStringFeature([[20, 20], [21, 21]]),
83
+ ]);
84
+ const output = graphGetNodeAndEdgeCount(input);
85
+
86
+ expect(output).toEqual({
87
+ nodeCount: 6,
88
+ edgeCount: 3
89
+ });
90
+ });
91
+ });
92
+
93
+
94
+ describe('for feature collection with multiple linestring', () => {
95
+ it('returns 1 when all lines share the same coordinate', () => {
96
+ const input = createFeatureCollection([
97
+ createLineStringFeature([[0, 0], [1, 1]]),
98
+ createLineStringFeature([[1, 1], [2, 2]]),
99
+ createLineStringFeature([[1, 1], [3, 3]]),
100
+ createLineStringFeature([[4, 4], [1, 1]]),
101
+ ]);
102
+ const output = graphGetNodeAndEdgeCount(input);
103
+
104
+ expect(output).toEqual({
105
+ nodeCount: 5,
106
+ edgeCount: 4
107
+ });
108
+ });
109
+
110
+ it('returns 2 when two disconnected groups exist', () => {
111
+ const input = createFeatureCollection([
112
+ createLineStringFeature([[0, 0], [1, 1]]),
113
+ createLineStringFeature([[1, 1], [2, 2]]),
114
+ createLineStringFeature([[10, 10], [11, 11]]),
115
+ createLineStringFeature([[11, 11], [12, 12]]),
116
+ ]);
117
+ const output = graphGetNodeAndEdgeCount(input);
118
+
119
+ expect(output).toEqual({
120
+ nodeCount: 6,
121
+ edgeCount: 4
122
+ });
123
+ });
124
+
125
+
126
+ it('returns 1 for a loop of connected lines', () => {
127
+ const input = createFeatureCollection([
128
+ createLineStringFeature([[0, 0], [1, 0]]),
129
+ createLineStringFeature([[1, 0], [1, 1]]),
130
+ createLineStringFeature([[1, 1], [0, 1]]),
131
+ createLineStringFeature([[0, 1], [0, 0]]),
132
+ ]);
133
+ const output = graphGetNodeAndEdgeCount(input);
134
+
135
+ expect(output).toEqual({
136
+ nodeCount: 4,
137
+ edgeCount: 4
138
+ });
139
+ });
140
+ });
141
+ });
142
+
143
+ describe('graphGetNodesAsPoints', () => {
144
+ it('returns an empty array for an empty feature collection', () => {
145
+ const input: FeatureCollection<LineString> = createFeatureCollection([]);
146
+ const output = graphGetNodesAsPoints(input);
147
+
148
+ expect(output).toEqual([]);
149
+ });
150
+
151
+ it('returns points for a single linestring', () => {
152
+ const input: FeatureCollection<LineString> = createFeatureCollection([
153
+ createLineStringFeature([[0, 0], [1, 1]])
154
+ ]);
155
+ const output = graphGetNodesAsPoints(input);
156
+
157
+ expect(output).toEqual([
158
+ {
159
+ type: 'Feature',
160
+ geometry: {
161
+ type: 'Point',
162
+ coordinates: [0, 0]
163
+ },
164
+ properties: {}
165
+ },
166
+ {
167
+ type: 'Feature',
168
+ geometry: {
169
+ type: 'Point',
170
+ coordinates: [1, 1]
171
+ },
172
+ properties: {}
173
+ }
174
+ ]);
175
+ });
176
+
177
+ it('returns points for multiple linestrings with unique coordinates', () => {
178
+ const input: FeatureCollection<LineString> = createFeatureCollection([
179
+ createLineStringFeature([[0, 0], [1, 1]]),
180
+ createLineStringFeature([[1, 1], [2, 2]]),
181
+ createLineStringFeature([[3, 3], [4, 4]])
182
+ ]);
183
+ const output = graphGetNodesAsPoints(input);
184
+
185
+ expect(output).toEqual([
186
+ {
187
+ type: 'Feature',
188
+ geometry: {
189
+ type: 'Point',
190
+ coordinates: [0, 0]
191
+ },
192
+ properties: {}
193
+ },
194
+ {
195
+ type: 'Feature',
196
+ geometry: {
197
+ type: 'Point',
198
+ coordinates: [1, 1]
199
+ },
200
+ properties: {}
201
+ },
202
+ {
203
+ type: 'Feature',
204
+ geometry: {
205
+ type: 'Point',
206
+ coordinates: [2, 2]
207
+ },
208
+ properties: {}
209
+ },
210
+ {
211
+ type: 'Feature',
212
+ geometry: {
213
+ type: 'Point',
214
+ coordinates: [3, 3]
215
+ },
216
+ properties: {}
217
+ },
218
+ {
219
+ type: 'Feature',
220
+ geometry: {
221
+ type: 'Point',
222
+ coordinates: [4, 4]
223
+ },
224
+ properties: {}
225
+ }
226
+ ]);
227
+ });
228
+
229
+ it('returns points for multiple linestrings with shared coordinates', () => {
230
+ const input: FeatureCollection<LineString> = createFeatureCollection([
231
+ createLineStringFeature([[0, 0], [1, 1]]),
232
+ createLineStringFeature([[1, 1], [2, 2]]),
233
+ createLineStringFeature([[1, 1], [3, 3]])
234
+ ]);
235
+ const output = graphGetNodesAsPoints(input);
236
+
237
+ expect(output).toEqual([
238
+ {
239
+ type: 'Feature',
240
+ geometry: {
241
+ type: 'Point',
242
+ coordinates: [0, 0]
243
+ },
244
+ properties: {}
245
+ },
246
+ {
247
+ type: 'Feature',
248
+ geometry: {
249
+ type: 'Point',
250
+ coordinates: [1, 1]
251
+ },
252
+ properties: {}
253
+ },
254
+ {
255
+ type: 'Feature',
256
+ geometry: {
257
+ type: 'Point',
258
+ coordinates: [2, 2]
259
+ },
260
+ properties: {}
261
+ },
262
+ {
263
+ type: 'Feature',
264
+ geometry: {
265
+ type: 'Point',
266
+ coordinates: [3, 3]
267
+ },
268
+ properties: {}
269
+ }
270
+ ]);
271
+ });
272
+
273
+ it('returns points for a loop of connected lines', () => {
274
+ const input: FeatureCollection<LineString> = createFeatureCollection([
275
+ createLineStringFeature([[0, 0], [1, 0]]),
276
+ createLineStringFeature([[1, 0], [1, 1]]),
277
+ createLineStringFeature([[1, 1], [0, 1]]),
278
+ createLineStringFeature([[0, 1], [0, 0]]),
279
+ ]);
280
+ const output = graphGetNodesAsPoints(input);
281
+
282
+ expect(output).toEqual([
283
+ {
284
+ type: 'Feature',
285
+ geometry: {
286
+ type: 'Point',
287
+ coordinates: [0, 0]
288
+ },
289
+ properties: {}
290
+ },
291
+ {
292
+ type: 'Feature',
293
+ geometry: {
294
+ type: 'Point',
295
+ coordinates: [1, 0]
296
+ },
297
+ properties: {}
298
+ },
299
+ {
300
+ type: 'Feature',
301
+ geometry: {
302
+ type: 'Point',
303
+ coordinates: [1, 1]
304
+ },
305
+ properties: {}
306
+ },
307
+ {
308
+ type: 'Feature',
309
+ geometry: {
310
+ type: 'Point',
311
+ coordinates: [0, 1]
312
+ },
313
+ properties: {}
314
+ }
315
+ ]);
316
+ });
317
+ })
@@ -0,0 +1,77 @@
1
+ import { Feature, FeatureCollection, LineString, Point, Position } from 'geojson'
2
+
3
+ /**
4
+ * Counts the unique nodes and edges in a GeoJSON FeatureCollection of LineString features.
5
+ * @param featureCollection - A GeoJSON FeatureCollection containing LineString features
6
+ * @returns An object containing the count of unique nodes and edges
7
+ */
8
+ export function graphGetNodeAndEdgeCount(
9
+ featureCollection: FeatureCollection<LineString>
10
+ ): { nodeCount: number; edgeCount: number } {
11
+ const nodeSet = new Set<string>()
12
+ const edgeSet = new Set<string>()
13
+
14
+ for (const feature of featureCollection.features) {
15
+ const coordinates = feature.geometry.coordinates
16
+
17
+ for (const coordinate of coordinates) {
18
+ nodeSet.add(JSON.stringify(coordinate))
19
+ }
20
+
21
+ for (let i = 0; i < coordinates.length - 1; i++) {
22
+ const coordinateOne = coordinates[i]
23
+ const coordinateTwo = coordinates[i + 1]
24
+
25
+ const edge = normalizeEdge(coordinateOne, coordinateTwo)
26
+ edgeSet.add(edge)
27
+ }
28
+ }
29
+
30
+ return {
31
+ nodeCount: nodeSet.size,
32
+ edgeCount: edgeSet.size,
33
+ }
34
+ }
35
+
36
+ function normalizeEdge(coordinateOne: Position, coordinateTwo: Position): string {
37
+ const stringOne = JSON.stringify(coordinateOne)
38
+ const stringTwo = JSON.stringify(coordinateTwo)
39
+
40
+ if (stringOne < stringTwo) {
41
+ return `${stringOne}|${stringTwo}`
42
+ }
43
+
44
+ return `${stringTwo}|${stringOne}`
45
+ }
46
+
47
+
48
+ /**
49
+ * Converts a FeatureCollection of LineString features into a FeatureCollection of Point features,
50
+ * where each unique coordinate in the LineStrings becomes a Point.
51
+ * @param lines - A GeoJSON FeatureCollection containing LineString features
52
+ * @returns A FeatureCollection of Point features representing unique nodes
53
+ */
54
+ export function graphGetNodesAsPoints(lines: FeatureCollection<LineString>): Feature<Point>[] {
55
+ const seen = new Set<string>();
56
+ const points: Feature<Point>[] = [];
57
+
58
+ for (const feature of lines.features) {
59
+ for (const coord of feature.geometry.coordinates) {
60
+ const key = coord.join(',');
61
+
62
+ if (!seen.has(key)) {
63
+ seen.add(key);
64
+ points.push({
65
+ type: 'Feature',
66
+ geometry: {
67
+ type: 'Point',
68
+ coordinates: coord
69
+ },
70
+ properties: {}
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ return points;
77
+ }
@@ -0,0 +1,16 @@
1
+ import { FeatureCollection, LineString } from 'geojson';
2
+ import { readFileSync } from 'fs';
3
+ import { graphGetUniqueSegments } from './unique-segments';
4
+ import { graphGetNodeAndEdgeCount } from './nodes';
5
+
6
+ describe('graphGetUniqueSegments', () => {
7
+ it('should not change the properties of the graph', () => {
8
+ const network = JSON.parse(readFileSync('src/data/network.geojson', 'utf-8')) as FeatureCollection<LineString>;
9
+
10
+ const networkAfter = graphGetUniqueSegments(network)
11
+
12
+ const afterNodeAndEdgeCount = graphGetNodeAndEdgeCount(networkAfter);
13
+ expect(afterNodeAndEdgeCount).toEqual(graphGetNodeAndEdgeCount(network));
14
+ expect(networkAfter.features.length).toEqual(afterNodeAndEdgeCount.edgeCount);
15
+ });
16
+ });
@@ -0,0 +1,69 @@
1
+ import {
2
+ Feature,
3
+ FeatureCollection,
4
+ LineString,
5
+ Position
6
+ } from 'geojson';
7
+
8
+ /**
9
+ * Normalize a segment so that [A, B] is equal to [B, A]
10
+ */
11
+ function normalizeSegment(start: Position, end: Position): [Position, Position] {
12
+ const [aLat, aLng] = start;
13
+ const [bLat, bLng] = end;
14
+
15
+ if (
16
+ aLat < bLat ||
17
+ (aLat === bLat && aLng <= bLng)
18
+ ) {
19
+ return [start, end];
20
+ }
21
+
22
+ return [end, start];
23
+ }
24
+
25
+ /**
26
+ * Convert a pair of Positions to a string key for deduplication
27
+ */
28
+ function segmentKey(start: Position, end: Position): string {
29
+ const [normalizedStart, normalizedEnd] = normalizeSegment(start, end);
30
+ return JSON.stringify([normalizedStart, normalizedEnd]);
31
+ }
32
+
33
+ /**
34
+ * Breaks LineStrings in a FeatureCollection into unique single line segments
35
+ */
36
+ export function graphGetUniqueSegments(
37
+ input: FeatureCollection<LineString>
38
+ ): FeatureCollection<LineString> {
39
+ const uniqueSegments = new Map<string, Feature<LineString>>();
40
+
41
+ for (const feature of input.features) {
42
+ const coordinates = feature.geometry.coordinates;
43
+
44
+ for (let index = 0; index < coordinates.length - 1; index++) {
45
+ const start = coordinates[index];
46
+ const end = coordinates[index + 1];
47
+
48
+ const key = segmentKey(start, end);
49
+
50
+ if (!uniqueSegments.has(key)) {
51
+ const segment: Feature<LineString> = {
52
+ type: 'Feature',
53
+ geometry: {
54
+ type: 'LineString',
55
+ coordinates: [start, end]
56
+ },
57
+ properties: {}
58
+ };
59
+
60
+ uniqueSegments.set(key, segment);
61
+ }
62
+ }
63
+ }
64
+
65
+ return {
66
+ type: 'FeatureCollection',
67
+ features: Array.from(uniqueSegments.values())
68
+ };
69
+ }
@@ -6,29 +6,40 @@ export class MinHeap implements Heap {
6
6
 
7
7
  insert(key: number, value: number): void {
8
8
  const node = { key, value, index: this.insertCounter++ };
9
- let idx = this.heap.length;
9
+ let currentIndex = this.heap.length;
10
10
  this.heap.push(node);
11
11
 
12
- // Optimized Bubble Up
13
- while (idx > 0) {
14
- const parentIdx = (idx - 1) >>> 1; // Fast Math.floor((idx - 1) / 2)
15
- const parent = this.heap[parentIdx];
16
- if (node.key > parent.key || (node.key === parent.key && node.index > parent.index)) break;
17
- this.heap[idx] = parent;
18
- idx = parentIdx;
12
+ while (currentIndex > 0) {
13
+ const parentIndex = (currentIndex - 1) >>> 1;
14
+ const parent = this.heap[parentIndex];
15
+
16
+ if (
17
+ key > parent.key ||
18
+ (key === parent.key && node.index > parent.index)
19
+ ) {
20
+ break;
21
+ }
22
+
23
+ this.heap[currentIndex] = parent;
24
+ currentIndex = parentIndex;
19
25
  }
20
- this.heap[idx] = node;
26
+
27
+ this.heap[currentIndex] = node;
21
28
  }
22
29
 
23
30
  extractMin(): number | null {
24
- const length = this.heap.length;
25
- if (length === 0) return null;
31
+ const heap = this.heap;
32
+ const length = heap.length;
33
+
34
+ if (length === 0) {
35
+ return null;
36
+ }
26
37
 
27
- const minNode = this.heap[0];
28
- const endNode = this.heap.pop()!;
38
+ const minNode = heap[0];
39
+ const endNode = heap.pop()!;
29
40
 
30
41
  if (length > 1) {
31
- this.heap[0] = endNode;
42
+ heap[0] = endNode;
32
43
  this.bubbleDown(0);
33
44
  }
34
45
 
@@ -39,52 +50,45 @@ export class MinHeap implements Heap {
39
50
  return this.heap.length;
40
51
  }
41
52
 
42
- private bubbleDown(idx: number): void {
43
- const { heap } = this;
53
+ private bubbleDown(index: number): void {
54
+ const heap = this.heap;
44
55
  const length = heap.length;
45
- // Grab the parent node once, then move it down only if needed
46
- const node = heap[idx];
56
+ const node = heap[index];
47
57
  const nodeKey = node.key;
48
58
  const nodeIndex = node.index;
49
59
 
50
- // eslint-disable-next-line no-constant-condition
51
60
  while (true) {
52
- // Calculate left and right child indexes
53
- const leftIdx = (idx << 1) + 1;
54
- if (leftIdx >= length) {
55
- // No children => we’re already in place
61
+ const leftChildIndex = (index << 1) + 1;
62
+ if (leftChildIndex >= length) {
56
63
  break;
57
64
  }
58
65
 
59
- // Assume left child is the smaller one by default
60
- let smallestIdx = leftIdx;
61
- let smallestKey = heap[leftIdx].key;
62
- let smallestIndex = heap[leftIdx].index;
63
-
64
- const rightIdx = leftIdx + 1;
65
- if (rightIdx < length) {
66
- // Compare left child vs. right child
67
- const rightKey = heap[rightIdx].key;
68
- const rightIndex = heap[rightIdx].index;
69
- if (rightKey < smallestKey || (rightKey === smallestKey && rightIndex < smallestIndex)) {
70
- smallestIdx = rightIdx;
71
- smallestKey = rightKey;
72
- smallestIndex = rightIndex;
66
+ let smallestIndex = leftChildIndex;
67
+ let smallest = heap[leftChildIndex];
68
+
69
+ const rightChildIndex = leftChildIndex + 1;
70
+ if (rightChildIndex < length) {
71
+ const right = heap[rightChildIndex];
72
+ if (
73
+ right.key < smallest.key ||
74
+ (right.key === smallest.key && right.index < smallest.index)
75
+ ) {
76
+ smallestIndex = rightChildIndex;
77
+ smallest = right;
73
78
  }
74
79
  }
75
80
 
76
- // Compare the smaller child with the parent
77
- if (smallestKey < nodeKey || (smallestKey === nodeKey && smallestIndex < nodeIndex)) {
78
- // Swap the smaller child up
79
- heap[idx] = heap[smallestIdx];
80
- idx = smallestIdx;
81
+ if (
82
+ smallest.key < nodeKey ||
83
+ (smallest.key === nodeKey && smallest.index < nodeIndex)
84
+ ) {
85
+ heap[index] = smallest;
86
+ index = smallestIndex;
81
87
  } else {
82
- // We’re in the correct position now, so stop
83
88
  break;
84
89
  }
85
90
  }
86
91
 
87
- // Place the original node in its final position
88
- heap[idx] = node;
92
+ heap[index] = node;
89
93
  }
90
- }
94
+ }
@@ -1,7 +1,9 @@
1
1
  import PathFinder, { pathToGeoJSON } from "geojson-path-finder";
2
2
  import { FeatureCollection, LineString, Position } from "geojson";
3
3
  import { haversineDistance, TerraRoute } from "./terra-route";
4
- import { createPointFeature, routeLength } from "./test-utils/test-utils";
4
+ import { routeLength } from "./test-utils/utils";
5
+ import { createPointFeature } from "./test-utils/create";
6
+
5
7
  import { readFileSync } from 'fs';
6
8
 
7
9
 
@@ -1,8 +1,19 @@
1
1
  import { createCheapRuler } from "./distance/cheap-ruler";
2
2
  import { TerraRoute } from "./terra-route";
3
- import { createFeatureCollection, createLineStringFeature, createPointFeature, generateConcentricRings, generateGridWithDiagonals, generateStarPolygon, generateTreeFeatureCollection, getReasonIfLineStringInvalid, getUniqueCoordinatesFromLineStrings, routeIsLongerThanDirectPath, startAndEndAreCorrect } from "./test-utils/test-utils";
3
+ import {
4
+ createFeatureCollection, createLineStringFeature, createPointFeature,
5
+ } from "./test-utils/create";
6
+
7
+ import {
8
+ generateConcentricRings, generateGridWithDiagonals, generateStarPolygon, generateTreeFeatureCollection,
9
+ } from "./test-utils/generate-network";
10
+
11
+ import {
12
+ getReasonIfLineStringInvalid, getUniqueCoordinatesFromLineStrings, routeIsLongerThanDirectPath, startAndEndAreCorrect
13
+ } from "./test-utils/utils";
4
14
 
5
15
  describe("TerraRoute", () => {
16
+
6
17
  let routeFinder: TerraRoute;
7
18
 
8
19
  beforeEach(() => {