@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
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { haversineDistance } from "@osmix/
|
|
11
|
-
|
|
12
|
-
import
|
|
10
|
+
import { haversineDistance } from "@osmix/geo/haversine-distance";
|
|
11
|
+
|
|
12
|
+
import { BinaryHeap } from "../binary-heap.ts";
|
|
13
|
+
import type { GraphEdge, PathSegment, RoutingAlgorithmFn } from "../types.ts";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* Default maximum speed (m/s) for the time heuristic when the caller does not
|
|
17
|
+
* pass the graph's real maximum: 130 km/h (~36.1 m/s).
|
|
17
18
|
*/
|
|
18
|
-
const
|
|
19
|
+
const DEFAULT_MAX_SPEED_MPS = (130 * 1000) / 3600;
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* A* shortest path algorithm with configurable heuristic.
|
|
@@ -24,87 +25,87 @@ const MAX_SPEED_MS = (130 * 1000) / 3600
|
|
|
24
25
|
* With an admissible heuristic (never overestimates), guarantees optimal paths.
|
|
25
26
|
*/
|
|
26
27
|
function shortestPath(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
graph: (nodeIndex: number) => GraphEdge[],
|
|
29
|
+
start: number,
|
|
30
|
+
end: number,
|
|
31
|
+
getWeight: (edge: GraphEdge) => number,
|
|
32
|
+
heuristic: (nodeIndex: number) => number,
|
|
32
33
|
): PathSegment[] | null {
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// Same node - trivial path
|
|
35
|
+
if (start === end) return [{ nodeIndex: start, cost: 0 }];
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const gScore = new Map<number, number>(); // Best known cost to reach node
|
|
38
|
+
const previous = new Map<number, PathSegment>(); // How we reached each node
|
|
39
|
+
const closed = new Set<number>(); // Fully processed nodes
|
|
40
|
+
const heap = new BinaryHeap();
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
gScore.set(start, 0);
|
|
43
|
+
heap.push(start, heuristic(start));
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
while (heap.size > 0) {
|
|
46
|
+
const current = heap.pop()!;
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Reached destination
|
|
49
|
+
if (current === end) return reconstructPath(previous, start, end);
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Already seen this node
|
|
52
|
+
if (closed.has(current)) continue;
|
|
53
|
+
closed.add(current);
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
const currentG = gScore.get(current)!;
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
for (const edge of graph(current)) {
|
|
58
|
+
const neighbor = edge.targetNodeIndex;
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
if (closed.has(neighbor)) continue;
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
const tentativeG = currentG + getWeight(edge);
|
|
63
|
+
const existingG = gScore.get(neighbor);
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
if (existingG === undefined || tentativeG < existingG) {
|
|
66
|
+
gScore.set(neighbor, tentativeG);
|
|
67
|
+
previous.set(neighbor, {
|
|
68
|
+
nodeIndex: neighbor,
|
|
69
|
+
wayIndex: edge.wayIndex,
|
|
70
|
+
previousNodeIndex: current,
|
|
71
|
+
cost: tentativeG,
|
|
72
|
+
});
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
const f = tentativeG + heuristic(neighbor);
|
|
75
|
+
heap.push(neighbor, f);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
return null;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
84
|
* Reconstruct path from predecessor map.
|
|
84
85
|
*/
|
|
85
86
|
function reconstructPath(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
previous: Map<number, PathSegment>,
|
|
88
|
+
start: number,
|
|
89
|
+
end: number,
|
|
89
90
|
): PathSegment[] {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
const path: PathSegment[] = [];
|
|
92
|
+
let current: number | undefined = end;
|
|
93
|
+
|
|
94
|
+
while (current !== undefined) {
|
|
95
|
+
const segment = previous.get(current);
|
|
96
|
+
if (!segment) {
|
|
97
|
+
path.unshift({ nodeIndex: current, cost: 0 });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
path.unshift(segment);
|
|
101
|
+
current = segment.previousNodeIndex;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (path.length === 0 || path[0]?.nodeIndex !== start) {
|
|
105
|
+
path.unshift({ nodeIndex: start, cost: 0 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return path;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/**
|
|
@@ -112,8 +113,8 @@ function reconstructPath(
|
|
|
112
113
|
* Explores nodes in order of increasing distance from start.
|
|
113
114
|
*/
|
|
114
115
|
export const dijkstra: RoutingAlgorithmFn = (graph, start, end, getWeight) => {
|
|
115
|
-
|
|
116
|
-
}
|
|
116
|
+
return shortestPath(graph, start, end, getWeight, () => 0);
|
|
117
|
+
};
|
|
117
118
|
|
|
118
119
|
/**
|
|
119
120
|
* A* algorithm - uses heuristic to guide search toward destination.
|
|
@@ -121,29 +122,32 @@ export const dijkstra: RoutingAlgorithmFn = (graph, start, end, getWeight) => {
|
|
|
121
122
|
*
|
|
122
123
|
* The heuristic is adapted based on the metric:
|
|
123
124
|
* - distance: haversine distance in meters
|
|
124
|
-
* - time: haversine distance /
|
|
125
|
+
* - time: haversine distance / `context.maxSpeedMps` (lower bound on travel time in seconds).
|
|
126
|
+
* Pass the graph's real maximum; the 130 km/h default overestimates on faster roads.
|
|
125
127
|
*/
|
|
126
128
|
export const astar: RoutingAlgorithmFn = (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
graph,
|
|
130
|
+
start,
|
|
131
|
+
end,
|
|
132
|
+
getWeight,
|
|
133
|
+
getCoord,
|
|
134
|
+
metric,
|
|
135
|
+
context,
|
|
133
136
|
) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
137
|
+
if (!getCoord) return null;
|
|
138
|
+
const maxSpeedMps = context?.maxSpeedMps ?? DEFAULT_MAX_SPEED_MPS;
|
|
139
|
+
|
|
140
|
+
const endCoord = getCoord(end);
|
|
141
|
+
if (!endCoord) return null;
|
|
142
|
+
|
|
143
|
+
// Haversine heuristic scaled by metric
|
|
144
|
+
// For time: divide by max speed to get admissible time estimate
|
|
145
|
+
const heuristic = (nodeIndex: number): number => {
|
|
146
|
+
const coord = getCoord(nodeIndex);
|
|
147
|
+
if (!coord) return 0;
|
|
148
|
+
const distance = haversineDistance(endCoord, coord);
|
|
149
|
+
return metric === "time" ? distance / maxSpeedMps : distance;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return shortestPath(graph, start, end, getWeight, heuristic);
|
|
153
|
+
};
|
package/src/binary-heap.ts
CHANGED
|
@@ -12,136 +12,140 @@
|
|
|
12
12
|
* Operations:
|
|
13
13
|
* - `push(item, priority)`: O(log n) - adds or updates item priority.
|
|
14
14
|
* - `pop()`: O(log n) - removes and returns minimum priority item.
|
|
15
|
+
* - `peekPriority()`: O(1) - priority of the minimum item.
|
|
15
16
|
* - `has(item)`: O(1) - checks if item is in heap.
|
|
16
17
|
*/
|
|
17
18
|
export class BinaryHeap {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
19
|
+
private heap: number[] = [];
|
|
20
|
+
private priorities: number[] = [];
|
|
21
|
+
private positions: Map<number, number> = new Map();
|
|
22
|
+
|
|
23
|
+
get size(): number {
|
|
24
|
+
return this.heap.length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add an item with given priority. If item exists, updates priority if lower.
|
|
29
|
+
*/
|
|
30
|
+
push(item: number, priority: number): void {
|
|
31
|
+
const existing = this.positions.get(item);
|
|
32
|
+
if (existing !== undefined) {
|
|
33
|
+
if (priority < this.priorities[existing]!) {
|
|
34
|
+
this.priorities[existing] = priority;
|
|
35
|
+
this.bubbleUp(existing);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const index = this.heap.length;
|
|
41
|
+
this.heap.push(item);
|
|
42
|
+
this.priorities.push(priority);
|
|
43
|
+
this.positions.set(item, index);
|
|
44
|
+
this.bubbleUp(index);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Remove and return the minimum-priority item.
|
|
49
|
+
*/
|
|
50
|
+
pop(): number | undefined {
|
|
51
|
+
if (this.heap.length === 0) return undefined;
|
|
52
|
+
|
|
53
|
+
const min = this.heap[0]!;
|
|
54
|
+
this.positions.delete(min);
|
|
55
|
+
|
|
56
|
+
const last = this.heap.pop()!;
|
|
57
|
+
const lastPriority = this.priorities.pop()!;
|
|
58
|
+
|
|
59
|
+
if (this.heap.length > 0) {
|
|
60
|
+
this.heap[0] = last;
|
|
61
|
+
this.priorities[0] = lastPriority;
|
|
62
|
+
this.positions.set(last, 0);
|
|
63
|
+
this.bubbleDown(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return min;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Priority of the minimum item, or `Infinity` when the heap is empty.
|
|
71
|
+
*/
|
|
72
|
+
peekPriority(): number {
|
|
73
|
+
return this.priorities[0] ?? Number.POSITIVE_INFINITY;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if item is in the heap.
|
|
78
|
+
*/
|
|
79
|
+
has(item: number): boolean {
|
|
80
|
+
return this.positions.has(item);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clear all items.
|
|
85
|
+
*/
|
|
86
|
+
clear(): void {
|
|
87
|
+
this.heap.length = 0;
|
|
88
|
+
this.priorities.length = 0;
|
|
89
|
+
this.positions.clear();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private bubbleUp(startIndex: number): void {
|
|
93
|
+
const item = this.heap[startIndex]!;
|
|
94
|
+
const priority = this.priorities[startIndex]!;
|
|
95
|
+
let index = startIndex;
|
|
96
|
+
|
|
97
|
+
while (index > 0) {
|
|
98
|
+
const parentIndex = (index - 1) >> 1;
|
|
99
|
+
const parentPriority = this.priorities[parentIndex]!;
|
|
100
|
+
|
|
101
|
+
if (priority >= parentPriority) break;
|
|
102
|
+
|
|
103
|
+
// Swap with parent
|
|
104
|
+
this.heap[index] = this.heap[parentIndex]!;
|
|
105
|
+
this.priorities[index] = parentPriority;
|
|
106
|
+
this.positions.set(this.heap[index]!, index);
|
|
107
|
+
|
|
108
|
+
index = parentIndex;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.heap[index] = item;
|
|
112
|
+
this.priorities[index] = priority;
|
|
113
|
+
this.positions.set(item, index);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private bubbleDown(startIndex: number): void {
|
|
117
|
+
const length = this.heap.length;
|
|
118
|
+
const item = this.heap[startIndex]!;
|
|
119
|
+
const priority = this.priorities[startIndex]!;
|
|
120
|
+
let index = startIndex;
|
|
121
|
+
|
|
122
|
+
while (true) {
|
|
123
|
+
const leftIndex = (index << 1) + 1;
|
|
124
|
+
const rightIndex = leftIndex + 1;
|
|
125
|
+
let smallest = index;
|
|
126
|
+
let smallestPriority = priority;
|
|
127
|
+
|
|
128
|
+
if (leftIndex < length && this.priorities[leftIndex]! < smallestPriority) {
|
|
129
|
+
smallest = leftIndex;
|
|
130
|
+
smallestPriority = this.priorities[leftIndex]!;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (rightIndex < length && this.priorities[rightIndex]! < smallestPriority) {
|
|
134
|
+
smallest = rightIndex;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (smallest === index) break;
|
|
138
|
+
|
|
139
|
+
// Swap with smallest child
|
|
140
|
+
this.heap[index] = this.heap[smallest]!;
|
|
141
|
+
this.priorities[index] = this.priorities[smallest]!;
|
|
142
|
+
this.positions.set(this.heap[index]!, index);
|
|
143
|
+
|
|
144
|
+
index = smallest;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.heap[index] = item;
|
|
148
|
+
this.priorities[index] = priority;
|
|
149
|
+
this.positions.set(item, index);
|
|
150
|
+
}
|
|
147
151
|
}
|