@osmix/router 0.0.9 → 0.0.11
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 +91 -0
- package/dist/algorithms/bidirectional.d.ts +1 -1
- package/dist/algorithms/bidirectional.d.ts.map +1 -1
- package/dist/algorithms/index.d.ts +3 -3
- package/dist/algorithms/index.d.ts.map +1 -1
- package/dist/algorithms/index.js +2 -2
- package/dist/algorithms/index.js.map +1 -1
- package/dist/algorithms/shortest-path.d.ts +1 -1
- package/dist/algorithms/shortest-path.d.ts.map +1 -1
- package/dist/algorithms/shortest-path.js +1 -1
- package/dist/algorithms/shortest-path.js.map +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +1 -1
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +2 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1 -1
- package/dist/router.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/package.json +9 -32
- package/src/algorithms/bidirectional.ts +1 -1
- package/src/algorithms/index.ts +3 -3
- package/src/algorithms/shortest-path.ts +2 -2
- package/src/graph.ts +2 -2
- package/src/index.ts +5 -5
- package/src/router.ts +3 -3
- package/src/utils.ts +1 -1
- package/test/algorithms.test.ts +466 -0
- package/test/router.bench.ts +257 -0
- package/test/router.test.ts +738 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { astar, bidirectional, dijkstra } from "../src/algorithms"
|
|
3
|
+
import type { GraphEdge } from "../src/types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Test suite for individual routing algorithms.
|
|
7
|
+
*
|
|
8
|
+
* These tests verify the core behavior of each algorithm in isolation,
|
|
9
|
+
* using simple mock graphs instead of full OSM data. This allows us to
|
|
10
|
+
* test specific edge cases and verify algorithm correctness.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Mock Graph Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a simple graph function from an adjacency list.
|
|
19
|
+
* The adjacency list maps node IDs to their outgoing edges.
|
|
20
|
+
*/
|
|
21
|
+
function createGraph(
|
|
22
|
+
adjacencyList: Map<number, GraphEdge[]>,
|
|
23
|
+
): (nodeId: number) => GraphEdge[] {
|
|
24
|
+
return (nodeId: number) => adjacencyList.get(nodeId) ?? []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Simple edge weight function that uses distance.
|
|
29
|
+
*/
|
|
30
|
+
function distanceWeight(edge: GraphEdge): number {
|
|
31
|
+
return edge.distance
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a mock edge with default values.
|
|
36
|
+
*/
|
|
37
|
+
function mockEdge(
|
|
38
|
+
targetNodeIndex: number,
|
|
39
|
+
wayIndex: number,
|
|
40
|
+
distance: number,
|
|
41
|
+
): GraphEdge {
|
|
42
|
+
return {
|
|
43
|
+
targetNodeIndex: targetNodeIndex,
|
|
44
|
+
wayIndex,
|
|
45
|
+
distance,
|
|
46
|
+
time: distance / 10, // 10 m/s default speed
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a simple linear graph: 1 <-> 2 <-> 3 <-> 4
|
|
52
|
+
* Bidirectional edges for proper routing.
|
|
53
|
+
*/
|
|
54
|
+
function createLinearGraph(): Map<number, GraphEdge[]> {
|
|
55
|
+
return new Map([
|
|
56
|
+
[1, [mockEdge(2, 100, 10)]],
|
|
57
|
+
[2, [mockEdge(1, 100, 10), mockEdge(3, 101, 10)]],
|
|
58
|
+
[3, [mockEdge(2, 101, 10), mockEdge(4, 102, 10)]],
|
|
59
|
+
[4, [mockEdge(3, 102, 10)]],
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a graph with multiple paths (bidirectional):
|
|
65
|
+
*
|
|
66
|
+
* 1 <---(way 100, dist 10)---> 2
|
|
67
|
+
* ^ ^
|
|
68
|
+
* | (way 101, dist 5) | (way 102, dist 5)
|
|
69
|
+
* v v
|
|
70
|
+
* 3 <---(way 103, dist 10)---> 4
|
|
71
|
+
*
|
|
72
|
+
* Path 1->2->4 has distance 15
|
|
73
|
+
* Path 1->3->4 has distance 15
|
|
74
|
+
*/
|
|
75
|
+
function createSquareGraph(): Map<number, GraphEdge[]> {
|
|
76
|
+
return new Map([
|
|
77
|
+
[1, [mockEdge(2, 100, 10), mockEdge(3, 101, 5)]],
|
|
78
|
+
[2, [mockEdge(1, 100, 10), mockEdge(4, 102, 5)]],
|
|
79
|
+
[3, [mockEdge(1, 101, 5), mockEdge(4, 103, 10)]],
|
|
80
|
+
[4, [mockEdge(2, 102, 5), mockEdge(3, 103, 10)]],
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Creates a graph where shortest path is indirect (bidirectional):
|
|
86
|
+
*
|
|
87
|
+
* 1 <---(way 100, dist 100)---> 4 (direct but long)
|
|
88
|
+
* ^
|
|
89
|
+
* | (way 101, dist 10)
|
|
90
|
+
* v
|
|
91
|
+
* 2 <---(way 102, dist 10)---> 3 <---(way 103, dist 10)---> 4
|
|
92
|
+
*
|
|
93
|
+
* Direct path: 100
|
|
94
|
+
* Indirect path: 30 (should be chosen)
|
|
95
|
+
*/
|
|
96
|
+
function createIndirectShorterGraph(): Map<number, GraphEdge[]> {
|
|
97
|
+
return new Map([
|
|
98
|
+
[
|
|
99
|
+
1,
|
|
100
|
+
[
|
|
101
|
+
mockEdge(4, 100, 100), // Direct but long
|
|
102
|
+
mockEdge(2, 101, 10), // Indirect start
|
|
103
|
+
],
|
|
104
|
+
],
|
|
105
|
+
[2, [mockEdge(1, 101, 10), mockEdge(3, 102, 10)]],
|
|
106
|
+
[3, [mockEdge(2, 102, 10), mockEdge(4, 103, 10)]],
|
|
107
|
+
[4, [mockEdge(1, 100, 100), mockEdge(3, 103, 10)]],
|
|
108
|
+
])
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates a disconnected graph where node 4 is unreachable.
|
|
113
|
+
* Nodes 1-3 are connected bidirectionally, but 4 has no edges.
|
|
114
|
+
*/
|
|
115
|
+
function createDisconnectedGraph(): Map<number, GraphEdge[]> {
|
|
116
|
+
return new Map([
|
|
117
|
+
[1, [mockEdge(2, 100, 10)]],
|
|
118
|
+
[2, [mockEdge(1, 100, 10), mockEdge(3, 101, 10)]],
|
|
119
|
+
[3, [mockEdge(2, 101, 10)]],
|
|
120
|
+
[4, []], // Unreachable - no edges to/from this node
|
|
121
|
+
])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates a mock coordinate function for A* heuristic.
|
|
126
|
+
*
|
|
127
|
+
* The coordinates are carefully chosen so that the heuristic (straight-line
|
|
128
|
+
* distance) is admissible - it never overestimates the actual path cost.
|
|
129
|
+
* This ensures A* will find optimal paths.
|
|
130
|
+
*
|
|
131
|
+
* Node layout (not to scale):
|
|
132
|
+
* 1 ---- 2 ---- 3 ---- 4
|
|
133
|
+
*
|
|
134
|
+
* Each segment is ~5m (0.00005 degrees), so total 1->4 is ~15m.
|
|
135
|
+
* Edge weights are 10+ units, so heuristic <= actual cost (admissible).
|
|
136
|
+
*/
|
|
137
|
+
function createCoordFn(): (nodeId: number) => [number, number] | undefined {
|
|
138
|
+
const coords = new Map<number, [number, number]>([
|
|
139
|
+
[1, [0, 0]],
|
|
140
|
+
[2, [0.00005, 0]], // ~5m east
|
|
141
|
+
[3, [0.0001, 0]], // ~10m east
|
|
142
|
+
[4, [0.00015, 0]], // ~15m east
|
|
143
|
+
])
|
|
144
|
+
return (nodeId: number) => coords.get(nodeId)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Dijkstra Tests
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe("dijkstra", () => {
|
|
152
|
+
it("should find direct path between adjacent nodes", () => {
|
|
153
|
+
const graph = createGraph(createLinearGraph())
|
|
154
|
+
const path = dijkstra(graph, 1, 2, distanceWeight)
|
|
155
|
+
|
|
156
|
+
expect(path).not.toBeNull()
|
|
157
|
+
expect(path?.length).toBe(2)
|
|
158
|
+
expect(path?.[0]?.nodeIndex).toBe(1)
|
|
159
|
+
expect(path?.[1]?.nodeIndex).toBe(2)
|
|
160
|
+
expect(path?.[1]?.wayIndex).toBe(100)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("should find path through multiple nodes", () => {
|
|
164
|
+
const graph = createGraph(createLinearGraph())
|
|
165
|
+
const path = dijkstra(graph, 1, 4, distanceWeight)
|
|
166
|
+
|
|
167
|
+
expect(path).not.toBeNull()
|
|
168
|
+
expect(path?.length).toBe(4)
|
|
169
|
+
|
|
170
|
+
// Verify node order
|
|
171
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
172
|
+
expect(nodeIds).toEqual([1, 2, 3, 4])
|
|
173
|
+
|
|
174
|
+
// Verify way indexes are tracked
|
|
175
|
+
expect(path?.[1]?.wayIndex).toBe(100)
|
|
176
|
+
expect(path?.[2]?.wayIndex).toBe(101)
|
|
177
|
+
expect(path?.[3]?.wayIndex).toBe(102)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("should return null when no path exists", () => {
|
|
181
|
+
const graph = createGraph(createDisconnectedGraph())
|
|
182
|
+
const path = dijkstra(graph, 1, 4, distanceWeight)
|
|
183
|
+
|
|
184
|
+
expect(path).toBeNull()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("should handle same start and end node", () => {
|
|
188
|
+
const graph = createGraph(createLinearGraph())
|
|
189
|
+
const path = dijkstra(graph, 1, 1, distanceWeight)
|
|
190
|
+
|
|
191
|
+
expect(path).not.toBeNull()
|
|
192
|
+
expect(path?.length).toBe(1)
|
|
193
|
+
expect(path?.[0]?.nodeIndex).toBe(1)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should find shortest path when indirect is shorter", () => {
|
|
197
|
+
const graph = createGraph(createIndirectShorterGraph())
|
|
198
|
+
const path = dijkstra(graph, 1, 4, distanceWeight)
|
|
199
|
+
|
|
200
|
+
expect(path).not.toBeNull()
|
|
201
|
+
|
|
202
|
+
// Should take indirect route 1->2->3->4 (cost 30) not 1->4 (cost 100)
|
|
203
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
204
|
+
expect(nodeIds).toEqual([1, 2, 3, 4])
|
|
205
|
+
|
|
206
|
+
// Verify total cost
|
|
207
|
+
const lastSegment = path?.[path.length - 1]
|
|
208
|
+
expect(lastSegment?.cost).toBe(30)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it("should calculate correct costs", () => {
|
|
212
|
+
const graph = createGraph(createLinearGraph())
|
|
213
|
+
const path = dijkstra(graph, 1, 4, distanceWeight)
|
|
214
|
+
|
|
215
|
+
expect(path).not.toBeNull()
|
|
216
|
+
|
|
217
|
+
// Each edge has distance 10
|
|
218
|
+
expect(path?.[0]?.cost).toBe(0) // Start node
|
|
219
|
+
expect(path?.[1]?.cost).toBe(10)
|
|
220
|
+
expect(path?.[2]?.cost).toBe(20)
|
|
221
|
+
expect(path?.[3]?.cost).toBe(30)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it("should track correct way indexes in path", () => {
|
|
225
|
+
const graph = createGraph(createSquareGraph())
|
|
226
|
+
const path = dijkstra(graph, 1, 4, distanceWeight)
|
|
227
|
+
|
|
228
|
+
expect(path).not.toBeNull()
|
|
229
|
+
|
|
230
|
+
// Verify way indexes are present for traversed edges
|
|
231
|
+
const wayIndexes = path?.slice(1).map((p) => p.wayIndex)
|
|
232
|
+
expect(wayIndexes?.every((idx) => idx !== undefined)).toBe(true)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// A* Tests
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
describe("astar", () => {
|
|
241
|
+
it("should find direct path between adjacent nodes", () => {
|
|
242
|
+
const graph = createGraph(createLinearGraph())
|
|
243
|
+
const path = astar(graph, 1, 2, distanceWeight, createCoordFn())
|
|
244
|
+
|
|
245
|
+
expect(path).not.toBeNull()
|
|
246
|
+
expect(path?.length).toBe(2)
|
|
247
|
+
expect(path?.[0]?.nodeIndex).toBe(1)
|
|
248
|
+
expect(path?.[1]?.nodeIndex).toBe(2)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it("should find path through multiple nodes", () => {
|
|
252
|
+
const graph = createGraph(createLinearGraph())
|
|
253
|
+
const path = astar(graph, 1, 4, distanceWeight, createCoordFn())
|
|
254
|
+
|
|
255
|
+
expect(path).not.toBeNull()
|
|
256
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
257
|
+
expect(nodeIds).toEqual([1, 2, 3, 4])
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("should return null when no path exists", () => {
|
|
261
|
+
const graph = createGraph(createDisconnectedGraph())
|
|
262
|
+
const path = astar(graph, 1, 4, distanceWeight, createCoordFn())
|
|
263
|
+
|
|
264
|
+
expect(path).toBeNull()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it("should handle same start and end node", () => {
|
|
268
|
+
const graph = createGraph(createLinearGraph())
|
|
269
|
+
const path = astar(graph, 1, 1, distanceWeight, createCoordFn())
|
|
270
|
+
|
|
271
|
+
expect(path).not.toBeNull()
|
|
272
|
+
expect(path?.length).toBe(1)
|
|
273
|
+
expect(path?.[0]?.nodeIndex).toBe(1)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it("should find shortest path when indirect is shorter", () => {
|
|
277
|
+
const graph = createGraph(createIndirectShorterGraph())
|
|
278
|
+
const path = astar(graph, 1, 4, distanceWeight, createCoordFn())
|
|
279
|
+
|
|
280
|
+
expect(path).not.toBeNull()
|
|
281
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
282
|
+
expect(nodeIds).toEqual([1, 2, 3, 4])
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("should use heuristic to guide search", () => {
|
|
286
|
+
// With heuristic, A* should explore fewer nodes than Dijkstra
|
|
287
|
+
// We can verify it finds the correct path
|
|
288
|
+
const graph = createGraph(createIndirectShorterGraph())
|
|
289
|
+
const path = astar(graph, 1, 4, distanceWeight, createCoordFn())
|
|
290
|
+
|
|
291
|
+
expect(path).not.toBeNull()
|
|
292
|
+
expect(path?.[path.length - 1]?.cost).toBe(30)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it("should return null when coordinate function is missing", () => {
|
|
296
|
+
const graph = createGraph(createLinearGraph())
|
|
297
|
+
const path = astar(graph, 1, 4, distanceWeight, undefined)
|
|
298
|
+
|
|
299
|
+
expect(path).toBeNull()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it("should handle missing coordinate for node gracefully", () => {
|
|
303
|
+
const graph = createGraph(createLinearGraph())
|
|
304
|
+
// Coordinate function that only knows about nodes 1 and 4
|
|
305
|
+
const partialCoordFn = (nodeId: number) => {
|
|
306
|
+
if (nodeId === 1) return [0, 0] as [number, number]
|
|
307
|
+
if (nodeId === 4) return [0.001, 0.001] as [number, number]
|
|
308
|
+
return undefined
|
|
309
|
+
}
|
|
310
|
+
const path = astar(graph, 1, 4, distanceWeight, partialCoordFn)
|
|
311
|
+
|
|
312
|
+
// A* should still find a path, using 0 heuristic for unknown nodes
|
|
313
|
+
expect(path).not.toBeNull()
|
|
314
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
315
|
+
expect(nodeIds).toEqual([1, 2, 3, 4])
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it("should track correct way indexes in path", () => {
|
|
319
|
+
const graph = createGraph(createSquareGraph())
|
|
320
|
+
const path = astar(graph, 1, 4, distanceWeight, createCoordFn())
|
|
321
|
+
|
|
322
|
+
expect(path).not.toBeNull()
|
|
323
|
+
const wayIndexes = path?.slice(1).map((p) => p.wayIndex)
|
|
324
|
+
expect(wayIndexes?.every((idx) => idx !== undefined)).toBe(true)
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Bidirectional Tests
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
describe("bidirectional", () => {
|
|
333
|
+
it("should find direct path between adjacent nodes", () => {
|
|
334
|
+
const graph = createGraph(createLinearGraph())
|
|
335
|
+
const path = bidirectional(graph, 1, 2, distanceWeight)
|
|
336
|
+
|
|
337
|
+
expect(path).not.toBeNull()
|
|
338
|
+
// Bidirectional may have different path structure but same nodes
|
|
339
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
340
|
+
expect(nodeIds).toContain(1)
|
|
341
|
+
expect(nodeIds).toContain(2)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it("should find path through multiple nodes", () => {
|
|
345
|
+
const graph = createGraph(createLinearGraph())
|
|
346
|
+
const path = bidirectional(graph, 1, 4, distanceWeight)
|
|
347
|
+
|
|
348
|
+
expect(path).not.toBeNull()
|
|
349
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
350
|
+
// Should contain all nodes in the path
|
|
351
|
+
expect(nodeIds).toContain(1)
|
|
352
|
+
expect(nodeIds).toContain(4)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it("should return null when no path exists", () => {
|
|
356
|
+
const graph = createGraph(createDisconnectedGraph())
|
|
357
|
+
const path = bidirectional(graph, 1, 4, distanceWeight)
|
|
358
|
+
|
|
359
|
+
expect(path).toBeNull()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it("should handle same start and end node", () => {
|
|
363
|
+
const graph = createGraph(createLinearGraph())
|
|
364
|
+
const path = bidirectional(graph, 1, 1, distanceWeight)
|
|
365
|
+
|
|
366
|
+
expect(path).not.toBeNull()
|
|
367
|
+
expect(path?.some((p) => p.nodeIndex === 1)).toBe(true)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it("should meet in the middle correctly", () => {
|
|
371
|
+
// Create a symmetric graph where meeting in middle is clear
|
|
372
|
+
// 1 <-> 2 <-> 3 <-> 4 <-> 5
|
|
373
|
+
const symmetricGraph = new Map([
|
|
374
|
+
[1, [mockEdge(2, 100, 10)]],
|
|
375
|
+
[2, [mockEdge(1, 100, 10), mockEdge(3, 101, 10)]],
|
|
376
|
+
[3, [mockEdge(2, 101, 10), mockEdge(4, 102, 10)]],
|
|
377
|
+
[4, [mockEdge(3, 102, 10), mockEdge(5, 103, 10)]],
|
|
378
|
+
[5, [mockEdge(4, 103, 10)]],
|
|
379
|
+
])
|
|
380
|
+
|
|
381
|
+
const graph = createGraph(symmetricGraph)
|
|
382
|
+
const path = bidirectional(graph, 1, 5, distanceWeight)
|
|
383
|
+
|
|
384
|
+
expect(path).not.toBeNull()
|
|
385
|
+
// Should include start and end
|
|
386
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
387
|
+
expect(nodeIds).toContain(1)
|
|
388
|
+
expect(nodeIds).toContain(5)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it("should combine forward and backward paths properly", () => {
|
|
392
|
+
const graph = createGraph(createSquareGraph())
|
|
393
|
+
const path = bidirectional(graph, 1, 4, distanceWeight)
|
|
394
|
+
|
|
395
|
+
expect(path).not.toBeNull()
|
|
396
|
+
|
|
397
|
+
// Path should go from 1 to 4
|
|
398
|
+
const nodeIds = path?.map((p) => p.nodeIndex)
|
|
399
|
+
expect(nodeIds?.[0]).toBe(1)
|
|
400
|
+
// Last node should be 4 or path should include 4
|
|
401
|
+
expect(nodeIds).toContain(4)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it("should track way indexes in combined path", () => {
|
|
405
|
+
const graph = createGraph(createSquareGraph())
|
|
406
|
+
const path = bidirectional(graph, 1, 4, distanceWeight)
|
|
407
|
+
|
|
408
|
+
expect(path).not.toBeNull()
|
|
409
|
+
|
|
410
|
+
// Way indexes should be present (except possibly for start node)
|
|
411
|
+
const wayIndexes = path
|
|
412
|
+
?.filter((p) => p.wayIndex !== undefined)
|
|
413
|
+
.map((p) => p.wayIndex)
|
|
414
|
+
expect(wayIndexes?.length).toBeGreaterThan(0)
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// Cross-Algorithm Comparison Tests
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
describe("algorithm consistency", () => {
|
|
423
|
+
it("should find same path length with all algorithms", () => {
|
|
424
|
+
const graph = createGraph(createIndirectShorterGraph())
|
|
425
|
+
|
|
426
|
+
const dijkstraPath = dijkstra(graph, 1, 4, distanceWeight)
|
|
427
|
+
const astarPath = astar(graph, 1, 4, distanceWeight, createCoordFn())
|
|
428
|
+
const biPath = bidirectional(graph, 1, 4, distanceWeight)
|
|
429
|
+
|
|
430
|
+
// All should find a path
|
|
431
|
+
expect(dijkstraPath).not.toBeNull()
|
|
432
|
+
expect(astarPath).not.toBeNull()
|
|
433
|
+
expect(biPath).not.toBeNull()
|
|
434
|
+
|
|
435
|
+
// Dijkstra and A* should have same final cost
|
|
436
|
+
const dijkstraCost = dijkstraPath?.[dijkstraPath.length - 1]?.cost
|
|
437
|
+
const astarCost = astarPath?.[astarPath.length - 1]?.cost
|
|
438
|
+
|
|
439
|
+
expect(dijkstraCost).toBe(astarCost)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it("all algorithms should return null for unreachable nodes", () => {
|
|
443
|
+
const graph = createGraph(createDisconnectedGraph())
|
|
444
|
+
|
|
445
|
+
expect(dijkstra(graph, 1, 4, distanceWeight)).toBeNull()
|
|
446
|
+
expect(astar(graph, 1, 4, distanceWeight, createCoordFn())).toBeNull()
|
|
447
|
+
expect(bidirectional(graph, 1, 4, distanceWeight)).toBeNull()
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it("all algorithms should handle trivial single-node path", () => {
|
|
451
|
+
const graph = createGraph(createLinearGraph())
|
|
452
|
+
|
|
453
|
+
const dijkstraPath = dijkstra(graph, 1, 1, distanceWeight)
|
|
454
|
+
const astarPath = astar(graph, 1, 1, distanceWeight, createCoordFn())
|
|
455
|
+
const biPath = bidirectional(graph, 1, 1, distanceWeight)
|
|
456
|
+
|
|
457
|
+
expect(dijkstraPath).not.toBeNull()
|
|
458
|
+
expect(astarPath).not.toBeNull()
|
|
459
|
+
expect(biPath).not.toBeNull()
|
|
460
|
+
|
|
461
|
+
// All should include node 1
|
|
462
|
+
expect(dijkstraPath?.some((p) => p.nodeIndex === 1)).toBe(true)
|
|
463
|
+
expect(astarPath?.some((p) => p.nodeIndex === 1)).toBe(true)
|
|
464
|
+
expect(biPath?.some((p) => p.nodeIndex === 1)).toBe(true)
|
|
465
|
+
})
|
|
466
|
+
})
|