@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +138 -53
  4. package/dist/algorithms/bidirectional.d.ts +6 -9
  5. package/dist/algorithms/bidirectional.d.ts.map +1 -1
  6. package/dist/algorithms/bidirectional.js +84 -104
  7. package/dist/algorithms/bidirectional.js.map +1 -1
  8. package/dist/algorithms/index.d.ts +2 -2
  9. package/dist/algorithms/index.d.ts.map +1 -1
  10. package/dist/algorithms/index.js +1 -1
  11. package/dist/algorithms/index.js.map +1 -1
  12. package/dist/algorithms/shortest-path.d.ts +2 -1
  13. package/dist/algorithms/shortest-path.d.ts.map +1 -1
  14. package/dist/algorithms/shortest-path.js +9 -7
  15. package/dist/algorithms/shortest-path.js.map +1 -1
  16. package/dist/binary-heap.d.ts +5 -0
  17. package/dist/binary-heap.d.ts.map +1 -1
  18. package/dist/binary-heap.js +11 -4
  19. package/dist/binary-heap.js.map +1 -1
  20. package/dist/graph.d.ts +35 -7
  21. package/dist/graph.d.ts.map +1 -1
  22. package/dist/graph.js +161 -119
  23. package/dist/graph.js.map +1 -1
  24. package/dist/index.d.ts +9 -4
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/router.d.ts +1 -1
  29. package/dist/router.d.ts.map +1 -1
  30. package/dist/router.js +6 -3
  31. package/dist/router.js.map +1 -1
  32. package/dist/topology-stats.d.ts +37 -0
  33. package/dist/topology-stats.d.ts.map +1 -0
  34. package/dist/topology-stats.js +62 -0
  35. package/dist/topology-stats.js.map +1 -0
  36. package/dist/types.d.ts +15 -2
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/utils.d.ts +1 -1
  39. package/dist/utils.d.ts.map +1 -1
  40. package/dist/utils.js +1 -7
  41. package/dist/utils.js.map +1 -1
  42. package/package.json +32 -29
  43. package/src/algorithms/bidirectional.ts +124 -136
  44. package/src/algorithms/index.ts +10 -10
  45. package/src/algorithms/shortest-path.ts +96 -92
  46. package/src/binary-heap.ts +133 -129
  47. package/src/graph.ts +389 -355
  48. package/src/index.ts +14 -9
  49. package/src/router.ts +221 -233
  50. package/src/topology-stats.ts +84 -0
  51. package/src/types.ts +102 -87
  52. package/src/utils.ts +67 -74
  53. package/test/algorithms.test.ts +396 -353
  54. package/test/binary-heap.test.ts +27 -0
  55. package/test/limits.test.ts +82 -0
  56. package/test/no-shared-array-buffer.test.ts +37 -0
  57. package/test/router.bench.ts +171 -188
  58. package/test/router.test.ts +685 -726
  59. package/test/spatial-capability.test.ts +23 -0
  60. package/test/topology-stats.test.ts +82 -0
  61. package/tsconfig.build.json +6 -3
  62. package/tsconfig.json +7 -7
@@ -7,15 +7,16 @@
7
7
  * @module
8
8
  */
9
9
 
10
- import { haversineDistance } from "@osmix/shared/haversine-distance"
11
- import { BinaryHeap } from "../binary-heap.ts"
12
- import type { GraphEdge, PathSegment, RoutingAlgorithmFn } from "../types.ts"
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
- * Maximum speed (m/s) for time-based heuristic.
16
- * Uses 130 km/h (~36.1 m/s) to ensure admissibility on all road types.
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 MAX_SPEED_MS = (130 * 1000) / 3600
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
- graph: (nodeIndex: number) => GraphEdge[],
28
- start: number,
29
- end: number,
30
- getWeight: (edge: GraphEdge) => number,
31
- heuristic: (nodeIndex: number) => number,
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
- // Same node - trivial path
34
- if (start === end) return [{ nodeIndex: start, cost: 0 }]
34
+ // Same node - trivial path
35
+ if (start === end) return [{ nodeIndex: start, cost: 0 }];
35
36
 
36
- const gScore = new Map<number, number>() // Best known cost to reach node
37
- const previous = new Map<number, PathSegment>() // How we reached each node
38
- const closed = new Set<number>() // Fully processed nodes
39
- const heap = new BinaryHeap()
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
- gScore.set(start, 0)
42
- heap.push(start, heuristic(start))
42
+ gScore.set(start, 0);
43
+ heap.push(start, heuristic(start));
43
44
 
44
- while (heap.size > 0) {
45
- const current = heap.pop()!
45
+ while (heap.size > 0) {
46
+ const current = heap.pop()!;
46
47
 
47
- // Reached destination
48
- if (current === end) return reconstructPath(previous, start, end)
48
+ // Reached destination
49
+ if (current === end) return reconstructPath(previous, start, end);
49
50
 
50
- // Already seen this node
51
- if (closed.has(current)) continue
52
- closed.add(current)
51
+ // Already seen this node
52
+ if (closed.has(current)) continue;
53
+ closed.add(current);
53
54
 
54
- const currentG = gScore.get(current)!
55
+ const currentG = gScore.get(current)!;
55
56
 
56
- for (const edge of graph(current)) {
57
- const neighbor = edge.targetNodeIndex
57
+ for (const edge of graph(current)) {
58
+ const neighbor = edge.targetNodeIndex;
58
59
 
59
- if (closed.has(neighbor)) continue
60
+ if (closed.has(neighbor)) continue;
60
61
 
61
- const tentativeG = currentG + getWeight(edge)
62
- const existingG = gScore.get(neighbor)
62
+ const tentativeG = currentG + getWeight(edge);
63
+ const existingG = gScore.get(neighbor);
63
64
 
64
- if (existingG === undefined || tentativeG < existingG) {
65
- gScore.set(neighbor, tentativeG)
66
- previous.set(neighbor, {
67
- nodeIndex: neighbor,
68
- wayIndex: edge.wayIndex,
69
- previousNodeIndex: current,
70
- cost: tentativeG,
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
- const f = tentativeG + heuristic(neighbor)
74
- heap.push(neighbor, f)
75
- }
76
- }
77
- }
74
+ const f = tentativeG + heuristic(neighbor);
75
+ heap.push(neighbor, f);
76
+ }
77
+ }
78
+ }
78
79
 
79
- return null
80
+ return null;
80
81
  }
81
82
 
82
83
  /**
83
84
  * Reconstruct path from predecessor map.
84
85
  */
85
86
  function reconstructPath(
86
- previous: Map<number, PathSegment>,
87
- start: number,
88
- end: number,
87
+ previous: Map<number, PathSegment>,
88
+ start: number,
89
+ end: number,
89
90
  ): PathSegment[] {
90
- const path: PathSegment[] = []
91
- let current: number | undefined = end
92
-
93
- while (current !== undefined) {
94
- const segment = previous.get(current)
95
- if (!segment) {
96
- path.unshift({ nodeIndex: current, cost: 0 })
97
- break
98
- }
99
- path.unshift(segment)
100
- current = segment.previousNodeIndex
101
- }
102
-
103
- if (path.length === 0 || path[0]?.nodeIndex !== start) {
104
- path.unshift({ nodeIndex: start, cost: 0 })
105
- }
106
-
107
- return path
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
- return shortestPath(graph, start, end, getWeight, () => 0)
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 / max speed (lower bound on travel time in seconds)
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
- graph,
128
- start,
129
- end,
130
- getWeight,
131
- getCoord,
132
- metric,
129
+ graph,
130
+ start,
131
+ end,
132
+ getWeight,
133
+ getCoord,
134
+ metric,
135
+ context,
133
136
  ) => {
134
- if (!getCoord) return null
135
-
136
- const endCoord = getCoord(end)
137
- if (!endCoord) return null
138
-
139
- // Haversine heuristic scaled by metric
140
- // For time: divide by max speed to get admissible time estimate
141
- const heuristic = (nodeIndex: number): number => {
142
- const coord = getCoord(nodeIndex)
143
- if (!coord) return 0
144
- const distance = haversineDistance(endCoord, coord)
145
- return metric === "time" ? distance / MAX_SPEED_MS : distance
146
- }
147
-
148
- return shortestPath(graph, start, end, getWeight, heuristic)
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
+ };
@@ -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
- private heap: number[] = []
19
- private priorities: number[] = []
20
- private positions: Map<number, number> = new Map()
21
-
22
- get size(): number {
23
- return this.heap.length
24
- }
25
-
26
- /**
27
- * Add an item with given priority. If item exists, updates priority if lower.
28
- */
29
- push(item: number, priority: number): void {
30
- const existing = this.positions.get(item)
31
- if (existing !== undefined) {
32
- if (priority < this.priorities[existing]!) {
33
- this.priorities[existing] = priority
34
- this.bubbleUp(existing)
35
- }
36
- return
37
- }
38
-
39
- const index = this.heap.length
40
- this.heap.push(item)
41
- this.priorities.push(priority)
42
- this.positions.set(item, index)
43
- this.bubbleUp(index)
44
- }
45
-
46
- /**
47
- * Remove and return the minimum-priority item.
48
- */
49
- pop(): number | undefined {
50
- if (this.heap.length === 0) return undefined
51
-
52
- const min = this.heap[0]!
53
- this.positions.delete(min)
54
-
55
- const last = this.heap.pop()!
56
- const lastPriority = this.priorities.pop()!
57
-
58
- if (this.heap.length > 0) {
59
- this.heap[0] = last
60
- this.priorities[0] = lastPriority
61
- this.positions.set(last, 0)
62
- this.bubbleDown(0)
63
- }
64
-
65
- return min
66
- }
67
-
68
- /**
69
- * Check if item is in the heap.
70
- */
71
- has(item: number): boolean {
72
- return this.positions.has(item)
73
- }
74
-
75
- /**
76
- * Clear all items.
77
- */
78
- clear(): void {
79
- this.heap.length = 0
80
- this.priorities.length = 0
81
- this.positions.clear()
82
- }
83
-
84
- private bubbleUp(startIndex: number): void {
85
- const item = this.heap[startIndex]!
86
- const priority = this.priorities[startIndex]!
87
- let index = startIndex
88
-
89
- while (index > 0) {
90
- const parentIndex = (index - 1) >> 1
91
- const parentPriority = this.priorities[parentIndex]!
92
-
93
- if (priority >= parentPriority) break
94
-
95
- // Swap with parent
96
- this.heap[index] = this.heap[parentIndex]!
97
- this.priorities[index] = parentPriority
98
- this.positions.set(this.heap[index]!, index)
99
-
100
- index = parentIndex
101
- }
102
-
103
- this.heap[index] = item
104
- this.priorities[index] = priority
105
- this.positions.set(item, index)
106
- }
107
-
108
- private bubbleDown(startIndex: number): void {
109
- const length = this.heap.length
110
- const item = this.heap[startIndex]!
111
- const priority = this.priorities[startIndex]!
112
- let index = startIndex
113
-
114
- while (true) {
115
- const leftIndex = (index << 1) + 1
116
- const rightIndex = leftIndex + 1
117
- let smallest = index
118
-
119
- if (
120
- leftIndex < length &&
121
- this.priorities[leftIndex]! < this.priorities[smallest]!
122
- ) {
123
- smallest = leftIndex
124
- }
125
-
126
- if (
127
- rightIndex < length &&
128
- this.priorities[rightIndex]! < this.priorities[smallest]!
129
- ) {
130
- smallest = rightIndex
131
- }
132
-
133
- if (smallest === index) break
134
-
135
- // Swap with smallest child
136
- this.heap[index] = this.heap[smallest]!
137
- this.priorities[index] = this.priorities[smallest]!
138
- this.positions.set(this.heap[index]!, index)
139
-
140
- index = smallest
141
- }
142
-
143
- this.heap[index] = item
144
- this.priorities[index] = priority
145
- this.positions.set(item, index)
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
  }