@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.
- package/CHANGELOG.md +33 -0
- package/README.md +49 -19
- package/dist/src/assert.d.ts +24 -0
- package/dist/src/assert.d.ts.map +1 -0
- package/dist/src/assert.js +28 -0
- package/dist/src/assert.js.map +1 -0
- package/dist/src/bbox-intersects.d.ts +15 -0
- package/dist/src/bbox-intersects.d.ts.map +1 -0
- package/dist/src/bbox-intersects.js +24 -0
- package/dist/src/bbox-intersects.js.map +1 -0
- package/dist/src/bytes-to-stream.d.ts +18 -0
- package/dist/src/bytes-to-stream.d.ts.map +1 -0
- package/dist/src/bytes-to-stream.js +25 -0
- package/dist/src/bytes-to-stream.js.map +1 -0
- package/dist/src/concat-bytes.d.ts +5 -0
- package/dist/src/concat-bytes.d.ts.map +1 -0
- package/dist/src/concat-bytes.js +14 -0
- package/dist/src/concat-bytes.js.map +1 -0
- package/dist/src/coordinates.d.ts +28 -0
- package/dist/src/coordinates.d.ts.map +1 -0
- package/dist/src/coordinates.js +38 -0
- package/dist/src/coordinates.js.map +1 -0
- package/dist/src/haversine-distance.d.ts +16 -0
- package/dist/src/haversine-distance.d.ts.map +1 -0
- package/dist/src/haversine-distance.js +26 -0
- package/dist/src/haversine-distance.js.map +1 -0
- package/dist/src/lineclip.d.ts +2 -0
- package/dist/src/lineclip.d.ts.map +1 -0
- package/dist/src/lineclip.js +3 -0
- package/dist/src/lineclip.js.map +1 -0
- package/dist/src/progress.d.ts +40 -0
- package/dist/src/progress.d.ts.map +1 -0
- package/dist/src/progress.js +40 -0
- package/dist/src/progress.js.map +1 -0
- package/dist/src/relation-kind.d.ts +69 -0
- package/dist/src/relation-kind.d.ts.map +1 -0
- package/dist/src/relation-kind.js +375 -0
- package/dist/src/relation-kind.js.map +1 -0
- package/dist/src/relation-multipolygon.d.ts +43 -0
- package/dist/src/relation-multipolygon.d.ts.map +1 -0
- package/dist/src/relation-multipolygon.js +195 -0
- package/dist/src/relation-multipolygon.js.map +1 -0
- package/dist/src/stream-to-bytes.d.ts +18 -0
- package/dist/src/stream-to-bytes.d.ts.map +1 -0
- package/dist/src/stream-to-bytes.js +30 -0
- package/dist/src/stream-to-bytes.js.map +1 -0
- package/dist/src/test/fixtures.d.ts +36 -0
- package/dist/src/test/fixtures.d.ts.map +1 -0
- package/dist/src/test/fixtures.js +172 -0
- package/dist/src/test/fixtures.js.map +1 -0
- package/dist/src/throttle.d.ts +25 -0
- package/dist/src/throttle.d.ts.map +1 -0
- package/dist/src/throttle.js +34 -0
- package/dist/src/throttle.js.map +1 -0
- package/dist/src/tile.d.ts +34 -0
- package/dist/src/tile.d.ts.map +1 -0
- package/dist/src/tile.js +72 -0
- package/dist/src/tile.js.map +1 -0
- package/dist/src/transform-bytes.d.ts +24 -0
- package/dist/src/transform-bytes.d.ts.map +1 -0
- package/dist/src/transform-bytes.js +28 -0
- package/dist/src/transform-bytes.js.map +1 -0
- package/dist/src/types.d.ts +99 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +9 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +70 -0
- package/dist/src/utils.js.map +1 -0
- package/dist/src/way-is-area.d.ts +24 -0
- package/dist/src/way-is-area.d.ts.map +1 -0
- package/dist/src/way-is-area.js +104 -0
- package/dist/src/way-is-area.js.map +1 -0
- package/dist/src/zigzag.d.ts +33 -0
- package/dist/src/zigzag.d.ts.map +1 -0
- package/dist/src/zigzag.js +40 -0
- package/dist/src/zigzag.js.map +1 -0
- package/dist/test/haversine-distance.test.d.ts +2 -0
- package/dist/test/haversine-distance.test.d.ts.map +1 -0
- package/dist/test/haversine-distance.test.js +8 -0
- package/dist/test/haversine-distance.test.js.map +1 -0
- package/dist/test/relation-kind.test.d.ts +2 -0
- package/dist/test/relation-kind.test.d.ts.map +1 -0
- package/dist/test/relation-kind.test.js +367 -0
- package/dist/test/relation-kind.test.js.map +1 -0
- package/dist/test/relation-multipolygon.test.d.ts +2 -0
- package/dist/test/relation-multipolygon.test.d.ts.map +1 -0
- package/dist/test/relation-multipolygon.test.js +237 -0
- package/dist/test/relation-multipolygon.test.js.map +1 -0
- package/dist/test/utils.test.d.ts +2 -0
- package/dist/test/utils.test.d.ts.map +1 -0
- package/dist/test/utils.test.js +76 -0
- package/dist/test/utils.test.js.map +1 -0
- package/dist/test/way-is-area.test.d.ts +2 -0
- package/dist/test/way-is-area.test.d.ts.map +1 -0
- package/dist/test/way-is-area.test.js +31 -0
- package/dist/test/way-is-area.test.js.map +1 -0
- package/package.json +9 -7
- package/src/assert.ts +21 -1
- package/src/bbox-intersects.ts +30 -0
- package/src/bytes-to-stream.ts +17 -0
- package/src/coordinates.ts +45 -0
- package/src/haversine-distance.ts +10 -1
- package/src/progress.ts +55 -0
- package/src/relation-kind.ts +446 -0
- package/src/relation-multipolygon.ts +225 -0
- package/src/stream-to-bytes.ts +17 -0
- package/src/test/fixtures.ts +18 -14
- package/src/throttle.ts +37 -0
- package/src/tile.ts +89 -0
- package/src/transform-bytes.ts +23 -0
- package/src/types.ts +93 -1
- package/src/utils.ts +79 -0
- package/src/way-is-area.ts +107 -0
- package/src/zigzag.ts +42 -0
- package/test/haversine-distance.test.ts +2 -2
- package/test/relation-kind.test.ts +426 -0
- package/test/relation-multipolygon.test.ts +265 -0
- package/test/utils.test.ts +103 -0
- package/test/way-is-area.test.ts +42 -0
- package/tsconfig/base.json +2 -0
- package/tsconfig/test.json +1 -0
- package/src/spherical-mercator.test.ts +0 -42
- 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
|
+
})
|