terra-route 0.0.8 → 0.0.10
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 +39 -58
- package/assets/logo-dark-mode.png +0 -0
- package/assets/logo.png +0 -0
- package/dist/graph/graph.d.ts +114 -0
- package/dist/graph/methods/bounding-box.d.ts +13 -0
- package/dist/graph/methods/connected.d.ts +9 -0
- package/dist/graph/methods/duplicates.d.ts +7 -0
- package/dist/graph/methods/leaf.d.ts +12 -0
- package/dist/graph/methods/nodes.d.ts +17 -0
- package/dist/graph/methods/spatial-index/geokdbush.d.ts +3 -0
- package/dist/graph/methods/spatial-index/kdbush.d.ts +16 -0
- package/dist/graph/methods/spatial-index/tinyqueue.d.ts +11 -0
- package/dist/graph/methods/unify.d.ts +2 -0
- package/dist/graph/methods/unique-segments.d.ts +5 -0
- package/dist/graph/methods/unique.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 +2 -0
- package/src/graph/graph.spec.ts +238 -0
- package/src/graph/graph.ts +212 -0
- package/src/graph/methods/bounding-box.spec.ts +199 -0
- package/src/graph/methods/bounding-box.ts +85 -0
- package/src/graph/methods/connected.spec.ts +219 -0
- package/src/graph/methods/connected.ts +168 -0
- package/src/graph/methods/duplicates.spec.ts +161 -0
- package/src/graph/methods/duplicates.ts +117 -0
- package/src/graph/methods/leaf.spec.ts +224 -0
- package/src/graph/methods/leaf.ts +88 -0
- package/src/graph/methods/nodes.spec.ts +317 -0
- package/src/graph/methods/nodes.ts +77 -0
- package/src/graph/methods/spatial-index/geokdbush.spec.ts +86 -0
- package/src/graph/methods/spatial-index/geokdbush.ts +189 -0
- package/src/graph/methods/spatial-index/kdbush.spec.ts +67 -0
- package/src/graph/methods/spatial-index/kdbush.ts +189 -0
- package/src/graph/methods/spatial-index/tinyqueue.spec.ts +51 -0
- package/src/graph/methods/spatial-index/tinyqueue.ts +108 -0
- package/src/graph/methods/unify.spec.ts +475 -0
- package/src/graph/methods/unify.ts +132 -0
- package/src/graph/methods/unique.spec.ts +65 -0
- package/src/graph/methods/unique.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
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { FeatureCollection, LineString } from 'geojson';
|
|
2
|
+
import { unifyCloseCoordinates } from './unify';
|
|
3
|
+
import { getReasonIfLineStringInvalid } from '../../test-utils/utils';
|
|
4
|
+
import { graphGetConnectedComponentCount } from './connected';
|
|
5
|
+
import { graphGetNodeAndEdgeCount } from './nodes';
|
|
6
|
+
import { createFeatureCollection, createLineStringFeature } from '../../test-utils/create';
|
|
7
|
+
|
|
8
|
+
describe('unifyCloseCoordinates', () => {
|
|
9
|
+
describe('for an empty feature collection', () => {
|
|
10
|
+
it('returns an empty feature collection', () => {
|
|
11
|
+
const input: FeatureCollection<LineString> = {
|
|
12
|
+
type: 'FeatureCollection',
|
|
13
|
+
features: []
|
|
14
|
+
};
|
|
15
|
+
const radiusMeters = 2; // meters
|
|
16
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
17
|
+
|
|
18
|
+
expect(output).toEqual({
|
|
19
|
+
type: 'FeatureCollection',
|
|
20
|
+
features: []
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('for a single linestring', () => {
|
|
26
|
+
it('does not modify coordinates', () => {
|
|
27
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
28
|
+
createLineStringFeature([
|
|
29
|
+
[0, 0],
|
|
30
|
+
[0.00001, 0.00001], // ~1.5m apart
|
|
31
|
+
[0.00002, 0.00002] // ~1.5m apart
|
|
32
|
+
]),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const radiusMeters = 2; // meters
|
|
36
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
37
|
+
const coords = output.features[0].geometry.coordinates;
|
|
38
|
+
|
|
39
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
40
|
+
|
|
41
|
+
expect(coords).toHaveLength(3); // Should unify to a single coordinate
|
|
42
|
+
expect(coords[0]).toEqual([0, 0]); // Both coordinates should unify to the first one
|
|
43
|
+
expect(coords[1]).toEqual([0.00001, 0.00001]); // Both coordinates should unify to the first one
|
|
44
|
+
expect(coords[2]).toEqual([0.00002, 0.00002]); // Both coordinates should unify to the first one
|
|
45
|
+
|
|
46
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
47
|
+
expect(graphGetNodeAndEdgeCount(output)).toEqual({ nodeCount: 3, edgeCount: 2 });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('for two linestrings', () => {
|
|
52
|
+
it('does nothing if linestrings are already connected', () => {
|
|
53
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
54
|
+
createLineStringFeature([
|
|
55
|
+
[0, 0],
|
|
56
|
+
[0.00001, 0.00001] // ~1.5m apart
|
|
57
|
+
]),
|
|
58
|
+
createLineStringFeature([
|
|
59
|
+
[0.00001, 0.00001],
|
|
60
|
+
[0.00002, 0.00002] // ~1.5m apart
|
|
61
|
+
]),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const radiusMeters = 2; // meters
|
|
65
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
66
|
+
|
|
67
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
68
|
+
const coords = output.features[0].geometry.coordinates;
|
|
69
|
+
expect(coords).toHaveLength(2);
|
|
70
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
71
|
+
expect(coords[1]).toEqual([0.00001, 0.00001]);
|
|
72
|
+
|
|
73
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
74
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
75
|
+
expect(coordsTwo).toHaveLength(2);
|
|
76
|
+
expect(coordsTwo[0]).toEqual([0.00001, 0.00001]);
|
|
77
|
+
expect(coordsTwo[1]).toEqual([0.00002, 0.00002]);
|
|
78
|
+
|
|
79
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
80
|
+
expect(graphGetNodeAndEdgeCount(output)).toEqual({ nodeCount: 3, edgeCount: 2 });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('unifies two nearby linestring coordinates if they are within tolerance', () => {
|
|
84
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
85
|
+
createLineStringFeature([
|
|
86
|
+
[0, 0],
|
|
87
|
+
[0.00001, 0.00001] // ~1.5m apart
|
|
88
|
+
]),
|
|
89
|
+
createLineStringFeature([
|
|
90
|
+
[0.00002, 0.00002],
|
|
91
|
+
[0.00003, 0.00003] // ~1.5m apart
|
|
92
|
+
]),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const radiusMeters = 2; // meters
|
|
96
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
97
|
+
|
|
98
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
99
|
+
const coords = output.features[0].geometry.coordinates;
|
|
100
|
+
expect(coords).toHaveLength(2);
|
|
101
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
102
|
+
expect(coords[1]).toEqual([0.00001, 0.00001]);
|
|
103
|
+
|
|
104
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
105
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
106
|
+
expect(coordsTwo).toHaveLength(2);
|
|
107
|
+
expect(coordsTwo[0]).toEqual([0.00001, 0.00001]);
|
|
108
|
+
expect(coordsTwo[1]).toEqual([0.00003, 0.00003]);
|
|
109
|
+
|
|
110
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
111
|
+
expect(graphGetNodeAndEdgeCount(output)).toEqual({ nodeCount: 3, edgeCount: 2 });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('does not unify coordinates if they are not within tolerance', () => {
|
|
115
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
116
|
+
createLineStringFeature([
|
|
117
|
+
[0, 0],
|
|
118
|
+
[1, 1]
|
|
119
|
+
]),
|
|
120
|
+
createLineStringFeature([
|
|
121
|
+
[10, 10],
|
|
122
|
+
[11, 11]
|
|
123
|
+
]),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const radiusMeters = 2; // meters
|
|
127
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
128
|
+
|
|
129
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
130
|
+
const coords = output.features[0].geometry.coordinates;
|
|
131
|
+
expect(coords).toHaveLength(2);
|
|
132
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
133
|
+
expect(coords[1]).toEqual([1, 1]);
|
|
134
|
+
|
|
135
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
136
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
137
|
+
expect(coordsTwo).toHaveLength(2);
|
|
138
|
+
expect(coordsTwo[0]).toEqual([10, 10]);
|
|
139
|
+
expect(coordsTwo[1]).toEqual([11, 11]);
|
|
140
|
+
|
|
141
|
+
expect(graphGetConnectedComponentCount(output)).toBe(2);
|
|
142
|
+
expect(graphGetNodeAndEdgeCount(output)).toEqual({ nodeCount: 4, edgeCount: 2 });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does unify both coordinates to corresponding coordinates in counterpart linestring if they are within tolerance', () => {
|
|
146
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
147
|
+
createLineStringFeature([
|
|
148
|
+
[0, 0],
|
|
149
|
+
[1, 1]
|
|
150
|
+
]),
|
|
151
|
+
createLineStringFeature([
|
|
152
|
+
[0.000001, 0.000001],
|
|
153
|
+
[1.000001, 1.000001]
|
|
154
|
+
]),
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const radiusMeters = 2; // meters
|
|
158
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
159
|
+
|
|
160
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
161
|
+
const coords = output.features[0].geometry.coordinates;
|
|
162
|
+
expect(coords).toHaveLength(2);
|
|
163
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
164
|
+
expect(coords[1]).toEqual([1, 1]);
|
|
165
|
+
|
|
166
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
167
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
168
|
+
expect(coordsTwo).toHaveLength(2);
|
|
169
|
+
expect(coordsTwo[0]).toEqual([0, 0]);
|
|
170
|
+
expect(coordsTwo[1]).toEqual([1, 1]);
|
|
171
|
+
|
|
172
|
+
// Only one because lines are now identical and connected
|
|
173
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
174
|
+
expect(graphGetNodeAndEdgeCount(output)).toEqual({ nodeCount: 2, edgeCount: 1 });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('for multiple linestrings', () => {
|
|
179
|
+
it('does nothing if linestrings already connected', () => {
|
|
180
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
181
|
+
createLineStringFeature([
|
|
182
|
+
[0, 0],
|
|
183
|
+
[0.00001, 0.00001] // ~1.5m apart
|
|
184
|
+
]),
|
|
185
|
+
createLineStringFeature([
|
|
186
|
+
[0.00001, 0.00001],
|
|
187
|
+
[0.00002, 0.00002] // ~1.5m apart
|
|
188
|
+
]),
|
|
189
|
+
createLineStringFeature([
|
|
190
|
+
[0.00002, 0.00002],
|
|
191
|
+
[0.00003, 0.00003] // ~1.5m apart
|
|
192
|
+
]),
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
const radiusMeters = 2; // meters
|
|
196
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
197
|
+
|
|
198
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
199
|
+
const coords = output.features[0].geometry.coordinates;
|
|
200
|
+
expect(coords).toHaveLength(2);
|
|
201
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
202
|
+
expect(coords[1]).toEqual([0.00001, 0.00001]);
|
|
203
|
+
|
|
204
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
205
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
206
|
+
expect(coordsTwo).toHaveLength(2);
|
|
207
|
+
expect(coordsTwo[0]).toEqual([0.00001, 0.00001]);
|
|
208
|
+
expect(coordsTwo[1]).toEqual([0.00002, 0.00002]);
|
|
209
|
+
|
|
210
|
+
expect(getReasonIfLineStringInvalid(output.features[2])).toBeUndefined();
|
|
211
|
+
const coordsThree = output.features[2].geometry.coordinates;
|
|
212
|
+
expect(coordsThree).toHaveLength(2);
|
|
213
|
+
expect(coordsThree[0]).toEqual([0.00002, 0.00002]);
|
|
214
|
+
expect(coordsThree[1]).toEqual([0.00003, 0.00003]);
|
|
215
|
+
|
|
216
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
217
|
+
expect(graphGetNodeAndEdgeCount(output)).toEqual({ nodeCount: 4, edgeCount: 3 });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('does not unify any linestrings coordinates if they are not within tolerance', () => {
|
|
221
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
222
|
+
createLineStringFeature([
|
|
223
|
+
[0, 0],
|
|
224
|
+
[0.00001, 0.00001] // ~1.5m apart
|
|
225
|
+
]),
|
|
226
|
+
createLineStringFeature([
|
|
227
|
+
[0.00002, 0.00002],
|
|
228
|
+
[0.00003, 0.00003] // ~1.5m apart
|
|
229
|
+
]),
|
|
230
|
+
createLineStringFeature([
|
|
231
|
+
[0.00004, 0.00004],
|
|
232
|
+
[0.00005, 0.00005] // ~1.5m apart
|
|
233
|
+
]),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
// Note the reduced radiusMeters
|
|
237
|
+
const radiusMeters = 1; // meters
|
|
238
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
239
|
+
|
|
240
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
241
|
+
const coords = output.features[0].geometry.coordinates;
|
|
242
|
+
expect(coords).toHaveLength(2);
|
|
243
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
244
|
+
expect(coords[1]).toEqual([0.00001, 0.00001]);
|
|
245
|
+
|
|
246
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
247
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
248
|
+
expect(coordsTwo).toHaveLength(2);
|
|
249
|
+
expect(coordsTwo[0]).toEqual([0.00002, 0.00002]);
|
|
250
|
+
expect(coordsTwo[1]).toEqual([0.00003, 0.00003]);
|
|
251
|
+
|
|
252
|
+
expect(getReasonIfLineStringInvalid(output.features[2])).toBeUndefined();
|
|
253
|
+
const coordsThree = output.features[2].geometry.coordinates;
|
|
254
|
+
expect(coordsThree).toHaveLength(2);
|
|
255
|
+
expect(coordsThree[0]).toEqual([0.00004, 0.00004]);
|
|
256
|
+
expect(coordsThree[1]).toEqual([0.00005, 0.00005]);
|
|
257
|
+
|
|
258
|
+
expect(graphGetConnectedComponentCount(output)).toBe(3);
|
|
259
|
+
expect(graphGetNodeAndEdgeCount(output)).toEqual({ nodeCount: 6, edgeCount: 3 });
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('unifies linestrings coordinates where they are within tolerance', () => {
|
|
263
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
264
|
+
createLineStringFeature([
|
|
265
|
+
[0, 0],
|
|
266
|
+
[0.00001, 0.00001] // ~1.5m apart
|
|
267
|
+
]),
|
|
268
|
+
createLineStringFeature([
|
|
269
|
+
[0.00002, 0.00002],
|
|
270
|
+
[0.00003, 0.00003] // ~1.5m apart
|
|
271
|
+
]),
|
|
272
|
+
createLineStringFeature([
|
|
273
|
+
[0.00004, 0.00004],
|
|
274
|
+
[0.00005, 0.00005] // ~1.5m apart
|
|
275
|
+
]),
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
const radiusMeters = 2; // meters
|
|
279
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
280
|
+
|
|
281
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
282
|
+
const coords = output.features[0].geometry.coordinates;
|
|
283
|
+
expect(coords).toHaveLength(2);
|
|
284
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
285
|
+
expect(coords[1]).toEqual([0.00001, 0.00001]);
|
|
286
|
+
|
|
287
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
288
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
289
|
+
expect(coordsTwo).toHaveLength(2);
|
|
290
|
+
expect(coordsTwo[0]).toEqual([0.00001, 0.00001]);
|
|
291
|
+
expect(coordsTwo[1]).toEqual([0.00003, 0.00003]);
|
|
292
|
+
|
|
293
|
+
expect(getReasonIfLineStringInvalid(output.features[2])).toBeUndefined();
|
|
294
|
+
const coordsThree = output.features[2].geometry.coordinates;
|
|
295
|
+
expect(coordsThree).toHaveLength(2);
|
|
296
|
+
expect(coordsThree[0]).toEqual([0.00003, 0.00003]);
|
|
297
|
+
expect(coordsThree[1]).toEqual([0.00005, 0.00005]);
|
|
298
|
+
|
|
299
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
300
|
+
expect(graphGetNodeAndEdgeCount(output)).toEqual({ nodeCount: 4, edgeCount: 3 });
|
|
301
|
+
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('unifies closest coordinate of linestrings if they are within tolerance and there are multiple options', () => {
|
|
305
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
306
|
+
createLineStringFeature([
|
|
307
|
+
[0, 0],
|
|
308
|
+
[0.00001, 0.00001]
|
|
309
|
+
]),
|
|
310
|
+
createLineStringFeature([
|
|
311
|
+
[0.000011, 0.000011],
|
|
312
|
+
[0.00003, 0.00003]
|
|
313
|
+
]),
|
|
314
|
+
createLineStringFeature([
|
|
315
|
+
[0.000012, 0.000012],
|
|
316
|
+
[0.00005, 0.00005]
|
|
317
|
+
]),
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
const radiusMeters = 2; // meters
|
|
321
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
322
|
+
|
|
323
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
324
|
+
const coords = output.features[0].geometry.coordinates;
|
|
325
|
+
expect(coords).toHaveLength(2);
|
|
326
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
327
|
+
expect(coords[1]).toEqual([0.00001, 0.00001]);
|
|
328
|
+
|
|
329
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
330
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
331
|
+
expect(coordsTwo).toHaveLength(2);
|
|
332
|
+
expect(coordsTwo[0]).toEqual([0.00001, 0.00001]);
|
|
333
|
+
expect(coordsTwo[1]).toEqual([0.00003, 0.00003]);
|
|
334
|
+
|
|
335
|
+
expect(getReasonIfLineStringInvalid(output.features[2])).toBeUndefined();
|
|
336
|
+
const coordsThree = output.features[2].geometry.coordinates;
|
|
337
|
+
expect(coordsThree).toHaveLength(2);
|
|
338
|
+
expect(coordsThree[0]).toEqual([0.00001, 0.00001]);
|
|
339
|
+
expect(coordsThree[1]).toEqual([0.00005, 0.00005]);
|
|
340
|
+
|
|
341
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('unifies first coordinate where they are within tolerance', () => {
|
|
345
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
346
|
+
createLineStringFeature([
|
|
347
|
+
[0, 0],
|
|
348
|
+
[0.00001, 0.00001]
|
|
349
|
+
]),
|
|
350
|
+
// Going off in a different direction
|
|
351
|
+
createLineStringFeature([
|
|
352
|
+
[0.000001, 0.000001],
|
|
353
|
+
[-0.00001, -0.00001]
|
|
354
|
+
]),
|
|
355
|
+
createLineStringFeature([
|
|
356
|
+
[0.0000011, 0.0000011],
|
|
357
|
+
[-0.00003, -0.00003]
|
|
358
|
+
]),
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
const radiusMeters = 2; // meters
|
|
362
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
363
|
+
|
|
364
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
365
|
+
const coords = output.features[0].geometry.coordinates;
|
|
366
|
+
expect(coords).toHaveLength(2);
|
|
367
|
+
expect(coords[0]).toEqual([0, 0]);
|
|
368
|
+
expect(coords[1]).toEqual([0.00001, 0.00001]);
|
|
369
|
+
|
|
370
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
371
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
372
|
+
expect(coordsTwo).toHaveLength(2);
|
|
373
|
+
expect(coordsTwo[0]).toEqual([0, 0]);
|
|
374
|
+
expect(coordsTwo[1]).toEqual([-0.00001, -0.00001]);
|
|
375
|
+
|
|
376
|
+
expect(getReasonIfLineStringInvalid(output.features[2])).toBeUndefined();
|
|
377
|
+
const coordsThree = output.features[2].geometry.coordinates;
|
|
378
|
+
expect(coordsThree).toHaveLength(2);
|
|
379
|
+
expect(coordsThree[0]).toEqual([0, 0]);
|
|
380
|
+
expect(coordsThree[1]).toEqual([-0.00003, -0.00003]);
|
|
381
|
+
|
|
382
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('unifies last coordinate where they are within tolerance', () => {
|
|
386
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
387
|
+
createLineStringFeature([
|
|
388
|
+
[-3, -3],
|
|
389
|
+
[0, 0]
|
|
390
|
+
]),
|
|
391
|
+
createLineStringFeature([
|
|
392
|
+
[-1, -1],
|
|
393
|
+
[0.000001, 0.000001]
|
|
394
|
+
]),
|
|
395
|
+
createLineStringFeature([
|
|
396
|
+
[-2, -2],
|
|
397
|
+
[0.0000011, 0.0000011]
|
|
398
|
+
]),
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
const radiusMeters = 2; // meters
|
|
402
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
403
|
+
|
|
404
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
405
|
+
const coords = output.features[0].geometry.coordinates;
|
|
406
|
+
expect(coords).toHaveLength(2);
|
|
407
|
+
expect(coords[0]).toEqual([-3, -3]);
|
|
408
|
+
expect(coords[1]).toEqual([0, 0]);
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
412
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
413
|
+
expect(coordsTwo).toHaveLength(2);
|
|
414
|
+
|
|
415
|
+
expect(coordsTwo[0]).toEqual([-1, -1]);
|
|
416
|
+
expect(coordsTwo[1]).toEqual([0, 0]);
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
expect(getReasonIfLineStringInvalid(output.features[2])).toBeUndefined();
|
|
420
|
+
const coordsThree = output.features[2].geometry.coordinates;
|
|
421
|
+
expect(coordsThree).toHaveLength(2);
|
|
422
|
+
expect(coordsThree[0]).toEqual([-2, -2]);
|
|
423
|
+
expect(coordsThree[1]).toEqual([0, 0]);
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('unifies middle coordinate where they are within tolerance', () => {
|
|
430
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
431
|
+
createLineStringFeature([
|
|
432
|
+
[-1, -1],
|
|
433
|
+
[0, 0],
|
|
434
|
+
[1, 1]
|
|
435
|
+
]),
|
|
436
|
+
createLineStringFeature([
|
|
437
|
+
[-2, -2],
|
|
438
|
+
[0.000001, 0.000001],
|
|
439
|
+
[2, 2]
|
|
440
|
+
]),
|
|
441
|
+
createLineStringFeature([
|
|
442
|
+
[-3, -3],
|
|
443
|
+
[0.0000011, 0.0000011],
|
|
444
|
+
[3, 3]
|
|
445
|
+
]),
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
const radiusMeters = 2; // meters
|
|
449
|
+
const output = unifyCloseCoordinates(input, radiusMeters);
|
|
450
|
+
|
|
451
|
+
expect(getReasonIfLineStringInvalid(output.features[0])).toBeUndefined();
|
|
452
|
+
const coords = output.features[0].geometry.coordinates;
|
|
453
|
+
expect(coords).toHaveLength(3);
|
|
454
|
+
expect(coords[0]).toEqual([-1, -1]);
|
|
455
|
+
expect(coords[1]).toEqual([0, 0]);
|
|
456
|
+
expect(coords[2]).toEqual([1, 1]);
|
|
457
|
+
|
|
458
|
+
expect(getReasonIfLineStringInvalid(output.features[1])).toBeUndefined();
|
|
459
|
+
const coordsTwo = output.features[1].geometry.coordinates;
|
|
460
|
+
expect(coordsTwo).toHaveLength(3);
|
|
461
|
+
expect(coordsTwo[0]).toEqual([-2, -2]);
|
|
462
|
+
expect(coordsTwo[1]).toEqual([0, 0]);
|
|
463
|
+
expect(coordsTwo[2]).toEqual([2, 2]);
|
|
464
|
+
|
|
465
|
+
expect(getReasonIfLineStringInvalid(output.features[2])).toBeUndefined();
|
|
466
|
+
const coordsThree = output.features[2].geometry.coordinates;
|
|
467
|
+
expect(coordsThree).toHaveLength(3);
|
|
468
|
+
expect(coordsThree[0]).toEqual([-3, -3]);
|
|
469
|
+
expect(coordsThree[1]).toEqual([0, 0]);
|
|
470
|
+
expect(coordsThree[2]).toEqual([3, 3]);
|
|
471
|
+
|
|
472
|
+
expect(graphGetConnectedComponentCount(output)).toBe(1);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { FeatureCollection, LineString, Position } from 'geojson';
|
|
2
|
+
import { haversineDistance } from '../../distance/haversine';
|
|
3
|
+
import { KDBush } from './spatial-index/kdbush';
|
|
4
|
+
import { around } from './spatial-index/geokdbush';
|
|
5
|
+
|
|
6
|
+
export function unifyCloseCoordinates(
|
|
7
|
+
featureCollection: FeatureCollection<LineString>,
|
|
8
|
+
radiusMeters: number
|
|
9
|
+
): FeatureCollection<LineString> {
|
|
10
|
+
if (featureCollection.features.length < 2) {
|
|
11
|
+
return featureCollection; // No features to process
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const globalSeen: Position[] = [];
|
|
15
|
+
const globalSeenSet = new Set<string>(); // Fast O(1) lookup for globalSeen
|
|
16
|
+
const mapping: Map<string, Position> = new Map();
|
|
17
|
+
|
|
18
|
+
// Build a one-time spatial index with all coordinates for efficient querying
|
|
19
|
+
const allCoords: Position[] = [];
|
|
20
|
+
const coordToGlobalIndex = new Map<string, number>();
|
|
21
|
+
|
|
22
|
+
// Collect all unique coordinates first
|
|
23
|
+
for (const feature of featureCollection.features) {
|
|
24
|
+
for (const coord of feature.geometry.coordinates) {
|
|
25
|
+
const key = `${coord[0]},${coord[1]}`; // Faster than join
|
|
26
|
+
if (!coordToGlobalIndex.has(key)) {
|
|
27
|
+
coordToGlobalIndex.set(key, allCoords.length);
|
|
28
|
+
allCoords.push(coord);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Build spatial index once for all coordinates
|
|
34
|
+
let spatialIndex: KDBush = new KDBush(allCoords.length);
|
|
35
|
+
for (const coord of allCoords) {
|
|
36
|
+
spatialIndex.add(coord[0], coord[1]); // lng, lat
|
|
37
|
+
}
|
|
38
|
+
spatialIndex.finish();
|
|
39
|
+
|
|
40
|
+
function findOrRegisterCoordinate(
|
|
41
|
+
coordinate: Position,
|
|
42
|
+
exclude: Position[],
|
|
43
|
+
futureConflictCheck: Set<string>
|
|
44
|
+
): Position {
|
|
45
|
+
let closest: Position | null = null;
|
|
46
|
+
let closestDistance = Infinity;
|
|
47
|
+
|
|
48
|
+
// if (globalSeen.length > 5) {
|
|
49
|
+
// Use spatial index for efficient querying
|
|
50
|
+
const radiusKm = radiusMeters / 1000; // Convert meters to kilometers
|
|
51
|
+
const candidateIndices = around(spatialIndex, coordinate[0], coordinate[1], Infinity, radiusKm);
|
|
52
|
+
|
|
53
|
+
for (const candidateIndex of candidateIndices) {
|
|
54
|
+
const candidateCoord = allCoords[candidateIndex];
|
|
55
|
+
const candidateKey = `${candidateCoord[0]},${candidateCoord[1]}`;
|
|
56
|
+
|
|
57
|
+
// Only consider coordinates that are in globalSeen (O(1) lookup)
|
|
58
|
+
if (!globalSeenSet.has(candidateKey)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (exclude.includes(candidateCoord)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (futureConflictCheck.has(candidateKey)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const distance = haversineDistance(candidateCoord, coordinate) * 1000; // Convert to meters
|
|
71
|
+
|
|
72
|
+
if (distance <= radiusMeters && distance < closestDistance) {
|
|
73
|
+
closest = candidateCoord;
|
|
74
|
+
closestDistance = distance;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if (closest !== null) {
|
|
80
|
+
return closest;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
globalSeen.push(coordinate);
|
|
84
|
+
globalSeenSet.add(`${coordinate[0]},${coordinate[1]}`);
|
|
85
|
+
return coordinate;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const updatedFeatures = featureCollection.features.map((feature) => {
|
|
89
|
+
const localSeen: Position[] = [];
|
|
90
|
+
const updatedCoordinates: Position[] = [];
|
|
91
|
+
const usedKeys = new Set<string>();
|
|
92
|
+
|
|
93
|
+
for (const coordinate of feature.geometry.coordinates) {
|
|
94
|
+
const key = `${coordinate[0]},${coordinate[1]}`;
|
|
95
|
+
|
|
96
|
+
if (!mapping.has(key)) {
|
|
97
|
+
const unified = findOrRegisterCoordinate(coordinate, localSeen, usedKeys);
|
|
98
|
+
const unifiedKey = `${unified[0]},${unified[1]}`;
|
|
99
|
+
|
|
100
|
+
// Avoid inserting a coordinate if it's going to cause duplication later in this feature
|
|
101
|
+
if (usedKeys.has(unifiedKey)) {
|
|
102
|
+
mapping.set(key, coordinate);
|
|
103
|
+
} else {
|
|
104
|
+
mapping.set(key, unified);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const unifiedCoordinate = mapping.get(key)!;
|
|
109
|
+
const unifiedKey = `${unifiedCoordinate[0]},${unifiedCoordinate[1]}`;
|
|
110
|
+
|
|
111
|
+
if (!usedKeys.has(unifiedKey)) {
|
|
112
|
+
updatedCoordinates.push(unifiedCoordinate);
|
|
113
|
+
usedKeys.add(unifiedKey);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
localSeen.push(coordinate);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...feature,
|
|
121
|
+
geometry: {
|
|
122
|
+
...feature.geometry,
|
|
123
|
+
coordinates: updatedCoordinates
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...featureCollection,
|
|
130
|
+
features: updatedFeatures
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { FeatureCollection, LineString } from 'geojson';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { graphGetUniqueSegments } from './unique';
|
|
4
|
+
import { graphGetNodeAndEdgeCount } from './nodes';
|
|
5
|
+
import { createFeatureCollection } from '../../test-utils/create';
|
|
6
|
+
|
|
7
|
+
describe('graphGetUniqueSegments', () => {
|
|
8
|
+
it('returns an empty feature collection for empty input', () => {
|
|
9
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([]);
|
|
10
|
+
const output = graphGetUniqueSegments(input);
|
|
11
|
+
expect(output).toEqual(createFeatureCollection([]));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns a single linestring for a single input linestring', () => {
|
|
15
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
16
|
+
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} }
|
|
17
|
+
]);
|
|
18
|
+
const output = graphGetUniqueSegments(input);
|
|
19
|
+
expect(output.features.length).toBe(1);
|
|
20
|
+
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns two linestrings for two unconnected linestrings', () => {
|
|
24
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
25
|
+
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
26
|
+
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[2, 2], [3, 3]] }, properties: {} }
|
|
27
|
+
]);
|
|
28
|
+
const output = graphGetUniqueSegments(input);
|
|
29
|
+
expect(output.features.length).toBe(2);
|
|
30
|
+
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
31
|
+
expect(output.features[1].geometry.coordinates).toEqual([[2, 2], [3, 3]]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns two linestrings for two connected linestrings', () => {
|
|
35
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
36
|
+
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
37
|
+
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[1, 1], [2, 2]] }, properties: {} }
|
|
38
|
+
]);
|
|
39
|
+
const output = graphGetUniqueSegments(input);
|
|
40
|
+
expect(output.features.length).toBe(2);
|
|
41
|
+
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
42
|
+
expect(output.features[1].geometry.coordinates).toEqual([[1, 1], [2, 2]]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns two linestrings if one is a subsection of the other', () => {
|
|
46
|
+
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
47
|
+
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
48
|
+
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1], [2, 2]] }, properties: {} }
|
|
49
|
+
]);
|
|
50
|
+
const output = graphGetUniqueSegments(input);
|
|
51
|
+
expect(output.features.length).toBe(2);
|
|
52
|
+
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
53
|
+
expect(output.features[1].geometry.coordinates).toEqual([[1, 1], [2, 2]]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should not change the properties of the graph', () => {
|
|
57
|
+
const network = JSON.parse(readFileSync('src/data/network.geojson', 'utf-8')) as FeatureCollection<LineString>;
|
|
58
|
+
|
|
59
|
+
const networkAfter = graphGetUniqueSegments(network)
|
|
60
|
+
|
|
61
|
+
const afterNodeAndEdgeCount = graphGetNodeAndEdgeCount(networkAfter);
|
|
62
|
+
expect(afterNodeAndEdgeCount).toEqual(graphGetNodeAndEdgeCount(network));
|
|
63
|
+
expect(networkAfter.features.length).toEqual(afterNodeAndEdgeCount.edgeCount);
|
|
64
|
+
});
|
|
65
|
+
});
|