@osmix/router 0.0.11 → 0.1.0
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/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +138 -53
- package/dist/algorithms/bidirectional.d.ts +6 -9
- package/dist/algorithms/bidirectional.d.ts.map +1 -1
- package/dist/algorithms/bidirectional.js +84 -104
- package/dist/algorithms/bidirectional.js.map +1 -1
- package/dist/algorithms/index.d.ts +2 -2
- package/dist/algorithms/index.d.ts.map +1 -1
- package/dist/algorithms/index.js +1 -1
- package/dist/algorithms/index.js.map +1 -1
- package/dist/algorithms/shortest-path.d.ts +2 -1
- package/dist/algorithms/shortest-path.d.ts.map +1 -1
- package/dist/algorithms/shortest-path.js +9 -7
- package/dist/algorithms/shortest-path.js.map +1 -1
- package/dist/binary-heap.d.ts +5 -0
- package/dist/binary-heap.d.ts.map +1 -1
- package/dist/binary-heap.js +11 -4
- package/dist/binary-heap.js.map +1 -1
- package/dist/graph.d.ts +35 -7
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +161 -119
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +6 -3
- package/dist/router.js.map +1 -1
- package/dist/topology-stats.d.ts +37 -0
- package/dist/topology-stats.d.ts.map +1 -0
- package/dist/topology-stats.js +62 -0
- package/dist/topology-stats.js.map +1 -0
- package/dist/types.d.ts +15 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -7
- package/dist/utils.js.map +1 -1
- package/package.json +32 -29
- package/src/algorithms/bidirectional.ts +124 -136
- package/src/algorithms/index.ts +10 -10
- package/src/algorithms/shortest-path.ts +96 -92
- package/src/binary-heap.ts +133 -129
- package/src/graph.ts +389 -355
- package/src/index.ts +14 -9
- package/src/router.ts +221 -233
- package/src/topology-stats.ts +84 -0
- package/src/types.ts +102 -87
- package/src/utils.ts +67 -74
- package/test/algorithms.test.ts +396 -353
- package/test/binary-heap.test.ts +27 -0
- package/test/limits.test.ts +82 -0
- package/test/no-shared-array-buffer.test.ts +37 -0
- package/test/router.bench.ts +171 -188
- package/test/router.test.ts +685 -726
- package/test/spatial-capability.test.ts +23 -0
- package/test/topology-stats.test.ts +82 -0
- package/tsconfig.build.json +6 -3
- package/tsconfig.json +7 -7
package/src/index.ts
CHANGED
|
@@ -9,18 +9,22 @@
|
|
|
9
9
|
* - **Graph construction**: Build routing graphs from OSM ways with highway filtering.
|
|
10
10
|
* - **Multiple algorithms**: Dijkstra, A*, and bidirectional search.
|
|
11
11
|
* - **Time-based routing**: Uses maxspeed tags and default speeds by highway type.
|
|
12
|
-
* - **One-way support**: Respects
|
|
12
|
+
* - **One-way support**: Respects explicit one-way tags and implicit roundabout direction.
|
|
13
13
|
* - **Snapping**: Find nearest routable node from arbitrary coordinates.
|
|
14
14
|
*
|
|
15
|
+
* Not modeled: turn restrictions, access tags, `oneway:*` per-mode overrides,
|
|
16
|
+
* conditional restrictions, turn costs and elevation. See the README "Limitations".
|
|
17
|
+
*
|
|
15
18
|
* @example
|
|
16
19
|
* ```ts
|
|
17
|
-
* import { buildGraph, Router
|
|
20
|
+
* import { buildGraph, Router } from "@osmix/router"
|
|
18
21
|
*
|
|
19
22
|
* const graph = buildGraph(osm)
|
|
20
23
|
* const router = new Router(osm, graph)
|
|
21
24
|
*
|
|
22
|
-
*
|
|
23
|
-
* const
|
|
25
|
+
* // Snap radius is in meters. Needs the "all" node spatial index.
|
|
26
|
+
* const start = graph.findNearestRoutableNode(osm, [-73.989, 40.733], 500)
|
|
27
|
+
* const end = graph.findNearestRoutableNode(osm, [-73.988, 40.734], 500)
|
|
24
28
|
*
|
|
25
29
|
* if (start && end) {
|
|
26
30
|
* const path = router.route(start.nodeIndex, end.nodeIndex)
|
|
@@ -34,8 +38,9 @@
|
|
|
34
38
|
* @module @osmix/router
|
|
35
39
|
*/
|
|
36
40
|
|
|
37
|
-
export * from "./algorithms/index.ts"
|
|
38
|
-
export * from "./graph.ts"
|
|
39
|
-
export * from "./router.ts"
|
|
40
|
-
export * from "./
|
|
41
|
-
export * from "./
|
|
41
|
+
export * from "./algorithms/index.ts";
|
|
42
|
+
export * from "./graph.ts";
|
|
43
|
+
export * from "./router.ts";
|
|
44
|
+
export * from "./topology-stats.ts";
|
|
45
|
+
export * from "./types.ts";
|
|
46
|
+
export * from "./utils.ts";
|
package/src/router.ts
CHANGED
|
@@ -7,24 +7,25 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { Osm } from "@osmix/core"
|
|
11
|
-
import type { LonLat } from "@osmix/
|
|
12
|
-
|
|
13
|
-
import
|
|
10
|
+
import type { Osm } from "@osmix/core";
|
|
11
|
+
import type { LonLat } from "@osmix/types";
|
|
12
|
+
|
|
13
|
+
import { routingAlgorithms } from "./algorithms/index.ts";
|
|
14
|
+
import type { RoutingGraph } from "./graph.ts";
|
|
14
15
|
import type {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "./types.ts"
|
|
16
|
+
PathSegment,
|
|
17
|
+
RouteOptions,
|
|
18
|
+
RoutePathInfo,
|
|
19
|
+
RouteResult,
|
|
20
|
+
RouteStatistics,
|
|
21
|
+
WaySegment,
|
|
22
|
+
} from "./types.ts";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Router for finding paths through OSM road networks.
|
|
25
26
|
*
|
|
26
27
|
* Uses pathfinding algorithms (Dijkstra, A*, or bidirectional) to find
|
|
27
|
-
* routes between node indexes. Use `
|
|
28
|
+
* routes between node indexes. Use `graph.findNearestRoutableNode()` to snap
|
|
28
29
|
* arbitrary coordinates to routable nodes before calling `route()`.
|
|
29
30
|
*
|
|
30
31
|
* @example
|
|
@@ -38,225 +39,212 @@ import type {
|
|
|
38
39
|
* ```
|
|
39
40
|
*/
|
|
40
41
|
export class Router {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
distance,
|
|
250
|
-
time,
|
|
251
|
-
}
|
|
252
|
-
currentDisplayName = displayName
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (currentSegment) {
|
|
257
|
-
segments.push(currentSegment)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return { segments, turnPoints }
|
|
261
|
-
}
|
|
42
|
+
readonly graph: RoutingGraph;
|
|
43
|
+
readonly osm: Osm;
|
|
44
|
+
private readonly defaults: Required<RouteOptions>;
|
|
45
|
+
|
|
46
|
+
constructor(osm: Osm, graph: RoutingGraph, options: Partial<RouteOptions> = {}) {
|
|
47
|
+
this.osm = osm;
|
|
48
|
+
this.graph = graph;
|
|
49
|
+
this.defaults = {
|
|
50
|
+
algorithm: options.algorithm ?? "astar",
|
|
51
|
+
metric: options.metric ?? "distance",
|
|
52
|
+
includeStats: options.includeStats ?? false,
|
|
53
|
+
includePathInfo: options.includePathInfo ?? false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find a route between two node indexes.
|
|
59
|
+
* Returns path segments or null if no path exists.
|
|
60
|
+
*/
|
|
61
|
+
route(
|
|
62
|
+
fromNodeIndex: number,
|
|
63
|
+
toNodeIndex: number,
|
|
64
|
+
options: Partial<RouteOptions> = {},
|
|
65
|
+
): PathSegment[] | null {
|
|
66
|
+
// Trivial case: same node
|
|
67
|
+
if (fromNodeIndex === toNodeIndex) return [{ nodeIndex: fromNodeIndex, cost: 0 }];
|
|
68
|
+
|
|
69
|
+
const algorithm = routingAlgorithms[options.algorithm ?? this.defaults.algorithm];
|
|
70
|
+
const metric = options.metric ?? this.defaults.metric;
|
|
71
|
+
|
|
72
|
+
return algorithm(
|
|
73
|
+
(nodeIndex) => this.graph.getEdges(nodeIndex),
|
|
74
|
+
fromNodeIndex,
|
|
75
|
+
toNodeIndex,
|
|
76
|
+
(edge) => (metric === "distance" ? edge.distance : edge.time),
|
|
77
|
+
(nodeIndex) => this.osm.nodes.getNodeLonLat({ index: nodeIndex }),
|
|
78
|
+
metric,
|
|
79
|
+
{
|
|
80
|
+
reverseGraph: (nodeIndex) => this.graph.getIncomingEdges(nodeIndex),
|
|
81
|
+
maxSpeedMps: this.graph.maxSpeedMps,
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Build route result with coordinates from path segments. */
|
|
87
|
+
buildResult(path: PathSegment[], options: Partial<RouteOptions> = {}): RouteResult {
|
|
88
|
+
const coordinates: LonLat[] = [];
|
|
89
|
+
const wayIndexes: number[] = [];
|
|
90
|
+
const nodeIndexes: number[] = [];
|
|
91
|
+
const seenNodes = new Set<number>();
|
|
92
|
+
|
|
93
|
+
let currentWay: number | undefined;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < path.length; i++) {
|
|
96
|
+
const seg = path[i]!;
|
|
97
|
+
const coord = this.osm.nodes.getNodeLonLat({ index: seg.nodeIndex });
|
|
98
|
+
if (coord) coordinates.push(coord);
|
|
99
|
+
|
|
100
|
+
// Track way transitions
|
|
101
|
+
if (seg.wayIndex !== undefined && seg.wayIndex !== currentWay) {
|
|
102
|
+
wayIndexes.push(seg.wayIndex);
|
|
103
|
+
currentWay = seg.wayIndex;
|
|
104
|
+
// Mark node at way transition
|
|
105
|
+
if (i > 0 && !seenNodes.has(seg.nodeIndex)) {
|
|
106
|
+
seenNodes.add(seg.nodeIndex);
|
|
107
|
+
nodeIndexes.push(seg.nodeIndex);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Mark start, end, and intersections
|
|
112
|
+
const isEndpoint = i === 0 || i === path.length - 1;
|
|
113
|
+
const isIntersection = this.graph.isIntersection(seg.nodeIndex);
|
|
114
|
+
if ((isEndpoint || isIntersection) && !seenNodes.has(seg.nodeIndex)) {
|
|
115
|
+
seenNodes.add(seg.nodeIndex);
|
|
116
|
+
nodeIndexes.push(seg.nodeIndex);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result: RouteResult = { coordinates, wayIndexes, nodeIndexes };
|
|
121
|
+
|
|
122
|
+
const includeStats = options.includeStats ?? this.defaults.includeStats;
|
|
123
|
+
const includePathInfo = options.includePathInfo ?? this.defaults.includePathInfo;
|
|
124
|
+
|
|
125
|
+
if (includeStats) {
|
|
126
|
+
const stats = this.getRouteStatistics(path);
|
|
127
|
+
result.distance = stats.distance;
|
|
128
|
+
result.time = stats.time;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (includePathInfo) {
|
|
132
|
+
const pathInfo = this.getRoutePathInfo(path);
|
|
133
|
+
result.segments = pathInfo.segments;
|
|
134
|
+
result.turnPoints = pathInfo.turnPoints;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Calculate route statistics (distance and time) from path segments.
|
|
142
|
+
* @param path - Path segments from route().
|
|
143
|
+
* @returns Total distance in meters and time in seconds.
|
|
144
|
+
*/
|
|
145
|
+
getRouteStatistics(path: PathSegment[]): RouteStatistics {
|
|
146
|
+
let distance = 0;
|
|
147
|
+
let time = 0;
|
|
148
|
+
|
|
149
|
+
for (const seg of path) {
|
|
150
|
+
if (seg.wayIndex !== undefined && seg.previousNodeIndex !== undefined) {
|
|
151
|
+
const edges = this.graph.getEdges(seg.previousNodeIndex);
|
|
152
|
+
const edge = edges.find(
|
|
153
|
+
(e) => e.targetNodeIndex === seg.nodeIndex && e.wayIndex === seg.wayIndex,
|
|
154
|
+
);
|
|
155
|
+
if (edge) {
|
|
156
|
+
distance += edge.distance;
|
|
157
|
+
time += edge.time;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { distance, time };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build route path info (segments and turn points) from path segments.
|
|
167
|
+
* Consecutive ways with the same name are merged into a single segment.
|
|
168
|
+
* @param path - Path segments from route().
|
|
169
|
+
* @returns Segments with per-way breakdown and turn point coordinates.
|
|
170
|
+
*/
|
|
171
|
+
getRoutePathInfo(path: PathSegment[]): RoutePathInfo {
|
|
172
|
+
// First, collect edge traversals with way info
|
|
173
|
+
interface EdgeTraversal {
|
|
174
|
+
wayIndex: number;
|
|
175
|
+
transitionNodeIndex: number;
|
|
176
|
+
distance: number;
|
|
177
|
+
time: number;
|
|
178
|
+
}
|
|
179
|
+
const edgeSequence: EdgeTraversal[] = [];
|
|
180
|
+
|
|
181
|
+
for (const seg of path) {
|
|
182
|
+
if (seg.wayIndex !== undefined && seg.previousNodeIndex !== undefined) {
|
|
183
|
+
const edges = this.graph.getEdges(seg.previousNodeIndex);
|
|
184
|
+
const edge = edges.find(
|
|
185
|
+
(e) => e.targetNodeIndex === seg.nodeIndex && e.wayIndex === seg.wayIndex,
|
|
186
|
+
);
|
|
187
|
+
if (edge) {
|
|
188
|
+
edgeSequence.push({
|
|
189
|
+
wayIndex: seg.wayIndex,
|
|
190
|
+
transitionNodeIndex: seg.previousNodeIndex,
|
|
191
|
+
distance: edge.distance,
|
|
192
|
+
time: edge.time,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Build segments, merging consecutive same-name ways
|
|
199
|
+
const segments: WaySegment[] = [];
|
|
200
|
+
const turnPoints: LonLat[] = [];
|
|
201
|
+
let currentSegment: WaySegment | null = null;
|
|
202
|
+
let currentDisplayName: string | null = null;
|
|
203
|
+
|
|
204
|
+
const getDisplayName = (tags?: Record<string, string | number>) => {
|
|
205
|
+
const name = (tags?.["name"] as string) ?? "";
|
|
206
|
+
const highway = (tags?.["highway"] as string) ?? "";
|
|
207
|
+
return name || highway;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
for (const { wayIndex, transitionNodeIndex, distance, time } of edgeSequence) {
|
|
211
|
+
const wayId = this.osm.ways.ids.at(wayIndex);
|
|
212
|
+
const tags = this.osm.ways.tags.getTags(wayIndex);
|
|
213
|
+
const name = (tags?.["name"] as string) ?? "";
|
|
214
|
+
const highway = (tags?.["highway"] as string) ?? "";
|
|
215
|
+
const displayName = getDisplayName(tags);
|
|
216
|
+
|
|
217
|
+
if (currentSegment && currentDisplayName === displayName) {
|
|
218
|
+
if (!currentSegment.wayIds.includes(wayId)) {
|
|
219
|
+
currentSegment.wayIds.push(wayId);
|
|
220
|
+
}
|
|
221
|
+
currentSegment.distance += distance;
|
|
222
|
+
currentSegment.time += time;
|
|
223
|
+
} else {
|
|
224
|
+
if (currentSegment) {
|
|
225
|
+
segments.push(currentSegment);
|
|
226
|
+
const coord = this.osm.nodes.getNodeLonLat({
|
|
227
|
+
index: transitionNodeIndex,
|
|
228
|
+
});
|
|
229
|
+
if (coord) {
|
|
230
|
+
turnPoints.push(coord);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
currentSegment = {
|
|
234
|
+
wayIds: [wayId],
|
|
235
|
+
name,
|
|
236
|
+
highway,
|
|
237
|
+
distance,
|
|
238
|
+
time,
|
|
239
|
+
};
|
|
240
|
+
currentDisplayName = displayName;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (currentSegment) {
|
|
245
|
+
segments.push(currentSegment);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { segments, turnPoints };
|
|
249
|
+
}
|
|
262
250
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { OsmTags } from "@osmix/types";
|
|
2
|
+
import { normalizedWayDirection } from "@osmix/types/way-direction";
|
|
3
|
+
|
|
4
|
+
import type { HighwayFilter } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
/** Counts that describe a routing network's shape. */
|
|
7
|
+
export interface RoutingTopologyStats {
|
|
8
|
+
/** Every node in the source, routable or not. */
|
|
9
|
+
nodes: number;
|
|
10
|
+
/** Nodes on at least one way the filter accepts. */
|
|
11
|
+
routableNodes: number;
|
|
12
|
+
/** Directed edges: two per segment, one for a one-way segment. */
|
|
13
|
+
edges: number;
|
|
14
|
+
/** Weakly connected components among routable nodes. */
|
|
15
|
+
components: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Anything that can list its ways: an `Osm`, or a planner view of pending changes. */
|
|
19
|
+
export interface RoutingTopologySource {
|
|
20
|
+
nodeCount: number;
|
|
21
|
+
ways(): Iterable<{ tags?: OsmTags | undefined; refs: readonly number[] }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Directed edges one way adds for `filter` (two per segment, one when one-way), or null when
|
|
26
|
+
* the filter does not route it. Direction follows the router: an unsupported one-way value
|
|
27
|
+
* counts as forward on a roundabout and as two-way elsewhere.
|
|
28
|
+
*/
|
|
29
|
+
export function routingWayEdges(
|
|
30
|
+
way: { tags?: OsmTags | undefined; refs: readonly number[] },
|
|
31
|
+
filter: HighwayFilter,
|
|
32
|
+
): number | null {
|
|
33
|
+
if (!filter(way.tags) || way.refs.length < 2) return null;
|
|
34
|
+
const normalized = normalizedWayDirection(way.tags);
|
|
35
|
+
const direction =
|
|
36
|
+
normalized === "unsupported"
|
|
37
|
+
? way.tags?.["junction"] === "roundabout"
|
|
38
|
+
? "forward"
|
|
39
|
+
: "both"
|
|
40
|
+
: normalized;
|
|
41
|
+
return (direction === "both" ? 2 : 1) * (way.refs.length - 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The same counts `RoutingGraph` would give for `filter`, computed from ways alone, without
|
|
46
|
+
* building the graph (no coordinates, speeds or CSR arrays). Direction follows the router: an
|
|
47
|
+
* unsupported one-way value counts as forward on a roundabout and as two-way elsewhere.
|
|
48
|
+
*/
|
|
49
|
+
export function routingTopologyStats(
|
|
50
|
+
source: RoutingTopologySource,
|
|
51
|
+
filter: HighwayFilter,
|
|
52
|
+
): RoutingTopologyStats {
|
|
53
|
+
const parent = new Map<number, number>();
|
|
54
|
+
const find = (id: number): number => {
|
|
55
|
+
let root = id;
|
|
56
|
+
while (parent.get(root) !== root) root = parent.get(root)!;
|
|
57
|
+
let cursor = id;
|
|
58
|
+
while (parent.get(cursor) !== cursor) {
|
|
59
|
+
const next = parent.get(cursor)!;
|
|
60
|
+
parent.set(cursor, root);
|
|
61
|
+
cursor = next;
|
|
62
|
+
}
|
|
63
|
+
return root;
|
|
64
|
+
};
|
|
65
|
+
const union = (a: number, b: number) => {
|
|
66
|
+
if (!parent.has(a)) parent.set(a, a);
|
|
67
|
+
if (!parent.has(b)) parent.set(b, b);
|
|
68
|
+
const left = find(a);
|
|
69
|
+
const right = find(b);
|
|
70
|
+
if (left !== right) parent.set(right, left);
|
|
71
|
+
};
|
|
72
|
+
let edges = 0;
|
|
73
|
+
for (const way of source.ways()) {
|
|
74
|
+
const wayEdges = routingWayEdges(way, filter);
|
|
75
|
+
if (wayEdges == null) continue;
|
|
76
|
+
for (let index = 0; index < way.refs.length - 1; index++) {
|
|
77
|
+
union(way.refs[index]!, way.refs[index + 1]!);
|
|
78
|
+
}
|
|
79
|
+
edges += wayEdges;
|
|
80
|
+
}
|
|
81
|
+
const roots = new Set<number>();
|
|
82
|
+
for (const id of parent.keys()) roots.add(find(id));
|
|
83
|
+
return { nodes: source.nodeCount, routableNodes: parent.size, edges, components: roots.size };
|
|
84
|
+
}
|