@osmix/geojson 0.0.15 → 0.0.16

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.
@@ -7,36 +7,28 @@
7
7
  * @module
8
8
  */
9
9
 
10
- import type { Osm } from "@osmix/core"
10
+ import type { OsmReader } from "@osmix/core/contracts";
11
+ import { buildRelationRings, getWayMembersByRole } from "@osmix/geo/relation-multipolygon";
12
+ import { wayIsArea } from "@osmix/geo/way-is-area";
13
+ import type { LonLat, OsmEntity, OsmNode, OsmRelation, OsmWay } from "@osmix/types";
11
14
  import {
12
- buildRelationLineStrings,
13
- collectRelationPoints,
14
- getRelationKind,
15
- isAreaRelation,
16
- } from "@osmix/shared/relation-kind"
17
- import {
18
- buildRelationRings,
19
- getWayMembersByRole,
20
- } from "@osmix/shared/relation-multipolygon"
21
- import type {
22
- LonLat,
23
- OsmEntity,
24
- OsmNode,
25
- OsmRelation,
26
- OsmWay,
27
- } from "@osmix/shared/types"
28
- import { isNode, isRelation, isWay } from "@osmix/shared/utils"
29
- import { wayIsArea } from "@osmix/shared/way-is-area"
15
+ buildRelationLineStrings,
16
+ collectRelationPoints,
17
+ getRelationKind,
18
+ isAreaRelation,
19
+ } from "@osmix/types/relation-kind";
20
+ import { isNode, isRelation, isWay } from "@osmix/types/utils";
30
21
  import type {
31
- GeometryCollection,
32
- LineString,
33
- MultiLineString,
34
- MultiPoint,
35
- MultiPolygon,
36
- Point,
37
- Polygon,
38
- } from "geojson"
39
- import type { OsmGeoJSONFeature } from "./types.ts"
22
+ GeometryCollection,
23
+ LineString,
24
+ MultiLineString,
25
+ MultiPoint,
26
+ MultiPolygon,
27
+ Point,
28
+ Polygon,
29
+ } from "geojson";
30
+
31
+ import type { OsmGeoJSONFeature } from "./types.ts";
40
32
 
41
33
  /**
42
34
  * Convert an OSM node to a GeoJSON Point feature.
@@ -51,20 +43,20 @@ import type { OsmGeoJSONFeature } from "./types.ts"
51
43
  * ```
52
44
  */
53
45
  export function nodeToFeature(node: OsmNode): OsmGeoJSONFeature<Point> {
54
- return {
55
- type: "Feature",
56
- id: node.id,
57
- geometry: {
58
- type: "Point",
59
- coordinates: [node.lon, node.lat],
60
- },
61
- properties: {
62
- id: node.id,
63
- type: "node",
64
- ...node.info,
65
- ...node.tags,
66
- },
67
- }
46
+ return {
47
+ type: "Feature",
48
+ id: node.id,
49
+ geometry: {
50
+ type: "Point",
51
+ coordinates: [node.lon, node.lat],
52
+ },
53
+ properties: {
54
+ id: node.id,
55
+ type: "node",
56
+ ...node.info,
57
+ ...node.tags,
58
+ },
59
+ };
68
60
  }
69
61
 
70
62
  /**
@@ -74,7 +66,7 @@ export function nodeToFeature(node: OsmNode): OsmGeoJSONFeature<Point> {
74
66
  * area-indicating tags (building, landuse, etc.) and ring closure.
75
67
  *
76
68
  * @param way - OSM way with id, refs, and optional tags.
77
- * @param refToPosition - Function to resolve node ID to [lon, lat] coordinates.
69
+ * @param refToPosition - Function to resolve node ID to coordinates, or null for a missing node.
78
70
  * @returns GeoJSON LineString or Polygon Feature with OSM properties.
79
71
  *
80
72
  * @example
@@ -83,28 +75,34 @@ export function nodeToFeature(node: OsmNode): OsmGeoJSONFeature<Point> {
83
75
  * ```
84
76
  */
85
77
  export function wayToFeature(
86
- way: OsmWay,
87
- refToPosition: (id: number) => [number, number],
78
+ way: OsmWay,
79
+ refToPosition: (id: number) => [number, number] | null | undefined,
88
80
  ): OsmGeoJSONFeature<LineString | Polygon> {
89
- return {
90
- type: "Feature",
91
- id: way.id,
92
- geometry: wayIsArea(way)
93
- ? {
94
- type: "Polygon",
95
- coordinates: [way.refs.map((r) => refToPosition(r))],
96
- }
97
- : {
98
- type: "LineString",
99
- coordinates: way.refs.map((r) => refToPosition(r)),
100
- },
101
- properties: {
102
- id: way.id,
103
- type: "way",
104
- ...way.info,
105
- ...way.tags,
106
- },
107
- }
81
+ const resolvedCoordinates: [number, number][] = [];
82
+ for (const ref of way.refs) {
83
+ const position = refToPosition(ref);
84
+ if (!position) throw Error(`Node reference missing for way ${way.id}`);
85
+ resolvedCoordinates.push(position);
86
+ }
87
+ return {
88
+ type: "Feature",
89
+ id: way.id,
90
+ geometry: wayIsArea(way)
91
+ ? {
92
+ type: "Polygon",
93
+ coordinates: [resolvedCoordinates],
94
+ }
95
+ : {
96
+ type: "LineString",
97
+ coordinates: resolvedCoordinates,
98
+ },
99
+ properties: {
100
+ id: way.id,
101
+ type: "way",
102
+ ...way.info,
103
+ ...way.tags,
104
+ },
105
+ };
108
106
  }
109
107
 
110
108
  /**
@@ -117,7 +115,7 @@ export function wayToFeature(
117
115
  * - **Other**: GeometryCollection (empty for logical relations)
118
116
  *
119
117
  * @param relation - OSM relation with id, members, and optional tags.
120
- * @param refToPosition - Function to resolve node ID to [lon, lat] coordinates.
118
+ * @param refToPosition - Function to resolve node ID to coordinates, or null for a missing node.
121
119
  * @param getWay - Optional function to resolve way ID to OsmWay (required for polygons/lines).
122
120
  * @returns GeoJSON Feature with appropriate geometry type.
123
121
  *
@@ -131,252 +129,245 @@ export function wayToFeature(
131
129
  * ```
132
130
  */
133
131
  export function relationToFeature(
134
- relation: OsmRelation,
135
- refToPosition: (id: number) => [number, number],
136
- getWay?: (wayId: number) => OsmWay | null,
132
+ relation: OsmRelation,
133
+ refToPosition: (id: number) => [number, number] | null | undefined,
134
+ getWay?: (wayId: number) => OsmWay | null,
137
135
  ): OsmGeoJSONFeature<
138
- | Polygon
139
- | MultiPolygon
140
- | LineString
141
- | MultiLineString
142
- | Point
143
- | MultiPoint
144
- | GeometryCollection
136
+ Polygon | MultiPolygon | LineString | MultiLineString | Point | MultiPoint | GeometryCollection
145
137
  > {
146
- const getNodeCoordinates = (nodeId: number): LonLat | undefined => {
147
- const pos = refToPosition(nodeId)
148
- return pos ? [pos[0], pos[1]] : undefined
149
- }
138
+ const getNodeCoordinates = (nodeId: number): LonLat | undefined => {
139
+ const pos = refToPosition(nodeId);
140
+ return pos ? [pos[0], pos[1]] : undefined;
141
+ };
150
142
 
151
- const kind = getRelationKind(relation)
143
+ const kind = getRelationKind(relation);
152
144
 
153
- // Handle area relations (multipolygon, boundary)
154
- if (isAreaRelation(relation) && getWay) {
155
- const rings = buildRelationRings(relation, getWay, getNodeCoordinates)
145
+ // Handle area relations (multipolygon, boundary)
146
+ if (isAreaRelation(relation) && getWay) {
147
+ const rings = buildRelationRings(relation, getWay, getNodeCoordinates);
156
148
 
157
- if (rings.length === 0) {
158
- // Fallback to simple MultiPolygon if we can't build rings
159
- return {
160
- type: "Feature",
161
- id: relation.id,
162
- geometry: {
163
- type: "MultiPolygon",
164
- coordinates: [],
165
- },
166
- properties: {
167
- id: relation.id,
168
- type: "relation",
169
- ...relation.info,
170
- ...relation.tags,
171
- },
172
- }
173
- }
149
+ if (rings.length === 0) {
150
+ // Fallback to simple MultiPolygon if we can't build rings
151
+ return {
152
+ type: "Feature",
153
+ id: relation.id,
154
+ geometry: {
155
+ type: "MultiPolygon",
156
+ coordinates: [],
157
+ },
158
+ properties: {
159
+ id: relation.id,
160
+ type: "relation",
161
+ ...relation.info,
162
+ ...relation.tags,
163
+ },
164
+ };
165
+ }
174
166
 
175
- // If only one polygon, return Polygon; otherwise MultiPolygon
176
- if (rings.length === 1) {
177
- return {
178
- type: "Feature",
179
- id: relation.id,
180
- geometry: {
181
- type: "Polygon",
182
- coordinates: rings[0]!,
183
- },
184
- properties: {
185
- id: relation.id,
186
- type: "relation",
187
- ...relation.info,
188
- ...relation.tags,
189
- },
190
- }
191
- }
167
+ // If only one polygon, return Polygon; otherwise MultiPolygon
168
+ if (rings.length === 1) {
169
+ return {
170
+ type: "Feature",
171
+ id: relation.id,
172
+ geometry: {
173
+ type: "Polygon",
174
+ coordinates: rings[0]!,
175
+ },
176
+ properties: {
177
+ id: relation.id,
178
+ type: "relation",
179
+ ...relation.info,
180
+ ...relation.tags,
181
+ },
182
+ };
183
+ }
192
184
 
193
- return {
194
- type: "Feature",
195
- id: relation.id,
196
- geometry: {
197
- type: "MultiPolygon",
198
- coordinates: rings,
199
- },
200
- properties: {
201
- id: relation.id,
202
- type: "relation",
203
- ...relation.info,
204
- ...relation.tags,
205
- },
206
- }
207
- }
185
+ return {
186
+ type: "Feature",
187
+ id: relation.id,
188
+ geometry: {
189
+ type: "MultiPolygon",
190
+ coordinates: rings,
191
+ },
192
+ properties: {
193
+ id: relation.id,
194
+ type: "relation",
195
+ ...relation.info,
196
+ ...relation.tags,
197
+ },
198
+ };
199
+ }
208
200
 
209
- // Handle line relations (route, multilinestring)
210
- if (kind === "line" && getWay) {
211
- const lineStrings = buildRelationLineStrings(
212
- relation,
213
- getWay,
214
- getNodeCoordinates,
215
- )
201
+ // Handle line relations (route, multilinestring)
202
+ if (kind === "line" && getWay) {
203
+ const lineStrings = buildRelationLineStrings(relation, getWay, getNodeCoordinates);
216
204
 
217
- if (lineStrings.length === 0) {
218
- return {
219
- type: "Feature",
220
- id: relation.id,
221
- geometry: {
222
- type: "MultiLineString",
223
- coordinates: [],
224
- },
225
- properties: {
226
- id: relation.id,
227
- type: "relation",
228
- ...relation.info,
229
- ...relation.tags,
230
- },
231
- }
232
- }
205
+ if (lineStrings.length === 0) {
206
+ return {
207
+ type: "Feature",
208
+ id: relation.id,
209
+ geometry: {
210
+ type: "MultiLineString",
211
+ coordinates: [],
212
+ },
213
+ properties: {
214
+ id: relation.id,
215
+ type: "relation",
216
+ ...relation.info,
217
+ ...relation.tags,
218
+ },
219
+ };
220
+ }
233
221
 
234
- if (lineStrings.length === 1) {
235
- return {
236
- type: "Feature",
237
- id: relation.id,
238
- geometry: {
239
- type: "LineString",
240
- coordinates: lineStrings[0]!,
241
- },
242
- properties: {
243
- id: relation.id,
244
- type: "relation",
245
- ...relation.info,
246
- ...relation.tags,
247
- },
248
- }
249
- }
222
+ if (lineStrings.length === 1) {
223
+ return {
224
+ type: "Feature",
225
+ id: relation.id,
226
+ geometry: {
227
+ type: "LineString",
228
+ coordinates: lineStrings[0]!,
229
+ },
230
+ properties: {
231
+ id: relation.id,
232
+ type: "relation",
233
+ ...relation.info,
234
+ ...relation.tags,
235
+ },
236
+ };
237
+ }
250
238
 
251
- return {
252
- type: "Feature",
253
- id: relation.id,
254
- geometry: {
255
- type: "MultiLineString",
256
- coordinates: lineStrings,
257
- },
258
- properties: {
259
- id: relation.id,
260
- type: "relation",
261
- ...relation.info,
262
- ...relation.tags,
263
- },
264
- }
265
- }
239
+ return {
240
+ type: "Feature",
241
+ id: relation.id,
242
+ geometry: {
243
+ type: "MultiLineString",
244
+ coordinates: lineStrings,
245
+ },
246
+ properties: {
247
+ id: relation.id,
248
+ type: "relation",
249
+ ...relation.info,
250
+ ...relation.tags,
251
+ },
252
+ };
253
+ }
266
254
 
267
- // Handle point relations (multipoint)
268
- if (kind === "point") {
269
- const points = collectRelationPoints(relation, getNodeCoordinates)
255
+ // Handle point relations (multipoint)
256
+ if (kind === "point") {
257
+ const points = collectRelationPoints(relation, getNodeCoordinates);
270
258
 
271
- if (points.length === 0) {
272
- return {
273
- type: "Feature",
274
- id: relation.id,
275
- geometry: {
276
- type: "MultiPoint",
277
- coordinates: [],
278
- },
279
- properties: {
280
- id: relation.id,
281
- type: "relation",
282
- ...relation.info,
283
- ...relation.tags,
284
- },
285
- }
286
- }
259
+ if (points.length === 0) {
260
+ return {
261
+ type: "Feature",
262
+ id: relation.id,
263
+ geometry: {
264
+ type: "MultiPoint",
265
+ coordinates: [],
266
+ },
267
+ properties: {
268
+ id: relation.id,
269
+ type: "relation",
270
+ ...relation.info,
271
+ ...relation.tags,
272
+ },
273
+ };
274
+ }
287
275
 
288
- if (points.length === 1) {
289
- return {
290
- type: "Feature",
291
- id: relation.id,
292
- geometry: {
293
- type: "Point",
294
- coordinates: points[0]!,
295
- },
296
- properties: {
297
- id: relation.id,
298
- type: "relation",
299
- ...relation.info,
300
- ...relation.tags,
301
- },
302
- }
303
- }
276
+ if (points.length === 1) {
277
+ return {
278
+ type: "Feature",
279
+ id: relation.id,
280
+ geometry: {
281
+ type: "Point",
282
+ coordinates: points[0]!,
283
+ },
284
+ properties: {
285
+ id: relation.id,
286
+ type: "relation",
287
+ ...relation.info,
288
+ ...relation.tags,
289
+ },
290
+ };
291
+ }
304
292
 
305
- return {
306
- type: "Feature",
307
- id: relation.id,
308
- geometry: {
309
- type: "MultiPoint",
310
- coordinates: points,
311
- },
312
- properties: {
313
- id: relation.id,
314
- type: "relation",
315
- ...relation.info,
316
- ...relation.tags,
317
- },
318
- }
319
- }
293
+ return {
294
+ type: "Feature",
295
+ id: relation.id,
296
+ geometry: {
297
+ type: "MultiPoint",
298
+ coordinates: points,
299
+ },
300
+ properties: {
301
+ id: relation.id,
302
+ type: "relation",
303
+ ...relation.info,
304
+ ...relation.tags,
305
+ },
306
+ };
307
+ }
320
308
 
321
- // Fallback for logical relations or when getWay is not provided
322
- // Return GeometryCollection or null geometry for logic-only relations
323
- if (kind === "logic" || kind === "super") {
324
- return {
325
- type: "Feature",
326
- id: relation.id,
327
- geometry: {
328
- type: "GeometryCollection",
329
- geometries: [],
330
- },
331
- properties: {
332
- id: relation.id,
333
- type: "relation",
334
- ...relation.info,
335
- ...relation.tags,
336
- },
337
- }
338
- }
309
+ // Fallback for logical relations or when getWay is not provided
310
+ // Return GeometryCollection or null geometry for logic-only relations
311
+ if (kind === "logic" || kind === "super") {
312
+ return {
313
+ type: "Feature",
314
+ id: relation.id,
315
+ geometry: {
316
+ type: "GeometryCollection",
317
+ geometries: [],
318
+ },
319
+ properties: {
320
+ id: relation.id,
321
+ type: "relation",
322
+ ...relation.info,
323
+ ...relation.tags,
324
+ },
325
+ };
326
+ }
339
327
 
340
- // Final fallback: try to create a simple representation from way members
341
- if (getWay) {
342
- const { outer } = getWayMembersByRole(relation)
343
- const coordinates: [number, number][][][] = []
328
+ // Final fallback: try to create a simple representation from way members
329
+ if (getWay) {
330
+ const { outer } = getWayMembersByRole(relation);
331
+ const coordinates: [number, number][][][] = [];
344
332
 
345
- if (outer.length > 0) {
346
- coordinates.push([outer.map((member) => refToPosition(member.ref))])
347
- }
333
+ if (outer.length > 0) {
334
+ const outerCoordinates = outer
335
+ .map((member) => refToPosition(member.ref))
336
+ .filter((position): position is [number, number] => position != null);
337
+ if (outerCoordinates.length > 0) coordinates.push([outerCoordinates]);
338
+ }
348
339
 
349
- return {
350
- type: "Feature",
351
- id: relation.id,
352
- geometry: {
353
- type: "MultiPolygon",
354
- coordinates: coordinates.length > 0 ? coordinates : [],
355
- },
356
- properties: {
357
- id: relation.id,
358
- type: "relation",
359
- ...relation.info,
360
- ...relation.tags,
361
- },
362
- }
363
- }
340
+ return {
341
+ type: "Feature",
342
+ id: relation.id,
343
+ geometry: {
344
+ type: "MultiPolygon",
345
+ coordinates: coordinates.length > 0 ? coordinates : [],
346
+ },
347
+ properties: {
348
+ id: relation.id,
349
+ type: "relation",
350
+ ...relation.info,
351
+ ...relation.tags,
352
+ },
353
+ };
354
+ }
364
355
 
365
- // No geometry available
366
- return {
367
- type: "Feature",
368
- id: relation.id,
369
- geometry: {
370
- type: "GeometryCollection",
371
- geometries: [],
372
- },
373
- properties: {
374
- id: relation.id,
375
- type: "relation",
376
- ...relation.info,
377
- ...relation.tags,
378
- },
379
- }
356
+ // No geometry available
357
+ return {
358
+ type: "Feature",
359
+ id: relation.id,
360
+ geometry: {
361
+ type: "GeometryCollection",
362
+ geometries: [],
363
+ },
364
+ properties: {
365
+ id: relation.id,
366
+ type: "relation",
367
+ ...relation.info,
368
+ ...relation.tags,
369
+ },
370
+ };
380
371
  }
381
372
 
382
373
  /**
@@ -399,29 +390,33 @@ export function relationToFeature(
399
390
  * ```
400
391
  */
401
392
  export function osmEntityToGeoJSONFeature(
402
- osm: Osm,
403
- entity: OsmEntity,
393
+ osm: OsmReader,
394
+ entity: OsmEntity,
404
395
  ): OsmGeoJSONFeature<
405
- | GeoJSON.Point
406
- | GeoJSON.LineString
407
- | GeoJSON.Polygon
408
- | GeoJSON.MultiPolygon
409
- | GeoJSON.MultiLineString
410
- | GeoJSON.MultiPoint
411
- | GeoJSON.GeometryCollection
396
+ | GeoJSON.Point
397
+ | GeoJSON.LineString
398
+ | GeoJSON.Polygon
399
+ | GeoJSON.MultiPolygon
400
+ | GeoJSON.MultiLineString
401
+ | GeoJSON.MultiPoint
402
+ | GeoJSON.GeometryCollection
412
403
  > {
413
- if (isNode(entity)) {
414
- return nodeToFeature(entity)
415
- }
416
- if (isWay(entity)) {
417
- return wayToFeature(entity, (ref) => osm.nodes.getNodeLonLat({ id: ref }))
418
- }
419
- if (isRelation(entity)) {
420
- return relationToFeature(
421
- entity,
422
- (ref) => osm.nodes.getNodeLonLat({ id: ref }),
423
- (ref) => osm.ways.getById(ref),
424
- )
425
- }
426
- throw new Error("Unknown entity type")
404
+ if (isNode(entity)) {
405
+ return nodeToFeature(entity);
406
+ }
407
+ if (isWay(entity)) {
408
+ return wayToFeature(entity, (ref) => {
409
+ const coordinate = osm.nodes.getNodeLonLat({ id: ref });
410
+ if (!coordinate) throw Error(`Node ${ref} not found for way geometry`);
411
+ return coordinate;
412
+ });
413
+ }
414
+ if (isRelation(entity)) {
415
+ return relationToFeature(
416
+ entity,
417
+ (ref) => osm.nodes.getNodeLonLat({ id: ref }) ?? undefined,
418
+ (ref) => osm.ways.getById(ref),
419
+ );
420
+ }
421
+ throw new Error("Unknown entity type");
427
422
  }