@osmix/geojson 0.0.4

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 (39) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +129 -0
  3. package/dist/src/entity-to-feature.d.ts +86 -0
  4. package/dist/src/entity-to-feature.d.ts.map +1 -0
  5. package/dist/src/entity-to-feature.js +345 -0
  6. package/dist/src/entity-to-feature.js.map +1 -0
  7. package/dist/src/index.d.ts +35 -0
  8. package/dist/src/index.d.ts.map +1 -0
  9. package/dist/src/index.js +35 -0
  10. package/dist/src/index.js.map +1 -0
  11. package/dist/src/osm-from-geojson.d.ts +65 -0
  12. package/dist/src/osm-from-geojson.d.ts.map +1 -0
  13. package/dist/src/osm-from-geojson.js +303 -0
  14. package/dist/src/osm-from-geojson.js.map +1 -0
  15. package/dist/src/types.d.ts +49 -0
  16. package/dist/src/types.d.ts.map +1 -0
  17. package/dist/src/types.js +5 -0
  18. package/dist/src/types.js.map +1 -0
  19. package/dist/src/utils.d.ts +30 -0
  20. package/dist/src/utils.d.ts.map +1 -0
  21. package/dist/src/utils.js +54 -0
  22. package/dist/src/utils.js.map +1 -0
  23. package/dist/test/entity-to-feature.test.d.ts +2 -0
  24. package/dist/test/entity-to-feature.test.d.ts.map +1 -0
  25. package/dist/test/entity-to-feature.test.js +57 -0
  26. package/dist/test/entity-to-feature.test.js.map +1 -0
  27. package/dist/test/osm-from-geojson.test.d.ts +2 -0
  28. package/dist/test/osm-from-geojson.test.d.ts.map +1 -0
  29. package/dist/test/osm-from-geojson.test.js +778 -0
  30. package/dist/test/osm-from-geojson.test.js.map +1 -0
  31. package/package.json +54 -0
  32. package/src/entity-to-feature.ts +427 -0
  33. package/src/index.ts +35 -0
  34. package/src/osm-from-geojson.ts +360 -0
  35. package/src/types.ts +77 -0
  36. package/src/utils.ts +58 -0
  37. package/test/entity-to-feature.test.ts +65 -0
  38. package/test/osm-from-geojson.test.ts +868 -0
  39. package/tsconfig.json +9 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * GeoJSON-to-OSM conversion utilities.
3
+ *
4
+ * Imports GeoJSON FeatureCollections into Osm indexes, mapping geometry
5
+ * types to appropriate OSM entity structures.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { Osm, type OsmOptions } from "@osmix/core"
11
+ import {
12
+ logProgress,
13
+ type ProgressEvent,
14
+ progressEvent,
15
+ } from "@osmix/shared/progress"
16
+ import type { OsmRelationMember, OsmTags } from "@osmix/shared/types"
17
+ import { rewindFeature } from "@placemarkio/geojson-rewind"
18
+ import type {
19
+ FeatureCollection,
20
+ LineString,
21
+ MultiPolygon,
22
+ Point,
23
+ Polygon,
24
+ } from "geojson"
25
+ import type { ReadOsmDataTypes } from "./types"
26
+ import { readDataAsGeoJSON } from "./utils"
27
+
28
+ /**
29
+ * Create an Osm index from GeoJSON data.
30
+ *
31
+ * Accepts various input formats (string, stream, buffer, or parsed object)
32
+ * and converts features to OSM entities:
33
+ * - Point → Node
34
+ * - LineString → Way with nodes
35
+ * - Polygon → Way (simple) or Relation (with holes)
36
+ * - MultiPolygon → Relation
37
+ *
38
+ * Feature IDs are preserved if numeric; otherwise sequential negative IDs are assigned.
39
+ * Feature properties become OSM tags.
40
+ *
41
+ * @param data - GeoJSON data in any supported format.
42
+ * @param options - Osm index options (id, header).
43
+ * @param onProgress - Progress callback for UI feedback.
44
+ * @returns Populated Osm index with built indexes.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { fromGeoJSON } from "@osmix/geojson"
49
+ *
50
+ * // From file
51
+ * const geojson = await Bun.file('./roads.geojson').json()
52
+ * const osm = await fromGeoJSON(geojson, { id: "roads" })
53
+ *
54
+ * // Query the imported data
55
+ * const highways = osm.ways.search("highway")
56
+ * ```
57
+ */
58
+ export async function fromGeoJSON(
59
+ data: ReadOsmDataTypes,
60
+ options: Partial<OsmOptions> = {},
61
+ onProgress: (progress: ProgressEvent) => void = logProgress,
62
+ ): Promise<Osm> {
63
+ const osm = new Osm(options)
64
+ for (const update of startCreateOsmFromGeoJSON(
65
+ osm,
66
+ await readDataAsGeoJSON(data),
67
+ )) {
68
+ onProgress(update)
69
+ }
70
+ return osm
71
+ }
72
+
73
+ /**
74
+ * Generator that converts GeoJSON features to OSM entities.
75
+ *
76
+ * This is the core conversion logic, yielding progress events as features
77
+ * are processed. Called by `fromGeoJSON` with progress handling.
78
+ *
79
+ * Geometry mapping:
80
+ * - **Point**: Creates a single node with feature properties as tags.
81
+ * - **LineString**: Creates nodes for each coordinate, then a way referencing them.
82
+ * - **Polygon**: Creates a way for simple polygons; for polygons with holes,
83
+ * creates separate ways for outer and inner rings plus a multipolygon relation.
84
+ * - **MultiPolygon**: Creates a multipolygon relation with all rings as way members.
85
+ *
86
+ * Coordinates are normalized using `rewind` to ensure correct winding order
87
+ * (outer rings counterclockwise, inner rings clockwise per OSM conventions).
88
+ *
89
+ * @param osm - Target Osm index to populate.
90
+ * @param geojson - Parsed GeoJSON FeatureCollection.
91
+ * @yields Progress events during conversion.
92
+ */
93
+ export function* startCreateOsmFromGeoJSON(
94
+ osm: Osm,
95
+ geojson: FeatureCollection<Point | LineString | Polygon | MultiPolygon>,
96
+ ): Generator<ProgressEvent> {
97
+ yield progressEvent("Converting GeoJSON to Osmix...")
98
+
99
+ // Map to track nodes by coordinate string for reuse when creating ways and relations
100
+ const nodeMap = new Map<string, number>()
101
+ let nextNodeId = -1
102
+ let nextWayId = -1
103
+ let nextRelationId = -1
104
+
105
+ // Helper to get or create a node for a coordinate
106
+ const getOrCreateNode = (lon: number, lat: number): number => {
107
+ const coordKey = `${lon},${lat}`
108
+ const existingNodeId = nodeMap.get(coordKey)
109
+ if (existingNodeId !== undefined) {
110
+ return existingNodeId
111
+ }
112
+
113
+ const nodeId = nextNodeId--
114
+ nodeMap.set(coordKey, nodeId)
115
+ osm.nodes.addNode({
116
+ id: nodeId,
117
+ lon,
118
+ lat,
119
+ })
120
+ return nodeId
121
+ }
122
+
123
+ // Process each feature
124
+ let count = 0
125
+ for (const feature of geojson.features) {
126
+ // Normalize winding order using rewind (outer rings counterclockwise, inner rings clockwise)
127
+ const normalizedFeature = rewindFeature(feature)
128
+ const tags = propertiesToTags(normalizedFeature.properties)
129
+ const featureId = extractFeatureId(normalizedFeature.id)
130
+
131
+ if (normalizedFeature.geometry?.type === "Point") {
132
+ const [lon, lat] = normalizedFeature.geometry.coordinates
133
+ if (lon === undefined || lat === undefined)
134
+ throw Error("Invalid GeoJSON coordinates in Point.")
135
+
136
+ const nodeId = featureId ?? nextNodeId--
137
+ osm.nodes.addNode({
138
+ id: nodeId,
139
+ lon,
140
+ lat,
141
+ tags,
142
+ })
143
+ nodeMap.set(`${lon},${lat}`, nodeId)
144
+ } else if (normalizedFeature.geometry?.type === "LineString") {
145
+ const coordinates = normalizedFeature.geometry.coordinates
146
+ if (coordinates.length < 2)
147
+ throw Error("Invalid GeoJSON coordinates in LineString.")
148
+
149
+ // Create or get nodes for each coordinate
150
+ const nodeRefs: number[] = []
151
+ for (const [lon, lat] of coordinates) {
152
+ if (lon === undefined || lat === undefined)
153
+ throw Error("Invalid GeoJSON coordinates in LineString.")
154
+ const nodeId = getOrCreateNode(lon, lat)
155
+ nodeRefs.push(nodeId)
156
+ }
157
+
158
+ // Create the way
159
+ const wayId = featureId ?? nextWayId--
160
+ osm.ways.addWay({
161
+ id: wayId,
162
+ refs: nodeRefs,
163
+ tags,
164
+ })
165
+ } else if (normalizedFeature.geometry?.type === "Polygon") {
166
+ const coordinates = normalizedFeature.geometry.coordinates
167
+ if (coordinates.length === 0) continue
168
+
169
+ // If the polygon contains holes, create a relation
170
+ const createRelation = coordinates.length > 1
171
+
172
+ // First ring is outer boundary, subsequent rings are holes
173
+ const outerRing = coordinates[0]
174
+ if (!outerRing || outerRing.length < 3) continue // Need at least 3 points for a polygon
175
+
176
+ // Create nodes for outer ring
177
+ const outerNodeRefs: number[] = []
178
+ for (const [lon, lat] of outerRing) {
179
+ if (lon === undefined || lat === undefined)
180
+ throw Error("Invalid GeoJSON coordinates in Polygon.")
181
+ const nodeId = getOrCreateNode(lon, lat)
182
+ outerNodeRefs.push(nodeId)
183
+ }
184
+
185
+ // Ensure the outer ring is closed (first and last node are the same)
186
+ if (outerNodeRefs[0] !== outerNodeRefs[outerNodeRefs.length - 1])
187
+ throw Error("Outer ring of Polygon is not closed.")
188
+
189
+ // Create way for outer ring with area tags
190
+ const outerWayId = createRelation
191
+ ? nextWayId--
192
+ : (featureId ?? nextWayId--)
193
+ osm.ways.addWay({
194
+ id: outerWayId,
195
+ refs: outerNodeRefs,
196
+ tags: createRelation ? { area: "yes" } : { area: "yes", ...tags },
197
+ })
198
+
199
+ // Create separate ways for holes
200
+ const holeWayIds: number[] = []
201
+ for (let i = 1; i < coordinates.length; i++) {
202
+ const holeRing = coordinates[i]
203
+ if (!holeRing || holeRing.length < 3)
204
+ throw Error("Hole ring of Polygon has less than 3 coordinates.")
205
+
206
+ const holeNodeRefs: number[] = []
207
+ for (const [lon, lat] of holeRing) {
208
+ if (lon === undefined || lat === undefined)
209
+ throw Error("Invalid GeoJSON coordinates in Polygon.")
210
+ const nodeId = getOrCreateNode(lon, lat)
211
+ holeNodeRefs.push(nodeId)
212
+ }
213
+
214
+ // Ensure the ring is closed
215
+ if (holeNodeRefs[0] !== holeNodeRefs[holeNodeRefs.length - 1])
216
+ throw Error("Hole ring of Polygon is not closed.")
217
+
218
+ // Create way for hole
219
+ const holeWayId = nextWayId--
220
+ osm.ways.addWay({
221
+ id: holeWayId,
222
+ refs: holeNodeRefs,
223
+ tags: { area: "yes" },
224
+ })
225
+ holeWayIds.push(holeWayId)
226
+ }
227
+
228
+ if (createRelation) {
229
+ osm.relations.addRelation({
230
+ id: featureId ?? nextRelationId--,
231
+ members: [
232
+ { type: "way", ref: outerWayId, role: "outer" },
233
+ ...holeWayIds.map(
234
+ (id) =>
235
+ ({ type: "way", ref: id, role: "inner" }) as OsmRelationMember,
236
+ ),
237
+ ],
238
+ tags: {
239
+ type: "multipolygon",
240
+ ...tags,
241
+ },
242
+ })
243
+ }
244
+ } else if (normalizedFeature.geometry?.type === "MultiPolygon") {
245
+ const coordinates = normalizedFeature.geometry.coordinates
246
+ if (coordinates.length === 0) continue
247
+
248
+ // Process each polygon in the MultiPolygon
249
+ const relationMembers: OsmRelationMember[] = []
250
+ for (const polygon of coordinates) {
251
+ if (polygon.length === 0) continue
252
+
253
+ const outerRing = polygon[0]
254
+ if (!outerRing || outerRing.length < 3) continue
255
+
256
+ // Create nodes for outer ring
257
+ const outerNodeRefs: number[] = []
258
+ for (const [lon, lat] of outerRing) {
259
+ if (lon === undefined || lat === undefined)
260
+ throw Error("Invalid GeoJSON coordinates in Polygon.")
261
+ const nodeId = getOrCreateNode(lon, lat)
262
+ outerNodeRefs.push(nodeId)
263
+ }
264
+
265
+ // Ensure the ring is closed
266
+ if (outerNodeRefs[0] !== outerNodeRefs[outerNodeRefs.length - 1])
267
+ throw Error("Outer ring of Polygon is not closed.")
268
+
269
+ // Create way for outer ring
270
+ const outerWayId = nextWayId--
271
+ osm.ways.addWay({
272
+ id: outerWayId,
273
+ refs: outerNodeRefs,
274
+ tags: { area: "yes" },
275
+ })
276
+ relationMembers.push({ type: "way", ref: outerWayId, role: "outer" })
277
+
278
+ // Create separate ways for holes in this polygon
279
+ for (let i = 1; i < polygon.length; i++) {
280
+ const holeRing = polygon[i]
281
+ if (!holeRing || holeRing.length < 3)
282
+ throw Error("Hole ring of Polygon has less than 3 coordinates.")
283
+
284
+ const holeNodeRefs: number[] = []
285
+ for (const [lon, lat] of holeRing) {
286
+ if (lon === undefined || lat === undefined)
287
+ throw Error("Invalid GeoJSON coordinates in Polygon.")
288
+ const nodeId = getOrCreateNode(lon, lat)
289
+ holeNodeRefs.push(nodeId)
290
+ }
291
+
292
+ // Ensure the ring is closed
293
+ if (holeNodeRefs[0] !== holeNodeRefs[holeNodeRefs.length - 1])
294
+ throw Error("Hole ring of Polygon is not closed.")
295
+
296
+ const holeWayId = nextWayId--
297
+ osm.ways.addWay({
298
+ id: holeWayId,
299
+ refs: holeNodeRefs,
300
+ tags: { area: "yes" },
301
+ })
302
+
303
+ relationMembers.push({ type: "way", ref: holeWayId, role: "inner" })
304
+ }
305
+ }
306
+
307
+ if (relationMembers.length > 0) {
308
+ osm.relations.addRelation({
309
+ id: featureId ?? nextRelationId--,
310
+ members: relationMembers,
311
+ tags: { type: "multipolygon", ...tags },
312
+ })
313
+ }
314
+ }
315
+
316
+ yield progressEvent(`Processed ${count++} features...`)
317
+ }
318
+
319
+ yield progressEvent(
320
+ "Finished converting GeoJSON to Osmix, building indexes...",
321
+ )
322
+
323
+ // Build indexes
324
+ osm.buildIndexes()
325
+ osm.buildSpatialIndexes()
326
+ }
327
+
328
+ // Helper to convert properties to tags
329
+ function propertiesToTags(
330
+ properties: Record<string, unknown> | null,
331
+ ): OsmTags | undefined {
332
+ if (!properties || Object.keys(properties).length === 0) {
333
+ return undefined
334
+ }
335
+
336
+ const tags: OsmTags = {}
337
+ for (const [key, value] of Object.entries(properties)) {
338
+ // Convert value to string or number
339
+ if (typeof value === "string" || typeof value === "number") {
340
+ tags[key] = value
341
+ } else if (value != null) {
342
+ // Convert other types to string
343
+ tags[key] = String(value)
344
+ }
345
+ }
346
+ return Object.keys(tags).length > 0 ? tags : undefined
347
+ }
348
+
349
+ // Helper to extract numeric ID from feature
350
+ function extractFeatureId(
351
+ featureId: string | number | undefined,
352
+ ): number | undefined {
353
+ if (featureId === undefined) return undefined
354
+ if (typeof featureId === "number") return featureId
355
+ if (typeof featureId === "string") {
356
+ const numId = Number.parseInt(featureId, 10)
357
+ if (!Number.isNaN(numId)) return numId
358
+ }
359
+ return undefined
360
+ }
package/src/types.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Type definitions for OSM-GeoJSON conversion.
3
+ * @module
4
+ */
5
+
6
+ import type { OsmEntityType, OsmInfoParsed, OsmTags } from "@osmix/shared/types"
7
+ import type {
8
+ Feature,
9
+ GeometryCollection,
10
+ LineString,
11
+ MultiLineString,
12
+ MultiPoint,
13
+ MultiPolygon,
14
+ Point,
15
+ Polygon,
16
+ } from "geojson"
17
+
18
+ /**
19
+ * GeoJSON Feature properties for OSM entities.
20
+ *
21
+ * Includes the OSM entity ID, type, and optionally tags and metadata.
22
+ * All OSM tags are spread into the properties object.
23
+ */
24
+ export type OsmGeoJSONProperties = {
25
+ /** OSM entity ID. */
26
+ id: number
27
+ /** Entity type: "node", "way", or "relation". */
28
+ type: OsmEntityType
29
+ /** OSM tags (key-value pairs). */
30
+ tags?: OsmTags
31
+ /** Entity metadata (version, timestamp, user, etc.). */
32
+ info?: OsmInfoParsed
33
+ }
34
+
35
+ /**
36
+ * GeoJSON Feature with OSM-specific properties.
37
+ *
38
+ * Generic over geometry type to support all OSM entity conversions:
39
+ * - Nodes → Point
40
+ * - Ways → LineString or Polygon
41
+ * - Relations → MultiPolygon, MultiLineString, MultiPoint, or GeometryCollection
42
+ */
43
+ export type OsmGeoJSONFeature<
44
+ T extends
45
+ | Point
46
+ | LineString
47
+ | Polygon
48
+ | MultiPolygon
49
+ | MultiLineString
50
+ | MultiPoint
51
+ | GeometryCollection,
52
+ > = Feature<T, OsmGeoJSONProperties>
53
+
54
+ /**
55
+ * GeoJSON FeatureCollection types that can be imported into an Osm index.
56
+ *
57
+ * Supports Point, LineString, Polygon, and MultiPolygon geometries.
58
+ * Other geometry types are not supported for import.
59
+ */
60
+ export type ImportableGeoJSON = GeoJSON.FeatureCollection<
61
+ GeoJSON.Point | GeoJSON.LineString | GeoJSON.Polygon | GeoJSON.MultiPolygon
62
+ >
63
+
64
+ /**
65
+ * Input types accepted by `fromGeoJSON`.
66
+ *
67
+ * Supports multiple formats:
68
+ * - `ArrayBufferLike` - Binary GeoJSON data
69
+ * - `ReadableStream` - Streaming GeoJSON
70
+ * - `string` - JSON string
71
+ * - `ImportableGeoJSON` - Already-parsed FeatureCollection
72
+ */
73
+ export type ReadOsmDataTypes =
74
+ | ArrayBufferLike
75
+ | ReadableStream
76
+ | string
77
+ | ImportableGeoJSON
package/src/utils.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Utility functions for GeoJSON data handling.
3
+ * @module
4
+ */
5
+
6
+ import type { ImportableGeoJSON, ReadOsmDataTypes } from "./types"
7
+
8
+ /**
9
+ * Parse GeoJSON from various input formats.
10
+ *
11
+ * Supports multiple input types for flexibility:
12
+ * - **string**: JSON string to parse
13
+ * - **ReadableStream**: Streaming JSON data
14
+ * - **ArrayBuffer/SharedArrayBuffer**: Binary data to decode and parse
15
+ * - **Object**: Already-parsed FeatureCollection (passed through)
16
+ *
17
+ * @param data - GeoJSON data in any supported format.
18
+ * @returns Parsed GeoJSON FeatureCollection.
19
+ * @throws If data is null or an unsupported type.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * // From string
24
+ * const geojson = await readDataAsGeoJSON('{"type":"FeatureCollection","features":[]}')
25
+ *
26
+ * // From fetch response
27
+ * const response = await fetch('/data.geojson')
28
+ * const geojson = await readDataAsGeoJSON(response.body!)
29
+ * ```
30
+ */
31
+ export async function readDataAsGeoJSON(
32
+ data: ReadOsmDataTypes,
33
+ ): Promise<ImportableGeoJSON> {
34
+ if (data == null) throw new Error("Data is null")
35
+ if (typeof data === "string") return JSON.parse(data) as ImportableGeoJSON
36
+
37
+ if (data instanceof ReadableStream) {
38
+ const reader = data.pipeThrough(new TextDecoderStream()).getReader()
39
+ let result = ""
40
+ while (true) {
41
+ const { done, value } = await reader.read()
42
+ result += value
43
+ if (done) break
44
+ }
45
+ return JSON.parse(result) as ImportableGeoJSON
46
+ }
47
+ if (data instanceof ArrayBuffer || data instanceof SharedArrayBuffer) {
48
+ const decoder = new TextDecoder()
49
+ const text = decoder.decode(new Uint8Array(data))
50
+ return JSON.parse(text) as ImportableGeoJSON
51
+ }
52
+ if (typeof data === "object" && "type" in data && "features" in data) {
53
+ return data as ImportableGeoJSON
54
+ }
55
+ throw new Error(
56
+ "Invalid data type. Accepts string, ReadableStream, ArrayBufferLike, or GeoJSON FeatureCollection.",
57
+ )
58
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import type { OsmNode, OsmRelation, OsmWay } from "@osmix/shared/types"
3
+ import {
4
+ nodeToFeature,
5
+ relationToFeature,
6
+ wayToFeature,
7
+ } from "../src/entity-to-feature"
8
+
9
+ describe("geojson helpers", () => {
10
+ const node0: OsmNode = { id: 1, lat: 0, lon: 0, tags: { amenity: "cafe" } }
11
+ const node1: OsmNode = { id: 2, lat: 1, lon: 1 }
12
+ const nodes: OsmNode[] = [node0, node1]
13
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]))
14
+ const refToPosition = (id: number) => {
15
+ const node = nodeMap.get(id)
16
+ if (!node) throw new Error(`Node ${id} not found`)
17
+ return [node.lon, node.lat] as [number, number]
18
+ }
19
+
20
+ it("converts node to GeoJSON Point", () => {
21
+ const feature = nodeToFeature(node0)
22
+ expect(feature.type).toBe("Feature")
23
+ expect(feature.geometry.type).toBe("Point")
24
+ expect(feature.geometry.coordinates).toEqual([node0.lon, node0.lat])
25
+ expect(feature.properties).toEqual({
26
+ id: node0.id,
27
+ type: "node",
28
+ ...node0.tags,
29
+ ...node0.info,
30
+ })
31
+ })
32
+
33
+ it("creates polygon for closed ways", () => {
34
+ const way: OsmWay = {
35
+ id: 10,
36
+ refs: [1, 2, 1],
37
+ tags: { area: "yes" },
38
+ }
39
+ const feature = wayToFeature(way, refToPosition)
40
+ expect(feature.geometry.type).toBe("Polygon")
41
+ })
42
+
43
+ it("builds relation geometry collection", () => {
44
+ // Create a way that forms a closed ring
45
+ const way: OsmWay = {
46
+ id: 10,
47
+ refs: [1, 2, 1], // Closed way
48
+ tags: { area: "yes" },
49
+ }
50
+ const wayMap = new Map([[way.id, way]])
51
+ const getWay = (wayId: number) => wayMap.get(wayId) ?? null
52
+
53
+ const relation: OsmRelation = {
54
+ id: 20,
55
+ members: [{ type: "way", ref: 10, role: "outer" }],
56
+ tags: { type: "multipolygon" },
57
+ }
58
+ const feature = relationToFeature(relation, refToPosition, getWay)
59
+ expect(feature.geometry.type).toBe("Polygon")
60
+ if (feature.geometry.type === "Polygon") {
61
+ expect(feature.geometry.coordinates?.[0]).toBeDefined()
62
+ expect(feature.geometry.coordinates[0]).toHaveLength(3) // Should have 3 coordinates (closed ring)
63
+ }
64
+ })
65
+ })