terra-route 0.0.8 → 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.
@@ -22,5 +22,6 @@ export const haversineDistance = (pointOne: Position, pointTwo: Position): numbe
22
22
  const radius = 6371e3;
23
23
  const distance = radius * c;
24
24
 
25
+
25
26
  return distance / 1000;
26
27
  }
@@ -0,0 +1,151 @@
1
+ import { Feature, FeatureCollection, LineString, Point } from "geojson";
2
+ import { graphGetConnectedComponentCount, graphGetConnectedComponents } from "./methods/connected";
3
+ import { graphGetNodeAndEdgeCount, graphGetNodesAsPoints } from "./methods/nodes";
4
+ import { graphGetUniqueSegments } from "./methods/unique-segments";
5
+ import { routeLength } from "../test-utils/utils";
6
+
7
+ /**
8
+ * Represents a graph constructed from a GeoJSON FeatureCollection of LineString features.
9
+ * This class provides methods to analyze the graph, including connected components, node and edge counts,
10
+ * and shortest paths. Coordinates in the LineStrings are considered connected if they share identical coordinates.
11
+ */
12
+ export class LineStringGraph {
13
+ constructor(network: FeatureCollection<LineString>) {
14
+ this.network = network;
15
+ }
16
+
17
+ private network: FeatureCollection<LineString>;
18
+
19
+ /**
20
+ * Sets the network for the graph.
21
+ * This method replaces the current network with a new one.
22
+ * @param network A GeoJSON FeatureCollection of LineString features representing the network.
23
+ */
24
+ setNetwork(network: FeatureCollection<LineString>) {
25
+ this.network = network;
26
+ }
27
+
28
+ /**
29
+ * Gets the current network of the graph.
30
+ * @returns A GeoJSON FeatureCollection of LineString features representing the network.
31
+ */
32
+ getNetwork(): FeatureCollection<LineString> {
33
+ return this.network;
34
+ }
35
+
36
+ /**
37
+ * Gets the connected components of the graph.
38
+ * @returns An array of FeatureCollection<LineString> representing the connected components.
39
+ */
40
+ getConnectedComponents(): FeatureCollection<LineString>[] {
41
+ return graphGetConnectedComponents(this.network)
42
+ }
43
+
44
+ /**
45
+ * Gets the count of connected components in the graph.
46
+ * @returns The number of connected components in the graph.
47
+ */
48
+ getConnectedComponentCount(): number {
49
+ return graphGetConnectedComponentCount(this.network);
50
+ }
51
+
52
+ /**
53
+ * Gets the count of unique nodes and edges in the graph.
54
+ * @returns An object containing the counts of nodes and edges.
55
+ */
56
+ getNodeAndEdgeCount(): { nodeCount: number, edgeCount: number } {
57
+ return graphGetNodeAndEdgeCount(this.network);
58
+ }
59
+
60
+ /**
61
+ * Gets the unique nodes of the graph as a FeatureCollection of Point features.
62
+ * @returns A FeatureCollection<Point> containing the nodes of the graph.
63
+ */
64
+ getNodes(): FeatureCollection<Point> {
65
+ const nodes = graphGetNodesAsPoints(this.network);
66
+ return {
67
+ type: "FeatureCollection",
68
+ features: nodes
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Gets the count of unique nodes in the graph.
74
+ * @returns The number of unique nodes in the graph.
75
+ */
76
+ getNodeCount(): number {
77
+ const { nodeCount } = this.getNodeAndEdgeCount();
78
+ return nodeCount;
79
+ }
80
+
81
+ /**
82
+ * Gets the unique edges of the graph as a FeatureCollection of LineString features. Each edge is represented as a LineString.
83
+ * This method ensures that each edge is unique, meaning that edges are not duplicated in the collection. Each linestring only
84
+ * two coordinates, representing the start and end points of the edge.
85
+ * @returns A FeatureCollection<LineString> containing the unique edges of the graph.
86
+ */
87
+ getEdges(): FeatureCollection<LineString> {
88
+ return graphGetUniqueSegments(this.network);
89
+ }
90
+
91
+ /**
92
+ * Gets the length of the longest edge in the graph based on the length of the LineString.
93
+ * If no edges exist, it returns -1.
94
+ * @returns The length of the longest edge in meters, or 0 if no edges exist.
95
+ */
96
+ getLongestEdgeLength(): number {
97
+ const longestEdge = this.getLongestEdge();
98
+ if (!longestEdge) {
99
+ return -1;
100
+ }
101
+ return routeLength(longestEdge);
102
+ }
103
+
104
+ /**
105
+ * Gets the length of the shortest edge in the graph based on the length of the LineString.
106
+ * If no edges exist, it returns -1.
107
+ * @returns The length of the shortest edge in meters, or 0 if no edges exist.
108
+ */
109
+ getShortestEdgeLength(): number {
110
+ const shortestEdge = this.getShortestEdge();
111
+ if (!shortestEdge) {
112
+ return -1;
113
+ }
114
+ return routeLength(shortestEdge);
115
+ }
116
+
117
+ /**
118
+ * Gets the longest edge in the graph based on the length of the LineString.
119
+ * @returns The longest edge as a Feature<LineString> or null if no edges exist.
120
+ */
121
+ getLongestEdge(): Feature<LineString> | null {
122
+ const edges = this.getEdges().features;
123
+ if (edges.length === 0) {
124
+ return null;
125
+ }
126
+ const longestEdges = edges.sort((a, b) => routeLength(a) - routeLength(b));
127
+ return longestEdges[longestEdges.length - 1];
128
+ }
129
+
130
+ /**
131
+ * Gets the shortest edge in the graph based on the length of the LineString.
132
+ * @returns The shortest edge as a Feature<LineString> or null if no edges exist.
133
+ */
134
+ getShortestEdge(): Feature<LineString> | null {
135
+ const edges = this.getEdges().features;
136
+ if (edges.length === 0) {
137
+ return null;
138
+ }
139
+ const shortestEdges = edges.sort((a, b) => routeLength(a) - routeLength(b));
140
+ return shortestEdges[0];
141
+ }
142
+
143
+ /**
144
+ * Gets the count of unique edges in the graph.
145
+ * @returns The number of unique edges in the graph.
146
+ */
147
+ getEdgeCount(): number {
148
+ const { edgeCount } = this.getNodeAndEdgeCount();
149
+ return edgeCount;
150
+ }
151
+ }
@@ -0,0 +1,217 @@
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 } from 'fs';
6
+
7
+ describe('countConnectedComponents', () => {
8
+ describe('for an empty feature collection', () => {
9
+ it('returns 0', () => {
10
+ const input: FeatureCollection<LineString> = createFeatureCollection([]);
11
+ const output = graphGetConnectedComponentCount(input);
12
+
13
+ expect(output).toBe(0);
14
+ });
15
+ });
16
+
17
+ describe('for feature collection with 1 linestring', () => {
18
+ it('returns 1', () => {
19
+ const input: FeatureCollection<LineString> = createFeatureCollection([
20
+ createLineStringFeature([[0, 0], [1, 1]])
21
+ ]);
22
+ const output = graphGetConnectedComponentCount(input);
23
+
24
+ expect(output).toBe(1);
25
+ });
26
+ });
27
+
28
+ describe('for feature collection with 2 linestring', () => {
29
+ it('returns 1 if connected', () => {
30
+ const input: FeatureCollection<LineString> = createFeatureCollection([
31
+ createLineStringFeature([[0, 0], [1, 1]]),
32
+ createLineStringFeature([[1, 1], [2, 2]]),
33
+
34
+ ]);
35
+ const output = graphGetConnectedComponentCount(input);
36
+
37
+ expect(output).toBe(1);
38
+ });
39
+
40
+ it('returns 2 if unconnected', () => {
41
+ const input: FeatureCollection<LineString> = createFeatureCollection([
42
+ createLineStringFeature([[0, 0], [1, 1]]),
43
+ createLineStringFeature([[10, 10], [11, 11]]),
44
+
45
+ ]);
46
+ const output = graphGetConnectedComponentCount(input);
47
+
48
+ expect(output).toBe(2);
49
+ });
50
+ });
51
+
52
+ describe('for feature collection with 3 linestring', () => {
53
+ it('returns 1 if connected', () => {
54
+ const input: FeatureCollection<LineString> = createFeatureCollection([
55
+ createLineStringFeature([[0, 0], [1, 1]]),
56
+ createLineStringFeature([[1, 1], [2, 2]]),
57
+ createLineStringFeature([[2, 2], [3, 3]]),
58
+
59
+ ]);
60
+ const output = graphGetConnectedComponentCount(input);
61
+
62
+ expect(output).toBe(1);
63
+ });
64
+
65
+ it('returns 3 if unconnected', () => {
66
+ const input: FeatureCollection<LineString> = createFeatureCollection([
67
+ createLineStringFeature([[0, 0], [1, 1]]),
68
+ createLineStringFeature([[10, 10], [11, 11]]),
69
+ createLineStringFeature([[20, 20], [21, 21]]),
70
+ ]);
71
+ const output = graphGetConnectedComponentCount(input);
72
+
73
+ expect(output).toBe(3);
74
+ });
75
+ });
76
+
77
+
78
+ describe('for feature collection with multiple linestring', () => {
79
+ it('returns 1 when all lines share the same coordinate', () => {
80
+ const input = createFeatureCollection([
81
+ createLineStringFeature([[0, 0], [1, 1]]),
82
+ createLineStringFeature([[1, 1], [2, 2]]),
83
+ createLineStringFeature([[1, 1], [3, 3]]),
84
+ createLineStringFeature([[4, 4], [1, 1]]),
85
+ ]);
86
+ const output = graphGetConnectedComponentCount(input);
87
+
88
+ expect(output).toBe(1);
89
+ });
90
+
91
+ it('returns 2 when two disconnected groups exist', () => {
92
+ const input = createFeatureCollection([
93
+ createLineStringFeature([[0, 0], [1, 1]]),
94
+ createLineStringFeature([[1, 1], [2, 2]]),
95
+ createLineStringFeature([[10, 10], [11, 11]]),
96
+ createLineStringFeature([[11, 11], [12, 12]]),
97
+ ]);
98
+ const output = graphGetConnectedComponentCount(input);
99
+
100
+ expect(output).toBe(2);
101
+ });
102
+
103
+
104
+ it('returns 1 for a loop of connected lines', () => {
105
+ const input = createFeatureCollection([
106
+ createLineStringFeature([[0, 0], [1, 0]]),
107
+ createLineStringFeature([[1, 0], [1, 1]]),
108
+ createLineStringFeature([[1, 1], [0, 1]]),
109
+ createLineStringFeature([[0, 1], [0, 0]]),
110
+ ]);
111
+ const output = graphGetConnectedComponentCount(input);
112
+
113
+ expect(output).toBe(1);
114
+ });
115
+ });
116
+
117
+ describe('for complex linestring network', () => {
118
+ it('returns 1 when tree is connected', () => {
119
+ const input = generateTreeFeatureCollection(3, 2)
120
+ const output = graphGetConnectedComponentCount(input)
121
+
122
+ expect(output).toBe(1);
123
+ });
124
+
125
+ it('returns 1 when it is connected correctly', () => {
126
+ const input = generateTreeFeatureCollection(3, 2)
127
+ const output = graphGetConnectedComponentCount(input)
128
+
129
+ expect(output).toBe(1);
130
+ });
131
+ })
132
+ });
133
+
134
+ describe('splitIntoConnectedComponents', () => {
135
+ it('returns empty array for empty feature collection', () => {
136
+ const input: FeatureCollection<LineString> = createFeatureCollection([]);
137
+ const output = graphGetConnectedComponents(input);
138
+
139
+ expect(output).toEqual([]);
140
+ });
141
+
142
+ it('returns single component for single linestring', () => {
143
+ const input: FeatureCollection<LineString> = createFeatureCollection([
144
+ createLineStringFeature([[0, 0], [1, 1]])
145
+ ]);
146
+ const output = graphGetConnectedComponents(input);
147
+
148
+ expect(output).toHaveLength(1);
149
+ expect(output[0].features).toHaveLength(1);
150
+ });
151
+
152
+ it('returns multiple components for disconnected linestrings', () => {
153
+ const input: FeatureCollection<LineString> = createFeatureCollection([
154
+ createLineStringFeature([[0, 0], [1, 1]]),
155
+ createLineStringFeature([[10, 10], [11, 11]])
156
+ ]);
157
+ const output = graphGetConnectedComponents(input);
158
+
159
+ expect(output).toHaveLength(2);
160
+ expect(output[0].features).toHaveLength(1);
161
+ expect(output[1].features).toHaveLength(1);
162
+ });
163
+
164
+ it('returns single component for connected linestrings', () => {
165
+ const input: FeatureCollection<LineString> = createFeatureCollection([
166
+ createLineStringFeature([[0, 0], [1, 1]]),
167
+ createLineStringFeature([[1, 1], [2, 2]])
168
+ ]);
169
+ const output = graphGetConnectedComponents(input);
170
+
171
+ expect(output).toHaveLength(1);
172
+ expect(output[0].features).toHaveLength(2);
173
+ expect(output[0].features[0].geometry.coordinates).toEqual([[0, 0], [1, 1]]);
174
+ expect(output[0].features[1].geometry.coordinates).toEqual([[1, 1], [2, 2]]);
175
+ });
176
+
177
+ it('returns multiple components for selection of connected linestrings', () => {
178
+ const input: FeatureCollection<LineString> = createFeatureCollection([
179
+ createLineStringFeature([[0, 0], [1, 1]]),
180
+ createLineStringFeature([[1, 1], [2, 2]]),
181
+ createLineStringFeature([[10, 10], [11, 11]]),
182
+ createLineStringFeature([[20, 20], [21, 21]])
183
+ ]);
184
+ const output = graphGetConnectedComponents(input);
185
+
186
+ expect(output).toHaveLength(3);
187
+ expect(output[0].features).toHaveLength(1);
188
+ expect(output[1].features).toHaveLength(1);
189
+ expect(output[2].features).toHaveLength(2);
190
+ });
191
+
192
+ it('returns single component for complex selection of linestrings', () => {
193
+ const network = JSON.parse(readFileSync('src/data/network.geojson', 'utf-8')) as FeatureCollection<LineString>;
194
+ const output = graphGetConnectedComponents(network);
195
+ expect(output).toHaveLength(1);
196
+ expect(output[0].features).toHaveLength(network.features.length);
197
+ });
198
+
199
+ it('returns multiple components for complex selection of linestrings', () => {
200
+ const network = JSON.parse(readFileSync('src/data/network-5-cc.geojson', 'utf-8')) as FeatureCollection<LineString>;
201
+ const output = graphGetConnectedComponents(network);
202
+ expect(output).toHaveLength(5);
203
+ expect(output[4].features > output[0].features).toBeTruthy();
204
+ });
205
+
206
+ it('ensures splitIntoConnectedComponents and countConnectedComponents are consistent', () => {
207
+ const network = JSON.parse(readFileSync('src/data/network.geojson', 'utf-8')) as FeatureCollection<LineString>;
208
+ const components = graphGetConnectedComponents(network);
209
+ const count = graphGetConnectedComponentCount(network);
210
+ expect(components.length).toBe(count);
211
+
212
+ const networkMultipleCC = JSON.parse(readFileSync('src/data/network-5-cc.geojson', 'utf-8')) as FeatureCollection<LineString>;
213
+ const componentsMultipleCC = graphGetConnectedComponents(networkMultipleCC);
214
+ const countMultipleCC = graphGetConnectedComponentCount(networkMultipleCC);
215
+ expect(componentsMultipleCC.length).toBe(countMultipleCC);
216
+ });
217
+ })
@@ -0,0 +1,168 @@
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
+ }