@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
@@ -1,738 +1,697 @@
1
- import { beforeAll, describe, expect, it } from "bun:test"
2
- import { Osm } from "@osmix/core"
3
- import { getFixtureFile, PBFs } from "@osmix/shared/fixtures"
4
- import { fromPbf } from "osmix"
5
- import { buildGraph, getTransferableBuffers, RoutingGraph } from "../src"
6
- import { Router } from "../src/router"
1
+ import { Osm } from "@osmix/core";
2
+ import { fromPbf } from "@osmix/load";
3
+ import { getFixtureFile, PBFs } from "@osmix/test-utils/fixtures";
4
+ import { beforeAll, describe, expect, it } from "vitest";
5
+
6
+ import { buildGraph, getTransferableBuffers, RoutingGraph } from "../src/index.ts";
7
+ import { Router } from "../src/router";
7
8
 
8
9
  /**
9
10
  * Create a simple OSM with a routeable network
10
11
  */
11
12
  function createTestOsm(): Osm {
12
- const osm = new Osm({ id: "test" })
13
-
14
- // Create a simple grid network
15
- // Nodes: 4 corners of a square
16
- osm.nodes.addNode({ id: 1, lat: 0, lon: 0 })
17
- osm.nodes.addNode({ id: 2, lat: 0, lon: 0.01 })
18
- osm.nodes.addNode({ id: 3, lat: 0.01, lon: 0 })
19
- osm.nodes.addNode({ id: 4, lat: 0.01, lon: 0.01 })
20
- // Center intersection node
21
- osm.nodes.addNode({ id: 5, lat: 0.005, lon: 0.005 })
22
-
23
- osm.nodes.buildIndex()
24
- osm.nodes.buildSpatialIndex()
25
-
26
- // Create ways connecting the nodes
27
- // Way 1: horizontal bottom (1 -> 2)
28
- osm.ways.addWay({
29
- id: 1,
30
- refs: [1, 2],
31
- tags: { highway: "primary" },
32
- })
33
-
34
- // Way 2: vertical left (1 -> 3)
35
- osm.ways.addWay({
36
- id: 2,
37
- refs: [1, 3],
38
- tags: { highway: "primary" },
39
- })
40
-
41
- // Way 3: horizontal top (3 -> 4)
42
- osm.ways.addWay({
43
- id: 3,
44
- refs: [3, 4],
45
- tags: { highway: "primary" },
46
- })
47
-
48
- // Way 4: vertical right (2 -> 4)
49
- osm.ways.addWay({
50
- id: 4,
51
- refs: [2, 4],
52
- tags: { highway: "primary" },
53
- })
54
-
55
- // Way 5: diagonal through center (1 -> 5 -> 4)
56
- osm.ways.addWay({
57
- id: 5,
58
- refs: [1, 5, 4],
59
- tags: { highway: "secondary" },
60
- })
61
-
62
- osm.ways.buildIndex()
63
- osm.ways.buildSpatialIndex()
64
- osm.buildIndexes()
65
-
66
- return osm
13
+ const osm = new Osm({ id: "test" });
14
+
15
+ // Create a simple grid network
16
+ // Nodes: 4 corners of a square
17
+ osm.nodes.addNode({ id: 1, lat: 0, lon: 0 });
18
+ osm.nodes.addNode({ id: 2, lat: 0, lon: 0.01 });
19
+ osm.nodes.addNode({ id: 3, lat: 0.01, lon: 0 });
20
+ osm.nodes.addNode({ id: 4, lat: 0.01, lon: 0.01 });
21
+ // Center intersection node
22
+ osm.nodes.addNode({ id: 5, lat: 0.005, lon: 0.005 });
23
+
24
+ osm.nodes.buildIndex();
25
+ osm.nodes.buildSpatialIndex();
26
+
27
+ // Create ways connecting the nodes
28
+ // Way 1: horizontal bottom (1 -> 2)
29
+ osm.ways.addWay({
30
+ id: 1,
31
+ refs: [1, 2],
32
+ tags: { highway: "primary" },
33
+ });
34
+
35
+ // Way 2: vertical left (1 -> 3)
36
+ osm.ways.addWay({
37
+ id: 2,
38
+ refs: [1, 3],
39
+ tags: { highway: "primary" },
40
+ });
41
+
42
+ // Way 3: horizontal top (3 -> 4)
43
+ osm.ways.addWay({
44
+ id: 3,
45
+ refs: [3, 4],
46
+ tags: { highway: "primary" },
47
+ });
48
+
49
+ // Way 4: vertical right (2 -> 4)
50
+ osm.ways.addWay({
51
+ id: 4,
52
+ refs: [2, 4],
53
+ tags: { highway: "primary" },
54
+ });
55
+
56
+ // Way 5: diagonal through center (1 -> 5 -> 4)
57
+ osm.ways.addWay({
58
+ id: 5,
59
+ refs: [1, 5, 4],
60
+ tags: { highway: "secondary" },
61
+ });
62
+
63
+ osm.ways.buildIndex();
64
+ osm.ways.buildSpatialIndex();
65
+ osm.buildIndexes();
66
+
67
+ return osm;
67
68
  }
68
69
 
69
70
  describe("Router", () => {
70
- it("should create a router with default config", () => {
71
- const osm = createTestOsm()
72
- const router = new Router(osm, buildGraph(osm))
73
-
74
- expect(router).toBeDefined()
75
- })
76
-
77
- it("should create a router with custom highway filter", () => {
78
- const osm = createTestOsm()
79
- const graph = buildGraph(osm, (tags) => tags?.["highway"] === "primary")
80
- const router = new Router(osm, graph)
81
-
82
- expect(router).toBeDefined()
83
- })
84
-
85
- it("should find a route between two nearby nodes", () => {
86
- const osm = createTestOsm()
87
- const graph = buildGraph(osm)
88
- const router = new Router(osm, graph)
89
-
90
- // Snap coordinates to nodes
91
- const from = graph.findNearestRoutableNode(osm, [0, 0], 500)
92
- const to = graph.findNearestRoutableNode(osm, [0, 0.01], 500)
93
-
94
- expect(from).not.toBeNull()
95
- expect(to).not.toBeNull()
96
-
97
- // Route between node indexes
98
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
99
-
100
- expect(path).not.toBeNull()
101
- expect(path!.length).toBeGreaterThan(0)
102
- })
103
-
104
- it("should return path segments with node and way info", () => {
105
- const osm = createTestOsm()
106
- const graph = buildGraph(osm)
107
- const router = new Router(osm, graph)
108
-
109
- const from = graph.findNearestRoutableNode(osm, [0, 0], 500)
110
- const to = graph.findNearestRoutableNode(osm, [0.01, 0.01], 500)
111
-
112
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
113
-
114
- expect(path).not.toBeNull()
115
- if (path) {
116
- // Verify path segments have required fields
117
- for (const segment of path) {
118
- expect(typeof segment.nodeIndex).toBe("number")
119
- expect(typeof segment.cost).toBe("number")
120
- }
121
-
122
- // Build result to get coordinates
123
- const result = router.buildResult(path)
124
- expect(result.coordinates.length).toBeGreaterThan(0)
125
- expect(Array.isArray(result.wayIndexes)).toBe(true)
126
- expect(Array.isArray(result.nodeIndexes)).toBe(true)
127
- }
128
- })
129
-
130
- it("should return null for unreachable destinations", () => {
131
- const osm = new Osm({ id: "test" })
132
- osm.nodes.addNode({ id: 1, lat: 0, lon: 0 })
133
- osm.nodes.addNode({ id: 2, lat: 10, lon: 10 }) // Far away, no way connecting
134
- osm.nodes.buildIndex()
135
- osm.nodes.buildSpatialIndex()
136
- osm.buildIndexes()
137
-
138
- const graph = buildGraph(osm)
139
-
140
- // Can't snap because no routable ways
141
- const from = graph.findNearestRoutableNode(osm, [0, 0], 500)
142
- const to = graph.findNearestRoutableNode(osm, [10, 10], 500)
143
-
144
- expect(from).toBeNull()
145
- expect(to).toBeNull()
146
- })
147
-
148
- it("should filter ways based on highway filter", () => {
149
- const osm = createTestOsm()
150
- // Only include primary highways
151
- const graph = buildGraph(osm, (tags) => tags?.["highway"] === "primary")
152
- const router = new Router(osm, graph)
153
-
154
- const from = graph.findNearestRoutableNode(osm, [0, 0], 500)
155
- const to = graph.findNearestRoutableNode(osm, [0.01, 0.01], 500)
156
-
157
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
158
-
159
- // Should find a route using only primary ways
160
- if (path) {
161
- const result = router.buildResult(path)
162
- // Way index 4 is the secondary highway (ID 5)
163
- expect(result.wayIndexes).not.toContain(4)
164
- }
165
- })
166
-
167
- it("should handle same start and end node", () => {
168
- const osm = createTestOsm()
169
- const graph = buildGraph(osm)
170
- const router = new Router(osm, graph)
171
-
172
- const from = graph.findNearestRoutableNode(osm, [0, 0], 500)
173
-
174
- const path = router.route(from!.nodeIndex, from!.nodeIndex)
175
-
176
- expect(path).not.toBeNull()
177
- expect(path!.length).toBe(1)
178
- expect(path![0]!.nodeIndex).toBe(from!.nodeIndex)
179
- })
180
-
181
- it("should use different algorithms", () => {
182
- const osm = createTestOsm()
183
- const graph = buildGraph(osm)
184
- const router = new Router(osm, graph)
185
-
186
- const from = graph.findNearestRoutableNode(osm, [0, 0], 500)
187
- const to = graph.findNearestRoutableNode(osm, [0.01, 0.01], 500)
188
-
189
- const path1 = router.route(from!.nodeIndex, to!.nodeIndex, {
190
- algorithm: "dijkstra",
191
- })
192
- const path2 = router.route(from!.nodeIndex, to!.nodeIndex, {
193
- algorithm: "astar",
194
- })
195
- const path3 = router.route(from!.nodeIndex, to!.nodeIndex, {
196
- algorithm: "bidirectional",
197
- })
198
-
199
- // All should find a path
200
- const paths = [path1, path2, path3].filter((p) => p !== null)
201
- expect(paths.length).toBe(3)
202
-
203
- // All paths should have segments
204
- for (const path of paths) {
205
- expect(path!.length).toBeGreaterThan(0)
206
- }
207
- })
208
-
209
- it("should support distance and time metrics", () => {
210
- const osm = createTestOsm()
211
- const graph = buildGraph(osm)
212
- const router = new Router(osm, graph)
213
-
214
- const from = graph.findNearestRoutableNode(osm, [0, 0], 500)
215
- const to = graph.findNearestRoutableNode(osm, [0.01, 0.01], 500)
216
-
217
- const path1 = router.route(from!.nodeIndex, to!.nodeIndex, {
218
- metric: "distance",
219
- })
220
- const path2 = router.route(from!.nodeIndex, to!.nodeIndex, {
221
- metric: "time",
222
- })
223
-
224
- // Both should find paths
225
- expect(path1).not.toBeNull()
226
- expect(path2).not.toBeNull()
227
- })
228
- })
71
+ it("should create a router with default config", () => {
72
+ const osm = createTestOsm();
73
+ const router = new Router(osm, buildGraph(osm));
74
+
75
+ expect(router).toBeDefined();
76
+ });
77
+
78
+ it("should create a router with custom highway filter", () => {
79
+ const osm = createTestOsm();
80
+ const graph = buildGraph(osm, (tags) => tags?.["highway"] === "primary");
81
+ const router = new Router(osm, graph);
82
+
83
+ expect(router).toBeDefined();
84
+ });
85
+
86
+ it("should find a route between two nearby nodes", () => {
87
+ const osm = createTestOsm();
88
+ const graph = buildGraph(osm);
89
+ const router = new Router(osm, graph);
90
+
91
+ // Snap coordinates to nodes
92
+ const from = graph.findNearestRoutableNode(osm, [0, 0], 500);
93
+ const to = graph.findNearestRoutableNode(osm, [0, 0.01], 500);
94
+
95
+ expect(from).not.toBeNull();
96
+ expect(to).not.toBeNull();
97
+
98
+ // Route between node indexes
99
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
100
+
101
+ expect(path).not.toBeNull();
102
+ expect(path!.length).toBeGreaterThan(0);
103
+ });
104
+
105
+ it("should return path segments with node and way info", () => {
106
+ const osm = createTestOsm();
107
+ const graph = buildGraph(osm);
108
+ const router = new Router(osm, graph);
109
+
110
+ const from = graph.findNearestRoutableNode(osm, [0, 0], 500);
111
+ const to = graph.findNearestRoutableNode(osm, [0.01, 0.01], 500);
112
+
113
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
114
+
115
+ expect(path).not.toBeNull();
116
+ if (path) {
117
+ // Verify path segments have required fields
118
+ for (const segment of path) {
119
+ expect(typeof segment.nodeIndex).toBe("number");
120
+ expect(typeof segment.cost).toBe("number");
121
+ }
122
+
123
+ // Build result to get coordinates
124
+ const result = router.buildResult(path);
125
+ expect(result.coordinates.length).toBeGreaterThan(0);
126
+ expect(Array.isArray(result.wayIndexes)).toBe(true);
127
+ expect(Array.isArray(result.nodeIndexes)).toBe(true);
128
+ }
129
+ });
130
+
131
+ it("should return null for unreachable destinations", () => {
132
+ const osm = new Osm({ id: "test" });
133
+ osm.nodes.addNode({ id: 1, lat: 0, lon: 0 });
134
+ osm.nodes.addNode({ id: 2, lat: 10, lon: 10 }); // Far away, no way connecting
135
+ osm.nodes.buildIndex();
136
+ osm.nodes.buildSpatialIndex();
137
+ osm.buildIndexes();
138
+
139
+ const graph = buildGraph(osm);
140
+
141
+ // Can't snap because no routable ways
142
+ const from = graph.findNearestRoutableNode(osm, [0, 0], 500);
143
+ const to = graph.findNearestRoutableNode(osm, [10, 10], 500);
144
+
145
+ expect(from).toBeNull();
146
+ expect(to).toBeNull();
147
+ });
148
+
149
+ it("should filter ways based on highway filter", () => {
150
+ const osm = createTestOsm();
151
+ // Only include primary highways
152
+ const graph = buildGraph(osm, (tags) => tags?.["highway"] === "primary");
153
+ const router = new Router(osm, graph);
154
+
155
+ const from = graph.findNearestRoutableNode(osm, [0, 0], 500);
156
+ const to = graph.findNearestRoutableNode(osm, [0.01, 0.01], 500);
157
+
158
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
159
+
160
+ // Should find a route using only primary ways
161
+ if (path) {
162
+ const result = router.buildResult(path);
163
+ // Way index 4 is the secondary highway (ID 5)
164
+ expect(result.wayIndexes).not.toContain(4);
165
+ }
166
+ });
167
+
168
+ it("treats roundabouts as one-way unless explicitly overridden", () => {
169
+ const createRoundabout = (oneway?: string) => {
170
+ const osm = new Osm({ id: `roundabout-${oneway ?? "implicit"}` });
171
+ osm.nodes.addNode({ id: 1, lat: 0, lon: 0 });
172
+ osm.nodes.addNode({ id: 2, lat: 0, lon: 0.001 });
173
+ osm.nodes.addNode({ id: 3, lat: 0.001, lon: 0.001 });
174
+ osm.ways.addWay({
175
+ id: 10,
176
+ refs: [1, 2, 3, 1],
177
+ tags: {
178
+ highway: "residential",
179
+ junction: "roundabout",
180
+ ...(oneway === undefined ? {} : { oneway }),
181
+ },
182
+ });
183
+ osm.buildIndexes();
184
+ osm.buildSpatialIndexes();
185
+ return osm;
186
+ };
187
+
188
+ const implicit = createRoundabout();
189
+ const implicitRoute = new Router(implicit, buildGraph(implicit)).route(1, 0);
190
+ expect(implicitRoute?.map((segment) => implicit.nodes.ids.at(segment.nodeIndex))).toEqual([
191
+ 2, 3, 1,
192
+ ]);
193
+
194
+ const overridden = createRoundabout("no");
195
+ const overriddenRoute = new Router(overridden, buildGraph(overridden)).route(1, 0);
196
+ expect(overriddenRoute?.map((segment) => overridden.nodes.ids.at(segment.nodeIndex))).toEqual([
197
+ 2, 1,
198
+ ]);
199
+ });
200
+
201
+ it("creates reverse-only edges for oneway=-1", () => {
202
+ const osm = new Osm({ id: "reverse-oneway" });
203
+ osm.nodes.addNode({ id: 1, lat: 0, lon: 0 });
204
+ osm.nodes.addNode({ id: 2, lat: 0, lon: 0.001 });
205
+ osm.nodes.addNode({ id: 3, lat: 0, lon: 0.002 });
206
+ osm.ways.addWay({
207
+ id: 10,
208
+ refs: [1, 2, 3],
209
+ tags: { highway: "residential", oneway: "-1" },
210
+ });
211
+ osm.buildIndexes();
212
+ osm.buildSpatialIndexes();
213
+ const router = new Router(osm, buildGraph(osm));
214
+
215
+ expect(router.route(0, 2)).toBeNull();
216
+ expect(router.route(2, 0)?.map((segment) => osm.nodes.ids.at(segment.nodeIndex))).toEqual([
217
+ 3, 2, 1,
218
+ ]);
219
+ });
220
+
221
+ it("should handle same start and end node", () => {
222
+ const osm = createTestOsm();
223
+ const graph = buildGraph(osm);
224
+ const router = new Router(osm, graph);
225
+
226
+ const from = graph.findNearestRoutableNode(osm, [0, 0], 500);
227
+
228
+ const path = router.route(from!.nodeIndex, from!.nodeIndex);
229
+
230
+ expect(path).not.toBeNull();
231
+ expect(path!.length).toBe(1);
232
+ expect(path![0]!.nodeIndex).toBe(from!.nodeIndex);
233
+ });
234
+
235
+ it("should use different algorithms", () => {
236
+ const osm = createTestOsm();
237
+ const graph = buildGraph(osm);
238
+ const router = new Router(osm, graph);
239
+
240
+ const from = graph.findNearestRoutableNode(osm, [0, 0], 500);
241
+ const to = graph.findNearestRoutableNode(osm, [0.01, 0.01], 500);
242
+
243
+ const path1 = router.route(from!.nodeIndex, to!.nodeIndex, {
244
+ algorithm: "dijkstra",
245
+ });
246
+ const path2 = router.route(from!.nodeIndex, to!.nodeIndex, {
247
+ algorithm: "astar",
248
+ });
249
+ const path3 = router.route(from!.nodeIndex, to!.nodeIndex, {
250
+ algorithm: "bidirectional",
251
+ });
252
+
253
+ // All should find a path
254
+ const paths = [path1, path2, path3].filter((p) => p !== null);
255
+ expect(paths.length).toBe(3);
256
+
257
+ // All paths should have segments
258
+ for (const path of paths) {
259
+ expect(path!.length).toBeGreaterThan(0);
260
+ }
261
+ });
262
+
263
+ it("should support distance and time metrics", () => {
264
+ const osm = createTestOsm();
265
+ const graph = buildGraph(osm);
266
+ const router = new Router(osm, graph);
267
+
268
+ const from = graph.findNearestRoutableNode(osm, [0, 0], 500);
269
+ const to = graph.findNearestRoutableNode(osm, [0.01, 0.01], 500);
270
+
271
+ const path1 = router.route(from!.nodeIndex, to!.nodeIndex, {
272
+ metric: "distance",
273
+ });
274
+ const path2 = router.route(from!.nodeIndex, to!.nodeIndex, {
275
+ metric: "time",
276
+ });
277
+
278
+ // Both should find paths
279
+ expect(path1).not.toBeNull();
280
+ expect(path2).not.toBeNull();
281
+ });
282
+ });
229
283
 
230
284
  describe("Router with Monaco PBF", () => {
231
- const monacoPbf = PBFs["monaco"]!
232
- let monacoOsm: Osm
233
-
234
- beforeAll(async () => {
235
- const pbfData = await getFixtureFile(monacoPbf.url)
236
- monacoOsm = await fromPbf(pbfData)
237
- })
238
-
239
- it("should create a router from Monaco OSM data", () => {
240
- const graph = buildGraph(monacoOsm)
241
- const router = new Router(monacoOsm, graph)
242
- expect(router).toBeDefined()
243
- // 11,414 total edges in the graph (bidirectional edges counted separately)
244
- expect(graph.edges).toBe(11_414)
245
- })
246
-
247
- it("should find routes between points in Monaco", () => {
248
- const graph = buildGraph(monacoOsm)
249
- const router = new Router(monacoOsm, graph)
250
-
251
- // Use coordinates known to have routeable nodes nearby
252
- const from = graph.findNearestRoutableNode(
253
- monacoOsm,
254
- [7.4229093, 43.7371175],
255
- 500,
256
- )
257
- const to = graph.findNearestRoutableNode(
258
- monacoOsm,
259
- [7.4259193, 43.7377731],
260
- 500,
261
- )
262
-
263
- expect(from).not.toBeNull()
264
- expect(to).not.toBeNull()
265
-
266
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
267
-
268
- expect(path).not.toBeNull()
269
- expect(path!.length).toBeGreaterThan(0)
270
-
271
- // Build result and verify coordinates
272
- const result = router.buildResult(path!)
273
- expect(result.coordinates.length).toBeGreaterThan(0)
274
- expect(result.wayIndexes.length).toBeGreaterThan(0)
275
-
276
- // Verify coordinates are in Monaco bbox
277
- for (const coord of result.coordinates) {
278
- expect(coord[0]).toBeGreaterThanOrEqual(7.4)
279
- expect(coord[0]).toBeLessThanOrEqual(7.45)
280
- expect(coord[1]).toBeGreaterThanOrEqual(43.72)
281
- expect(coord[1]).toBeLessThanOrEqual(43.76)
282
- }
283
- })
284
-
285
- it("should return routes with valid way indexes from Monaco", () => {
286
- const graph = buildGraph(monacoOsm)
287
- const router = new Router(monacoOsm, graph)
288
-
289
- const from = graph.findNearestRoutableNode(
290
- monacoOsm,
291
- [7.4229093, 43.7371175],
292
- 500,
293
- )
294
- const to = graph.findNearestRoutableNode(
295
- monacoOsm,
296
- [7.4259193, 43.7377731],
297
- 500,
298
- )
299
-
300
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
301
-
302
- if (path) {
303
- const result = router.buildResult(path)
304
- // Verify way indexes are valid
305
- for (const wayIndex of result.wayIndexes) {
306
- const way = monacoOsm.ways.getByIndex(wayIndex)
307
- expect(way).not.toBeNull()
308
- expect(way?.tags?.["highway"]).toBeDefined()
309
- }
310
- }
311
- })
312
-
313
- it("should find routes using different algorithms in Monaco", () => {
314
- const graph = buildGraph(monacoOsm)
315
- const router = new Router(monacoOsm, graph)
316
-
317
- const from = graph.findNearestRoutableNode(
318
- monacoOsm,
319
- [7.4229093, 43.7371175],
320
- 500,
321
- )
322
- const to = graph.findNearestRoutableNode(
323
- monacoOsm,
324
- [7.4259193, 43.7377731],
325
- 500,
326
- )
327
-
328
- const dijkstraPath = router.route(from!.nodeIndex, to!.nodeIndex, {
329
- algorithm: "dijkstra",
330
- })
331
- const astarPath = router.route(from!.nodeIndex, to!.nodeIndex, {
332
- algorithm: "astar",
333
- })
334
- const bidirectionalPath = router.route(from!.nodeIndex, to!.nodeIndex, {
335
- algorithm: "bidirectional",
336
- })
337
-
338
- // All should find paths
339
- const paths = [dijkstraPath, astarPath, bidirectionalPath].filter(
340
- (p) => p !== null,
341
- )
342
- expect(paths.length).toBe(3)
343
-
344
- // All paths should have segments
345
- for (const path of paths) {
346
- expect(path!.length).toBeGreaterThan(0)
347
- }
348
- })
349
-
350
- it("should handle time-based routing in Monaco", () => {
351
- const graph = buildGraph(monacoOsm)
352
- const router = new Router(monacoOsm, graph)
353
-
354
- const from = graph.findNearestRoutableNode(
355
- monacoOsm,
356
- [7.4229093, 43.7371175],
357
- 500,
358
- )
359
- const to = graph.findNearestRoutableNode(
360
- monacoOsm,
361
- [7.4259193, 43.7377731],
362
- 500,
363
- )
364
-
365
- const path = router.route(from!.nodeIndex, to!.nodeIndex, {
366
- metric: "time",
367
- })
368
-
369
- expect(path).not.toBeNull()
370
- expect(path!.length).toBeGreaterThan(0)
371
- })
372
-
373
- it("should return null when snap fails", () => {
374
- const graph = buildGraph(monacoOsm)
375
-
376
- // Point far outside Monaco
377
- const farPoint = graph.findNearestRoutableNode(monacoOsm, [10.0, 50.0], 100)
378
- expect(farPoint).toBeNull()
379
- })
380
-
381
- it("should calculate route statistics with getRouteStatistics()", () => {
382
- const graph = buildGraph(monacoOsm)
383
- const router = new Router(monacoOsm, graph)
384
-
385
- const from = graph.findNearestRoutableNode(
386
- monacoOsm,
387
- [7.4229093, 43.7371175],
388
- 500,
389
- )
390
- const to = graph.findNearestRoutableNode(
391
- monacoOsm,
392
- [7.4259193, 43.7377731],
393
- 500,
394
- )
395
-
396
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
397
- expect(path).not.toBeNull()
398
-
399
- const stats = router.getRouteStatistics(path!)
400
-
401
- // Should have positive distance and time
402
- expect(stats.distance).toBeGreaterThan(0)
403
- expect(stats.time).toBeGreaterThan(0)
404
-
405
- // Distance should be reasonable for Monaco (< 10km)
406
- expect(stats.distance).toBeLessThan(10_000)
407
- })
408
-
409
- it("should build path info with getRoutePathInfo()", () => {
410
- const graph = buildGraph(monacoOsm)
411
- const router = new Router(monacoOsm, graph)
412
-
413
- const from = graph.findNearestRoutableNode(
414
- monacoOsm,
415
- [7.4229093, 43.7371175],
416
- 500,
417
- )
418
- const to = graph.findNearestRoutableNode(
419
- monacoOsm,
420
- [7.4259193, 43.7377731],
421
- 500,
422
- )
423
-
424
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
425
- expect(path).not.toBeNull()
426
-
427
- const pathInfo = router.getRoutePathInfo(path!)
428
-
429
- // Should have at least one segment
430
- expect(pathInfo.segments.length).toBeGreaterThan(0)
431
-
432
- // Each segment should have required fields
433
- for (const segment of pathInfo.segments) {
434
- expect(Array.isArray(segment.wayIds)).toBe(true)
435
- expect(segment.wayIds.length).toBeGreaterThan(0)
436
- expect(typeof segment.name).toBe("string")
437
- expect(typeof segment.highway).toBe("string")
438
- expect(segment.distance).toBeGreaterThan(0)
439
- expect(segment.time).toBeGreaterThan(0)
440
- }
441
-
442
- // Turn points should be coordinates
443
- for (const turnPoint of pathInfo.turnPoints) {
444
- expect(Array.isArray(turnPoint)).toBe(true)
445
- expect(turnPoint.length).toBe(2)
446
- expect(typeof turnPoint[0]).toBe("number")
447
- expect(typeof turnPoint[1]).toBe("number")
448
- }
449
- })
450
-
451
- it("should include stats in buildResult() when includeStats is true", () => {
452
- const graph = buildGraph(monacoOsm)
453
- const router = new Router(monacoOsm, graph)
454
-
455
- const from = graph.findNearestRoutableNode(
456
- monacoOsm,
457
- [7.4229093, 43.7371175],
458
- 500,
459
- )
460
- const to = graph.findNearestRoutableNode(
461
- monacoOsm,
462
- [7.4259193, 43.7377731],
463
- 500,
464
- )
465
-
466
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
467
- expect(path).not.toBeNull()
468
-
469
- // Without options - no stats
470
- const resultWithoutStats = router.buildResult(path!)
471
- expect(resultWithoutStats.distance).toBeUndefined()
472
- expect(resultWithoutStats.time).toBeUndefined()
473
-
474
- // With includeStats: true
475
- const resultWithStats = router.buildResult(path!, { includeStats: true })
476
- expect(resultWithStats.distance).toBeGreaterThan(0)
477
- expect(resultWithStats.time).toBeGreaterThan(0)
478
- })
479
-
480
- it("should include path info in buildResult() when includePathInfo is true", () => {
481
- const graph = buildGraph(monacoOsm)
482
- const router = new Router(monacoOsm, graph)
483
-
484
- const from = graph.findNearestRoutableNode(
485
- monacoOsm,
486
- [7.4229093, 43.7371175],
487
- 500,
488
- )
489
- const to = graph.findNearestRoutableNode(
490
- monacoOsm,
491
- [7.4259193, 43.7377731],
492
- 500,
493
- )
494
-
495
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
496
- expect(path).not.toBeNull()
497
-
498
- // Without options - no path info
499
- const resultWithoutPathInfo = router.buildResult(path!)
500
- expect(resultWithoutPathInfo.segments).toBeUndefined()
501
- expect(resultWithoutPathInfo.turnPoints).toBeUndefined()
502
-
503
- // With includePathInfo: true
504
- const resultWithPathInfo = router.buildResult(path!, {
505
- includePathInfo: true,
506
- })
507
- expect(resultWithPathInfo.segments).toBeDefined()
508
- expect(resultWithPathInfo.segments!.length).toBeGreaterThan(0)
509
- expect(resultWithPathInfo.turnPoints).toBeDefined()
510
- })
511
-
512
- it("should include both stats and path info when both options are true", () => {
513
- const graph = buildGraph(monacoOsm)
514
- const router = new Router(monacoOsm, graph)
515
-
516
- const from = graph.findNearestRoutableNode(
517
- monacoOsm,
518
- [7.4229093, 43.7371175],
519
- 500,
520
- )
521
- const to = graph.findNearestRoutableNode(
522
- monacoOsm,
523
- [7.4259193, 43.7377731],
524
- 500,
525
- )
526
-
527
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
528
- expect(path).not.toBeNull()
529
-
530
- const result = router.buildResult(path!, {
531
- includeStats: true,
532
- includePathInfo: true,
533
- })
534
-
535
- // Should have coordinates (always present)
536
- expect(result.coordinates.length).toBeGreaterThan(0)
537
-
538
- // Should have stats
539
- expect(result.distance).toBeGreaterThan(0)
540
- expect(result.time).toBeGreaterThan(0)
541
-
542
- // Should have path info
543
- expect(result.segments).toBeDefined()
544
- expect(result.segments!.length).toBeGreaterThan(0)
545
- expect(result.turnPoints).toBeDefined()
546
-
547
- // Verify segment totals roughly match stats
548
- const segmentDistanceSum = result.segments!.reduce(
549
- (sum, seg) => sum + seg.distance,
550
- 0,
551
- )
552
- const segmentTimeSum = result.segments!.reduce(
553
- (sum, seg) => sum + seg.time,
554
- 0,
555
- )
556
-
557
- expect(segmentDistanceSum).toBeCloseTo(result.distance!, 1)
558
- expect(segmentTimeSum).toBeCloseTo(result.time!, 1)
559
- })
560
-
561
- it("should use default options from constructor", () => {
562
- const graph = buildGraph(monacoOsm)
563
- const router = new Router(monacoOsm, graph, {
564
- includeStats: true,
565
- includePathInfo: true,
566
- })
567
-
568
- const from = graph.findNearestRoutableNode(
569
- monacoOsm,
570
- [7.4229093, 43.7371175],
571
- 500,
572
- )
573
- const to = graph.findNearestRoutableNode(
574
- monacoOsm,
575
- [7.4259193, 43.7377731],
576
- 500,
577
- )
578
-
579
- const path = router.route(from!.nodeIndex, to!.nodeIndex)
580
- expect(path).not.toBeNull()
581
-
582
- // buildResult without options should use constructor defaults
583
- const result = router.buildResult(path!)
584
-
585
- expect(result.distance).toBeGreaterThan(0)
586
- expect(result.time).toBeGreaterThan(0)
587
- expect(result.segments).toBeDefined()
588
- expect(result.turnPoints).toBeDefined()
589
- })
590
- })
285
+ const monacoPbf = PBFs["monaco"]!;
286
+ let monacoOsm: Osm;
287
+
288
+ beforeAll(async () => {
289
+ const pbfData = await getFixtureFile(monacoPbf.url);
290
+ monacoOsm = await fromPbf(pbfData);
291
+ });
292
+
293
+ it("should create a router from Monaco OSM data", () => {
294
+ const graph = buildGraph(monacoOsm);
295
+ const router = new Router(monacoOsm, graph);
296
+ expect(router).toBeDefined();
297
+ // Directed edges are counted separately; roundabout and reverse-oneway edges are directional.
298
+ expect(graph.edges).toBe(10_831);
299
+ });
300
+
301
+ it("should find routes between points in Monaco", () => {
302
+ const graph = buildGraph(monacoOsm);
303
+ const router = new Router(monacoOsm, graph);
304
+
305
+ // Use coordinates known to have routeable nodes nearby
306
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
307
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
308
+
309
+ expect(from).not.toBeNull();
310
+ expect(to).not.toBeNull();
311
+
312
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
313
+
314
+ expect(path).not.toBeNull();
315
+ expect(path!.length).toBeGreaterThan(0);
316
+
317
+ // Build result and verify coordinates
318
+ const result = router.buildResult(path!);
319
+ expect(result.coordinates.length).toBeGreaterThan(0);
320
+ expect(result.wayIndexes.length).toBeGreaterThan(0);
321
+
322
+ // Verify coordinates are in Monaco bbox
323
+ for (const coord of result.coordinates) {
324
+ expect(coord[0]).toBeGreaterThanOrEqual(7.4);
325
+ expect(coord[0]).toBeLessThanOrEqual(7.45);
326
+ expect(coord[1]).toBeGreaterThanOrEqual(43.72);
327
+ expect(coord[1]).toBeLessThanOrEqual(43.76);
328
+ }
329
+ });
330
+
331
+ it("should return routes with valid way indexes from Monaco", () => {
332
+ const graph = buildGraph(monacoOsm);
333
+ const router = new Router(monacoOsm, graph);
334
+
335
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
336
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
337
+
338
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
339
+
340
+ if (path) {
341
+ const result = router.buildResult(path);
342
+ // Verify way indexes are valid
343
+ for (const wayIndex of result.wayIndexes) {
344
+ const way = monacoOsm.ways.getByIndex(wayIndex);
345
+ expect(way).not.toBeNull();
346
+ expect(way?.tags?.["highway"]).toBeDefined();
347
+ }
348
+ }
349
+ });
350
+
351
+ it("should find routes using different algorithms in Monaco", () => {
352
+ const graph = buildGraph(monacoOsm);
353
+ const router = new Router(monacoOsm, graph);
354
+
355
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
356
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
357
+
358
+ const dijkstraPath = router.route(from!.nodeIndex, to!.nodeIndex, {
359
+ algorithm: "dijkstra",
360
+ });
361
+ const astarPath = router.route(from!.nodeIndex, to!.nodeIndex, {
362
+ algorithm: "astar",
363
+ });
364
+ const bidirectionalPath = router.route(from!.nodeIndex, to!.nodeIndex, {
365
+ algorithm: "bidirectional",
366
+ });
367
+
368
+ // All should find paths
369
+ const paths = [dijkstraPath, astarPath, bidirectionalPath].filter((p) => p !== null);
370
+ expect(paths.length).toBe(3);
371
+
372
+ // All paths should have segments
373
+ for (const path of paths) {
374
+ expect(path!.length).toBeGreaterThan(0);
375
+ }
376
+ });
377
+
378
+ it("should handle time-based routing in Monaco", () => {
379
+ const graph = buildGraph(monacoOsm);
380
+ const router = new Router(monacoOsm, graph);
381
+
382
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
383
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
384
+
385
+ const path = router.route(from!.nodeIndex, to!.nodeIndex, {
386
+ metric: "time",
387
+ });
388
+
389
+ expect(path).not.toBeNull();
390
+ expect(path!.length).toBeGreaterThan(0);
391
+ });
392
+
393
+ it("should return null when snap fails", () => {
394
+ const graph = buildGraph(monacoOsm);
395
+
396
+ // Point far outside Monaco
397
+ const farPoint = graph.findNearestRoutableNode(monacoOsm, [10.0, 50.0], 100);
398
+ expect(farPoint).toBeNull();
399
+ });
400
+
401
+ it("should calculate route statistics with getRouteStatistics()", () => {
402
+ const graph = buildGraph(monacoOsm);
403
+ const router = new Router(monacoOsm, graph);
404
+
405
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
406
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
407
+
408
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
409
+ expect(path).not.toBeNull();
410
+
411
+ const stats = router.getRouteStatistics(path!);
412
+
413
+ // Should have positive distance and time
414
+ expect(stats.distance).toBeGreaterThan(0);
415
+ expect(stats.time).toBeGreaterThan(0);
416
+
417
+ // Distance should be reasonable for Monaco (< 10km)
418
+ expect(stats.distance).toBeLessThan(10_000);
419
+ });
420
+
421
+ it("should build path info with getRoutePathInfo()", () => {
422
+ const graph = buildGraph(monacoOsm);
423
+ const router = new Router(monacoOsm, graph);
424
+
425
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
426
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
427
+
428
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
429
+ expect(path).not.toBeNull();
430
+
431
+ const pathInfo = router.getRoutePathInfo(path!);
432
+
433
+ // Should have at least one segment
434
+ expect(pathInfo.segments.length).toBeGreaterThan(0);
435
+
436
+ // Each segment should have required fields
437
+ for (const segment of pathInfo.segments) {
438
+ expect(Array.isArray(segment.wayIds)).toBe(true);
439
+ expect(segment.wayIds.length).toBeGreaterThan(0);
440
+ expect(typeof segment.name).toBe("string");
441
+ expect(typeof segment.highway).toBe("string");
442
+ expect(segment.distance).toBeGreaterThan(0);
443
+ expect(segment.time).toBeGreaterThan(0);
444
+ }
445
+
446
+ // Turn points should be coordinates
447
+ for (const turnPoint of pathInfo.turnPoints) {
448
+ expect(Array.isArray(turnPoint)).toBe(true);
449
+ expect(turnPoint.length).toBe(2);
450
+ expect(typeof turnPoint[0]).toBe("number");
451
+ expect(typeof turnPoint[1]).toBe("number");
452
+ }
453
+ });
454
+
455
+ it("should include stats in buildResult() when includeStats is true", () => {
456
+ const graph = buildGraph(monacoOsm);
457
+ const router = new Router(monacoOsm, graph);
458
+
459
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
460
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
461
+
462
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
463
+ expect(path).not.toBeNull();
464
+
465
+ // Without options - no stats
466
+ const resultWithoutStats = router.buildResult(path!);
467
+ expect(resultWithoutStats.distance).toBeUndefined();
468
+ expect(resultWithoutStats.time).toBeUndefined();
469
+
470
+ // With includeStats: true
471
+ const resultWithStats = router.buildResult(path!, { includeStats: true });
472
+ expect(resultWithStats.distance).toBeGreaterThan(0);
473
+ expect(resultWithStats.time).toBeGreaterThan(0);
474
+ });
475
+
476
+ it("should include path info in buildResult() when includePathInfo is true", () => {
477
+ const graph = buildGraph(monacoOsm);
478
+ const router = new Router(monacoOsm, graph);
479
+
480
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
481
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
482
+
483
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
484
+ expect(path).not.toBeNull();
485
+
486
+ // Without options - no path info
487
+ const resultWithoutPathInfo = router.buildResult(path!);
488
+ expect(resultWithoutPathInfo.segments).toBeUndefined();
489
+ expect(resultWithoutPathInfo.turnPoints).toBeUndefined();
490
+
491
+ // With includePathInfo: true
492
+ const resultWithPathInfo = router.buildResult(path!, {
493
+ includePathInfo: true,
494
+ });
495
+ expect(resultWithPathInfo.segments).toBeDefined();
496
+ expect(resultWithPathInfo.segments!.length).toBeGreaterThan(0);
497
+ expect(resultWithPathInfo.turnPoints).toBeDefined();
498
+ });
499
+
500
+ it("should include both stats and path info when both options are true", () => {
501
+ const graph = buildGraph(monacoOsm);
502
+ const router = new Router(monacoOsm, graph);
503
+
504
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
505
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
506
+
507
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
508
+ expect(path).not.toBeNull();
509
+
510
+ const result = router.buildResult(path!, {
511
+ includeStats: true,
512
+ includePathInfo: true,
513
+ });
514
+
515
+ // Should have coordinates (always present)
516
+ expect(result.coordinates.length).toBeGreaterThan(0);
517
+
518
+ // Should have stats
519
+ expect(result.distance).toBeGreaterThan(0);
520
+ expect(result.time).toBeGreaterThan(0);
521
+
522
+ // Should have path info
523
+ expect(result.segments).toBeDefined();
524
+ expect(result.segments!.length).toBeGreaterThan(0);
525
+ expect(result.turnPoints).toBeDefined();
526
+
527
+ // Verify segment totals roughly match stats
528
+ const segmentDistanceSum = result.segments!.reduce((sum, seg) => sum + seg.distance, 0);
529
+ const segmentTimeSum = result.segments!.reduce((sum, seg) => sum + seg.time, 0);
530
+
531
+ expect(segmentDistanceSum).toBeCloseTo(result.distance!, 1);
532
+ expect(segmentTimeSum).toBeCloseTo(result.time!, 1);
533
+ });
534
+
535
+ it("should use default options from constructor", () => {
536
+ const graph = buildGraph(monacoOsm);
537
+ const router = new Router(monacoOsm, graph, {
538
+ includeStats: true,
539
+ includePathInfo: true,
540
+ });
541
+
542
+ const from = graph.findNearestRoutableNode(monacoOsm, [7.4229093, 43.7371175], 500);
543
+ const to = graph.findNearestRoutableNode(monacoOsm, [7.4259193, 43.7377731], 500);
544
+
545
+ const path = router.route(from!.nodeIndex, to!.nodeIndex);
546
+ expect(path).not.toBeNull();
547
+
548
+ // buildResult without options should use constructor defaults
549
+ const result = router.buildResult(path!);
550
+
551
+ expect(result.distance).toBeGreaterThan(0);
552
+ expect(result.time).toBeGreaterThan(0);
553
+ expect(result.segments).toBeDefined();
554
+ expect(result.turnPoints).toBeDefined();
555
+ });
556
+ });
591
557
 
592
558
  describe("RoutingGraph Serialization", () => {
593
- it("should serialize and reconstruct graph correctly", () => {
594
- const osm = new Osm({ id: "test" })
595
-
596
- // Create a simple network
597
- osm.nodes.addNode({ id: 1, lat: 0, lon: 0 })
598
- osm.nodes.addNode({ id: 2, lat: 0, lon: 0.01 })
599
- osm.nodes.addNode({ id: 3, lat: 0.01, lon: 0.01 })
600
- osm.nodes.buildIndex()
601
- osm.nodes.buildSpatialIndex()
602
-
603
- osm.ways.addWay({
604
- id: 1,
605
- refs: [1, 2, 3],
606
- tags: { highway: "primary" },
607
- })
608
- osm.ways.buildIndex()
609
- osm.ways.buildSpatialIndex()
610
- osm.buildIndexes()
611
-
612
- // Build and compact the graph
613
- const originalGraph = buildGraph(osm)
614
-
615
- // Get transferables
616
- const transferables = originalGraph.transferables()
617
-
618
- // Verify transferables structure
619
- expect(transferables.nodeCount).toBe(osm.nodes.size)
620
- expect(transferables.edgeCount).toBe(originalGraph.edges)
621
- // Buffers can be either ArrayBuffer or SharedArrayBuffer depending on runtime
622
- const isBuffer = (b: unknown) =>
623
- b instanceof ArrayBuffer || b instanceof SharedArrayBuffer
624
- expect(isBuffer(transferables.edgeOffsets)).toBe(true)
625
- expect(isBuffer(transferables.edgeTargets)).toBe(true)
626
- expect(isBuffer(transferables.edgeDistances)).toBe(true)
627
- expect(isBuffer(transferables.edgeTimes)).toBe(true)
628
- expect(isBuffer(transferables.routableBits)).toBe(true)
629
- expect(isBuffer(transferables.intersectionBits)).toBe(true)
630
-
631
- // Reconstruct graph from transferables
632
- const reconstructedGraph = new RoutingGraph(transferables)
633
-
634
- // Verify properties match
635
- expect(reconstructedGraph.size).toBe(originalGraph.size)
636
- expect(reconstructedGraph.edges).toBe(originalGraph.edges)
637
-
638
- // Verify edges for each node match
639
- for (let i = 0; i < osm.nodes.size; i++) {
640
- const origEdges = originalGraph.getEdges(i)
641
- const reconEdges = reconstructedGraph.getEdges(i)
642
- expect(reconEdges.length).toBe(origEdges.length)
643
-
644
- for (let j = 0; j < origEdges.length; j++) {
645
- expect(reconEdges[j]!.targetNodeIndex).toBe(
646
- origEdges[j]!.targetNodeIndex,
647
- )
648
- expect(reconEdges[j]!.wayIndex).toBe(origEdges[j]!.wayIndex)
649
- expect(reconEdges[j]!.distance).toBeCloseTo(origEdges[j]!.distance, 2)
650
- expect(reconEdges[j]!.time).toBeCloseTo(origEdges[j]!.time, 2)
651
- }
652
- }
653
-
654
- // Verify routable and intersection flags
655
- expect(reconstructedGraph.isRoutable(0)).toBe(originalGraph.isRoutable(0))
656
- expect(reconstructedGraph.isRoutable(1)).toBe(originalGraph.isRoutable(1))
657
- expect(reconstructedGraph.isRoutable(2)).toBe(originalGraph.isRoutable(2))
658
- expect(reconstructedGraph.isIntersection(0)).toBe(
659
- originalGraph.isIntersection(0),
660
- )
661
- expect(reconstructedGraph.isIntersection(1)).toBe(
662
- originalGraph.isIntersection(1),
663
- )
664
- })
665
-
666
- it("should produce working router after reconstruction", () => {
667
- const osm = new Osm({ id: "test" })
668
-
669
- osm.nodes.addNode({ id: 1, lat: 0, lon: 0 })
670
- osm.nodes.addNode({ id: 2, lat: 0, lon: 0.01 })
671
- osm.nodes.addNode({ id: 3, lat: 0.01, lon: 0.01 })
672
- osm.nodes.buildIndex()
673
- osm.nodes.buildSpatialIndex()
674
-
675
- osm.ways.addWay({
676
- id: 1,
677
- refs: [1, 2, 3],
678
- tags: { highway: "primary" },
679
- })
680
- osm.ways.buildIndex()
681
- osm.ways.buildSpatialIndex()
682
- osm.buildIndexes()
683
-
684
- const originalGraph = buildGraph(osm)
685
- const transferables = originalGraph.transferables()
686
- const reconstructedGraph = new RoutingGraph(transferables)
687
-
688
- // Create routers with both graphs
689
- const router1 = new Router(osm, originalGraph)
690
- const router2 = new Router(osm, reconstructedGraph)
691
-
692
- // Both should find the same route
693
- const path1 = router1.route(0, 2)
694
- const path2 = router2.route(0, 2)
695
-
696
- expect(path1).not.toBeNull()
697
- expect(path2).not.toBeNull()
698
- expect(path1!.length).toBe(path2!.length)
699
-
700
- for (let i = 0; i < path1!.length; i++) {
701
- expect(path1![i]!.nodeIndex).toBe(path2![i]!.nodeIndex)
702
- }
703
- })
704
-
705
- it("should provide transferable buffers for postMessage", () => {
706
- const osm = new Osm({ id: "test" })
707
- osm.nodes.addNode({ id: 1, lat: 0, lon: 0 })
708
- osm.nodes.addNode({ id: 2, lat: 0, lon: 0.01 })
709
- osm.nodes.buildIndex()
710
- osm.nodes.buildSpatialIndex()
711
- osm.ways.addWay({
712
- id: 1,
713
- refs: [1, 2],
714
- tags: { highway: "primary" },
715
- })
716
- osm.ways.buildIndex()
717
- osm.ways.buildSpatialIndex()
718
- osm.buildIndexes()
719
-
720
- const graph = buildGraph(osm)
721
- const transferables = graph.transferables()
722
- const buffers = getTransferableBuffers(transferables)
723
-
724
- // When SharedArrayBuffer is available, returns empty (they're shared, not transferred).
725
- // When only ArrayBuffer is available, returns all 7 buffers.
726
- const usesSharedArrayBuffer =
727
- typeof SharedArrayBuffer !== "undefined" &&
728
- transferables.edgeOffsets instanceof SharedArrayBuffer
729
- if (usesSharedArrayBuffer) {
730
- expect(buffers.length).toBe(0)
731
- } else {
732
- expect(buffers.length).toBe(7)
733
- for (const buffer of buffers) {
734
- expect(buffer).toBeInstanceOf(ArrayBuffer)
735
- }
736
- }
737
- })
738
- })
559
+ it("should serialize and reconstruct graph correctly", () => {
560
+ const osm = new Osm({ id: "test" });
561
+
562
+ // Create a simple network
563
+ osm.nodes.addNode({ id: 1, lat: 0, lon: 0 });
564
+ osm.nodes.addNode({ id: 2, lat: 0, lon: 0.01 });
565
+ osm.nodes.addNode({ id: 3, lat: 0.01, lon: 0.01 });
566
+ osm.nodes.buildIndex();
567
+ osm.nodes.buildSpatialIndex();
568
+
569
+ osm.ways.addWay({
570
+ id: 1,
571
+ refs: [1, 2, 3],
572
+ tags: { highway: "primary" },
573
+ });
574
+ osm.ways.buildIndex();
575
+ osm.ways.buildSpatialIndex();
576
+ osm.buildIndexes();
577
+
578
+ // Build and compact the graph
579
+ const originalGraph = buildGraph(osm);
580
+
581
+ // Get transferables
582
+ const transferables = originalGraph.transferables();
583
+
584
+ // Verify transferables structure
585
+ expect(transferables.nodeCount).toBe(osm.nodes.size);
586
+ expect(transferables.edgeCount).toBe(originalGraph.edges);
587
+ // Buffers can be either ArrayBuffer or SharedArrayBuffer depending on runtime
588
+ const isBuffer = (b: unknown) => b instanceof ArrayBuffer || b instanceof SharedArrayBuffer;
589
+ expect(isBuffer(transferables.edgeOffsets)).toBe(true);
590
+ expect(isBuffer(transferables.edgeTargets)).toBe(true);
591
+ expect(isBuffer(transferables.edgeDistances)).toBe(true);
592
+ expect(isBuffer(transferables.edgeTimes)).toBe(true);
593
+ expect(isBuffer(transferables.routableBits)).toBe(true);
594
+ expect(isBuffer(transferables.intersectionBits)).toBe(true);
595
+
596
+ // Reconstruct graph from transferables
597
+ const reconstructedGraph = new RoutingGraph(transferables);
598
+
599
+ // Verify properties match
600
+ expect(reconstructedGraph.size).toBe(originalGraph.size);
601
+ expect(reconstructedGraph.edges).toBe(originalGraph.edges);
602
+
603
+ // Verify edges for each node match
604
+ for (let i = 0; i < osm.nodes.size; i++) {
605
+ const origEdges = originalGraph.getEdges(i);
606
+ const reconEdges = reconstructedGraph.getEdges(i);
607
+ expect(reconEdges.length).toBe(origEdges.length);
608
+
609
+ for (let j = 0; j < origEdges.length; j++) {
610
+ expect(reconEdges[j]!.targetNodeIndex).toBe(origEdges[j]!.targetNodeIndex);
611
+ expect(reconEdges[j]!.wayIndex).toBe(origEdges[j]!.wayIndex);
612
+ expect(reconEdges[j]!.distance).toBeCloseTo(origEdges[j]!.distance, 2);
613
+ expect(reconEdges[j]!.time).toBeCloseTo(origEdges[j]!.time, 2);
614
+ }
615
+ }
616
+
617
+ // Verify routable and intersection flags
618
+ expect(reconstructedGraph.isRoutable(0)).toBe(originalGraph.isRoutable(0));
619
+ expect(reconstructedGraph.isRoutable(1)).toBe(originalGraph.isRoutable(1));
620
+ expect(reconstructedGraph.isRoutable(2)).toBe(originalGraph.isRoutable(2));
621
+ expect(reconstructedGraph.isIntersection(0)).toBe(originalGraph.isIntersection(0));
622
+ expect(reconstructedGraph.isIntersection(1)).toBe(originalGraph.isIntersection(1));
623
+ });
624
+
625
+ it("should produce working router after reconstruction", () => {
626
+ const osm = new Osm({ id: "test" });
627
+
628
+ osm.nodes.addNode({ id: 1, lat: 0, lon: 0 });
629
+ osm.nodes.addNode({ id: 2, lat: 0, lon: 0.01 });
630
+ osm.nodes.addNode({ id: 3, lat: 0.01, lon: 0.01 });
631
+ osm.nodes.buildIndex();
632
+ osm.nodes.buildSpatialIndex();
633
+
634
+ osm.ways.addWay({
635
+ id: 1,
636
+ refs: [1, 2, 3],
637
+ tags: { highway: "primary" },
638
+ });
639
+ osm.ways.buildIndex();
640
+ osm.ways.buildSpatialIndex();
641
+ osm.buildIndexes();
642
+
643
+ const originalGraph = buildGraph(osm);
644
+ const transferables = originalGraph.transferables();
645
+ const reconstructedGraph = new RoutingGraph(transferables);
646
+
647
+ // Create routers with both graphs
648
+ const router1 = new Router(osm, originalGraph);
649
+ const router2 = new Router(osm, reconstructedGraph);
650
+
651
+ // Both should find the same route
652
+ const path1 = router1.route(0, 2);
653
+ const path2 = router2.route(0, 2);
654
+
655
+ expect(path1).not.toBeNull();
656
+ expect(path2).not.toBeNull();
657
+ expect(path1!.length).toBe(path2!.length);
658
+
659
+ for (let i = 0; i < path1!.length; i++) {
660
+ expect(path1![i]!.nodeIndex).toBe(path2![i]!.nodeIndex);
661
+ }
662
+ });
663
+
664
+ it("should provide transferable buffers for postMessage", () => {
665
+ const osm = new Osm({ id: "test" });
666
+ osm.nodes.addNode({ id: 1, lat: 0, lon: 0 });
667
+ osm.nodes.addNode({ id: 2, lat: 0, lon: 0.01 });
668
+ osm.nodes.buildIndex();
669
+ osm.nodes.buildSpatialIndex();
670
+ osm.ways.addWay({
671
+ id: 1,
672
+ refs: [1, 2],
673
+ tags: { highway: "primary" },
674
+ });
675
+ osm.ways.buildIndex();
676
+ osm.ways.buildSpatialIndex();
677
+ osm.buildIndexes();
678
+
679
+ const graph = buildGraph(osm);
680
+ const transferables = graph.transferables();
681
+ const buffers = getTransferableBuffers(transferables);
682
+
683
+ // When SharedArrayBuffer is available, returns empty (they're shared, not transferred).
684
+ // When only ArrayBuffer is available, returns all 7 buffers.
685
+ const usesSharedArrayBuffer =
686
+ typeof SharedArrayBuffer !== "undefined" &&
687
+ transferables.edgeOffsets instanceof SharedArrayBuffer;
688
+ if (usesSharedArrayBuffer) {
689
+ expect(buffers.length).toBe(0);
690
+ } else {
691
+ expect(buffers.length).toBe(7);
692
+ for (const buffer of buffers) {
693
+ expect(buffer).toBeInstanceOf(ArrayBuffer);
694
+ }
695
+ }
696
+ });
697
+ });