terra-route 0.0.11 → 0.0.12
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 +2 -12
- package/dist/terra-route.cjs +1 -1
- package/dist/terra-route.cjs.map +1 -1
- package/dist/terra-route.d.ts +1 -2
- 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/package.json +1 -1
- package/src/terra-route.ts +1 -2
- package/src/graph/graph.spec.ts +0 -238
- package/src/graph/graph.ts +0 -212
- package/src/graph/methods/bounding-box.spec.ts +0 -199
- package/src/graph/methods/bounding-box.ts +0 -85
- package/src/graph/methods/connected.spec.ts +0 -219
- package/src/graph/methods/connected.ts +0 -168
- package/src/graph/methods/duplicates.spec.ts +0 -161
- package/src/graph/methods/duplicates.ts +0 -117
- package/src/graph/methods/leaf.spec.ts +0 -224
- package/src/graph/methods/leaf.ts +0 -88
- package/src/graph/methods/nodes.spec.ts +0 -317
- package/src/graph/methods/nodes.ts +0 -77
- package/src/graph/methods/spatial-index/geokdbush.spec.ts +0 -86
- package/src/graph/methods/spatial-index/geokdbush.ts +0 -189
- package/src/graph/methods/spatial-index/kdbush.spec.ts +0 -67
- package/src/graph/methods/spatial-index/kdbush.ts +0 -189
- package/src/graph/methods/spatial-index/tinyqueue.spec.ts +0 -51
- package/src/graph/methods/spatial-index/tinyqueue.ts +0 -108
- package/src/graph/methods/unify.spec.ts +0 -475
- package/src/graph/methods/unify.ts +0 -132
- package/src/graph/methods/unique.spec.ts +0 -65
- package/src/graph/methods/unique.ts +0 -69
|
@@ -1,317 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,77 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { KDBush } from './kdbush';
|
|
2
|
-
import { around, distance } from './geokdbush';
|
|
3
|
-
|
|
4
|
-
describe('geokdbush', () => {
|
|
5
|
-
const points = [
|
|
6
|
-
[0, 0], [1, 1], [2, 2], [3, 3],
|
|
7
|
-
[1, 0], [0, 1], [3, 2], [2, 3]
|
|
8
|
-
];
|
|
9
|
-
const index = new KDBush(points.length);
|
|
10
|
-
for (const p of points) {
|
|
11
|
-
index.add(p[0], p[1]);
|
|
12
|
-
}
|
|
13
|
-
index.finish();
|
|
14
|
-
|
|
15
|
-
it('should return points within a given radius', () => {
|
|
16
|
-
const d = distance(0, 0, 1, 1);
|
|
17
|
-
const result = around(index, 0, 0, Infinity, d + 1);
|
|
18
|
-
const resultPoints = result.map(i => points[i]);
|
|
19
|
-
expect(resultPoints).toEqual(expect.arrayContaining([
|
|
20
|
-
[0, 0], [1, 1], [1, 0], [0, 1]
|
|
21
|
-
]));
|
|
22
|
-
expect(resultPoints.length).toBe(4);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('defaults to maxResults and maxDistance being Infinity', () => {
|
|
26
|
-
const result = around(index, 0, 0);
|
|
27
|
-
expect(result.length).toBe(points.length);
|
|
28
|
-
const resultPoints = result.map(i => points[i]);
|
|
29
|
-
expect(resultPoints).toEqual(expect.arrayContaining(points));
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should return an empty array if no points are within the radius', () => {
|
|
33
|
-
const result = around(index, 10, 10, Infinity, 1);
|
|
34
|
-
expect(result).toEqual([]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should handle maxResults correctly', () => {
|
|
38
|
-
const result = around(index, 0, 0, 2);
|
|
39
|
-
expect(result.length).toBe(2);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should also export a distance function', () => {
|
|
43
|
-
expect(distance(0, 0, 1, 1)).toBeCloseTo(157.2493, 2);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('with a larger number of points', () => {
|
|
47
|
-
const largePoints: [number, number][] = [];
|
|
48
|
-
for (let i = 0; i < 1000; i++) {
|
|
49
|
-
// Points around London
|
|
50
|
-
largePoints.push([
|
|
51
|
-
-0.1278 + (Math.random() - 0.5) * 2,
|
|
52
|
-
51.5074 + (Math.random() - 0.5) * 2
|
|
53
|
-
]);
|
|
54
|
-
}
|
|
55
|
-
const index = new KDBush(largePoints.length, 16);
|
|
56
|
-
for (const p of largePoints) {
|
|
57
|
-
index.add(p[0], p[1]);
|
|
58
|
-
}
|
|
59
|
-
index.finish();
|
|
60
|
-
|
|
61
|
-
it('should find points around a location', () => {
|
|
62
|
-
const results = around(index, -0.1278, 51.5074, 10);
|
|
63
|
-
expect(results.length).toBe(10);
|
|
64
|
-
results.forEach(i => {
|
|
65
|
-
const p = largePoints[i];
|
|
66
|
-
expect(distance(p[0], p[1], -0.1278, 51.5074)).toBeLessThan(200); // 200km, rough check
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should find points within a radius', () => {
|
|
71
|
-
const results = around(index, -0.1278, 51.5074, Infinity, 10); // 10km radius
|
|
72
|
-
expect(results.length).toBeGreaterThan(0);
|
|
73
|
-
results.forEach(i => {
|
|
74
|
-
const p = largePoints[i];
|
|
75
|
-
expect(distance(p[0], p[1], -0.1278, 51.5074)).toBeLessThanOrEqual(10);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should work with maxResults being undefined', () => {
|
|
81
|
-
const result = around(index, 0, 0, undefined);
|
|
82
|
-
expect(result.length).toBe(points.length);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
// Adapted from https://github.com/mourner/geokdbush
|
|
2
|
-
|
|
3
|
-
// ISC License
|
|
4
|
-
|
|
5
|
-
// Copyright (c) 2017, Vladimir Agafonkin
|
|
6
|
-
|
|
7
|
-
// Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
8
|
-
// with or without fee is hereby granted, provided that the above copyright notice
|
|
9
|
-
// and this permission notice appear in all copies.
|
|
10
|
-
|
|
11
|
-
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
12
|
-
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
13
|
-
// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
14
|
-
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
15
|
-
// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
16
|
-
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
17
|
-
// THIS SOFTWARE.
|
|
18
|
-
|
|
19
|
-
import { KDBush } from './kdbush';
|
|
20
|
-
import TinyQueue from './tinyqueue';
|
|
21
|
-
|
|
22
|
-
const earthRadius = 6371;
|
|
23
|
-
const rad = Math.PI / 180;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Distance is in kilometers
|
|
27
|
-
export function around(index: KDBush, lng: number, lat: number, maxResults = Infinity, maxDistanceKm = Infinity) {
|
|
28
|
-
let maxHaverSinDist = 1;
|
|
29
|
-
const result = [];
|
|
30
|
-
|
|
31
|
-
if (maxResults === undefined) {
|
|
32
|
-
maxResults = Infinity;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (maxDistanceKm !== undefined) {
|
|
36
|
-
maxHaverSinDist = haverSin(maxDistanceKm / earthRadius);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// a distance-sorted priority queue that will contain both points and kd-tree nodes
|
|
40
|
-
const q = new TinyQueue([], compareDist);
|
|
41
|
-
|
|
42
|
-
// an object that represents the top kd-tree node (the whole Earth)
|
|
43
|
-
let node = {
|
|
44
|
-
left: 0, // left index in the kd-tree array
|
|
45
|
-
right: index.ids.length - 1, // right index
|
|
46
|
-
axis: 0, // 0 for longitude axis and 1 for latitude axis
|
|
47
|
-
dist: 0, // will hold the lower bound of children's distances to the query point
|
|
48
|
-
minLng: -180, // bounding box of the node
|
|
49
|
-
minLat: -90,
|
|
50
|
-
maxLng: 180,
|
|
51
|
-
maxLat: 90
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const cosLat = Math.cos(lat * rad);
|
|
55
|
-
|
|
56
|
-
while (node) {
|
|
57
|
-
const right = node.right;
|
|
58
|
-
const left = node.left;
|
|
59
|
-
|
|
60
|
-
if (right - left <= index.nodeSize) { // leaf node
|
|
61
|
-
|
|
62
|
-
// add all points of the leaf node to the queue
|
|
63
|
-
for (let i = left; i <= right; i++) {
|
|
64
|
-
const id = index.ids[i];
|
|
65
|
-
|
|
66
|
-
const dist = haverSinDist(lng, lat, index.coords[2 * i], index.coords[2 * i + 1], cosLat);
|
|
67
|
-
q.push({ id, dist });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
} else { // not a leaf node (has child nodes)
|
|
71
|
-
|
|
72
|
-
const m = (left + right) >> 1; // middle index
|
|
73
|
-
const midLng = index.coords[2 * m];
|
|
74
|
-
const midLat = index.coords[2 * m + 1];
|
|
75
|
-
|
|
76
|
-
// add middle point to the queue
|
|
77
|
-
const id = index.ids[m];
|
|
78
|
-
const dist = haverSinDist(lng, lat, midLng, midLat, cosLat);
|
|
79
|
-
q.push({ id, dist });
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const nextAxis = (node.axis + 1) % 2;
|
|
83
|
-
|
|
84
|
-
// first half of the node
|
|
85
|
-
const leftNode = {
|
|
86
|
-
left,
|
|
87
|
-
right: m - 1,
|
|
88
|
-
axis: nextAxis,
|
|
89
|
-
minLng: node.minLng,
|
|
90
|
-
minLat: node.minLat,
|
|
91
|
-
maxLng: node.axis === 0 ? midLng : node.maxLng,
|
|
92
|
-
maxLat: node.axis === 1 ? midLat : node.maxLat,
|
|
93
|
-
dist: 0
|
|
94
|
-
};
|
|
95
|
-
// second half of the node
|
|
96
|
-
const rightNode = {
|
|
97
|
-
left: m + 1,
|
|
98
|
-
right,
|
|
99
|
-
axis: nextAxis,
|
|
100
|
-
minLng: node.axis === 0 ? midLng : node.minLng,
|
|
101
|
-
minLat: node.axis === 1 ? midLat : node.minLat,
|
|
102
|
-
maxLng: node.maxLng,
|
|
103
|
-
maxLat: node.maxLat,
|
|
104
|
-
dist: 0
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
leftNode.dist = boxDist(lng, lat, cosLat, leftNode);
|
|
108
|
-
rightNode.dist = boxDist(lng, lat, cosLat, rightNode);
|
|
109
|
-
|
|
110
|
-
// add child nodes to the queue
|
|
111
|
-
q.push(leftNode);
|
|
112
|
-
q.push(rightNode);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// fetch closest points from the queue; they're guaranteed to be closer
|
|
116
|
-
// than all remaining points (both individual and those in kd-tree nodes),
|
|
117
|
-
// since each node's distance is a lower bound of distances to its children
|
|
118
|
-
while (q.length && q.peek().id != null) {
|
|
119
|
-
const candidate = q.pop()!;
|
|
120
|
-
if (candidate.dist > maxHaverSinDist) return result;
|
|
121
|
-
result.push(candidate.id);
|
|
122
|
-
if (result.length === maxResults) return result;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// the next closest kd-tree node
|
|
126
|
-
node = q.pop();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// lower bound for distance from a location to points inside a bounding box
|
|
133
|
-
function boxDist(lng: number, lat: number, cosLat: number, node: any) {
|
|
134
|
-
const minLng = node.minLng;
|
|
135
|
-
const maxLng = node.maxLng;
|
|
136
|
-
const minLat = node.minLat;
|
|
137
|
-
const maxLat = node.maxLat;
|
|
138
|
-
|
|
139
|
-
// query point is between minimum and maximum longitudes
|
|
140
|
-
if (lng >= minLng && lng <= maxLng) {
|
|
141
|
-
if (lat < minLat) return haverSin((lat - minLat) * rad);
|
|
142
|
-
if (lat > maxLat) return haverSin((lat - maxLat) * rad);
|
|
143
|
-
return 0;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// query point is west or east of the bounding box;
|
|
147
|
-
// calculate the extremum for great circle distance from query point to the closest longitude;
|
|
148
|
-
const haverSinDLng = Math.min(haverSin((lng - minLng) * rad), haverSin((lng - maxLng) * rad));
|
|
149
|
-
const extremumLat = vertexLat(lat, haverSinDLng);
|
|
150
|
-
|
|
151
|
-
// if extremum is inside the box, return the distance to it
|
|
152
|
-
if (extremumLat > minLat && extremumLat < maxLat) {
|
|
153
|
-
return haverSinDistPartial(haverSinDLng, cosLat, lat, extremumLat);
|
|
154
|
-
}
|
|
155
|
-
// otherwise return the distan e to one of the bbox corners (whichever is closest)
|
|
156
|
-
return Math.min(
|
|
157
|
-
haverSinDistPartial(haverSinDLng, cosLat, lat, minLat),
|
|
158
|
-
haverSinDistPartial(haverSinDLng, cosLat, lat, maxLat)
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function compareDist(a: any, b: any) {
|
|
163
|
-
return a.dist - b.dist;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function haverSin(theta: number) {
|
|
167
|
-
const s = Math.sin(theta / 2);
|
|
168
|
-
return s * s;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function haverSinDistPartial(haverSinDLng: number, cosLat1: number, lat1: number, lat2: number) {
|
|
172
|
-
return cosLat1 * Math.cos(lat2 * rad) * haverSinDLng + haverSin((lat1 - lat2) * rad);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function haverSinDist(lng1: number, lat1: number, lng2: number, lat2: number, cosLat1: number) {
|
|
176
|
-
const haverSinDLng = haverSin((lng1 - lng2) * rad);
|
|
177
|
-
return haverSinDistPartial(haverSinDLng, cosLat1, lat1, lat2);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function distance(lng1: number, lat1: number, lng2: number, lat2: number) {
|
|
181
|
-
const h = haverSinDist(lng1, lat1, lng2, lat2, Math.cos(lat1 * rad));
|
|
182
|
-
return 2 * earthRadius * Math.asin(Math.sqrt(h));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function vertexLat(lat: number, haverSinDLng: number) {
|
|
186
|
-
const cosDLng = 1 - 2 * haverSinDLng;
|
|
187
|
-
if (cosDLng <= 0) return lat > 0 ? 90 : -90;
|
|
188
|
-
return Math.atan(Math.tan(lat * rad) / cosDLng) / rad;
|
|
189
|
-
}
|