@osmix/router 0.0.13 → 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 +64 -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 +1 -1
- 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 -6
- package/tsconfig.json +7 -7
package/src/graph.ts
CHANGED
|
@@ -11,16 +11,19 @@
|
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { BufferConstructor, type BufferType, type Osm } from "@osmix/core"
|
|
15
|
-
import { haversineDistance } from "@osmix/
|
|
16
|
-
import
|
|
14
|
+
import { BufferConstructor, type BufferType, type Osm } from "@osmix/core";
|
|
15
|
+
import { haversineDistance } from "@osmix/geo/haversine-distance";
|
|
16
|
+
import { BitSet } from "@osmix/shared/bit-set";
|
|
17
|
+
import type { LonLat } from "@osmix/types";
|
|
18
|
+
import { normalizedWayDirection } from "@osmix/types/way-direction";
|
|
19
|
+
|
|
17
20
|
import type {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "./types.ts"
|
|
23
|
-
import { DEFAULT_SPEEDS, defaultHighwayFilter, getSpeedLimit } from "./utils.ts"
|
|
21
|
+
DefaultSpeeds,
|
|
22
|
+
GraphEdge,
|
|
23
|
+
HighwayFilter,
|
|
24
|
+
RoutingGraphTransferables,
|
|
25
|
+
} from "./types.ts";
|
|
26
|
+
import { DEFAULT_SPEEDS, defaultHighwayFilter, getSpeedLimit } from "./utils.ts";
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Routing graph built from OSM ways and nodes.
|
|
@@ -31,7 +34,8 @@ import { DEFAULT_SPEEDS, defaultHighwayFilter, getSpeedLimit } from "./utils.ts"
|
|
|
31
34
|
* @example Build from OSM data
|
|
32
35
|
* ```ts
|
|
33
36
|
* const graph = new RoutingGraph(osm)
|
|
34
|
-
* const
|
|
37
|
+
* const router = new Router(osm, graph)
|
|
38
|
+
* const path = router.route(startNodeIndex, endNodeIndex)
|
|
35
39
|
* ```
|
|
36
40
|
*
|
|
37
41
|
* @example Transfer between workers
|
|
@@ -45,330 +49,365 @@ import { DEFAULT_SPEEDS, defaultHighwayFilter, getSpeedLimit } from "./utils.ts"
|
|
|
45
49
|
* ```
|
|
46
50
|
*/
|
|
47
51
|
export class RoutingGraph {
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
52
|
+
/** Total number of nodes in the source OSM data. */
|
|
53
|
+
private nodeCount = 0;
|
|
54
|
+
/** Total number of edges in the graph. */
|
|
55
|
+
private edgeCount = 0;
|
|
56
|
+
|
|
57
|
+
// CSR format arrays (populated after compact())
|
|
58
|
+
private edgeOffsets: Uint32Array | null = null;
|
|
59
|
+
private edgeTargets: Uint32Array | null = null;
|
|
60
|
+
private edgeWayIndexes: Uint32Array | null = null;
|
|
61
|
+
private edgeDistances: Float32Array | null = null;
|
|
62
|
+
private edgeTimes: Float32Array | null = null;
|
|
63
|
+
private routable: BitSet | null = null;
|
|
64
|
+
private intersections: BitSet | null = null;
|
|
65
|
+
|
|
66
|
+
// Incoming-edge CSR, built on first use by getIncomingEdges(). Per worker; not transferred.
|
|
67
|
+
private reverseOffsets: Uint32Array | null = null;
|
|
68
|
+
/** Forward edge index for each incoming edge, grouped by target node. */
|
|
69
|
+
private reverseEdges: Uint32Array | null = null;
|
|
70
|
+
private reverseSources: Uint32Array | null = null;
|
|
71
|
+
private cachedMaxSpeedMps: number | null = null;
|
|
72
|
+
|
|
73
|
+
// Expose highway filter and default speeds
|
|
74
|
+
readonly filter: HighwayFilter;
|
|
75
|
+
readonly defaultSpeeds: DefaultSpeeds;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a RoutingGraph.
|
|
79
|
+
*
|
|
80
|
+
* @param source - Either an Osm instance to build from, or RoutingGraphTransferables to reconstruct.
|
|
81
|
+
* @param filter - Function to determine which ways are routable (only used when building from Osm).
|
|
82
|
+
* @param defaultSpeeds - Speed limits by highway type (only used when building from Osm).
|
|
83
|
+
*/
|
|
84
|
+
constructor(
|
|
85
|
+
source: Osm | RoutingGraphTransferables,
|
|
86
|
+
filter: HighwayFilter = defaultHighwayFilter,
|
|
87
|
+
defaultSpeeds: DefaultSpeeds = DEFAULT_SPEEDS,
|
|
88
|
+
) {
|
|
89
|
+
this.filter = filter;
|
|
90
|
+
this.defaultSpeeds = defaultSpeeds;
|
|
91
|
+
if ("nodeCount" in source) {
|
|
92
|
+
// Reconstruct from transferables
|
|
93
|
+
this.fromTransferables(source);
|
|
94
|
+
} else {
|
|
95
|
+
// Build from OSM data
|
|
96
|
+
this.buildFromOsm(source, filter, defaultSpeeds);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reconstruct from transferables (worker transfer).
|
|
102
|
+
*/
|
|
103
|
+
private fromTransferables(t: RoutingGraphTransferables) {
|
|
104
|
+
this.nodeCount = t.nodeCount;
|
|
105
|
+
this.edgeCount = t.edgeCount;
|
|
106
|
+
this.edgeOffsets = new Uint32Array(t.edgeOffsets);
|
|
107
|
+
this.edgeTargets = new Uint32Array(t.edgeTargets);
|
|
108
|
+
this.edgeWayIndexes = new Uint32Array(t.edgeWayIndexes);
|
|
109
|
+
this.edgeDistances = new Float32Array(t.edgeDistances);
|
|
110
|
+
this.edgeTimes = new Float32Array(t.edgeTimes);
|
|
111
|
+
this.routable = new BitSet(t.nodeCount, t.routableBits);
|
|
112
|
+
this.intersections = new BitSet(t.nodeCount, t.intersectionBits);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build the graph from OSM data in two passes over the routable ways, straight into typed
|
|
117
|
+
* arrays: the first counts each node's outgoing edges and sets the node flags, the second
|
|
118
|
+
* fills the CSR arrays. No per-edge objects or `Map`/`Set` (V8 caps those near 2^24 entries).
|
|
119
|
+
*/
|
|
120
|
+
private buildFromOsm(osm: Osm, filter: HighwayFilter, defaultSpeeds: DefaultSpeeds) {
|
|
121
|
+
this.nodeCount = osm.nodes.size;
|
|
122
|
+
const bitsetLength = BitSet.byteLength(this.nodeCount);
|
|
123
|
+
this.routable = new BitSet(this.nodeCount, new BufferConstructor(bitsetLength));
|
|
124
|
+
this.intersections = new BitSet(this.nodeCount, new BufferConstructor(bitsetLength));
|
|
125
|
+
const offsets = new Uint32Array(
|
|
126
|
+
new BufferConstructor((this.nodeCount + 1) * Uint32Array.BYTES_PER_ELEMENT),
|
|
127
|
+
);
|
|
128
|
+
this.edgeOffsets = offsets;
|
|
129
|
+
|
|
130
|
+
// Pass 1: count outgoing edges into offsets[from + 1], and mark routable nodes and
|
|
131
|
+
// intersections (a node seen a second time).
|
|
132
|
+
this.forEachSegment(osm, filter, defaultSpeeds, (from, to, direction) => {
|
|
133
|
+
if (direction !== "reverse") offsets[from + 1]!++;
|
|
134
|
+
if (direction !== "forward") offsets[to + 1]!++;
|
|
135
|
+
this.markRoutable(from);
|
|
136
|
+
this.markRoutable(to);
|
|
137
|
+
});
|
|
138
|
+
for (let n = 0; n < this.nodeCount; n++) offsets[n + 1]! += offsets[n]!;
|
|
139
|
+
this.edgeCount = offsets[this.nodeCount]!;
|
|
140
|
+
|
|
141
|
+
this.edgeTargets = new Uint32Array(
|
|
142
|
+
new BufferConstructor(this.edgeCount * Uint32Array.BYTES_PER_ELEMENT),
|
|
143
|
+
);
|
|
144
|
+
this.edgeWayIndexes = new Uint32Array(
|
|
145
|
+
new BufferConstructor(this.edgeCount * Uint32Array.BYTES_PER_ELEMENT),
|
|
146
|
+
);
|
|
147
|
+
this.edgeDistances = new Float32Array(
|
|
148
|
+
new BufferConstructor(this.edgeCount * Float32Array.BYTES_PER_ELEMENT),
|
|
149
|
+
);
|
|
150
|
+
this.edgeTimes = new Float32Array(
|
|
151
|
+
new BufferConstructor(this.edgeCount * Float32Array.BYTES_PER_ELEMENT),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Pass 2: fill each node's edges in way order.
|
|
155
|
+
const cursor = offsets.slice(0, this.nodeCount);
|
|
156
|
+
const addEdge = (
|
|
157
|
+
from: number,
|
|
158
|
+
to: number,
|
|
159
|
+
wayIndex: number,
|
|
160
|
+
distance: number,
|
|
161
|
+
time: number,
|
|
162
|
+
) => {
|
|
163
|
+
const edgeIndex = cursor[from]!++;
|
|
164
|
+
this.edgeTargets![edgeIndex] = to;
|
|
165
|
+
this.edgeWayIndexes![edgeIndex] = wayIndex;
|
|
166
|
+
this.edgeDistances![edgeIndex] = distance;
|
|
167
|
+
this.edgeTimes![edgeIndex] = time;
|
|
168
|
+
};
|
|
169
|
+
this.forEachSegment(osm, filter, defaultSpeeds, (from, to, direction, wayIndex, speedMps) => {
|
|
170
|
+
const distance = haversineDistance(
|
|
171
|
+
osm.nodes.getNodeLonLat({ index: from }),
|
|
172
|
+
osm.nodes.getNodeLonLat({ index: to }),
|
|
173
|
+
);
|
|
174
|
+
const time = distance / speedMps;
|
|
175
|
+
if (direction !== "reverse") addEdge(from, to, wayIndex, distance, time);
|
|
176
|
+
if (direction !== "forward") addEdge(to, from, wayIndex, distance, time);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Visit each segment of each routable way, in way order, with its direction and speed. */
|
|
181
|
+
private forEachSegment(
|
|
182
|
+
osm: Osm,
|
|
183
|
+
filter: HighwayFilter,
|
|
184
|
+
defaultSpeeds: DefaultSpeeds,
|
|
185
|
+
visit: (
|
|
186
|
+
from: number,
|
|
187
|
+
to: number,
|
|
188
|
+
direction: "forward" | "reverse" | "both",
|
|
189
|
+
wayIndex: number,
|
|
190
|
+
speedMps: number,
|
|
191
|
+
) => void,
|
|
192
|
+
) {
|
|
193
|
+
for (let wayIndex = 0; wayIndex < osm.ways.size; wayIndex++) {
|
|
194
|
+
const tags = osm.ways.tags.getTags(wayIndex);
|
|
195
|
+
if (!filter(tags)) continue;
|
|
196
|
+
|
|
197
|
+
const refs = osm.ways.getRefIds(wayIndex);
|
|
198
|
+
if (refs.length < 2) continue;
|
|
199
|
+
|
|
200
|
+
const normalizedDirection = normalizedWayDirection(tags);
|
|
201
|
+
// Preserve the existing router approximation for unsupported dynamic values.
|
|
202
|
+
// Matching does not use this fallback to establish direction equivalence.
|
|
203
|
+
const direction =
|
|
204
|
+
normalizedDirection === "unsupported"
|
|
205
|
+
? tags?.["junction"] === "roundabout"
|
|
206
|
+
? "forward"
|
|
207
|
+
: "both"
|
|
208
|
+
: normalizedDirection;
|
|
209
|
+
const speedMps = (getSpeedLimit(tags, defaultSpeeds) * 1_000) / 60 / 60;
|
|
210
|
+
|
|
211
|
+
let from = osm.nodes.ids.getIndexFromId(refs[0]!);
|
|
212
|
+
for (let i = 1; i < refs.length; i++) {
|
|
213
|
+
const to = osm.nodes.ids.getIndexFromId(refs[i]!);
|
|
214
|
+
// A ref to a node outside the dataset (for example, cut off by an extract) has index -1.
|
|
215
|
+
// Skip the segments that touch it rather than store a wrapped Uint32 index.
|
|
216
|
+
if (from >= 0 && to >= 0) visit(from, to, direction, wayIndex, speedMps);
|
|
217
|
+
from = to;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Mark a node routable, or an intersection if it was already routable. */
|
|
223
|
+
private markRoutable(nodeIndex: number) {
|
|
224
|
+
// Segment endpoints are resolved node indexes, so they are in range.
|
|
225
|
+
if (this.routable!.hasUnchecked(nodeIndex)) this.intersections!.addUnchecked(nodeIndex);
|
|
226
|
+
else this.routable!.addUnchecked(nodeIndex);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if a node is part of the routable network.
|
|
231
|
+
*/
|
|
232
|
+
isRoutable(nodeIndex: number): boolean {
|
|
233
|
+
if (nodeIndex < 0 || nodeIndex >= this.nodeCount) return false;
|
|
234
|
+
return this.routable!.hasUnchecked(nodeIndex);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Check if a node is an intersection (multiple ways meet).
|
|
239
|
+
*/
|
|
240
|
+
isIntersection(nodeIndex: number): boolean {
|
|
241
|
+
if (nodeIndex < 0 || nodeIndex >= this.nodeCount) return false;
|
|
242
|
+
return this.intersections!.hasUnchecked(nodeIndex);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get outgoing edges from a node.
|
|
247
|
+
*/
|
|
248
|
+
getEdges(nodeIndex: number): GraphEdge[] {
|
|
249
|
+
if (nodeIndex < 0 || nodeIndex >= this.nodeCount || !this.edgeOffsets || !this.edgeTargets) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const start = this.edgeOffsets[nodeIndex]!;
|
|
254
|
+
const end = this.edgeOffsets[nodeIndex + 1]!;
|
|
255
|
+
const edges: GraphEdge[] = [];
|
|
256
|
+
|
|
257
|
+
for (let i = start; i < end; i++) {
|
|
258
|
+
edges.push({
|
|
259
|
+
targetNodeIndex: this.edgeTargets[i]!,
|
|
260
|
+
wayIndex: this.edgeWayIndexes![i]!,
|
|
261
|
+
distance: this.edgeDistances![i]!,
|
|
262
|
+
time: this.edgeTimes![i]!,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return edges;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get incoming edges of a node. In each returned edge, `targetNodeIndex` is the
|
|
271
|
+
* edge's source node. The reverse index (4 bytes per node plus 8 bytes per edge)
|
|
272
|
+
* is built on the first call.
|
|
273
|
+
*/
|
|
274
|
+
getIncomingEdges(nodeIndex: number): GraphEdge[] {
|
|
275
|
+
if (nodeIndex < 0 || nodeIndex >= this.nodeCount || !this.edgeOffsets || !this.edgeTargets) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
if (!this.reverseOffsets) this.buildReverseIndex();
|
|
279
|
+
const start = this.reverseOffsets![nodeIndex]!;
|
|
280
|
+
const end = this.reverseOffsets![nodeIndex + 1]!;
|
|
281
|
+
const edges: GraphEdge[] = [];
|
|
282
|
+
for (let i = start; i < end; i++) {
|
|
283
|
+
const edgeIndex = this.reverseEdges![i]!;
|
|
284
|
+
edges.push({
|
|
285
|
+
targetNodeIndex: this.reverseSources![i]!,
|
|
286
|
+
wayIndex: this.edgeWayIndexes![edgeIndex]!,
|
|
287
|
+
distance: this.edgeDistances![edgeIndex]!,
|
|
288
|
+
time: this.edgeTimes![edgeIndex]!,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return edges;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private buildReverseIndex() {
|
|
295
|
+
const offsets = this.edgeOffsets!;
|
|
296
|
+
const targets = this.edgeTargets!;
|
|
297
|
+
const reverseOffsets = new Uint32Array(this.nodeCount + 1);
|
|
298
|
+
for (let i = 0; i < this.edgeCount; i++) reverseOffsets[targets[i]! + 1]!++;
|
|
299
|
+
for (let n = 0; n < this.nodeCount; n++) {
|
|
300
|
+
reverseOffsets[n + 1]! += reverseOffsets[n]!;
|
|
301
|
+
}
|
|
302
|
+
const cursor = reverseOffsets.slice(0, this.nodeCount);
|
|
303
|
+
const reverseEdges = new Uint32Array(this.edgeCount);
|
|
304
|
+
const reverseSources = new Uint32Array(this.edgeCount);
|
|
305
|
+
for (let source = 0; source < this.nodeCount; source++) {
|
|
306
|
+
for (let e = offsets[source]!; e < offsets[source + 1]!; e++) {
|
|
307
|
+
const slot = cursor[targets[e]!]!++;
|
|
308
|
+
reverseEdges[slot] = e;
|
|
309
|
+
reverseSources[slot] = source;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
this.reverseOffsets = reverseOffsets;
|
|
313
|
+
this.reverseEdges = reverseEdges;
|
|
314
|
+
this.reverseSources = reverseSources;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Fastest edge speed in the graph, in meters per second. The A* time heuristic
|
|
319
|
+
* divides by this so that it never overestimates travel time.
|
|
320
|
+
*/
|
|
321
|
+
get maxSpeedMps(): number {
|
|
322
|
+
if (this.cachedMaxSpeedMps !== null) return this.cachedMaxSpeedMps;
|
|
323
|
+
let max = 0;
|
|
324
|
+
for (let i = 0; i < this.edgeCount; i++) {
|
|
325
|
+
const time = this.edgeTimes![i]!;
|
|
326
|
+
if (time > 0) max = Math.max(max, this.edgeDistances![i]! / time);
|
|
327
|
+
}
|
|
328
|
+
// Float32 rounding can shave a little off the true speed; keep the bound safe.
|
|
329
|
+
this.cachedMaxSpeedMps = max > 0 ? max * (1 + 1e-6) : Number.POSITIVE_INFINITY;
|
|
330
|
+
return this.cachedMaxSpeedMps;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get transferable buffers for passing to another thread.
|
|
335
|
+
*/
|
|
336
|
+
transferables(): RoutingGraphTransferables {
|
|
337
|
+
return {
|
|
338
|
+
nodeCount: this.nodeCount,
|
|
339
|
+
edgeCount: this.edgeCount,
|
|
340
|
+
edgeOffsets: this.edgeOffsets!.buffer as BufferType,
|
|
341
|
+
edgeTargets: this.edgeTargets!.buffer as BufferType,
|
|
342
|
+
edgeWayIndexes: this.edgeWayIndexes!.buffer as BufferType,
|
|
343
|
+
edgeDistances: this.edgeDistances!.buffer as BufferType,
|
|
344
|
+
edgeTimes: this.edgeTimes!.buffer as BufferType,
|
|
345
|
+
routableBits: this.routable!.buffer as BufferType,
|
|
346
|
+
intersectionBits: this.intersections!.buffer as BufferType,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Number of node slots in the graph: every node in the source OSM data, not only
|
|
352
|
+
* routable ones. Use `isRoutable()` to test a node.
|
|
353
|
+
*/
|
|
354
|
+
get size(): number {
|
|
355
|
+
return this.nodeCount;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get the number of edges in the graph.
|
|
360
|
+
*/
|
|
361
|
+
get edges(): number {
|
|
362
|
+
return this.edgeCount;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Find the nearest routable OSM node from a geographic point.
|
|
367
|
+
*
|
|
368
|
+
* Searches for nodes within the given radius that are part of the routing
|
|
369
|
+
* graph (i.e., lie on a routable way). Returns the closest match with its
|
|
370
|
+
* coordinates and distance.
|
|
371
|
+
*
|
|
372
|
+
* Snaps to graph nodes only, not to points along edges. Requires the "all" node
|
|
373
|
+
* spatial index; throws `SpatialIndexNotBuiltError` without it.
|
|
374
|
+
*
|
|
375
|
+
* @param osm - The OSM dataset.
|
|
376
|
+
* @param point - The [lon, lat] coordinates to search from.
|
|
377
|
+
* @param maxDistanceM - Maximum search radius in meters.
|
|
378
|
+
* @returns The nearest routable node, or null if none found.
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```ts
|
|
382
|
+
* const nearest = graph.findNearestRoutableNode(osm, [-73.989, 40.733], 500)
|
|
383
|
+
* if (nearest) {
|
|
384
|
+
* console.log(`Found node ${nearest.nodeIndex} at ${nearest.distance}m`)
|
|
385
|
+
* }
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
findNearestRoutableNode(osm: Osm, point: LonLat, maxDistanceM: number) {
|
|
389
|
+
const nearby = osm.nodes.findIndexesWithinRadius(point[0], point[1], maxDistanceM / 1_000);
|
|
390
|
+
|
|
391
|
+
let best: {
|
|
392
|
+
nodeIndex: number;
|
|
393
|
+
coordinates: LonLat;
|
|
394
|
+
distance: number;
|
|
395
|
+
} | null = null;
|
|
396
|
+
let bestDistM = Number.POSITIVE_INFINITY;
|
|
397
|
+
|
|
398
|
+
for (const nodeIndex of nearby) {
|
|
399
|
+
if (!this.isRoutable(nodeIndex)) continue;
|
|
400
|
+
|
|
401
|
+
const nodeCoord = osm.nodes.getNodeLonLat({ index: nodeIndex });
|
|
402
|
+
const distance = haversineDistance(point, nodeCoord);
|
|
403
|
+
if (distance < bestDistM && distance <= maxDistanceM) {
|
|
404
|
+
bestDistM = distance;
|
|
405
|
+
best = { nodeIndex, coordinates: nodeCoord, distance };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return best;
|
|
410
|
+
}
|
|
372
411
|
}
|
|
373
412
|
|
|
374
413
|
/**
|
|
@@ -382,11 +421,11 @@ export class RoutingGraph {
|
|
|
382
421
|
* @returns A RoutingGraph ready for pathfinding.
|
|
383
422
|
*/
|
|
384
423
|
export function buildGraph(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
424
|
+
osm: Osm,
|
|
425
|
+
filter: HighwayFilter = defaultHighwayFilter,
|
|
426
|
+
defaultSpeeds: DefaultSpeeds = DEFAULT_SPEEDS,
|
|
388
427
|
): RoutingGraph {
|
|
389
|
-
|
|
428
|
+
return new RoutingGraph(osm, filter, defaultSpeeds);
|
|
390
429
|
}
|
|
391
430
|
|
|
392
431
|
/**
|
|
@@ -394,21 +433,16 @@ export function buildGraph(
|
|
|
394
433
|
* Note: SharedArrayBuffers don't need to be transferred (they're shared automatically),
|
|
395
434
|
* but ArrayBuffers do. This returns only the ArrayBuffers that need explicit transfer.
|
|
396
435
|
*/
|
|
397
|
-
export function getTransferableBuffers(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// Only ArrayBuffers need to be transferred; SharedArrayBuffers are shared automatically
|
|
410
|
-
return buffers.filter(
|
|
411
|
-
(b): b is ArrayBuffer =>
|
|
412
|
-
b instanceof ArrayBuffer && !(b instanceof SharedArrayBuffer),
|
|
413
|
-
)
|
|
436
|
+
export function getTransferableBuffers(t: RoutingGraphTransferables): ArrayBuffer[] {
|
|
437
|
+
const buffers = [
|
|
438
|
+
t.edgeOffsets,
|
|
439
|
+
t.edgeTargets,
|
|
440
|
+
t.edgeWayIndexes,
|
|
441
|
+
t.edgeDistances,
|
|
442
|
+
t.edgeTimes,
|
|
443
|
+
t.routableBits,
|
|
444
|
+
t.intersectionBits,
|
|
445
|
+
];
|
|
446
|
+
// Only ArrayBuffers need to be transferred; SharedArrayBuffers are shared automatically
|
|
447
|
+
return buffers.filter((b): b is ArrayBuffer => b instanceof ArrayBuffer);
|
|
414
448
|
}
|