@osmix/router 0.0.2

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 (64) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +149 -0
  3. package/dist/src/algorithms/bidirectional.d.ts +19 -0
  4. package/dist/src/algorithms/bidirectional.d.ts.map +1 -0
  5. package/dist/src/algorithms/bidirectional.js +128 -0
  6. package/dist/src/algorithms/bidirectional.js.map +1 -0
  7. package/dist/src/algorithms/index.d.ts +17 -0
  8. package/dist/src/algorithms/index.d.ts.map +1 -0
  9. package/dist/src/algorithms/index.js +19 -0
  10. package/dist/src/algorithms/index.js.map +1 -0
  11. package/dist/src/algorithms/shortest-path.d.ts +24 -0
  12. package/dist/src/algorithms/shortest-path.d.ts.map +1 -0
  13. package/dist/src/algorithms/shortest-path.js +115 -0
  14. package/dist/src/algorithms/shortest-path.js.map +1 -0
  15. package/dist/src/binary-heap.d.ts +40 -0
  16. package/dist/src/binary-heap.d.ts.map +1 -0
  17. package/dist/src/binary-heap.js +122 -0
  18. package/dist/src/binary-heap.js.map +1 -0
  19. package/dist/src/graph.d.ts +135 -0
  20. package/dist/src/graph.d.ts.map +1 -0
  21. package/dist/src/graph.js +339 -0
  22. package/dist/src/graph.js.map +1 -0
  23. package/dist/src/index.d.ts +41 -0
  24. package/dist/src/index.d.ts.map +1 -0
  25. package/dist/src/index.js +41 -0
  26. package/dist/src/index.js.map +1 -0
  27. package/dist/src/router.d.ts +55 -0
  28. package/dist/src/router.d.ts.map +1 -0
  29. package/dist/src/router.js +189 -0
  30. package/dist/src/router.js.map +1 -0
  31. package/dist/src/types.d.ts +118 -0
  32. package/dist/src/types.d.ts.map +1 -0
  33. package/dist/src/types.js +5 -0
  34. package/dist/src/types.js.map +1 -0
  35. package/dist/src/utils.d.ts +20 -0
  36. package/dist/src/utils.d.ts.map +1 -0
  37. package/dist/src/utils.js +101 -0
  38. package/dist/src/utils.js.map +1 -0
  39. package/dist/test/algorithms.test.d.ts +2 -0
  40. package/dist/test/algorithms.test.d.ts.map +1 -0
  41. package/dist/test/algorithms.test.js +382 -0
  42. package/dist/test/algorithms.test.js.map +1 -0
  43. package/dist/test/router.bench.d.ts +2 -0
  44. package/dist/test/router.bench.d.ts.map +1 -0
  45. package/dist/test/router.bench.js +191 -0
  46. package/dist/test/router.bench.js.map +1 -0
  47. package/dist/test/router.test.d.ts +2 -0
  48. package/dist/test/router.test.d.ts.map +1 -0
  49. package/dist/test/router.test.js +515 -0
  50. package/dist/test/router.test.js.map +1 -0
  51. package/package.json +51 -0
  52. package/src/algorithms/bidirectional.ts +147 -0
  53. package/src/algorithms/index.ts +23 -0
  54. package/src/algorithms/shortest-path.ts +149 -0
  55. package/src/binary-heap.ts +147 -0
  56. package/src/graph.ts +414 -0
  57. package/src/index.ts +41 -0
  58. package/src/router.ts +262 -0
  59. package/src/types.ts +138 -0
  60. package/src/utils.ts +114 -0
  61. package/test/algorithms.test.ts +466 -0
  62. package/test/router.bench.ts +257 -0
  63. package/test/router.test.ts +738 -0
  64. package/tsconfig.json +9 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Bidirectional breadth-first search algorithm.
3
+ *
4
+ * Searches from both start and end simultaneously, terminating when the
5
+ * frontiers meet. Very fast for finding any path, but does not guarantee
6
+ * the shortest path. Use Dijkstra or A* when optimality is required.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { PathSegment, RoutingAlgorithmFn } from "../types"
12
+
13
+ /**
14
+ * Bidirectional BFS - fast path finding from both ends.
15
+ *
16
+ * Uses breadth-first search from both start and end, terminating when
17
+ * the frontiers meet. Very fast for finding any path, though not always
18
+ * the optimal shortest path. Use Dijkstra or A* if optimality is required.
19
+ */
20
+ export const bidirectional: RoutingAlgorithmFn = (
21
+ graph,
22
+ start,
23
+ end,
24
+ getWeight,
25
+ ) => {
26
+ if (start === end) return [{ nodeIndex: start, cost: 0 }]
27
+
28
+ // Forward search state
29
+ const fDist = new Map<number, number>()
30
+ const fPrev = new Map<number, PathSegment>()
31
+ const fQueue: number[] = [start]
32
+
33
+ // Backward search state
34
+ const bDist = new Map<number, number>()
35
+ const bPrev = new Map<number, PathSegment>()
36
+ const bQueue: number[] = [end]
37
+
38
+ fDist.set(start, 0)
39
+ bDist.set(end, 0)
40
+
41
+ let meetNode: number | null = null
42
+
43
+ while (fQueue.length > 0 && bQueue.length > 0) {
44
+ // Expand forward
45
+ if (fQueue.length > 0) {
46
+ const current = fQueue.shift()!
47
+ const currentD = fDist.get(current)!
48
+
49
+ // Check if backward search reached this node
50
+ if (bDist.has(current)) {
51
+ meetNode = current
52
+ break
53
+ }
54
+
55
+ for (const edge of graph(current)) {
56
+ const neighbor = edge.targetNodeIndex
57
+ if (fDist.has(neighbor)) continue
58
+
59
+ const newD = currentD + getWeight(edge)
60
+ fDist.set(neighbor, newD)
61
+ fPrev.set(neighbor, {
62
+ nodeIndex: neighbor,
63
+ wayIndex: edge.wayIndex,
64
+ previousNodeIndex: current,
65
+ cost: newD,
66
+ })
67
+ fQueue.push(neighbor)
68
+
69
+ if (bDist.has(neighbor)) {
70
+ meetNode = neighbor
71
+ break
72
+ }
73
+ }
74
+
75
+ if (meetNode !== null) break
76
+ }
77
+
78
+ // Expand backward
79
+ if (bQueue.length > 0) {
80
+ const current = bQueue.shift()!
81
+ const currentD = bDist.get(current)!
82
+
83
+ // Check if forward search reached this node
84
+ if (fDist.has(current)) {
85
+ meetNode = current
86
+ break
87
+ }
88
+
89
+ for (const edge of graph(current)) {
90
+ const neighbor = edge.targetNodeIndex
91
+ if (bDist.has(neighbor)) continue
92
+
93
+ const newD = currentD + getWeight(edge)
94
+ bDist.set(neighbor, newD)
95
+ bPrev.set(neighbor, {
96
+ nodeIndex: neighbor,
97
+ wayIndex: edge.wayIndex,
98
+ previousNodeIndex: current,
99
+ cost: newD,
100
+ })
101
+ bQueue.push(neighbor)
102
+
103
+ if (fDist.has(neighbor)) {
104
+ meetNode = neighbor
105
+ break
106
+ }
107
+ }
108
+
109
+ if (meetNode !== null) break
110
+ }
111
+ }
112
+
113
+ if (meetNode === null) return null
114
+
115
+ // Build forward path: start -> meet
116
+ const forward: PathSegment[] = []
117
+ let curr: number | undefined = meetNode
118
+ while (curr !== undefined && curr !== start) {
119
+ const seg = fPrev.get(curr)
120
+ if (!seg) break
121
+ forward.unshift(seg)
122
+ curr = seg.previousNodeIndex
123
+ }
124
+ forward.unshift({ nodeIndex: start, cost: 0 })
125
+
126
+ // Build backward path: meet -> end (reverse direction)
127
+ // bDist represents distance FROM end TO node, so it decreases as we approach end.
128
+ // Correct cost = forward cost to meet + (backward dist at meet - backward dist at curr)
129
+ const meetForwardCost = fDist.get(meetNode)!
130
+ const meetBackwardCost = bDist.get(meetNode)!
131
+ curr = meetNode
132
+ while (curr !== undefined && curr !== end) {
133
+ const seg = bPrev.get(curr)
134
+ if (!seg) break
135
+ curr = seg.previousNodeIndex
136
+ if (curr !== undefined) {
137
+ forward.push({
138
+ nodeIndex: curr,
139
+ wayIndex: seg.wayIndex,
140
+ previousNodeIndex: seg.nodeIndex,
141
+ cost: meetForwardCost + meetBackwardCost - (bDist.get(curr) ?? 0),
142
+ })
143
+ }
144
+ }
145
+
146
+ return forward
147
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Routing algorithm implementations.
3
+ *
4
+ * Provides three algorithms:
5
+ * - `dijkstra`: Optimal shortest path, explores all directions equally.
6
+ * - `astar`: Optimal with heuristic guidance, faster for point-to-point.
7
+ * - `bidirectional`: Fast BFS from both ends, finds *a* path (not always optimal).
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { RoutingAlgorithm, RoutingAlgorithmFn } from "../types"
13
+ import { bidirectional } from "./bidirectional"
14
+ import { astar, dijkstra } from "./shortest-path"
15
+
16
+ export { astar, bidirectional, dijkstra }
17
+ export type { RoutingAlgorithmFn }
18
+
19
+ export const routingAlgorithms: Record<RoutingAlgorithm, RoutingAlgorithmFn> = {
20
+ dijkstra,
21
+ astar,
22
+ bidirectional,
23
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Shortest path algorithms (Dijkstra and A*).
3
+ *
4
+ * Both algorithms guarantee optimal paths. A* uses a heuristic to guide
5
+ * the search toward the destination, making it faster for most queries.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { haversineDistance } from "@osmix/shared/haversine-distance"
11
+ import { BinaryHeap } from "../binary-heap"
12
+ import type { GraphEdge, PathSegment, RoutingAlgorithmFn } from "../types"
13
+
14
+ /**
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.
17
+ */
18
+ const MAX_SPEED_MS = (130 * 1000) / 3600
19
+
20
+ /**
21
+ * A* shortest path algorithm with configurable heuristic.
22
+ *
23
+ * When heuristic returns 0 for all nodes, this is equivalent to Dijkstra's algorithm.
24
+ * With an admissible heuristic (never overestimates), guarantees optimal paths.
25
+ */
26
+ function shortestPath(
27
+ graph: (nodeIndex: number) => GraphEdge[],
28
+ start: number,
29
+ end: number,
30
+ getWeight: (edge: GraphEdge) => number,
31
+ heuristic: (nodeIndex: number) => number,
32
+ ): PathSegment[] | null {
33
+ // Same node - trivial path
34
+ if (start === end) return [{ nodeIndex: start, cost: 0 }]
35
+
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()
40
+
41
+ gScore.set(start, 0)
42
+ heap.push(start, heuristic(start))
43
+
44
+ while (heap.size > 0) {
45
+ const current = heap.pop()!
46
+
47
+ // Reached destination
48
+ if (current === end) return reconstructPath(previous, start, end)
49
+
50
+ // Already seen this node
51
+ if (closed.has(current)) continue
52
+ closed.add(current)
53
+
54
+ const currentG = gScore.get(current)!
55
+
56
+ for (const edge of graph(current)) {
57
+ const neighbor = edge.targetNodeIndex
58
+
59
+ if (closed.has(neighbor)) continue
60
+
61
+ const tentativeG = currentG + getWeight(edge)
62
+ const existingG = gScore.get(neighbor)
63
+
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
+ })
72
+
73
+ const f = tentativeG + heuristic(neighbor)
74
+ heap.push(neighbor, f)
75
+ }
76
+ }
77
+ }
78
+
79
+ return null
80
+ }
81
+
82
+ /**
83
+ * Reconstruct path from predecessor map.
84
+ */
85
+ function reconstructPath(
86
+ previous: Map<number, PathSegment>,
87
+ start: number,
88
+ end: number,
89
+ ): 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
108
+ }
109
+
110
+ /**
111
+ * Dijkstra's algorithm - optimal shortest path without heuristic guidance.
112
+ * Explores nodes in order of increasing distance from start.
113
+ */
114
+ export const dijkstra: RoutingAlgorithmFn = (graph, start, end, getWeight) => {
115
+ return shortestPath(graph, start, end, getWeight, () => 0)
116
+ }
117
+
118
+ /**
119
+ * A* algorithm - uses heuristic to guide search toward destination.
120
+ * Typically faster than Dijkstra for point-to-point queries.
121
+ *
122
+ * The heuristic is adapted based on the metric:
123
+ * - distance: haversine distance in meters
124
+ * - time: haversine distance / max speed (lower bound on travel time in seconds)
125
+ */
126
+ export const astar: RoutingAlgorithmFn = (
127
+ graph,
128
+ start,
129
+ end,
130
+ getWeight,
131
+ getCoord,
132
+ metric,
133
+ ) => {
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
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Priority queue implementation for pathfinding algorithms.
3
+ * @module
4
+ */
5
+
6
+ /**
7
+ * Min-heap priority queue optimized for pathfinding.
8
+ *
9
+ * Uses a binary heap stored in a flat array for cache-friendly access.
10
+ * Tracks item positions to support O(log n) priority updates (decrease-key).
11
+ *
12
+ * Operations:
13
+ * - `push(item, priority)`: O(log n) - adds or updates item priority.
14
+ * - `pop()`: O(log n) - removes and returns minimum priority item.
15
+ * - `has(item)`: O(1) - checks if item is in heap.
16
+ */
17
+ 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
+ }
147
+ }