@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# @osmix/router
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 66eb7cd: `bidirectional` is now bidirectional Dijkstra: it searches backward over incoming edges, returns an optimal path and respects one-way edges. It was a breadth-first search that could return a longer path. Routing algorithms take an optional `RoutingAlgorithmContext` (`reverseGraph`, `maxSpeedMps`), which `Router` supplies; `RoutingGraph` adds `getIncomingEdges` and `maxSpeedMps`, and A*'s time heuristic uses the graph's maximum speed.
|
|
8
|
+
- 66eb7cd: Add `routingTopologyStats`, which counts a routing network's nodes, edges and components from its ways without building a `RoutingGraph`.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 466adc0: Share one-way normalization between exact reconciliation, fuzzy way matching, and routing. Respect explicit `no`, `false`, and `0` on roundabouts, recognize equivalent supported aliases, and compare reversed geometry consistently. Prevent unsupported values from being treated as direction-equivalent during matching while retaining the router's documented fallback behavior.
|
|
13
|
+
|
|
14
|
+
Block fuzzy matches when endpoint geometry cannot establish the orientation needed to compare one-way travel, including closed one-way roundabouts. Bidirectional ways without recognized direction-sensitive tags remain eligible.
|
|
15
|
+
|
|
16
|
+
- 8be5c80: Allow Osmix to use its ArrayBuffer fallback when SharedArrayBuffer is unavailable, so browser
|
|
17
|
+
applications no longer need to install a global SharedArrayBuffer shim.
|
|
18
|
+
- cd987f5: Preserve input topology during merges, conservatively reconcile compatible patch entities with the base,
|
|
19
|
+
validate routing-sensitive references, and insert multiple intersections in way order. Within-file duplicate
|
|
20
|
+
scans in the Merge app are now diagnostic only; regenerate older merged PBFs from their source inputs. Correct
|
|
21
|
+
the router priority queue so shortest-path searches visit lower-cost states first, and honor the one-way
|
|
22
|
+
direction implied by OSM roundabouts plus reverse one-way (`oneway=-1`) tags.
|
|
23
|
+
|
|
24
|
+
Restore the original 1-meter matching behavior as explicit, cross-dataset fuzzy conflation for imported data.
|
|
25
|
+
Callers select transferable properties independently from patch-network attachment; exact merge behavior
|
|
26
|
+
remains the default. Unique, high-confidence pedestrian and one-to-one-way matches can apply automatically,
|
|
27
|
+
while routing properties, motor roads, ambiguity, relation involvement, and uncertain geometry require review.
|
|
28
|
+
Grade conflicts, restrictions, protected tags, dangling references, way collapse, and base-topology rewrites
|
|
29
|
+
remain blocked. Matching candidates and their evidence are inspectable with `discoverConflationCandidates`;
|
|
30
|
+
merge plans carry the CAR/WALK topology diagnostics, and the Merge app reviews the plan before applying it.
|
|
31
|
+
|
|
32
|
+
- Updated dependencies [e4785fe]
|
|
33
|
+
- Updated dependencies [df04f92]
|
|
34
|
+
- Updated dependencies [e4785fe]
|
|
35
|
+
- Updated dependencies [66eb7cd]
|
|
36
|
+
- Updated dependencies [466adc0]
|
|
37
|
+
- Updated dependencies [66eb7cd]
|
|
38
|
+
- Updated dependencies [66eb7cd]
|
|
39
|
+
- Updated dependencies [8be5c80]
|
|
40
|
+
- @osmix/shared@0.2.0
|
|
41
|
+
- @osmix/core@0.3.0
|
|
42
|
+
- @osmix/types@0.2.0
|
|
43
|
+
- @osmix/geo@0.1.1
|
|
44
|
+
|
|
45
|
+
## 0.0.14
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- c7a5a35: Harden parsing, loading, changeset generation, worker orchestration, and package verification across Osmix.
|
|
50
|
+
|
|
51
|
+
- `@osmix/pbf`, `@osmix/json`, and `@osmix/load` now validate PBF framing and decompression limits, normalize every supported input form, apply timestamp granularity consistently, translate tags before filtering, remove dangling filtered references, and build every requested spatial index.
|
|
52
|
+
- `@osmix/core` now returns `null` for unknown node-coordinate lookups, safely handles incomplete geometry, caches relation-to-way membership, and avoids repeated ID lookups and per-entity object allocation during sorted iteration.
|
|
53
|
+
- `@osmix/change` now preserves relation references during deduplication, escapes OSC XML attributes, applies changesets non-destructively, allocates collision-free IDs for empty or unsorted data, and resolves pending intersection geometry safely. Its intersection runtime is now vendored and strictly typed, removing the upstream test/build dependency graph.
|
|
54
|
+
- `osmix` now preserves sliced typed-array views during transfer, disposes and terminates owned workers reliably, uses collision-safe worker registries, and supports all documented PBF input forms across local and remote APIs.
|
|
55
|
+
- `@osmix/geojson`, `@osmix/vt`, and `@osmix/shortbread` now consume nullable or read-only geometry data without aliasing or mutation hazards.
|
|
56
|
+
- `@osmix/gtfs` and `@osmix/router` documentation now uses the current archive, routing, transfer, and distance APIs.
|
|
57
|
+
- `@osmix/shared` and `@osmix/test-utils` add executable package smoke coverage for byte streams and checked-in fixtures.
|
|
58
|
+
- Public package examples are classified as compilable or schematic and are checked against current exports and argument types.
|
|
59
|
+
|
|
60
|
+
- Updated dependencies [368d103]
|
|
61
|
+
- Updated dependencies [368d103]
|
|
62
|
+
- Updated dependencies [c7a5a35]
|
|
63
|
+
- @osmix/core@0.2.0
|
|
64
|
+
- @osmix/geo@0.1.0
|
|
65
|
+
- @osmix/types@0.1.0
|
|
66
|
+
|
|
67
|
+
## 0.0.13
|
|
68
|
+
|
|
69
|
+
### Patch Changes
|
|
70
|
+
|
|
71
|
+
- Updated dependencies [d67e38d]
|
|
72
|
+
- @osmix/core@0.1.10
|
|
73
|
+
|
|
74
|
+
## 0.0.12
|
|
75
|
+
|
|
76
|
+
### Patch Changes
|
|
77
|
+
|
|
78
|
+
- Updated dependencies [6144903]
|
|
79
|
+
- @osmix/core@0.1.9
|
|
80
|
+
|
|
3
81
|
## 0.0.11
|
|
4
82
|
|
|
5
83
|
### Patch Changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Conveyal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
## Highlights
|
|
6
6
|
|
|
7
7
|
- Builds a directed graph from OSM ways and nodes
|
|
8
|
+
- Respects explicit one-way tags and the one-way direction implied by `junction=roundabout`
|
|
8
9
|
- Configurable highway type filtering
|
|
9
|
-
- Multiple routing algorithms (Dijkstra, A
|
|
10
|
+
- Multiple routing algorithms (Dijkstra, A\*, bidirectional search)
|
|
10
11
|
- Support for both distance and time-based routing
|
|
11
12
|
- Returns detailed route information including coordinates, way IDs, and node IDs
|
|
12
13
|
- Serializable graph format for Web Worker support
|
|
@@ -14,35 +15,34 @@
|
|
|
14
15
|
## Installation
|
|
15
16
|
|
|
16
17
|
```sh
|
|
17
|
-
|
|
18
|
+
pnpm add @osmix/router
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
## Usage
|
|
21
22
|
|
|
22
|
-
```ts
|
|
23
|
-
import {
|
|
24
|
-
import { Osm } from "@osmix/core"
|
|
23
|
+
```ts check-docs
|
|
24
|
+
import { Osm, Router, RoutingGraph } from "osmix";
|
|
25
25
|
|
|
26
|
-
const osm = new Osm()
|
|
26
|
+
const osm = new Osm();
|
|
27
27
|
// ... load OSM data into osm ...
|
|
28
28
|
|
|
29
29
|
// Build routing graph
|
|
30
|
-
const graph = new RoutingGraph(osm)
|
|
30
|
+
const graph = new RoutingGraph(osm);
|
|
31
31
|
|
|
32
32
|
// Snap coordinates to nearest routable nodes
|
|
33
|
-
const from =
|
|
34
|
-
const to =
|
|
33
|
+
const from = graph.findNearestRoutableNode(osm, [-73.989, 40.733], 500);
|
|
34
|
+
const to = graph.findNearestRoutableNode(osm, [-73.988, 40.734], 500);
|
|
35
35
|
|
|
36
36
|
if (from && to) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
const router = new Router(osm, graph);
|
|
38
|
+
const path = router.route(from.nodeIndex, to.nodeIndex);
|
|
39
|
+
|
|
40
|
+
if (path) {
|
|
41
|
+
const result = router.buildResult(path);
|
|
42
|
+
console.log(result.coordinates); // Array of [lon, lat] coordinates
|
|
43
|
+
console.log(result.wayIndexes); // Array of way indexes used
|
|
44
|
+
console.log(result.nodeIndexes); // Array of node indexes for turns
|
|
45
|
+
}
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
@@ -52,53 +52,101 @@ if (from && to) {
|
|
|
52
52
|
|
|
53
53
|
Build and manage a routing graph from OSM data. The graph uses a CSR (Compressed Sparse Row) format for efficient memory usage and cache locality.
|
|
54
54
|
|
|
55
|
-
```ts
|
|
56
|
-
import { RoutingGraph, defaultHighwayFilter } from "
|
|
55
|
+
```ts check-docs osm
|
|
56
|
+
import { RoutingGraph, defaultHighwayFilter } from "osmix";
|
|
57
57
|
|
|
58
58
|
// Build from OSM data
|
|
59
|
-
const graph = new RoutingGraph(osm, defaultHighwayFilter)
|
|
59
|
+
const graph = new RoutingGraph(osm, defaultHighwayFilter);
|
|
60
|
+
const nodeIndex = 0;
|
|
60
61
|
|
|
61
62
|
// Properties
|
|
62
|
-
graph.size // Number of routable
|
|
63
|
-
graph.edges // Total edge count
|
|
64
|
-
graph.
|
|
65
|
-
graph.isIntersection(nodeIndex) // Check if node is an intersection
|
|
66
|
-
graph.getEdges(nodeIndex) // Get outgoing edges from node
|
|
63
|
+
graph.size; // Number of node slots: every node in the dataset, routable or not
|
|
64
|
+
graph.edges; // Total edge count
|
|
65
|
+
graph.isRoutable(nodeIndex); // Check if node is routable
|
|
66
|
+
graph.isIntersection(nodeIndex); // Check if node is an intersection
|
|
67
|
+
graph.getEdges(nodeIndex); // Get outgoing edges from node
|
|
68
|
+
graph.getIncomingEdges(nodeIndex); // Get incoming edges (targetNodeIndex is the source)
|
|
69
|
+
graph.maxSpeedMps; // Fastest edge speed, used to bound the A* time heuristic
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
#### `findNearestRoutableNode(osm, point,
|
|
72
|
+
#### `findNearestRoutableNode(osm, point, maxDistanceM)`
|
|
70
73
|
|
|
71
|
-
Snap a geographic coordinate to the nearest routable node.
|
|
74
|
+
Snap a geographic coordinate to the nearest routable node. The radius is in meters. It snaps to graph nodes only, not to points along an edge. It needs the all-node spatial index (the Full load profile) and throws `SpatialIndexNotBuiltError` without it.
|
|
72
75
|
|
|
73
|
-
```ts
|
|
74
|
-
const nearest = graph.findNearestRoutableNode(osm, [-73.989, 40.733],
|
|
76
|
+
```ts check-docs router-context
|
|
77
|
+
const nearest = graph.findNearestRoutableNode(osm, [-73.989, 40.733], 500);
|
|
75
78
|
if (nearest) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
console.log(nearest.nodeIndex); // Internal node index
|
|
80
|
+
console.log(nearest.coordinates); // Snapped [lon, lat]
|
|
81
|
+
console.log(nearest.distance); // Distance from input point (meters)
|
|
79
82
|
}
|
|
80
83
|
```
|
|
81
84
|
|
|
82
85
|
**Constructor parameters:**
|
|
83
86
|
|
|
84
87
|
- `osm` - The `@osmix/core` dataset.
|
|
85
|
-
- `filter` - Optional function `(tags?) => boolean` to select routable ways. Default:
|
|
86
|
-
- `defaultSpeeds` - Optional speed limits (km/h) by highway type.
|
|
88
|
+
- `filter` - Optional function `(tags?) => boolean` to select routable ways. Default: `defaultHighwayFilter` (motorway to service roads). `defaultPedestrianFilter` selects footways, paths, cycleways, bridleways and steps.
|
|
89
|
+
- `defaultSpeeds` - Optional speed limits (km/h) by highway type. Default: `DEFAULT_SPEEDS`. A way's `maxspeed` tag takes priority (`parseMaxSpeed` reads numbers, `mph`, `walk` and `none`). Unknown types use 50 km/h.
|
|
90
|
+
|
|
91
|
+
Ways with a ref to a node that is not in the dataset (for example, after a bbox extract) keep their other segments. The segments that touch the missing node are not added.
|
|
92
|
+
|
|
93
|
+
Way directionality is currently graph-wide: custom filters can select pedestrian ways, but they
|
|
94
|
+
do not disable `oneway` or implicit roundabout direction. A custom filter does not supply a complete pedestrian or turn-restriction policy. The [optional local R5 comparison](../osmix/test/r5/README.md) evaluates declared fixture checks against a specific R5 checkout; coordinate linking and diagnostic observations do not establish complete modal legality.
|
|
95
|
+
|
|
96
|
+
#### Way direction
|
|
97
|
+
|
|
98
|
+
Graph construction and exact/fuzzy way matching share `normalizedWayDirection(tags)`, available from
|
|
99
|
+
`osmix` or `@osmix/types/way-direction`. It returns `OsmWayDirection`: `forward`, `reverse`, `both`, or
|
|
100
|
+
`unsupported`. Forward follows the way's ordered node references; reverse travels against that order.
|
|
101
|
+
|
|
102
|
+
| `oneway` value | Normalized direction |
|
|
103
|
+
| ------------------------------------------- | -------------------------------- |
|
|
104
|
+
| `yes`, `true`, `1` | `forward` |
|
|
105
|
+
| `reverse`, `-1` | `reverse` |
|
|
106
|
+
| `no`, `false`, `0` | `both`, including on roundabouts |
|
|
107
|
+
| Absent or empty, with `junction=roundabout` | `forward` |
|
|
108
|
+
| Absent or empty, on other ways | `both` |
|
|
109
|
+
| Any other nonempty value | `unsupported` |
|
|
110
|
+
|
|
111
|
+
One-way aliases are case-insensitive, and numeric values normalize like their string equivalents. Values
|
|
112
|
+
are not trimmed: `" yes "` is unsupported. The roundabout implication requires the literal
|
|
113
|
+
`junction=roundabout` value.
|
|
114
|
+
|
|
115
|
+
```ts check-docs
|
|
116
|
+
import { normalizedWayDirection } from "osmix";
|
|
117
|
+
|
|
118
|
+
const direction = normalizedWayDirection({ junction: "roundabout", oneway: "0" });
|
|
119
|
+
console.log(direction); // "both"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Unsupported values such as `reversible` and `alternating` prevent exact or fuzzy way matching from treating
|
|
123
|
+
two ways as direction-equivalent, even when their values are identical. The routing graph retains its existing
|
|
124
|
+
approximation for such values: forward on roundabouts and both directions on other ways. This fallback does
|
|
125
|
+
not model their actual rules. Conditional, time-dependent, lane-specific, and mode-specific one-way rules are
|
|
126
|
+
not evaluated by this normalization.
|
|
127
|
+
|
|
128
|
+
Fuzzy matching also needs a reliable geometry orientation before comparing one-way travel. It blocks one-way
|
|
129
|
+
candidates whose endpoints fit equally well in either order, including closed one-way loops. This matching
|
|
130
|
+
limit does not change their routing graph: graph edges always follow the stored references and normalized
|
|
131
|
+
tag direction.
|
|
87
132
|
|
|
88
133
|
#### Serialization (Web Worker support)
|
|
89
134
|
|
|
90
135
|
`RoutingGraph` can be serialized and transferred between Web Workers:
|
|
91
136
|
|
|
92
|
-
```ts
|
|
137
|
+
```ts check-docs router-transfer
|
|
138
|
+
import { getTransferableBuffers, RoutingGraph } from "@osmix/router";
|
|
139
|
+
|
|
93
140
|
// Build graph and get transferables
|
|
94
|
-
const
|
|
95
|
-
const
|
|
141
|
+
const transferables = graph.transferables();
|
|
142
|
+
const buffers = getTransferableBuffers(transferables);
|
|
96
143
|
|
|
97
|
-
// Transfer to worker
|
|
98
|
-
worker.postMessage(transferables)
|
|
144
|
+
// Transfer to worker without cloning ArrayBuffers
|
|
145
|
+
worker.postMessage(transferables, buffers);
|
|
99
146
|
|
|
100
147
|
// Reconstruct in worker
|
|
101
|
-
const reconstructed = new RoutingGraph(transferables)
|
|
148
|
+
const reconstructed = new RoutingGraph(transferables);
|
|
149
|
+
console.log(reconstructed.size);
|
|
102
150
|
```
|
|
103
151
|
|
|
104
152
|
The `transferables()` method returns an object containing:
|
|
@@ -112,38 +160,75 @@ The `transferables()` method returns an object containing:
|
|
|
112
160
|
|
|
113
161
|
High-level routing interface.
|
|
114
162
|
|
|
115
|
-
|
|
116
|
-
|
|
163
|
+
Schematic construction with route options:
|
|
164
|
+
|
|
165
|
+
```ts schematic
|
|
166
|
+
const router = new Router(osm, graph, { algorithm: "astar", metric: "time" });
|
|
117
167
|
```
|
|
118
168
|
|
|
119
169
|
**Methods:**
|
|
120
170
|
|
|
121
171
|
- `route(fromNodeIndex, toNodeIndex, options?)` - Find path between nodes. Returns `PathSegment[]` or `null`.
|
|
122
|
-
- `buildResult(path)` - Convert path to `RouteResult` with coordinates and
|
|
172
|
+
- `buildResult(path, options?)` - Convert path to `RouteResult` with `coordinates`, `wayIndexes` and `nodeIndexes`.
|
|
173
|
+
- `getRouteStatistics(path)` - Total `distance` (meters) and `time` (seconds).
|
|
174
|
+
- `getRoutePathInfo(path)` - `segments` (consecutive ways with the same name merged) and `turnPoints`.
|
|
123
175
|
|
|
124
176
|
**Options:**
|
|
125
177
|
|
|
126
178
|
- `algorithm` - `"dijkstra"` | `"astar"` | `"bidirectional"` (default: `"astar"`)
|
|
127
179
|
- `metric` - `"distance"` | `"time"` (default: `"distance"`)
|
|
180
|
+
- `includeStats` - Add `distance` and `time` to the `RouteResult` (default: `false`)
|
|
181
|
+
- `includePathInfo` - Add `segments` and `turnPoints` to the `RouteResult` (default: `false`)
|
|
182
|
+
|
|
183
|
+
### `routingTopologyStats(source, filter)`
|
|
184
|
+
|
|
185
|
+
Gives the counts a `RoutingGraph` built with `filter` would have (`nodes`, `routableNodes`, directed `edges` and weakly connected `components`) without building the graph. `source` is anything with a `nodeCount` and a `ways()` iterable of `{ tags, refs }`, so an `Osm` or a view of pending changes both work. Direction follows the graph, including the roundabout fallback.
|
|
186
|
+
|
|
187
|
+
```ts schematic
|
|
188
|
+
const stats = routingTopologyStats({ nodeCount: osm.nodes.size, ways: () => osm.ways }, filter);
|
|
189
|
+
```
|
|
128
190
|
|
|
129
191
|
### Algorithms
|
|
130
192
|
|
|
131
|
-
| Algorithm | Optimal? |
|
|
132
|
-
| --------------- | -------- |
|
|
133
|
-
| `dijkstra` | Yes |
|
|
134
|
-
| `astar` | Yes |
|
|
135
|
-
| `bidirectional` |
|
|
193
|
+
| Algorithm | Optimal? | Search |
|
|
194
|
+
| --------------- | -------- | ----------------------------------------------------------------- |
|
|
195
|
+
| `dijkstra` | Yes | Expands in all directions from the start |
|
|
196
|
+
| `astar` | Yes | Guided toward the end by straight-line distance (default) |
|
|
197
|
+
| `bidirectional` | Yes | Dijkstra from both ends; the backward half follows incoming edges |
|
|
198
|
+
|
|
199
|
+
For the `time` metric, A* divides the straight-line distance by `graph.maxSpeedMps`, so the heuristic stays admissible when a `maxspeed` is above 130 km/h. `bidirectional` builds an index of incoming edges on its first use (4 bytes for each node plus 8 bytes for each edge).
|
|
200
|
+
|
|
201
|
+
A custom algorithm has the `RoutingAlgorithmFn` signature. Its last argument, `RoutingAlgorithmContext`, carries `reverseGraph` and `maxSpeedMps`.
|
|
202
|
+
|
|
203
|
+
## Limitations
|
|
204
|
+
|
|
205
|
+
The router reads only the `highway`, `oneway`, `junction` and `maxspeed` tags. It does not model:
|
|
206
|
+
|
|
207
|
+
- Turn restrictions (relations)
|
|
208
|
+
- Access tags (`access`, `motor_vehicle`, `foot`, `bicycle` and more)
|
|
209
|
+
- Mode-specific, conditional or lane-specific one-way tags
|
|
210
|
+
- Turn costs or elevation
|
|
211
|
+
- A bicycle profile (`defaultPedestrianFilter` includes cycleways)
|
|
212
|
+
|
|
213
|
+
Memory and search:
|
|
214
|
+
|
|
215
|
+
- The graph uses 16 bytes for each directed edge, plus about 4.25 bytes for each node in the dataset, routable or not. The build reads the routable ways twice and writes straight into these arrays, so its peak is the final size plus one 4-byte cursor for each node.
|
|
216
|
+
- Node, edge and way indexes are `Uint32`. Distances and times are `Float32`.
|
|
217
|
+
- A search has no distance or time limit, and you cannot cancel it. If no route exists, the search examines the whole connected network before it returns `null`.
|
|
218
|
+
- The worker keeps one graph (one filter) for each dataset.
|
|
219
|
+
|
|
220
|
+
See [docs/limits.md](../../docs/limits.md#router-limits) for measured graph sizes.
|
|
136
221
|
|
|
137
222
|
## Related Packages
|
|
138
223
|
|
|
139
224
|
- [`@osmix/core`](../core/README.md) - In-memory OSM index with spatial queries.
|
|
225
|
+
- [`@osmix/load`](../load/README.md) - Load PBF data into `Osm` indexes (`fromPbf`).
|
|
140
226
|
- [`@osmix/shared`](../shared/README.md) - Haversine distance and coordinate utilities.
|
|
141
227
|
- [`osmix`](../osmix/README.md) - High-level API with worker support for routing.
|
|
142
228
|
|
|
143
229
|
## Development
|
|
144
230
|
|
|
145
|
-
- `
|
|
146
|
-
- `
|
|
147
|
-
- `bun run typecheck packages/router`
|
|
231
|
+
- `pnpm run test packages/router`
|
|
232
|
+
- `pnpm exec oxlint packages/router`
|
|
148
233
|
|
|
149
|
-
Run `
|
|
234
|
+
Run `pnpm run verify` at the repo root before publishing.
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bidirectional
|
|
2
|
+
* Bidirectional Dijkstra.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Runs Dijkstra forward from the start over outgoing edges and backward from
|
|
5
|
+
* the end over incoming edges, and stops when neither search can improve the
|
|
6
|
+
* best meeting point. Returns an optimal path and respects one-way edges.
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import type { RoutingAlgorithmFn } from "../types.ts";
|
|
11
11
|
/**
|
|
12
|
-
* Bidirectional
|
|
13
|
-
*
|
|
14
|
-
* Uses breadth-first search from both start and end, terminating when
|
|
15
|
-
* the frontiers meet. Very fast for finding any path, though not always
|
|
16
|
-
* the optimal shortest path. Use Dijkstra or A* if optimality is required.
|
|
12
|
+
* Bidirectional Dijkstra. Requires `context.reverseGraph`, which `Router`
|
|
13
|
+
* supplies from `RoutingGraph.getIncomingEdges`.
|
|
17
14
|
*/
|
|
18
15
|
export declare const bidirectional: RoutingAlgorithmFn;
|
|
19
16
|
//# sourceMappingURL=bidirectional.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bidirectional.d.ts","sourceRoot":"","sources":["../../src/algorithms/bidirectional.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"bidirectional.d.ts","sourceRoot":"","sources":["../../src/algorithms/bidirectional.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAA0B,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAQ9E;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,kBA+G3B,CAAC"}
|
|
@@ -1,128 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bidirectional
|
|
2
|
+
* Bidirectional Dijkstra.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Runs Dijkstra forward from the start over outgoing edges and backward from
|
|
5
|
+
* the end over incoming edges, and stops when neither search can improve the
|
|
6
|
+
* best meeting point. Returns an optimal path and respects one-way edges.
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
+
import { BinaryHeap } from "../binary-heap.js";
|
|
10
11
|
/**
|
|
11
|
-
* Bidirectional
|
|
12
|
-
*
|
|
13
|
-
* Uses breadth-first search from both start and end, terminating when
|
|
14
|
-
* the frontiers meet. Very fast for finding any path, though not always
|
|
15
|
-
* the optimal shortest path. Use Dijkstra or A* if optimality is required.
|
|
12
|
+
* Bidirectional Dijkstra. Requires `context.reverseGraph`, which `Router`
|
|
13
|
+
* supplies from `RoutingGraph.getIncomingEdges`.
|
|
16
14
|
*/
|
|
17
|
-
export const bidirectional = (graph, start, end, getWeight) => {
|
|
15
|
+
export const bidirectional = (graph, start, end, getWeight, _getCoord, _metric, context) => {
|
|
18
16
|
if (start === end)
|
|
19
17
|
return [{ nodeIndex: start, cost: 0 }];
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const reverseGraph = context?.reverseGraph;
|
|
19
|
+
if (!reverseGraph) {
|
|
20
|
+
throw Error("Bidirectional routing needs context.reverseGraph (incoming edges). " +
|
|
21
|
+
"Router passes RoutingGraph.getIncomingEdges.");
|
|
22
|
+
}
|
|
23
|
+
const fDist = new Map([[start, 0]]);
|
|
24
|
+
const bDist = new Map([[end, 0]]);
|
|
22
25
|
const fPrev = new Map();
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
const bNext = new Map();
|
|
27
|
+
const fHeap = new BinaryHeap();
|
|
28
|
+
const bHeap = new BinaryHeap();
|
|
29
|
+
const fClosed = new Set();
|
|
30
|
+
const bClosed = new Set();
|
|
31
|
+
fHeap.push(start, 0);
|
|
32
|
+
bHeap.push(end, 0);
|
|
33
|
+
let best = Number.POSITIVE_INFINITY;
|
|
34
|
+
let meet = -1;
|
|
35
|
+
const relax = (current, edges, dist, otherDist, closed, heap, record) => {
|
|
36
|
+
const currentD = dist.get(current);
|
|
37
|
+
for (const edge of edges) {
|
|
38
|
+
const neighbor = edge.targetNodeIndex;
|
|
39
|
+
if (closed.has(neighbor))
|
|
40
|
+
continue;
|
|
41
|
+
const cost = currentD + getWeight(edge);
|
|
42
|
+
const existing = dist.get(neighbor);
|
|
43
|
+
if (existing !== undefined && cost >= existing)
|
|
44
|
+
continue;
|
|
45
|
+
dist.set(neighbor, cost);
|
|
46
|
+
record(neighbor, edge, cost);
|
|
47
|
+
heap.push(neighbor, cost);
|
|
48
|
+
const other = otherDist.get(neighbor);
|
|
49
|
+
if (other !== undefined && cost + other < best) {
|
|
50
|
+
best = cost + other;
|
|
51
|
+
meet = neighbor;
|
|
40
52
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
while (fHeap.size > 0 && bHeap.size > 0) {
|
|
56
|
+
// Stop once no unsettled node can lie on a path shorter than `best`.
|
|
57
|
+
if (fHeap.peekPriority() + bHeap.peekPriority() >= best)
|
|
58
|
+
break;
|
|
59
|
+
if (fHeap.peekPriority() <= bHeap.peekPriority()) {
|
|
60
|
+
const current = fHeap.pop();
|
|
61
|
+
if (fClosed.has(current))
|
|
62
|
+
continue;
|
|
63
|
+
fClosed.add(current);
|
|
64
|
+
relax(current, graph(current), fDist, bDist, fClosed, fHeap, (neighbor, edge, cost) => {
|
|
47
65
|
fPrev.set(neighbor, {
|
|
48
66
|
nodeIndex: neighbor,
|
|
49
67
|
wayIndex: edge.wayIndex,
|
|
50
68
|
previousNodeIndex: current,
|
|
51
|
-
cost
|
|
69
|
+
cost,
|
|
52
70
|
});
|
|
53
|
-
|
|
54
|
-
if (bDist.has(neighbor)) {
|
|
55
|
-
meetNode = neighbor;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (meetNode !== null)
|
|
60
|
-
break;
|
|
71
|
+
});
|
|
61
72
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
for (const edge of graph(current)) {
|
|
72
|
-
const neighbor = edge.targetNodeIndex;
|
|
73
|
-
if (bDist.has(neighbor))
|
|
74
|
-
continue;
|
|
75
|
-
const newD = currentD + getWeight(edge);
|
|
76
|
-
bDist.set(neighbor, newD);
|
|
77
|
-
bPrev.set(neighbor, {
|
|
78
|
-
nodeIndex: neighbor,
|
|
79
|
-
wayIndex: edge.wayIndex,
|
|
80
|
-
previousNodeIndex: current,
|
|
81
|
-
cost: newD,
|
|
82
|
-
});
|
|
83
|
-
bQueue.push(neighbor);
|
|
84
|
-
if (fDist.has(neighbor)) {
|
|
85
|
-
meetNode = neighbor;
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (meetNode !== null)
|
|
90
|
-
break;
|
|
73
|
+
else {
|
|
74
|
+
const current = bHeap.pop();
|
|
75
|
+
if (bClosed.has(current))
|
|
76
|
+
continue;
|
|
77
|
+
bClosed.add(current);
|
|
78
|
+
relax(current, reverseGraph(current), bDist, fDist, bClosed, bHeap, (neighbor, edge) => {
|
|
79
|
+
bNext.set(neighbor, { nextNodeIndex: current, wayIndex: edge.wayIndex });
|
|
80
|
+
});
|
|
91
81
|
}
|
|
92
82
|
}
|
|
93
|
-
if (
|
|
83
|
+
if (meet === -1)
|
|
94
84
|
return null;
|
|
95
|
-
//
|
|
96
|
-
const
|
|
97
|
-
let
|
|
98
|
-
while (
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
forward.unshift(seg);
|
|
103
|
-
curr = seg.previousNodeIndex;
|
|
85
|
+
// Forward half: start → meet.
|
|
86
|
+
const path = [];
|
|
87
|
+
let node = meet;
|
|
88
|
+
while (node !== start) {
|
|
89
|
+
const segment = fPrev.get(node);
|
|
90
|
+
path.unshift(segment);
|
|
91
|
+
node = segment.previousNodeIndex;
|
|
104
92
|
}
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (curr !== undefined) {
|
|
118
|
-
forward.push({
|
|
119
|
-
nodeIndex: curr,
|
|
120
|
-
wayIndex: seg.wayIndex,
|
|
121
|
-
previousNodeIndex: seg.nodeIndex,
|
|
122
|
-
cost: meetForwardCost + meetBackwardCost - (bDist.get(curr) ?? 0),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
93
|
+
path.unshift({ nodeIndex: start, cost: 0 });
|
|
94
|
+
// Backward half: meet → end, with costs measured from the start.
|
|
95
|
+
node = meet;
|
|
96
|
+
while (node !== end) {
|
|
97
|
+
const step = bNext.get(node);
|
|
98
|
+
path.push({
|
|
99
|
+
nodeIndex: step.nextNodeIndex,
|
|
100
|
+
wayIndex: step.wayIndex,
|
|
101
|
+
previousNodeIndex: node,
|
|
102
|
+
cost: best - bDist.get(step.nextNodeIndex),
|
|
103
|
+
});
|
|
104
|
+
node = step.nextNodeIndex;
|
|
125
105
|
}
|
|
126
|
-
return
|
|
106
|
+
return path;
|
|
127
107
|
};
|
|
128
108
|
//# sourceMappingURL=bidirectional.js.map
|