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
|
@@ -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
|
+
}
|
package/src/heap/min-heap.ts
CHANGED
|
@@ -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
|
|
9
|
+
let currentIndex = this.heap.length;
|
|
10
10
|
this.heap.push(node);
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
26
|
+
|
|
27
|
+
this.heap[currentIndex] = node;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
extractMin(): number | null {
|
|
24
|
-
const
|
|
25
|
-
|
|
31
|
+
const heap = this.heap;
|
|
32
|
+
const length = heap.length;
|
|
33
|
+
|
|
34
|
+
if (length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
26
37
|
|
|
27
|
-
const minNode =
|
|
28
|
-
const endNode =
|
|
38
|
+
const minNode = heap[0];
|
|
39
|
+
const endNode = heap.pop()!;
|
|
29
40
|
|
|
30
41
|
if (length > 1) {
|
|
31
|
-
|
|
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(
|
|
43
|
-
const
|
|
53
|
+
private bubbleDown(index: number): void {
|
|
54
|
+
const heap = this.heap;
|
|
44
55
|
const length = heap.length;
|
|
45
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
package/src/terra-route.spec.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { createCheapRuler } from "./distance/cheap-ruler";
|
|
2
2
|
import { TerraRoute } from "./terra-route";
|
|
3
|
-
import {
|
|
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(() => {
|