@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.
package/src/index.ts CHANGED
@@ -30,6 +30,6 @@
30
30
  * @module @osmix/geojson
31
31
  */
32
32
 
33
- export * from "./entity-to-feature.ts"
34
- export * from "./osm-from-geojson.ts"
35
- export * from "./types.ts"
33
+ export * from "./entity-to-feature.ts";
34
+ export * from "./osm-from-geojson.ts";
35
+ export * from "./types.ts";
@@ -7,23 +7,14 @@
7
7
  * @module
8
8
  */
9
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.ts"
26
- import { readDataAsGeoJSON } from "./utils.ts"
10
+ import { Osm, type OsmOptions, type OsmWriter } from "@osmix/core";
11
+ import { logProgress, type ProgressEvent, progressEvent } from "@osmix/shared/progress";
12
+ import type { OsmRelationMember, OsmTags } from "@osmix/types";
13
+ import { rewindFeature } from "@placemarkio/geojson-rewind";
14
+ import type { FeatureCollection, LineString, MultiPolygon, Point, Polygon } from "geojson";
15
+
16
+ import type { ReadOsmDataTypes } from "./types.ts";
17
+ import { readDataAsGeoJSON } from "./utils.ts";
27
18
 
28
19
  /**
29
20
  * Create an Osm index from GeoJSON data.
@@ -56,18 +47,15 @@ import { readDataAsGeoJSON } from "./utils.ts"
56
47
  * ```
57
48
  */
58
49
  export async function fromGeoJSON(
59
- data: ReadOsmDataTypes,
60
- options: Partial<OsmOptions> = {},
61
- onProgress: (progress: ProgressEvent) => void = logProgress,
50
+ data: ReadOsmDataTypes,
51
+ options: Partial<OsmOptions> = {},
52
+ onProgress: (progress: ProgressEvent) => void = logProgress,
62
53
  ): 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
54
+ const osm = new Osm(options);
55
+ for (const update of startCreateOsmFromGeoJSON(osm, await readDataAsGeoJSON(data))) {
56
+ onProgress(update);
57
+ }
58
+ return osm;
71
59
  }
72
60
 
73
61
  /**
@@ -91,270 +79,261 @@ export async function fromGeoJSON(
91
79
  * @yields Progress events during conversion.
92
80
  */
93
81
  export function* startCreateOsmFromGeoJSON(
94
- osm: Osm,
95
- geojson: FeatureCollection<Point | LineString | Polygon | MultiPolygon>,
82
+ osm: OsmWriter,
83
+ geojson: FeatureCollection<Point | LineString | Polygon | MultiPolygon>,
96
84
  ): 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()
85
+ yield progressEvent("Converting GeoJSON to Osmix...");
86
+
87
+ // Map to track nodes by coordinate string for reuse when creating ways and relations
88
+ const nodeMap = new Map<string, number>();
89
+ let nextNodeId = -1;
90
+ let nextWayId = -1;
91
+ let nextRelationId = -1;
92
+
93
+ // Helper to get or create a node for a coordinate
94
+ const getOrCreateNode = (lon: number, lat: number): number => {
95
+ const coordKey = `${lon},${lat}`;
96
+ const existingNodeId = nodeMap.get(coordKey);
97
+ if (existingNodeId !== undefined) {
98
+ return existingNodeId;
99
+ }
100
+
101
+ const nodeId = nextNodeId--;
102
+ nodeMap.set(coordKey, nodeId);
103
+ osm.nodes.addNode({
104
+ id: nodeId,
105
+ lon,
106
+ lat,
107
+ });
108
+ return nodeId;
109
+ };
110
+
111
+ // Process each feature
112
+ let count = 0;
113
+ for (const feature of geojson.features) {
114
+ // Normalize winding order using rewind (outer rings counterclockwise, inner rings clockwise)
115
+ const normalizedFeature = rewindFeature(feature);
116
+ const tags = propertiesToTags(normalizedFeature.properties);
117
+ const featureId = extractFeatureId(normalizedFeature.id);
118
+
119
+ if (normalizedFeature.geometry?.type === "Point") {
120
+ const [lon, lat] = normalizedFeature.geometry.coordinates;
121
+ if (lon === undefined || lat === undefined)
122
+ throw Error("Invalid GeoJSON coordinates in Point.");
123
+
124
+ const nodeId = featureId ?? nextNodeId--;
125
+ osm.nodes.addNode({
126
+ id: nodeId,
127
+ lon,
128
+ lat,
129
+ tags,
130
+ });
131
+ nodeMap.set(`${lon},${lat}`, nodeId);
132
+ } else if (normalizedFeature.geometry?.type === "LineString") {
133
+ const coordinates = normalizedFeature.geometry.coordinates;
134
+ if (coordinates.length < 2) throw Error("Invalid GeoJSON coordinates in LineString.");
135
+
136
+ // Create or get nodes for each coordinate
137
+ const nodeRefs: number[] = [];
138
+ for (const [lon, lat] of coordinates) {
139
+ if (lon === undefined || lat === undefined)
140
+ throw Error("Invalid GeoJSON coordinates in LineString.");
141
+ const nodeId = getOrCreateNode(lon, lat);
142
+ nodeRefs.push(nodeId);
143
+ }
144
+
145
+ // Create the way
146
+ const wayId = featureId ?? nextWayId--;
147
+ osm.ways.addWay({
148
+ id: wayId,
149
+ refs: nodeRefs,
150
+ tags,
151
+ });
152
+ } else if (normalizedFeature.geometry?.type === "Polygon") {
153
+ const coordinates = normalizedFeature.geometry.coordinates;
154
+ if (coordinates.length === 0) continue;
155
+
156
+ // If the polygon contains holes, create a relation
157
+ const createRelation = coordinates.length > 1;
158
+
159
+ // First ring is outer boundary, subsequent rings are holes
160
+ const outerRing = coordinates[0];
161
+ if (!outerRing || outerRing.length < 3) continue; // Need at least 3 points for a polygon
162
+
163
+ // Create nodes for outer ring
164
+ const outerNodeRefs: number[] = [];
165
+ for (const [lon, lat] of outerRing) {
166
+ if (lon === undefined || lat === undefined)
167
+ throw Error("Invalid GeoJSON coordinates in Polygon.");
168
+ const nodeId = getOrCreateNode(lon, lat);
169
+ outerNodeRefs.push(nodeId);
170
+ }
171
+
172
+ // Ensure the outer ring is closed (first and last node are the same)
173
+ if (outerNodeRefs[0] !== outerNodeRefs[outerNodeRefs.length - 1])
174
+ throw Error("Outer ring of Polygon is not closed.");
175
+
176
+ // Create way for outer ring with area tags
177
+ const outerWayId = createRelation ? nextWayId-- : (featureId ?? nextWayId--);
178
+ osm.ways.addWay({
179
+ id: outerWayId,
180
+ refs: outerNodeRefs,
181
+ tags: createRelation ? { area: "yes" } : { area: "yes", ...tags },
182
+ });
183
+
184
+ // Create separate ways for holes
185
+ const holeWayIds: number[] = [];
186
+ for (let i = 1; i < coordinates.length; i++) {
187
+ const holeRing = coordinates[i];
188
+ if (!holeRing || holeRing.length < 3)
189
+ throw Error("Hole ring of Polygon has less than 3 coordinates.");
190
+
191
+ const holeNodeRefs: number[] = [];
192
+ for (const [lon, lat] of holeRing) {
193
+ if (lon === undefined || lat === undefined)
194
+ throw Error("Invalid GeoJSON coordinates in Polygon.");
195
+ const nodeId = getOrCreateNode(lon, lat);
196
+ holeNodeRefs.push(nodeId);
197
+ }
198
+
199
+ // Ensure the ring is closed
200
+ if (holeNodeRefs[0] !== holeNodeRefs[holeNodeRefs.length - 1])
201
+ throw Error("Hole ring of Polygon is not closed.");
202
+
203
+ // Create way for hole
204
+ const holeWayId = nextWayId--;
205
+ osm.ways.addWay({
206
+ id: holeWayId,
207
+ refs: holeNodeRefs,
208
+ tags: { area: "yes" },
209
+ });
210
+ holeWayIds.push(holeWayId);
211
+ }
212
+
213
+ if (createRelation) {
214
+ osm.relations.addRelation({
215
+ id: featureId ?? nextRelationId--,
216
+ members: [
217
+ { type: "way", ref: outerWayId, role: "outer" },
218
+ ...holeWayIds.map(
219
+ (id) => ({ type: "way", ref: id, role: "inner" }) as OsmRelationMember,
220
+ ),
221
+ ],
222
+ tags: {
223
+ type: "multipolygon",
224
+ ...tags,
225
+ },
226
+ });
227
+ }
228
+ } else if (normalizedFeature.geometry?.type === "MultiPolygon") {
229
+ const coordinates = normalizedFeature.geometry.coordinates;
230
+ if (coordinates.length === 0) continue;
231
+
232
+ // Process each polygon in the MultiPolygon
233
+ const relationMembers: OsmRelationMember[] = [];
234
+ for (const polygon of coordinates) {
235
+ if (polygon.length === 0) continue;
236
+
237
+ const outerRing = polygon[0];
238
+ if (!outerRing || outerRing.length < 3) continue;
239
+
240
+ // Create nodes for outer ring
241
+ const outerNodeRefs: number[] = [];
242
+ for (const [lon, lat] of outerRing) {
243
+ if (lon === undefined || lat === undefined)
244
+ throw Error("Invalid GeoJSON coordinates in Polygon.");
245
+ const nodeId = getOrCreateNode(lon, lat);
246
+ outerNodeRefs.push(nodeId);
247
+ }
248
+
249
+ // Ensure the ring is closed
250
+ if (outerNodeRefs[0] !== outerNodeRefs[outerNodeRefs.length - 1])
251
+ throw Error("Outer ring of Polygon is not closed.");
252
+
253
+ // Create way for outer ring
254
+ const outerWayId = nextWayId--;
255
+ osm.ways.addWay({
256
+ id: outerWayId,
257
+ refs: outerNodeRefs,
258
+ tags: { area: "yes" },
259
+ });
260
+ relationMembers.push({ type: "way", ref: outerWayId, role: "outer" });
261
+
262
+ // Create separate ways for holes in this polygon
263
+ for (let i = 1; i < polygon.length; i++) {
264
+ const holeRing = polygon[i];
265
+ if (!holeRing || holeRing.length < 3)
266
+ throw Error("Hole ring of Polygon has less than 3 coordinates.");
267
+
268
+ const holeNodeRefs: number[] = [];
269
+ for (const [lon, lat] of holeRing) {
270
+ if (lon === undefined || lat === undefined)
271
+ throw Error("Invalid GeoJSON coordinates in Polygon.");
272
+ const nodeId = getOrCreateNode(lon, lat);
273
+ holeNodeRefs.push(nodeId);
274
+ }
275
+
276
+ // Ensure the ring is closed
277
+ if (holeNodeRefs[0] !== holeNodeRefs[holeNodeRefs.length - 1])
278
+ throw Error("Hole ring of Polygon is not closed.");
279
+
280
+ const holeWayId = nextWayId--;
281
+ osm.ways.addWay({
282
+ id: holeWayId,
283
+ refs: holeNodeRefs,
284
+ tags: { area: "yes" },
285
+ });
286
+
287
+ relationMembers.push({ type: "way", ref: holeWayId, role: "inner" });
288
+ }
289
+ }
290
+
291
+ if (relationMembers.length > 0) {
292
+ osm.relations.addRelation({
293
+ id: featureId ?? nextRelationId--,
294
+ members: relationMembers,
295
+ tags: { type: "multipolygon", ...tags },
296
+ });
297
+ }
298
+ }
299
+
300
+ yield progressEvent(`Processed ${count++} features...`);
301
+ }
302
+
303
+ yield progressEvent("Finished converting GeoJSON to Osmix, building indexes...");
304
+
305
+ // Build indexes
306
+ osm.buildIndexes();
307
+ osm.buildSpatialIndexes();
326
308
  }
327
309
 
328
310
  // 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
311
+ function propertiesToTags(properties: Record<string, unknown> | null): OsmTags | undefined {
312
+ if (!properties || Object.keys(properties).length === 0) {
313
+ return undefined;
314
+ }
315
+
316
+ const tags: OsmTags = {};
317
+ for (const [key, value] of Object.entries(properties)) {
318
+ // Convert value to string or number
319
+ if (typeof value === "string" || typeof value === "number") {
320
+ tags[key] = value;
321
+ } else if (typeof value === "boolean") {
322
+ tags[key] = String(value);
323
+ } else if (value != null && typeof value === "object") {
324
+ tags[key] = JSON.stringify(value);
325
+ }
326
+ }
327
+ return Object.keys(tags).length > 0 ? tags : undefined;
347
328
  }
348
329
 
349
330
  // 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
331
+ function extractFeatureId(featureId: string | number | undefined): number | undefined {
332
+ if (featureId === undefined) return undefined;
333
+ if (typeof featureId === "number") return featureId;
334
+ if (typeof featureId === "string") {
335
+ const numId = Number.parseInt(featureId, 10);
336
+ if (!Number.isNaN(numId)) return numId;
337
+ }
338
+ return undefined;
360
339
  }