terra-route 0.0.11 → 0.0.13
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 +7 -17
- 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/instructions.md +13 -0
- package/package.json +2 -1
- package/src/terra-route.compare.spec.ts +81 -0
- package/src/terra-route.spec.ts +576 -0
- package/src/terra-route.ts +370 -154
- 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,85 +0,0 @@
|
|
|
1
|
-
import { Feature, FeatureCollection, LineString, Position } from 'geojson'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Type representing a bounding box as [minLng, minLat, maxLng, maxLat]
|
|
5
|
-
*/
|
|
6
|
-
export type BoundingBox = [number, number, number, number]
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Filters a FeatureCollection of LineString features to only include LineStrings
|
|
10
|
-
* that are completely within the specified bounding box.
|
|
11
|
-
* @param featureCollection - A GeoJSON FeatureCollection containing LineString features
|
|
12
|
-
* @param boundingBox - A bounding box array in the format [minLng, minLat, maxLng, maxLat]
|
|
13
|
-
* @returns A new FeatureCollection<LineString> containing only the LineStrings completely within the bounding box
|
|
14
|
-
*/
|
|
15
|
-
export function getNetworkInBoundingBox(
|
|
16
|
-
featureCollection: FeatureCollection<LineString>,
|
|
17
|
-
boundingBox: BoundingBox
|
|
18
|
-
): FeatureCollection<LineString> {
|
|
19
|
-
const [minLng, minLat, maxLng, maxLat] = boundingBox
|
|
20
|
-
|
|
21
|
-
// Validate bounding box
|
|
22
|
-
if (minLng >= maxLng || minLat >= maxLat) {
|
|
23
|
-
throw new Error('Invalid bounding box: min values must be less than max values')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const filteredFeatures: Feature<LineString>[] = []
|
|
27
|
-
|
|
28
|
-
for (const feature of featureCollection.features) {
|
|
29
|
-
if (isLineStringCompletelyWithinBounds(feature, minLng, minLat, maxLng, maxLat)) {
|
|
30
|
-
filteredFeatures.push(feature)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
type: 'FeatureCollection',
|
|
36
|
-
features: filteredFeatures
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Checks if a LineString feature is completely within the specified bounds.
|
|
42
|
-
* @param lineStringFeature - A GeoJSON Feature<LineString>
|
|
43
|
-
* @param minLng - Minimum longitude
|
|
44
|
-
* @param minLat - Minimum latitude
|
|
45
|
-
* @param maxLng - Maximum longitude
|
|
46
|
-
* @param maxLat - Maximum latitude
|
|
47
|
-
* @returns true if all coordinates of the LineString are within the bounds, false otherwise
|
|
48
|
-
*/
|
|
49
|
-
function isLineStringCompletelyWithinBounds(
|
|
50
|
-
lineStringFeature: Feature<LineString>,
|
|
51
|
-
minLng: number,
|
|
52
|
-
minLat: number,
|
|
53
|
-
maxLng: number,
|
|
54
|
-
maxLat: number
|
|
55
|
-
): boolean {
|
|
56
|
-
const coordinates = lineStringFeature.geometry.coordinates
|
|
57
|
-
|
|
58
|
-
for (const coordinate of coordinates) {
|
|
59
|
-
if (!isCoordinateWithinBounds(coordinate, minLng, minLat, maxLng, maxLat)) {
|
|
60
|
-
return false
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return true
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Checks if a coordinate is within the specified bounds.
|
|
69
|
-
* @param coordinate - A coordinate position [lng, lat]
|
|
70
|
-
* @param minLng - Minimum longitude
|
|
71
|
-
* @param minLat - Minimum latitude
|
|
72
|
-
* @param maxLng - Maximum longitude
|
|
73
|
-
* @param maxLat - Maximum latitude
|
|
74
|
-
* @returns true if the coordinate is within the bounds, false otherwise
|
|
75
|
-
*/
|
|
76
|
-
function isCoordinateWithinBounds(
|
|
77
|
-
coordinate: Position,
|
|
78
|
-
minLng: number,
|
|
79
|
-
minLat: number,
|
|
80
|
-
maxLng: number,
|
|
81
|
-
maxLat: number
|
|
82
|
-
): boolean {
|
|
83
|
-
const [lng, lat] = coordinate
|
|
84
|
-
return lng >= minLng && lng <= maxLng && lat >= minLat && lat <= maxLat
|
|
85
|
-
}
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { FeatureCollection, LineString } from 'geojson';
|
|
2
|
-
import { graphGetConnectedComponentCount, graphGetConnectedComponents } from './connected';
|
|
3
|
-
import { generateTreeFeatureCollection } from '../../test-utils/generate-network';
|
|
4
|
-
import { createFeatureCollection, createLineStringFeature } from '../../test-utils/create';
|
|
5
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
6
|
-
import { unifyCloseCoordinates } from './unify';
|
|
7
|
-
|
|
8
|
-
describe('countConnectedComponents', () => {
|
|
9
|
-
describe('for an empty feature collection', () => {
|
|
10
|
-
it('returns 0', () => {
|
|
11
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([]);
|
|
12
|
-
const output = graphGetConnectedComponentCount(input);
|
|
13
|
-
|
|
14
|
-
expect(output).toBe(0);
|
|
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 = graphGetConnectedComponentCount(input);
|
|
24
|
-
|
|
25
|
-
expect(output).toBe(1);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('for feature collection with 2 linestring', () => {
|
|
30
|
-
it('returns 1 if connected', () => {
|
|
31
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
32
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
33
|
-
createLineStringFeature([[1, 1], [2, 2]]),
|
|
34
|
-
|
|
35
|
-
]);
|
|
36
|
-
const output = graphGetConnectedComponentCount(input);
|
|
37
|
-
|
|
38
|
-
expect(output).toBe(1);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('returns 2 if unconnected', () => {
|
|
42
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
43
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
44
|
-
createLineStringFeature([[10, 10], [11, 11]]),
|
|
45
|
-
|
|
46
|
-
]);
|
|
47
|
-
const output = graphGetConnectedComponentCount(input);
|
|
48
|
-
|
|
49
|
-
expect(output).toBe(2);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('for feature collection with 3 linestring', () => {
|
|
54
|
-
it('returns 1 if connected', () => {
|
|
55
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
56
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
57
|
-
createLineStringFeature([[1, 1], [2, 2]]),
|
|
58
|
-
createLineStringFeature([[2, 2], [3, 3]]),
|
|
59
|
-
|
|
60
|
-
]);
|
|
61
|
-
const output = graphGetConnectedComponentCount(input);
|
|
62
|
-
|
|
63
|
-
expect(output).toBe(1);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns 3 if unconnected', () => {
|
|
67
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
68
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
69
|
-
createLineStringFeature([[10, 10], [11, 11]]),
|
|
70
|
-
createLineStringFeature([[20, 20], [21, 21]]),
|
|
71
|
-
]);
|
|
72
|
-
const output = graphGetConnectedComponentCount(input);
|
|
73
|
-
|
|
74
|
-
expect(output).toBe(3);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
describe('for feature collection with multiple linestring', () => {
|
|
80
|
-
it('returns 1 when all lines share the same coordinate', () => {
|
|
81
|
-
const input = createFeatureCollection([
|
|
82
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
83
|
-
createLineStringFeature([[1, 1], [2, 2]]),
|
|
84
|
-
createLineStringFeature([[1, 1], [3, 3]]),
|
|
85
|
-
createLineStringFeature([[4, 4], [1, 1]]),
|
|
86
|
-
]);
|
|
87
|
-
const output = graphGetConnectedComponentCount(input);
|
|
88
|
-
|
|
89
|
-
expect(output).toBe(1);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('returns 2 when two disconnected groups exist', () => {
|
|
93
|
-
const input = createFeatureCollection([
|
|
94
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
95
|
-
createLineStringFeature([[1, 1], [2, 2]]),
|
|
96
|
-
createLineStringFeature([[10, 10], [11, 11]]),
|
|
97
|
-
createLineStringFeature([[11, 11], [12, 12]]),
|
|
98
|
-
]);
|
|
99
|
-
const output = graphGetConnectedComponentCount(input);
|
|
100
|
-
|
|
101
|
-
expect(output).toBe(2);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
it('returns 1 for a loop of connected lines', () => {
|
|
106
|
-
const input = createFeatureCollection([
|
|
107
|
-
createLineStringFeature([[0, 0], [1, 0]]),
|
|
108
|
-
createLineStringFeature([[1, 0], [1, 1]]),
|
|
109
|
-
createLineStringFeature([[1, 1], [0, 1]]),
|
|
110
|
-
createLineStringFeature([[0, 1], [0, 0]]),
|
|
111
|
-
]);
|
|
112
|
-
const output = graphGetConnectedComponentCount(input);
|
|
113
|
-
|
|
114
|
-
expect(output).toBe(1);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('for complex linestring network', () => {
|
|
119
|
-
it('returns 1 when tree is connected', () => {
|
|
120
|
-
const input = generateTreeFeatureCollection(3, 2)
|
|
121
|
-
const output = graphGetConnectedComponentCount(input)
|
|
122
|
-
|
|
123
|
-
expect(output).toBe(1);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('returns 1 when it is connected correctly', () => {
|
|
127
|
-
const input = generateTreeFeatureCollection(3, 2)
|
|
128
|
-
const output = graphGetConnectedComponentCount(input)
|
|
129
|
-
|
|
130
|
-
expect(output).toBe(1);
|
|
131
|
-
});
|
|
132
|
-
})
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('splitIntoConnectedComponents', () => {
|
|
136
|
-
it('returns empty array for empty feature collection', () => {
|
|
137
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([]);
|
|
138
|
-
const output = graphGetConnectedComponents(input);
|
|
139
|
-
|
|
140
|
-
expect(output).toEqual([]);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('returns single component for single linestring', () => {
|
|
144
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
145
|
-
createLineStringFeature([[0, 0], [1, 1]])
|
|
146
|
-
]);
|
|
147
|
-
const output = graphGetConnectedComponents(input);
|
|
148
|
-
|
|
149
|
-
expect(output).toHaveLength(1);
|
|
150
|
-
expect(output[0].features).toHaveLength(1);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('returns multiple components for disconnected linestrings', () => {
|
|
154
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
155
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
156
|
-
createLineStringFeature([[10, 10], [11, 11]])
|
|
157
|
-
]);
|
|
158
|
-
const output = graphGetConnectedComponents(input);
|
|
159
|
-
|
|
160
|
-
expect(output).toHaveLength(2);
|
|
161
|
-
expect(output[0].features).toHaveLength(1);
|
|
162
|
-
expect(output[1].features).toHaveLength(1);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('returns single component for connected linestrings', () => {
|
|
166
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
167
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
168
|
-
createLineStringFeature([[1, 1], [2, 2]])
|
|
169
|
-
]);
|
|
170
|
-
const output = graphGetConnectedComponents(input);
|
|
171
|
-
|
|
172
|
-
expect(output).toHaveLength(1);
|
|
173
|
-
expect(output[0].features).toHaveLength(2);
|
|
174
|
-
expect(output[0].features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
175
|
-
expect(output[0].features[1].geometry.coordinates).toEqual([[1, 1], [2, 2]]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('returns multiple components for selection of connected linestrings', () => {
|
|
179
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
180
|
-
createLineStringFeature([[0, 0], [1, 1]]),
|
|
181
|
-
createLineStringFeature([[1, 1], [2, 2]]),
|
|
182
|
-
createLineStringFeature([[10, 10], [11, 11]]),
|
|
183
|
-
createLineStringFeature([[20, 20], [21, 21]])
|
|
184
|
-
]);
|
|
185
|
-
const output = graphGetConnectedComponents(input);
|
|
186
|
-
|
|
187
|
-
expect(output).toHaveLength(3);
|
|
188
|
-
expect(output[0].features).toHaveLength(1);
|
|
189
|
-
expect(output[1].features).toHaveLength(1);
|
|
190
|
-
expect(output[2].features).toHaveLength(2);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('returns single component for complex selection of linestrings', () => {
|
|
194
|
-
const network = JSON.parse(readFileSync('src/data/network.geojson', 'utf-8')) as FeatureCollection<LineString>;
|
|
195
|
-
const output = graphGetConnectedComponents(network);
|
|
196
|
-
expect(output).toHaveLength(1);
|
|
197
|
-
expect(output[0].features).toHaveLength(network.features.length);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('returns multiple components for complex selection of linestrings', () => {
|
|
201
|
-
const network = JSON.parse(readFileSync('src/data/network-5-cc.geojson', 'utf-8')) as FeatureCollection<LineString>;
|
|
202
|
-
const output = graphGetConnectedComponents(network);
|
|
203
|
-
expect(output).toHaveLength(5);
|
|
204
|
-
expect(output[4].features > output[0].features).toBeTruthy();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('ensures splitIntoConnectedComponents and countConnectedComponents are consistent', () => {
|
|
208
|
-
const network = JSON.parse(readFileSync('src/data/network.geojson', 'utf-8')) as FeatureCollection<LineString>;
|
|
209
|
-
const components = graphGetConnectedComponents(network);
|
|
210
|
-
const count = graphGetConnectedComponentCount(network);
|
|
211
|
-
expect(components.length).toBe(count);
|
|
212
|
-
|
|
213
|
-
const networkMultipleCC = JSON.parse(readFileSync('src/data/network-5-cc.geojson', 'utf-8')) as FeatureCollection<LineString>;
|
|
214
|
-
const componentsMultipleCC = graphGetConnectedComponents(networkMultipleCC);
|
|
215
|
-
const countMultipleCC = graphGetConnectedComponentCount(networkMultipleCC);
|
|
216
|
-
expect(componentsMultipleCC.length).toBe(countMultipleCC);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
})
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { Feature, FeatureCollection, LineString, Position } from 'geojson'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Counts the number of connected components in a graph represented by LineString features in a GeoJSON FeatureCollection.
|
|
5
|
-
* Each LineString is treated as an edge in the graph, and connected components are determined by shared coordinates.
|
|
6
|
-
* @param featureCollection - A GeoJSON FeatureCollection containing LineString features
|
|
7
|
-
* @returns The number of connected components in the graph represented by the LineStrings
|
|
8
|
-
*/
|
|
9
|
-
export function graphGetConnectedComponentCount(
|
|
10
|
-
featureCollection: FeatureCollection<LineString>
|
|
11
|
-
): number {
|
|
12
|
-
const features = featureCollection.features
|
|
13
|
-
const numberOfFeatures = features.length
|
|
14
|
-
|
|
15
|
-
// Map coordinates to feature indices
|
|
16
|
-
const coordinateToFeatureIndices = new Map<string, number[]>()
|
|
17
|
-
|
|
18
|
-
for (let index = 0; index < numberOfFeatures; index++) {
|
|
19
|
-
const coordinates = features[index].geometry.coordinates
|
|
20
|
-
|
|
21
|
-
for (const coordinate of coordinates) {
|
|
22
|
-
const key = coordinateKey(coordinate)
|
|
23
|
-
|
|
24
|
-
if (!coordinateToFeatureIndices.has(key)) {
|
|
25
|
-
coordinateToFeatureIndices.set(key, [])
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
coordinateToFeatureIndices.get(key)!.push(index)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Build adjacency list for the graph
|
|
33
|
-
const adjacencyList: number[][] = Array.from({ length: numberOfFeatures }, () => [])
|
|
34
|
-
|
|
35
|
-
for (const indices of coordinateToFeatureIndices.values()) {
|
|
36
|
-
for (let i = 0; i < indices.length; i++) {
|
|
37
|
-
for (let j = i + 1; j < indices.length; j++) {
|
|
38
|
-
const a = indices[i]
|
|
39
|
-
const b = indices[j]
|
|
40
|
-
adjacencyList[a].push(b)
|
|
41
|
-
adjacencyList[b].push(a)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const visited = new Array<boolean>(numberOfFeatures).fill(false)
|
|
47
|
-
let connectedComponents = 0
|
|
48
|
-
|
|
49
|
-
for (let index = 0; index < numberOfFeatures; index++) {
|
|
50
|
-
if (!visited[index]) {
|
|
51
|
-
dfs(index, adjacencyList, visited)
|
|
52
|
-
connectedComponents++
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return connectedComponents
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Depth-first search to mark all reachable nodes from the given index.
|
|
61
|
-
* @param index - The current node index to start DFS from.
|
|
62
|
-
* @param adjacencyList - The adjacency list representing the graph.
|
|
63
|
-
* @param visited - An array to keep track of visited nodes.
|
|
64
|
-
*/
|
|
65
|
-
function dfs(index: number, adjacencyList: number[][], visited: boolean[]): void {
|
|
66
|
-
visited[index] = true
|
|
67
|
-
|
|
68
|
-
for (const neighbor of adjacencyList[index]) {
|
|
69
|
-
if (!visited[neighbor]) {
|
|
70
|
-
dfs(neighbor, adjacencyList, visited)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function coordinateKey(position: Position): string {
|
|
76
|
-
return `${position[0]},${position[1]}`
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function graphGetConnectedComponents(
|
|
80
|
-
featureCollection: FeatureCollection<LineString>
|
|
81
|
-
): FeatureCollection<LineString>[] {
|
|
82
|
-
const features = featureCollection.features
|
|
83
|
-
const graph: Map<number, Set<number>> = new Map()
|
|
84
|
-
const coordinateMap: Map<string, Set<number>> = new Map()
|
|
85
|
-
|
|
86
|
-
function coordinateKey(coordinate: Position): string {
|
|
87
|
-
return `${coordinate[0]},${coordinate[1]}`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Build coordinate map: coordinate string -> Set of feature indices
|
|
91
|
-
for (let index = 0; index < features.length; index++) {
|
|
92
|
-
const coordinates = features[index].geometry.coordinates
|
|
93
|
-
|
|
94
|
-
for (const coordinate of coordinates) {
|
|
95
|
-
const key = coordinateKey(coordinate)
|
|
96
|
-
|
|
97
|
-
if (!coordinateMap.has(key)) {
|
|
98
|
-
coordinateMap.set(key, new Set())
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
coordinateMap.get(key)!.add(index)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Build adjacency list for graph
|
|
106
|
-
for (let index = 0; index < features.length; index++) {
|
|
107
|
-
graph.set(index, new Set())
|
|
108
|
-
|
|
109
|
-
const coordinates = features[index].geometry.coordinates
|
|
110
|
-
for (const coordinate of coordinates) {
|
|
111
|
-
const key = coordinateKey(coordinate)
|
|
112
|
-
const neighbors = coordinateMap.get(key)
|
|
113
|
-
|
|
114
|
-
if (neighbors) {
|
|
115
|
-
for (const neighborIndex of neighbors) {
|
|
116
|
-
if (neighborIndex !== index) {
|
|
117
|
-
graph.get(index)!.add(neighborIndex)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// DFS to find connected components
|
|
125
|
-
const visited = new Set<number>()
|
|
126
|
-
const components: FeatureCollection<LineString>[] = []
|
|
127
|
-
|
|
128
|
-
function dfs(startIndex: number, currentComponent: Feature<LineString>[]): void {
|
|
129
|
-
const stack: number[] = [startIndex]
|
|
130
|
-
|
|
131
|
-
while (stack.length > 0) {
|
|
132
|
-
const currentIndex = stack.pop()!
|
|
133
|
-
|
|
134
|
-
if (visited.has(currentIndex)) {
|
|
135
|
-
continue
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
visited.add(currentIndex)
|
|
139
|
-
currentComponent.push(features[currentIndex])
|
|
140
|
-
|
|
141
|
-
const neighbors = graph.get(currentIndex)
|
|
142
|
-
if (neighbors) {
|
|
143
|
-
for (const neighbor of neighbors) {
|
|
144
|
-
if (!visited.has(neighbor)) {
|
|
145
|
-
stack.push(neighbor)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
for (let index = 0; index < features.length; index++) {
|
|
153
|
-
if (!visited.has(index)) {
|
|
154
|
-
const component: Feature<LineString>[] = []
|
|
155
|
-
dfs(index, component)
|
|
156
|
-
components.push({
|
|
157
|
-
type: 'FeatureCollection',
|
|
158
|
-
features: component
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// Sort components by the number of features in ascending order
|
|
165
|
-
components.sort((a, b) => a.features.length - b.features.length)
|
|
166
|
-
|
|
167
|
-
return components
|
|
168
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { FeatureCollection, LineString } from 'geojson';
|
|
2
|
-
import { createFeatureCollection } from '../../test-utils/create';
|
|
3
|
-
import { removeDuplicateAndSubsectionLines } from './duplicates';
|
|
4
|
-
import { readFileSync } from 'fs';
|
|
5
|
-
import { graphGetNodeAndEdgeCount } from './nodes';
|
|
6
|
-
|
|
7
|
-
describe('removeDuplicateAndSubsectionLines', () => {
|
|
8
|
-
describe('for an empty feature collection', () => {
|
|
9
|
-
it('returns 0', () => {
|
|
10
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([]);
|
|
11
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
12
|
-
|
|
13
|
-
expect(output).toEqual(createFeatureCollection([]));
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('for feature collection with 1 linestring', () => {
|
|
18
|
-
it('returns 1', () => {
|
|
19
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
20
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} }
|
|
21
|
-
]);
|
|
22
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
23
|
-
|
|
24
|
-
expect(output.features.length).toBe(1);
|
|
25
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('for feature collection with 2 linestrings', () => {
|
|
30
|
-
it('handles duplicates', () => {
|
|
31
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
32
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
33
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} }
|
|
34
|
-
]);
|
|
35
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
36
|
-
|
|
37
|
-
expect(output.features.length).toBe(1);
|
|
38
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('returns one linestring if one is a subsection of the other', () => {
|
|
42
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
43
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
44
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1], [2, 2]] }, properties: {} }
|
|
45
|
-
]);
|
|
46
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
47
|
-
|
|
48
|
-
expect(output.features.length).toBe(1);
|
|
49
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1], [2, 2]]);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('returns both if they are not subsections or duplicates', () => {
|
|
53
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
54
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
55
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[2, 2], [3, 3]] }, properties: {} }
|
|
56
|
-
]);
|
|
57
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
58
|
-
|
|
59
|
-
expect(output.features.length).toBe(2);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('returns one linestring if one is a inverse duplicate of the other', () => {
|
|
63
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
64
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
65
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[1, 1], [0, 0]] }, properties: {} }
|
|
66
|
-
]);
|
|
67
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
68
|
-
expect(output.features.length).toBe(1);
|
|
69
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('for feature collection with 3 linestrings', () => {
|
|
74
|
-
it('returns 1 linestring if all are duplicates', () => {
|
|
75
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
76
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
77
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
78
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} }
|
|
79
|
-
]);
|
|
80
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
81
|
-
expect(output.features.length).toBe(1);
|
|
82
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('returns 2 linestrings if one is a subsection of another', () => {
|
|
86
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
87
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
88
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1], [2, 2]] }, properties: {} },
|
|
89
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[3, 3], [4, 4]] }, properties: {} }
|
|
90
|
-
]);
|
|
91
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
92
|
-
expect(output.features.length).toBe(2);
|
|
93
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1], [2, 2]]);
|
|
94
|
-
expect(output.features[1].geometry.coordinates).toEqual([[3, 3], [4, 4]]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('returns all if none are subsections or duplicates', () => {
|
|
98
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
99
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
100
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[2, 2], [3, 3]] }, properties: {} },
|
|
101
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[4, 4], [5, 5]] }, properties: {} }
|
|
102
|
-
]);
|
|
103
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
104
|
-
expect(output.features.length).toBe(3);
|
|
105
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
|
|
106
|
-
expect(output.features[1].geometry.coordinates).toEqual([[2, 2], [3, 3]]);
|
|
107
|
-
expect(output.features[2].geometry.coordinates).toEqual([[4, 4], [5, 5]]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('returns 1 linestring if one is a subsection of another and the third is a duplicate', () => {
|
|
111
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
112
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
113
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1], [2, 2]] }, properties: {} },
|
|
114
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} }
|
|
115
|
-
]);
|
|
116
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
117
|
-
expect(output.features.length).toBe(1);
|
|
118
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1], [2, 2]]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('returns 1 linestring if one is a subsection of another and the third is an inverse duplicate', () => {
|
|
122
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
123
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
124
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1], [2, 2]] }, properties: {} },
|
|
125
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[1, 1], [0, 0]] }, properties: {} }
|
|
126
|
-
]);
|
|
127
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
128
|
-
expect(output.features.length).toBe(1);
|
|
129
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1], [2, 2]]);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('for feature collection with multiple linestrings', () => {
|
|
134
|
-
it('handles multiple duplicates and subsections', () => {
|
|
135
|
-
const input: FeatureCollection<LineString> = createFeatureCollection([
|
|
136
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} },
|
|
137
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1], [2, 2]] }, properties: {} },
|
|
138
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[3, 3], [4, 4]] }, properties: {} },
|
|
139
|
-
{ type: 'Feature', geometry: { type: 'LineString', coordinates: [[3, 3], [4, 4], [5, 5]] }, properties: {} }
|
|
140
|
-
]);
|
|
141
|
-
const output = removeDuplicateAndSubsectionLines(input);
|
|
142
|
-
|
|
143
|
-
expect(output.features.length).toBe(2);
|
|
144
|
-
expect(output.features[0].geometry.coordinates).toEqual([[0, 0], [1, 1], [2, 2]]);
|
|
145
|
-
expect(output.features[1].geometry.coordinates).toEqual([[3, 3], [4, 4], [5, 5]]);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('ensures it does not effect the network in a complex scenario', () => {
|
|
149
|
-
const network = JSON.parse(readFileSync('src/data/network.geojson', 'utf-8')) as FeatureCollection<LineString>;
|
|
150
|
-
|
|
151
|
-
expect(network.features.length).toBe(811);
|
|
152
|
-
|
|
153
|
-
const after = removeDuplicateAndSubsectionLines(network);
|
|
154
|
-
|
|
155
|
-
// There are no duplicates or subsections in the network
|
|
156
|
-
expect(after.features.length).toBe(811);
|
|
157
|
-
|
|
158
|
-
expect(graphGetNodeAndEdgeCount(removeDuplicateAndSubsectionLines(after))).toEqual(graphGetNodeAndEdgeCount(network))
|
|
159
|
-
});
|
|
160
|
-
})
|
|
161
|
-
});
|