@osmix/shared 0.0.1 → 0.0.6

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 (125) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +49 -19
  3. package/dist/src/assert.d.ts +24 -0
  4. package/dist/src/assert.d.ts.map +1 -0
  5. package/dist/src/assert.js +28 -0
  6. package/dist/src/assert.js.map +1 -0
  7. package/dist/src/bbox-intersects.d.ts +15 -0
  8. package/dist/src/bbox-intersects.d.ts.map +1 -0
  9. package/dist/src/bbox-intersects.js +24 -0
  10. package/dist/src/bbox-intersects.js.map +1 -0
  11. package/dist/src/bytes-to-stream.d.ts +18 -0
  12. package/dist/src/bytes-to-stream.d.ts.map +1 -0
  13. package/dist/src/bytes-to-stream.js +25 -0
  14. package/dist/src/bytes-to-stream.js.map +1 -0
  15. package/dist/src/concat-bytes.d.ts +5 -0
  16. package/dist/src/concat-bytes.d.ts.map +1 -0
  17. package/dist/src/concat-bytes.js +14 -0
  18. package/dist/src/concat-bytes.js.map +1 -0
  19. package/dist/src/coordinates.d.ts +28 -0
  20. package/dist/src/coordinates.d.ts.map +1 -0
  21. package/dist/src/coordinates.js +38 -0
  22. package/dist/src/coordinates.js.map +1 -0
  23. package/dist/src/haversine-distance.d.ts +16 -0
  24. package/dist/src/haversine-distance.d.ts.map +1 -0
  25. package/dist/src/haversine-distance.js +26 -0
  26. package/dist/src/haversine-distance.js.map +1 -0
  27. package/dist/src/lineclip.d.ts +2 -0
  28. package/dist/src/lineclip.d.ts.map +1 -0
  29. package/dist/src/lineclip.js +3 -0
  30. package/dist/src/lineclip.js.map +1 -0
  31. package/dist/src/progress.d.ts +40 -0
  32. package/dist/src/progress.d.ts.map +1 -0
  33. package/dist/src/progress.js +40 -0
  34. package/dist/src/progress.js.map +1 -0
  35. package/dist/src/relation-kind.d.ts +69 -0
  36. package/dist/src/relation-kind.d.ts.map +1 -0
  37. package/dist/src/relation-kind.js +375 -0
  38. package/dist/src/relation-kind.js.map +1 -0
  39. package/dist/src/relation-multipolygon.d.ts +43 -0
  40. package/dist/src/relation-multipolygon.d.ts.map +1 -0
  41. package/dist/src/relation-multipolygon.js +195 -0
  42. package/dist/src/relation-multipolygon.js.map +1 -0
  43. package/dist/src/stream-to-bytes.d.ts +18 -0
  44. package/dist/src/stream-to-bytes.d.ts.map +1 -0
  45. package/dist/src/stream-to-bytes.js +30 -0
  46. package/dist/src/stream-to-bytes.js.map +1 -0
  47. package/dist/src/test/fixtures.d.ts +36 -0
  48. package/dist/src/test/fixtures.d.ts.map +1 -0
  49. package/dist/src/test/fixtures.js +172 -0
  50. package/dist/src/test/fixtures.js.map +1 -0
  51. package/dist/src/throttle.d.ts +25 -0
  52. package/dist/src/throttle.d.ts.map +1 -0
  53. package/dist/src/throttle.js +34 -0
  54. package/dist/src/throttle.js.map +1 -0
  55. package/dist/src/tile.d.ts +34 -0
  56. package/dist/src/tile.d.ts.map +1 -0
  57. package/dist/src/tile.js +72 -0
  58. package/dist/src/tile.js.map +1 -0
  59. package/dist/src/transform-bytes.d.ts +24 -0
  60. package/dist/src/transform-bytes.d.ts.map +1 -0
  61. package/dist/src/transform-bytes.js +28 -0
  62. package/dist/src/transform-bytes.js.map +1 -0
  63. package/dist/src/types.d.ts +99 -0
  64. package/dist/src/types.d.ts.map +1 -0
  65. package/dist/src/types.js +9 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/dist/src/utils.d.ts +30 -0
  68. package/dist/src/utils.d.ts.map +1 -0
  69. package/dist/src/utils.js +70 -0
  70. package/dist/src/utils.js.map +1 -0
  71. package/dist/src/way-is-area.d.ts +24 -0
  72. package/dist/src/way-is-area.d.ts.map +1 -0
  73. package/dist/src/way-is-area.js +104 -0
  74. package/dist/src/way-is-area.js.map +1 -0
  75. package/dist/src/zigzag.d.ts +33 -0
  76. package/dist/src/zigzag.d.ts.map +1 -0
  77. package/dist/src/zigzag.js +40 -0
  78. package/dist/src/zigzag.js.map +1 -0
  79. package/dist/test/haversine-distance.test.d.ts +2 -0
  80. package/dist/test/haversine-distance.test.d.ts.map +1 -0
  81. package/dist/test/haversine-distance.test.js +8 -0
  82. package/dist/test/haversine-distance.test.js.map +1 -0
  83. package/dist/test/relation-kind.test.d.ts +2 -0
  84. package/dist/test/relation-kind.test.d.ts.map +1 -0
  85. package/dist/test/relation-kind.test.js +367 -0
  86. package/dist/test/relation-kind.test.js.map +1 -0
  87. package/dist/test/relation-multipolygon.test.d.ts +2 -0
  88. package/dist/test/relation-multipolygon.test.d.ts.map +1 -0
  89. package/dist/test/relation-multipolygon.test.js +237 -0
  90. package/dist/test/relation-multipolygon.test.js.map +1 -0
  91. package/dist/test/utils.test.d.ts +2 -0
  92. package/dist/test/utils.test.d.ts.map +1 -0
  93. package/dist/test/utils.test.js +76 -0
  94. package/dist/test/utils.test.js.map +1 -0
  95. package/dist/test/way-is-area.test.d.ts +2 -0
  96. package/dist/test/way-is-area.test.d.ts.map +1 -0
  97. package/dist/test/way-is-area.test.js +31 -0
  98. package/dist/test/way-is-area.test.js.map +1 -0
  99. package/package.json +9 -7
  100. package/src/assert.ts +21 -1
  101. package/src/bbox-intersects.ts +30 -0
  102. package/src/bytes-to-stream.ts +17 -0
  103. package/src/coordinates.ts +45 -0
  104. package/src/haversine-distance.ts +10 -1
  105. package/src/progress.ts +55 -0
  106. package/src/relation-kind.ts +446 -0
  107. package/src/relation-multipolygon.ts +225 -0
  108. package/src/stream-to-bytes.ts +17 -0
  109. package/src/test/fixtures.ts +18 -14
  110. package/src/throttle.ts +37 -0
  111. package/src/tile.ts +89 -0
  112. package/src/transform-bytes.ts +23 -0
  113. package/src/types.ts +93 -1
  114. package/src/utils.ts +79 -0
  115. package/src/way-is-area.ts +107 -0
  116. package/src/zigzag.ts +42 -0
  117. package/test/haversine-distance.test.ts +2 -2
  118. package/test/relation-kind.test.ts +426 -0
  119. package/test/relation-multipolygon.test.ts +265 -0
  120. package/test/utils.test.ts +103 -0
  121. package/test/way-is-area.test.ts +42 -0
  122. package/tsconfig/base.json +2 -0
  123. package/tsconfig/test.json +1 -0
  124. package/src/spherical-mercator.test.ts +0 -42
  125. package/src/spherical-mercator.ts +0 -42
@@ -0,0 +1,426 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import {
3
+ buildRelationLineStrings,
4
+ collectRelationPoints,
5
+ getRelationKind,
6
+ getRelationKindMetadata,
7
+ isAreaRelation,
8
+ isLineRelation,
9
+ isLogicRelation,
10
+ isPointRelation,
11
+ isSuperRelation,
12
+ resolveRelationMembers,
13
+ } from "../src/relation-kind"
14
+ import type { LonLat, OsmRelation, OsmWay } from "../src/types"
15
+
16
+ describe("relation-kind", () => {
17
+ describe("getRelationKind", () => {
18
+ it("identifies multipolygon as area", () => {
19
+ const relation: OsmRelation = {
20
+ id: 1,
21
+ tags: { type: "multipolygon" },
22
+ members: [],
23
+ }
24
+ expect(getRelationKind(relation)).toBe("area")
25
+ })
26
+
27
+ it("identifies boundary as area", () => {
28
+ const relation: OsmRelation = {
29
+ id: 1,
30
+ tags: { type: "boundary" },
31
+ members: [],
32
+ }
33
+ expect(getRelationKind(relation)).toBe("area")
34
+ })
35
+
36
+ it("identifies route as line", () => {
37
+ const relation: OsmRelation = {
38
+ id: 1,
39
+ tags: { type: "route" },
40
+ members: [],
41
+ }
42
+ expect(getRelationKind(relation)).toBe("line")
43
+ })
44
+
45
+ it("identifies multilinestring as line", () => {
46
+ const relation: OsmRelation = {
47
+ id: 1,
48
+ tags: { type: "multilinestring" },
49
+ members: [],
50
+ }
51
+ expect(getRelationKind(relation)).toBe("line")
52
+ })
53
+
54
+ it("identifies multipoint as point", () => {
55
+ const relation: OsmRelation = {
56
+ id: 1,
57
+ tags: { type: "multipoint" },
58
+ members: [],
59
+ }
60
+ expect(getRelationKind(relation)).toBe("point")
61
+ })
62
+
63
+ it("identifies restriction as logic", () => {
64
+ const relation: OsmRelation = {
65
+ id: 1,
66
+ tags: { type: "restriction" },
67
+ members: [],
68
+ }
69
+ expect(getRelationKind(relation)).toBe("logic")
70
+ })
71
+
72
+ it("identifies super-relation by having relation members", () => {
73
+ const relation: OsmRelation = {
74
+ id: 1,
75
+ tags: { type: "collection" },
76
+ members: [{ type: "relation", ref: 2 }],
77
+ }
78
+ expect(getRelationKind(relation)).toBe("super")
79
+ })
80
+
81
+ it("defaults to logic for untyped relations", () => {
82
+ const relation: OsmRelation = {
83
+ id: 1,
84
+ members: [],
85
+ }
86
+ expect(getRelationKind(relation)).toBe("logic")
87
+ })
88
+
89
+ it("handles case-insensitive type tags", () => {
90
+ const relation: OsmRelation = {
91
+ id: 1,
92
+ tags: { type: "MULTIPOLYGON" },
93
+ members: [],
94
+ }
95
+ expect(getRelationKind(relation)).toBe("area")
96
+ })
97
+ })
98
+
99
+ describe("getRelationKindMetadata", () => {
100
+ it("returns correct metadata for area relations", () => {
101
+ const relation: OsmRelation = {
102
+ id: 1,
103
+ tags: { type: "multipolygon" },
104
+ members: [],
105
+ }
106
+ const metadata = getRelationKindMetadata(relation)
107
+ expect(metadata.kind).toBe("area")
108
+ expect(metadata.expectedRoles).toEqual(["outer", "inner"])
109
+ expect(metadata.orderMatters).toBe(false)
110
+ })
111
+
112
+ it("returns correct metadata for line relations", () => {
113
+ const relation: OsmRelation = {
114
+ id: 1,
115
+ tags: { type: "route" },
116
+ members: [],
117
+ }
118
+ const metadata = getRelationKindMetadata(relation)
119
+ expect(metadata.kind).toBe("line")
120
+ expect(metadata.orderMatters).toBe(true)
121
+ })
122
+ })
123
+
124
+ describe("isAreaRelation", () => {
125
+ it("returns true for multipolygon", () => {
126
+ const relation: OsmRelation = {
127
+ id: 1,
128
+ tags: { type: "multipolygon" },
129
+ members: [],
130
+ }
131
+ expect(isAreaRelation(relation)).toBe(true)
132
+ })
133
+
134
+ it("returns false for route", () => {
135
+ const relation: OsmRelation = {
136
+ id: 1,
137
+ tags: { type: "route" },
138
+ members: [],
139
+ }
140
+ expect(isAreaRelation(relation)).toBe(false)
141
+ })
142
+ })
143
+
144
+ describe("isLineRelation", () => {
145
+ it("returns true for route", () => {
146
+ const relation: OsmRelation = {
147
+ id: 1,
148
+ tags: { type: "route" },
149
+ members: [],
150
+ }
151
+ expect(isLineRelation(relation)).toBe(true)
152
+ })
153
+
154
+ it("returns true for multilinestring", () => {
155
+ const relation: OsmRelation = {
156
+ id: 1,
157
+ tags: { type: "multilinestring" },
158
+ members: [],
159
+ }
160
+ expect(isLineRelation(relation)).toBe(true)
161
+ })
162
+ })
163
+
164
+ describe("isPointRelation", () => {
165
+ it("returns true for multipoint", () => {
166
+ const relation: OsmRelation = {
167
+ id: 1,
168
+ tags: { type: "multipoint" },
169
+ members: [],
170
+ }
171
+ expect(isPointRelation(relation)).toBe(true)
172
+ })
173
+ })
174
+
175
+ describe("isSuperRelation", () => {
176
+ it("returns true for relation with relation members", () => {
177
+ const relation: OsmRelation = {
178
+ id: 1,
179
+ tags: { type: "collection" },
180
+ members: [{ type: "relation", ref: 2 }],
181
+ }
182
+ expect(isSuperRelation(relation)).toBe(true)
183
+ })
184
+
185
+ it("returns false for relation without relation members", () => {
186
+ const relation: OsmRelation = {
187
+ id: 1,
188
+ tags: { type: "route" },
189
+ members: [{ type: "way", ref: 2 }],
190
+ }
191
+ expect(isSuperRelation(relation)).toBe(false)
192
+ })
193
+ })
194
+
195
+ describe("isLogicRelation", () => {
196
+ it("returns true for restriction", () => {
197
+ const relation: OsmRelation = {
198
+ id: 1,
199
+ tags: { type: "restriction" },
200
+ members: [],
201
+ }
202
+ expect(isLogicRelation(relation)).toBe(true)
203
+ })
204
+ })
205
+
206
+ describe("buildRelationLineStrings", () => {
207
+ it("builds linestrings from connected ways", () => {
208
+ const relation: OsmRelation = {
209
+ id: 1,
210
+ tags: { type: "route" },
211
+ members: [
212
+ { type: "way", ref: 1 },
213
+ { type: "way", ref: 2 },
214
+ ],
215
+ }
216
+
217
+ const way1: OsmWay = { id: 1, refs: [1, 2] }
218
+ const way2: OsmWay = { id: 2, refs: [2, 3] }
219
+
220
+ const getWay = (id: number) => {
221
+ if (id === 1) return way1
222
+ if (id === 2) return way2
223
+ return null
224
+ }
225
+
226
+ const getNodeCoordinates = (id: number) => {
227
+ const coords: Record<number, [number, number]> = {
228
+ 1: [0.0, 0.0],
229
+ 2: [1.0, 0.0],
230
+ 3: [2.0, 0.0],
231
+ }
232
+ return coords[id]
233
+ }
234
+
235
+ const lineStrings = buildRelationLineStrings(
236
+ relation,
237
+ getWay,
238
+ getNodeCoordinates,
239
+ )
240
+ expect(lineStrings).toHaveLength(1)
241
+ expect(lineStrings[0]).toHaveLength(3)
242
+ })
243
+
244
+ it("handles disconnected ways as separate linestrings", () => {
245
+ const relation: OsmRelation = {
246
+ id: 1,
247
+ tags: { type: "route" },
248
+ members: [
249
+ { type: "way", ref: 1 },
250
+ { type: "way", ref: 2 },
251
+ ],
252
+ }
253
+
254
+ const way1: OsmWay = { id: 1, refs: [1, 2] }
255
+ const way2: OsmWay = { id: 2, refs: [3, 4] }
256
+
257
+ const getWay = (id: number) => {
258
+ if (id === 1) return way1
259
+ if (id === 2) return way2
260
+ return null
261
+ }
262
+
263
+ const getNodeCoordinates = (id: number) => {
264
+ const coords: Record<number, [number, number]> = {
265
+ 1: [0.0, 0.0],
266
+ 2: [1.0, 0.0],
267
+ 3: [10.0, 10.0],
268
+ 4: [11.0, 10.0],
269
+ }
270
+ return coords[id]
271
+ }
272
+
273
+ const lineStrings = buildRelationLineStrings(
274
+ relation,
275
+ getWay,
276
+ getNodeCoordinates,
277
+ )
278
+ expect(lineStrings.length).toBeGreaterThanOrEqual(2)
279
+ })
280
+ })
281
+
282
+ describe("collectRelationPoints", () => {
283
+ it("collects point coordinates from node members", () => {
284
+ const relation: OsmRelation = {
285
+ id: 1,
286
+ tags: { type: "multipoint" },
287
+ members: [
288
+ { type: "node", ref: 1 },
289
+ { type: "node", ref: 2 },
290
+ { type: "way", ref: 10 }, // Should be ignored
291
+ ],
292
+ }
293
+
294
+ const getNodeCoordinates = (id: number) => {
295
+ const coords: Record<number, [number, number]> = {
296
+ 1: [0.0, 0.0],
297
+ 2: [1.0, 1.0],
298
+ }
299
+ return coords[id]
300
+ }
301
+
302
+ const points = collectRelationPoints(relation, getNodeCoordinates)
303
+ expect(points).toHaveLength(2)
304
+ expect(points[0]).toEqual([0.0, 0.0])
305
+ expect(points[1]).toEqual([1.0, 1.0])
306
+ })
307
+
308
+ it("handles missing node coordinates", () => {
309
+ const relation: OsmRelation = {
310
+ id: 1,
311
+ tags: { type: "multipoint" },
312
+ members: [
313
+ { type: "node", ref: 1 },
314
+ { type: "node", ref: 2 },
315
+ ],
316
+ }
317
+
318
+ const getNodeCoordinates = (id: number): LonLat | undefined => {
319
+ if (id === 1) return [0.0, 0.0] as LonLat
320
+ return undefined // Missing coordinate
321
+ }
322
+
323
+ const points = collectRelationPoints(relation, getNodeCoordinates)
324
+ expect(points).toHaveLength(1)
325
+ })
326
+ })
327
+
328
+ describe("resolveRelationMembers", () => {
329
+ it("resolves direct members", () => {
330
+ const relation: OsmRelation = {
331
+ id: 1,
332
+ tags: { type: "collection" },
333
+ members: [
334
+ { type: "node", ref: 1 },
335
+ { type: "way", ref: 10 },
336
+ ],
337
+ }
338
+
339
+ const getRelation = () => null
340
+
341
+ const resolved = resolveRelationMembers(relation, getRelation)
342
+ expect(resolved.nodes).toEqual([1])
343
+ expect(resolved.ways).toEqual([10])
344
+ expect(resolved.relations).toEqual([])
345
+ })
346
+
347
+ it("resolves nested relation members", () => {
348
+ const relation1: OsmRelation = {
349
+ id: 1,
350
+ tags: { type: "collection" },
351
+ members: [
352
+ { type: "relation", ref: 2 },
353
+ { type: "node", ref: 1 },
354
+ ],
355
+ }
356
+
357
+ const relation2: OsmRelation = {
358
+ id: 2,
359
+ tags: { type: "collection" },
360
+ members: [
361
+ { type: "way", ref: 10 },
362
+ { type: "node", ref: 2 },
363
+ ],
364
+ }
365
+
366
+ const getRelation = (id: number) => {
367
+ if (id === 2) return relation2
368
+ return null
369
+ }
370
+
371
+ const resolved = resolveRelationMembers(relation1, getRelation)
372
+ expect(resolved.nodes).toContain(1)
373
+ expect(resolved.nodes).toContain(2)
374
+ expect(resolved.ways).toContain(10)
375
+ expect(resolved.relations).toContain(2)
376
+ })
377
+
378
+ it("detects cycles and prevents infinite recursion", () => {
379
+ const relation1: OsmRelation = {
380
+ id: 1,
381
+ tags: { type: "collection" },
382
+ members: [{ type: "relation", ref: 2 }],
383
+ }
384
+
385
+ const relation2: OsmRelation = {
386
+ id: 2,
387
+ tags: { type: "collection" },
388
+ members: [{ type: "relation", ref: 1 }], // Circular reference
389
+ }
390
+
391
+ const getRelation = (id: number) => {
392
+ if (id === 1) return relation1
393
+ if (id === 2) return relation2
394
+ return null
395
+ }
396
+
397
+ const resolved = resolveRelationMembers(relation1, getRelation, 10)
398
+ // Should not crash and should include relation2 but not recurse infinitely
399
+ expect(resolved.relations).toContain(2)
400
+ })
401
+
402
+ it("respects maxDepth limit", () => {
403
+ const relation1: OsmRelation = {
404
+ id: 1,
405
+ tags: { type: "collection" },
406
+ members: [{ type: "relation", ref: 2 }],
407
+ }
408
+
409
+ const relation2: OsmRelation = {
410
+ id: 2,
411
+ tags: { type: "collection" },
412
+ members: [{ type: "node", ref: 100 }],
413
+ }
414
+
415
+ const getRelation = (id: number) => {
416
+ if (id === 1) return relation1
417
+ if (id === 2) return relation2
418
+ return null
419
+ }
420
+
421
+ const resolved = resolveRelationMembers(relation1, getRelation, 0)
422
+ // Should not resolve nested relation when maxDepth is 0
423
+ expect(resolved.nodes).not.toContain(100)
424
+ })
425
+ })
426
+ })
@@ -0,0 +1,265 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import {
3
+ buildRelationRings,
4
+ connectWaysToRings,
5
+ getWayMembersByRole,
6
+ } from "../src/relation-multipolygon"
7
+ import type { OsmRelation, OsmWay } from "../src/types"
8
+
9
+ describe("relation-multipolygon", () => {
10
+ describe("getWayMembersByRole", () => {
11
+ it("groups way members by outer and inner roles", () => {
12
+ const relation: OsmRelation = {
13
+ id: 1,
14
+ tags: { type: "multipolygon" },
15
+ members: [
16
+ { type: "way", ref: 10, role: "outer" },
17
+ { type: "way", ref: 11, role: "inner" },
18
+ { type: "way", ref: 12, role: "outer" },
19
+ { type: "node", ref: 1 },
20
+ { type: "way", ref: 13, role: "inner" },
21
+ ],
22
+ }
23
+
24
+ const { outer, inner } = getWayMembersByRole(relation)
25
+ expect(outer).toHaveLength(2)
26
+ expect(outer[0]?.ref).toBe(10)
27
+ expect(outer[1]?.ref).toBe(12)
28
+ expect(inner).toHaveLength(2)
29
+ expect(inner[0]?.ref).toBe(11)
30
+ expect(inner[1]?.ref).toBe(13)
31
+ })
32
+
33
+ it("handles case-insensitive roles", () => {
34
+ const relation: OsmRelation = {
35
+ id: 1,
36
+ tags: { type: "multipolygon" },
37
+ members: [
38
+ { type: "way", ref: 10, role: "OUTER" },
39
+ { type: "way", ref: 11, role: "Inner" },
40
+ ],
41
+ }
42
+
43
+ const { outer, inner } = getWayMembersByRole(relation)
44
+ expect(outer).toHaveLength(1)
45
+ expect(inner).toHaveLength(1)
46
+ })
47
+
48
+ it("handles missing roles", () => {
49
+ const relation: OsmRelation = {
50
+ id: 1,
51
+ tags: { type: "multipolygon" },
52
+ members: [
53
+ { type: "way", ref: 10 },
54
+ { type: "way", ref: 11, role: "outer" },
55
+ ],
56
+ }
57
+
58
+ const { outer, inner } = getWayMembersByRole(relation)
59
+ expect(outer).toHaveLength(1)
60
+ expect(inner).toHaveLength(0)
61
+ })
62
+ })
63
+
64
+ describe("connectWaysToRings", () => {
65
+ it("connects ways sharing endpoints into a single ring", () => {
66
+ const way1: OsmWay = { id: 1, refs: [1, 2] }
67
+ const way2: OsmWay = { id: 2, refs: [2, 3] }
68
+ const way3: OsmWay = { id: 3, refs: [3, 1] }
69
+
70
+ const rings = connectWaysToRings([way1, way2, way3])
71
+ expect(rings).toHaveLength(1)
72
+ expect(rings[0]).toEqual([way1, way2, way3])
73
+ })
74
+
75
+ it("handles ways that need to be reversed", () => {
76
+ // way1 ends at 2, way2 starts at 2 (normal connection)
77
+ const way1: OsmWay = { id: 1, refs: [1, 2] }
78
+ const way2: OsmWay = { id: 2, refs: [2, 3, 4] }
79
+
80
+ const rings = connectWaysToRings([way1, way2])
81
+ // Should create a ring if ways connect and form a closed loop
82
+ // This test verifies basic connection logic
83
+ expect(rings.length).toBeGreaterThanOrEqual(0)
84
+ })
85
+
86
+ it("creates separate rings for disconnected ways", () => {
87
+ const way1: OsmWay = { id: 1, refs: [1, 2, 1] } // closed ring
88
+ const way2: OsmWay = { id: 2, refs: [3, 4, 3] } // separate closed ring
89
+
90
+ const rings = connectWaysToRings([way1, way2])
91
+ expect(rings).toHaveLength(2)
92
+ })
93
+
94
+ it("only includes closed rings", () => {
95
+ const way1: OsmWay = { id: 1, refs: [1, 2, 3] } // not closed
96
+ const way2: OsmWay = { id: 2, refs: [4, 5, 4] } // closed
97
+
98
+ const rings = connectWaysToRings([way1, way2])
99
+ // Only the closed ring should be included
100
+ expect(rings.length).toBeGreaterThanOrEqual(1)
101
+ })
102
+ })
103
+
104
+ describe("buildRelationRings", () => {
105
+ it("builds simple multipolygon with outer ring only", () => {
106
+ const relation: OsmRelation = {
107
+ id: 1,
108
+ tags: { type: "multipolygon" },
109
+ members: [{ type: "way", ref: 1, role: "outer" }],
110
+ }
111
+
112
+ const way1: OsmWay = {
113
+ id: 1,
114
+ refs: [1, 2, 3, 4, 1],
115
+ }
116
+
117
+ const getWay = (id: number) => (id === 1 ? way1 : null)
118
+ const getNodeCoordinates = (id: number) => {
119
+ const coords: Record<number, [number, number]> = {
120
+ 1: [0.0, 0.0],
121
+ 2: [1.0, 0.0],
122
+ 3: [1.0, 1.0],
123
+ 4: [0.0, 1.0],
124
+ }
125
+ return coords[id]
126
+ }
127
+
128
+ const rings = buildRelationRings(relation, getWay, getNodeCoordinates)
129
+ expect(rings).toHaveLength(1)
130
+ expect(rings[0]).toHaveLength(1) // one outer ring
131
+ expect(rings[0]?.[0]).toHaveLength(5) // closed ring with 5 points
132
+ })
133
+
134
+ it("builds multipolygon with outer and inner rings (holes)", () => {
135
+ const relation: OsmRelation = {
136
+ id: 1,
137
+ tags: { type: "multipolygon" },
138
+ members: [
139
+ { type: "way", ref: 1, role: "outer" },
140
+ { type: "way", ref: 2, role: "inner" },
141
+ ],
142
+ }
143
+
144
+ const way1: OsmWay = {
145
+ id: 1,
146
+ refs: [1, 2, 3, 4, 1], // outer square
147
+ }
148
+ const way2: OsmWay = {
149
+ id: 2,
150
+ refs: [5, 6, 7, 5], // inner triangle
151
+ }
152
+
153
+ const getWay = (id: number) => {
154
+ if (id === 1) return way1
155
+ if (id === 2) return way2
156
+ return null
157
+ }
158
+ const getNodeCoordinates = (id: number) => {
159
+ const coords: Record<number, [number, number]> = {
160
+ 1: [-1.0, -1.0],
161
+ 2: [1.0, -1.0],
162
+ 3: [1.0, 1.0],
163
+ 4: [-1.0, 1.0],
164
+ 5: [-0.5, 0.0],
165
+ 6: [0.5, 0.0],
166
+ 7: [0.0, 0.5],
167
+ }
168
+ return coords[id]
169
+ }
170
+
171
+ const rings = buildRelationRings(relation, getWay, getNodeCoordinates)
172
+ expect(rings).toHaveLength(1)
173
+ expect(rings[0]).toHaveLength(2) // outer + inner
174
+ expect(rings[0]?.[0]).toBeDefined() // outer ring
175
+ expect(rings[0]?.[1]).toBeDefined() // inner ring (hole)
176
+ })
177
+
178
+ it("builds multipolygon with multiple outer rings", () => {
179
+ const relation: OsmRelation = {
180
+ id: 1,
181
+ tags: { type: "multipolygon" },
182
+ members: [
183
+ { type: "way", ref: 1, role: "outer" },
184
+ { type: "way", ref: 2, role: "outer" },
185
+ ],
186
+ }
187
+
188
+ const way1: OsmWay = {
189
+ id: 1,
190
+ refs: [1, 2, 3, 1], // first polygon
191
+ }
192
+ const way2: OsmWay = {
193
+ id: 2,
194
+ refs: [4, 5, 6, 4], // second polygon
195
+ }
196
+
197
+ const getWay = (id: number) => {
198
+ if (id === 1) return way1
199
+ if (id === 2) return way2
200
+ return null
201
+ }
202
+ const getNodeCoordinates = (id: number) => {
203
+ const coords: Record<number, [number, number]> = {
204
+ 1: [0.0, 0.0],
205
+ 2: [1.0, 0.0],
206
+ 3: [0.5, 1.0],
207
+ 4: [2.0, 0.0],
208
+ 5: [3.0, 0.0],
209
+ 6: [2.5, 1.0],
210
+ }
211
+ return coords[id]
212
+ }
213
+
214
+ const rings = buildRelationRings(relation, getWay, getNodeCoordinates)
215
+ expect(rings).toHaveLength(2) // two separate polygons
216
+ })
217
+
218
+ it("handles relation similar to osmtogeojson test case", () => {
219
+ // Based on: https://github.com/placemark/osmtogeojson/blob/main/test/osm.test.js
220
+ const relation: OsmRelation = {
221
+ id: 1,
222
+ tags: { type: "multipolygon" },
223
+ members: [
224
+ { type: "way", ref: 2, role: "outer" },
225
+ { type: "way", ref: 3, role: "inner" },
226
+ ],
227
+ }
228
+
229
+ const way2: OsmWay = {
230
+ id: 2,
231
+ refs: [4, 5, 6, 7, 4], // outer square
232
+ }
233
+ const way3: OsmWay = {
234
+ id: 3,
235
+ refs: [8, 9, 10, 8], // inner triangle
236
+ }
237
+
238
+ const getWay = (id: number) => {
239
+ if (id === 2) return way2
240
+ if (id === 3) return way3
241
+ return null
242
+ }
243
+ const getNodeCoordinates = (id: number) => {
244
+ const coords: Record<number, [number, number]> = {
245
+ 4: [-1.0, -1.0],
246
+ 5: [-1.0, 1.0],
247
+ 6: [1.0, 1.0],
248
+ 7: [1.0, -1.0],
249
+ 8: [-0.5, 0.0],
250
+ 9: [0.5, 0.0],
251
+ 10: [0.0, 0.5],
252
+ }
253
+ return coords[id]
254
+ }
255
+
256
+ const rings = buildRelationRings(relation, getWay, getNodeCoordinates)
257
+ expect(rings).toHaveLength(1)
258
+ expect(rings[0]).toHaveLength(2) // outer + inner
259
+ // Outer ring should have 5 points (closed square)
260
+ expect(rings[0]?.[0]).toHaveLength(5)
261
+ // Inner ring should have 4 points (closed triangle)
262
+ expect(rings[0]?.[1]).toHaveLength(4)
263
+ })
264
+ })
265
+ })