@osmix/geoparquet 0.1.4 → 0.1.5

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/package.json CHANGED
@@ -2,26 +2,11 @@
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@osmix/geoparquet",
4
4
  "description": "Import OSM data from GeoParquet files.",
5
- "version": "0.1.4",
5
+ "version": "0.1.5",
6
6
  "type": "module",
7
- "main": "./src/index.ts",
8
- "types": "./src/index.ts",
7
+ "types": "./dist/index.d.ts",
9
8
  "publishConfig": {
10
- "access": "public",
11
- "registry": "https://registry.npmjs.org/",
12
- "main": "./dist/index.js",
13
- "types": "./dist/index.d.ts",
14
- "exports": {
15
- ".": {
16
- "import": "./dist/index.js",
17
- "types": "./dist/index.d.ts"
18
- }
19
- },
20
- "files": [
21
- "dist",
22
- "README.md",
23
- "LICENSE"
24
- ]
9
+ "access": "public"
25
10
  },
26
11
  "license": "MIT",
27
12
  "repository": {
@@ -42,14 +27,28 @@
42
27
  "typecheck": "tsgo --noEmit"
43
28
  },
44
29
  "devDependencies": {
45
- "@types/bun": "^1.3.3",
46
- "@types/geojson": "^7946.0.16",
47
- "typescript": "^5.9.0"
30
+ "@types/bun": "catalog:",
31
+ "@types/geojson": "catalog:",
32
+ "typescript": "catalog:"
48
33
  },
49
34
  "dependencies": {
50
- "@osmix/core": "0.1.5",
51
- "@osmix/shared": "0.0.10",
52
- "@placemarkio/geojson-rewind": "^1.0.3",
35
+ "@osmix/core": "workspace:*",
36
+ "@osmix/shared": "workspace:*",
37
+ "@placemarkio/geojson-rewind": "catalog:",
53
38
  "hyparquet": "^1.24.1"
54
- }
39
+ },
40
+ "exports": {
41
+ ".": {
42
+ "types": "./src/index.ts",
43
+ "bun": "./src/index.ts",
44
+ "development|production": "./src/index.ts",
45
+ "default": "./dist/index.js"
46
+ }
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "src",
51
+ "README.md",
52
+ "LICENSE"
53
+ ]
55
54
  }
package/CHANGELOG.md DELETED
@@ -1,56 +0,0 @@
1
- # @osmix/geoparquet
2
-
3
- ## 0.1.4
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [12728ed]
8
- - @osmix/shared@0.0.10
9
- - @osmix/core@0.1.5
10
-
11
- ## 0.1.3
12
-
13
- ### Patch Changes
14
-
15
- - f32e4ee: General cleanup
16
- - Updated dependencies [f32e4ee]
17
- - @osmix/core@0.1.4
18
- - @osmix/shared@0.0.9
19
-
20
- ## 0.1.2
21
-
22
- ### Patch Changes
23
-
24
- - f468db5: Fix publishing (2)
25
- - Updated dependencies [f468db5]
26
- - @osmix/core@0.1.3
27
- - @osmix/shared@0.0.8
28
-
29
- ## 0.1.1
30
-
31
- ### Patch Changes
32
-
33
- - 68d6bd8: Fix publishing for packages.
34
- - Updated dependencies [68d6bd8]
35
- - @osmix/core@0.1.2
36
- - @osmix/shared@0.0.7
37
-
38
- ## 0.1.0
39
-
40
- ### Minor Changes
41
-
42
- - 31fa333: Add `@osmix/geoparquet` package for importing OSM data from OpenStreetMap US Layercake GeoParquet files.
43
-
44
- Features:
45
-
46
- - `fromGeoParquet()` function to create Osm indexes from GeoParquet files
47
- - WKB geometry parsing for Point, LineString, Polygon, and MultiPolygon
48
- - Support for file paths, URLs, and ArrayBuffer inputs
49
- - Customizable column mapping for different parquet schemas
50
-
51
- ### Patch Changes
52
-
53
- - 54fe002: Add fromGeoParquet to OsmixWorker and OsmixRemote
54
- - 31fa333: Import GeoParquet
55
- - Updated dependencies [d4f4b1f]
56
- - @osmix/core@0.1.1
@@ -1,40 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Download Monaco highway data from LayerCake (OSM US) using DuckDB
4
- # Monaco bounding box coordinates from MapTiler:
5
- # - xmin: 7.409205
6
- # - xmax: 7.448637
7
- # - ymin: 43.72335
8
- # - ymax: 43.75169
9
-
10
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
- OUTPUT_DIR="$SCRIPT_DIR/../../../fixtures"
12
- OUTPUT_FILE="$OUTPUT_DIR/monaco-highways.parquet"
13
-
14
- echo "Downloading Monaco highway data from LayerCake..."
15
- echo "Output: $OUTPUT_FILE"
16
-
17
- duckdb <<EOF
18
- INSTALL spatial;
19
- LOAD spatial;
20
-
21
- COPY (
22
- FROM 'https://data.openstreetmap.us/layercake/highways.parquet'
23
- SELECT
24
- id,
25
- tags,
26
- geometry
27
- WHERE
28
- bbox.xmin >= 7.409205 AND bbox.xmax <= 7.448637 AND
29
- bbox.ymin >= 43.72335 AND bbox.ymax <= 43.75169
30
- ) TO '$OUTPUT_FILE' WITH (FORMAT PARQUET);
31
- EOF
32
-
33
- if [ -f "$OUTPUT_FILE" ]; then
34
- echo "Successfully downloaded Monaco highways to $OUTPUT_FILE"
35
- echo "File size: $(ls -lh "$OUTPUT_FILE" | awk '{print $5}')"
36
- else
37
- echo "Error: Failed to download file"
38
- exit 1
39
- fi
40
-
@@ -1,520 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { GeoParquetOsmBuilder } from "../src/from-geoparquet"
3
- import type { GeoParquetRow } from "../src/types"
4
-
5
- /**
6
- * Create WKB Point geometry.
7
- */
8
- function createPointWkb(lon: number, lat: number): Uint8Array {
9
- const buffer = new ArrayBuffer(21)
10
- const view = new DataView(buffer)
11
- let offset = 0
12
-
13
- view.setUint8(offset, 1) // little endian
14
- offset += 1
15
- view.setUint32(offset, 1, true) // Point type
16
- offset += 4
17
- view.setFloat64(offset, lon, true)
18
- offset += 8
19
- view.setFloat64(offset, lat, true)
20
-
21
- return new Uint8Array(buffer)
22
- }
23
-
24
- /**
25
- * Create WKB LineString geometry.
26
- */
27
- function createLineStringWkb(coords: [number, number][]): Uint8Array {
28
- const buffer = new ArrayBuffer(1 + 4 + 4 + coords.length * 16)
29
- const view = new DataView(buffer)
30
- let offset = 0
31
-
32
- view.setUint8(offset, 1) // little endian
33
- offset += 1
34
- view.setUint32(offset, 2, true) // LineString type
35
- offset += 4
36
- view.setUint32(offset, coords.length, true) // num points
37
- offset += 4
38
-
39
- for (const [lon, lat] of coords) {
40
- view.setFloat64(offset, lon, true)
41
- offset += 8
42
- view.setFloat64(offset, lat, true)
43
- offset += 8
44
- }
45
-
46
- return new Uint8Array(buffer)
47
- }
48
-
49
- /**
50
- * Create WKB Polygon geometry.
51
- */
52
- function createPolygonWkb(rings: [number, number][][]): Uint8Array {
53
- const totalPoints = rings.reduce((sum, ring) => sum + ring.length, 0)
54
- const buffer = new ArrayBuffer(
55
- 1 + 4 + 4 + rings.length * 4 + totalPoints * 16,
56
- )
57
- const view = new DataView(buffer)
58
- let offset = 0
59
-
60
- view.setUint8(offset, 1) // little endian
61
- offset += 1
62
- view.setUint32(offset, 3, true) // Polygon type
63
- offset += 4
64
- view.setUint32(offset, rings.length, true) // num rings
65
- offset += 4
66
-
67
- for (const ring of rings) {
68
- view.setUint32(offset, ring.length, true)
69
- offset += 4
70
- for (const [lon, lat] of ring) {
71
- view.setFloat64(offset, lon, true)
72
- offset += 8
73
- view.setFloat64(offset, lat, true)
74
- offset += 8
75
- }
76
- }
77
-
78
- return new Uint8Array(buffer)
79
- }
80
-
81
- /**
82
- * Helper function to process rows using the builder.
83
- */
84
- function processRows(
85
- rows: GeoParquetRow[],
86
- options?: { idColumn?: string; geometryColumn?: string; tagsColumn?: string },
87
- ) {
88
- const builder = new GeoParquetOsmBuilder({}, options, () => {})
89
- builder.processGeoParquetRows(rows as unknown as Record<string, unknown>[])
90
- return builder.buildOsm()
91
- }
92
-
93
- describe("@osmix/geoparquet: GeoParquetOsmBuilder", () => {
94
- it("should convert Point features to Nodes", () => {
95
- const rows: GeoParquetRow[] = [
96
- {
97
- type: "node",
98
- id: 100n,
99
- geometry: createPointWkb(-122.4194, 37.7749),
100
- tags: { name: "San Francisco", population: "873965" },
101
- bbox: [-122.4194, 37.7749, -122.4194, 37.7749],
102
- },
103
- {
104
- type: "node",
105
- id: 200n,
106
- geometry: createPointWkb(-122.4094, 37.7849),
107
- tags: { name: "Another Point" },
108
- bbox: [-122.4094, 37.7849, -122.4094, 37.7849],
109
- },
110
- ]
111
-
112
- const osm = processRows(rows)
113
-
114
- expect(osm.nodes.size).toBe(2)
115
- expect(osm.ways.size).toBe(0)
116
-
117
- // Get nodes by index to check values
118
- const node1 = osm.nodes.getByIndex(0)
119
- const node2 = osm.nodes.getByIndex(1)
120
-
121
- expect(node1).toBeDefined()
122
- expect(node1?.lon).toBeCloseTo(-122.4194, 4)
123
- expect(node1?.lat).toBeCloseTo(37.7749, 4)
124
- expect(node1?.tags?.["name"]).toBe("San Francisco")
125
- expect(node1?.tags?.["population"]).toBe("873965")
126
-
127
- expect(node2).toBeDefined()
128
- expect(node2?.lon).toBeCloseTo(-122.4094, 4)
129
- expect(node2?.lat).toBeCloseTo(37.7849, 4)
130
- })
131
-
132
- it("should convert Point features to Nodes with auto-generated IDs", () => {
133
- const rows: GeoParquetRow[] = [
134
- {
135
- type: "node",
136
- id: undefined as unknown as bigint, // No ID provided
137
- geometry: createPointWkb(-122.4194, 37.7749),
138
- tags: { name: "San Francisco" },
139
- bbox: [-122.4194, 37.7749, -122.4194, 37.7749],
140
- },
141
- ]
142
-
143
- const osm = processRows(rows)
144
-
145
- expect(osm.nodes.size).toBe(1)
146
-
147
- // Auto-generated IDs start at -1
148
- const node = osm.nodes.getById(-1)
149
- expect(node).toBeDefined()
150
- expect(node?.lon).toBeCloseTo(-122.4194, 4)
151
- expect(node?.lat).toBeCloseTo(37.7749, 4)
152
- expect(node?.tags?.["name"]).toBe("San Francisco")
153
- })
154
-
155
- it("should convert LineString features to Ways with Nodes", () => {
156
- const rows: GeoParquetRow[] = [
157
- {
158
- type: "way",
159
- id: undefined as unknown as bigint, // Auto-generate IDs
160
- geometry: createLineStringWkb([
161
- [-122.4194, 37.7749],
162
- [-122.4094, 37.7849],
163
- [-122.3994, 37.7949],
164
- ]),
165
- tags: { highway: "primary", name: "Main Street" },
166
- bbox: [-122.4194, 37.7749, -122.3994, 37.7949],
167
- },
168
- ]
169
-
170
- const osm = processRows(rows)
171
-
172
- expect(osm.nodes.size).toBe(3)
173
- expect(osm.ways.size).toBe(1)
174
-
175
- const way = osm.ways.getById(-1)
176
- expect(way).toBeDefined()
177
- expect(way?.refs).toHaveLength(3)
178
- expect(way?.tags?.["highway"]).toBe("primary")
179
- expect(way?.tags?.["name"]).toBe("Main Street")
180
-
181
- // Verify nodes were created with auto-generated IDs
182
- const node1 = osm.nodes.getById(way!.refs[0]!)
183
- const node2 = osm.nodes.getById(way!.refs[1]!)
184
- const node3 = osm.nodes.getById(way!.refs[2]!)
185
-
186
- expect(node1?.lon).toBeCloseTo(-122.4194, 4)
187
- expect(node1?.lat).toBeCloseTo(37.7749, 4)
188
- expect(node2?.lon).toBeCloseTo(-122.4094, 4)
189
- expect(node2?.lat).toBeCloseTo(37.7849, 4)
190
- expect(node3?.lon).toBeCloseTo(-122.3994, 4)
191
- expect(node3?.lat).toBeCloseTo(37.7949, 4)
192
- })
193
-
194
- it("should convert Polygon features to Ways with area tags", () => {
195
- const rows: GeoParquetRow[] = [
196
- {
197
- type: "way",
198
- id: undefined as unknown as bigint, // Auto-generate IDs
199
- geometry: createPolygonWkb([
200
- [
201
- [-122.4194, 37.7749],
202
- [-122.4094, 37.7749],
203
- [-122.4094, 37.7849],
204
- [-122.4194, 37.7849],
205
- [-122.4194, 37.7749], // closed
206
- ],
207
- ]),
208
- tags: { building: "yes", name: "Test Building" },
209
- bbox: [-122.4194, 37.7749, -122.4094, 37.7849],
210
- },
211
- ]
212
-
213
- const osm = processRows(rows)
214
-
215
- expect(osm.nodes.size).toBe(4) // 4 unique nodes
216
- expect(osm.ways.size).toBe(1)
217
- expect(osm.relations.size).toBe(0)
218
-
219
- const way = osm.ways.getById(-1)
220
- expect(way).toBeDefined()
221
- expect(way?.tags?.["building"]).toBe("yes")
222
- expect(way?.tags?.["name"]).toBe("Test Building")
223
- expect(way?.tags?.["area"]).toBe("yes")
224
- expect(way?.refs).toHaveLength(5) // 4 unique + closing
225
- expect(way?.refs[0]).toBe(way?.refs[4]) // Ring is closed
226
- })
227
-
228
- it("should convert Polygon with holes to relation with multiple Ways", () => {
229
- const rows: GeoParquetRow[] = [
230
- {
231
- type: "relation",
232
- id: undefined as unknown as bigint, // Auto-generate IDs
233
- geometry: createPolygonWkb([
234
- // Outer ring
235
- [
236
- [-122.4194, 37.7749],
237
- [-122.4094, 37.7749],
238
- [-122.4094, 37.7849],
239
- [-122.4194, 37.7849],
240
- [-122.4194, 37.7749],
241
- ],
242
- // Hole
243
- [
244
- [-122.4164, 37.7779],
245
- [-122.4144, 37.7779],
246
- [-122.4144, 37.7799],
247
- [-122.4164, 37.7799],
248
- [-122.4164, 37.7779],
249
- ],
250
- ]),
251
- tags: { building: "yes" },
252
- bbox: [-122.4194, 37.7749, -122.4094, 37.7849],
253
- },
254
- ]
255
-
256
- const osm = processRows(rows)
257
-
258
- expect(osm.ways.size).toBe(2)
259
- expect(osm.relations.size).toBe(1)
260
-
261
- // Get ways by index since IDs are auto-generated
262
- const outerWay = osm.ways.getByIndex(0)
263
- expect(outerWay).toBeDefined()
264
- expect(outerWay?.tags?.["area"]).toBe("yes")
265
- expect(outerWay?.tags?.["building"]).toBeUndefined() // Tags go on relation
266
-
267
- const holeWay = osm.ways.getByIndex(1)
268
- expect(holeWay).toBeDefined()
269
- expect(holeWay?.tags?.["area"]).toBe("yes")
270
-
271
- const relation = osm.relations.getById(-1)
272
- expect(relation).toBeDefined()
273
- expect(relation?.tags?.["type"]).toBe("multipolygon")
274
- expect(relation?.tags?.["building"]).toBe("yes")
275
- expect(relation?.members).toHaveLength(2)
276
- expect(relation?.members[0]?.type).toBe("way")
277
- expect(relation?.members[0]?.role).toBe("outer")
278
- expect(relation?.members[1]?.type).toBe("way")
279
- expect(relation?.members[1]?.role).toBe("inner")
280
- })
281
-
282
- it("should handle JSON string tags", () => {
283
- const rows: GeoParquetRow[] = [
284
- {
285
- type: "node",
286
- id: undefined as unknown as bigint,
287
- geometry: createPointWkb(-122.4194, 37.7749),
288
- tags: '{"name":"Test","highway":"primary"}',
289
- bbox: [-122.4194, 37.7749, -122.4194, 37.7749],
290
- },
291
- ]
292
-
293
- const osm = processRows(rows)
294
-
295
- const node = osm.nodes.getById(-1)
296
- expect(node?.tags?.["name"]).toBe("Test")
297
- expect(node?.tags?.["highway"]).toBe("primary")
298
- })
299
-
300
- it("should handle null tags", () => {
301
- const rows: GeoParquetRow[] = [
302
- {
303
- type: "node",
304
- id: undefined as unknown as bigint,
305
- geometry: createPointWkb(-122.4194, 37.7749),
306
- tags: null as unknown as string,
307
- bbox: [-122.4194, 37.7749, -122.4194, 37.7749],
308
- },
309
- ]
310
-
311
- const osm = processRows(rows)
312
-
313
- const node = osm.nodes.getById(-1)
314
- expect(node?.tags).toBeUndefined()
315
- })
316
-
317
- it("should skip rows with missing geometry", () => {
318
- const rows: GeoParquetRow[] = [
319
- {
320
- type: "node",
321
- id: undefined as unknown as bigint,
322
- geometry: undefined as unknown as Uint8Array,
323
- tags: { name: "Missing geometry" },
324
- bbox: [0, 0, 0, 0],
325
- },
326
- {
327
- type: "node",
328
- id: undefined as unknown as bigint,
329
- geometry: createPointWkb(-122.4194, 37.7749),
330
- tags: { name: "Valid point" },
331
- bbox: [-122.4194, 37.7749, -122.4194, 37.7749],
332
- },
333
- ]
334
-
335
- const osm = processRows(rows)
336
-
337
- expect(osm.nodes.size).toBe(1)
338
- const node = osm.nodes.getById(-1)
339
- expect(node?.tags?.["name"]).toBe("Valid point")
340
- })
341
-
342
- it("should reuse nodes when features share coordinates", () => {
343
- const rows: GeoParquetRow[] = [
344
- {
345
- type: "way",
346
- id: undefined as unknown as bigint,
347
- geometry: createLineStringWkb([
348
- [-122.4194, 37.7749],
349
- [-122.4094, 37.7849],
350
- ]),
351
- tags: { highway: "primary" },
352
- bbox: [-122.4194, 37.7749, -122.4094, 37.7849],
353
- },
354
- {
355
- type: "way",
356
- id: undefined as unknown as bigint,
357
- geometry: createLineStringWkb([
358
- [-122.4094, 37.7849], // Shared coordinate
359
- [-122.3994, 37.7949],
360
- ]),
361
- tags: { highway: "secondary" },
362
- bbox: [-122.4094, 37.7849, -122.3994, 37.7949],
363
- },
364
- ]
365
-
366
- const osm = processRows(rows)
367
-
368
- // Should have 3 nodes (shared coordinate is reused)
369
- expect(osm.nodes.size).toBe(3)
370
- expect(osm.ways.size).toBe(2)
371
-
372
- const way1 = osm.ways.getById(-1)
373
- const way2 = osm.ways.getById(-2)
374
-
375
- expect(way1?.refs).toHaveLength(2)
376
- expect(way2?.refs).toHaveLength(2)
377
- // Shared node
378
- expect(way1?.refs[1]).toBe(way2?.refs[0])
379
- })
380
-
381
- it("should build indexes after processing", () => {
382
- const rows: GeoParquetRow[] = [
383
- {
384
- type: "node",
385
- id: undefined as unknown as bigint,
386
- geometry: createPointWkb(-122.4194, 37.7749),
387
- tags: { name: "Test" },
388
- bbox: [-122.4194, 37.7749, -122.4194, 37.7749],
389
- },
390
- ]
391
-
392
- const osm = processRows(rows)
393
-
394
- expect(osm.isReady()).toBe(true)
395
- expect(osm.nodes.isReady()).toBe(true)
396
- expect(osm.ways.isReady()).toBe(true)
397
- })
398
-
399
- it("should handle object tags", () => {
400
- const rows: GeoParquetRow[] = [
401
- {
402
- type: "node",
403
- id: undefined as unknown as bigint,
404
- geometry: createPointWkb(-122.4194, 37.7749),
405
- tags: { name: "Test", highway: "primary" },
406
- bbox: [-122.4194, 37.7749, -122.4194, 37.7749],
407
- },
408
- ]
409
-
410
- const osm = processRows(rows)
411
-
412
- const node = osm.nodes.getById(-1)
413
- expect(node?.tags?.["name"]).toBe("Test")
414
- expect(node?.tags?.["highway"]).toBe("primary")
415
- })
416
-
417
- it("should handle custom column names", () => {
418
- const rows = [
419
- {
420
- type: "node",
421
- osm_id: undefined,
422
- geom: createPointWkb(-122.4194, 37.7749),
423
- properties: { name: "Custom columns" },
424
- bbox: [-122.4194, 37.7749, -122.4194, 37.7749],
425
- },
426
- ] as unknown as GeoParquetRow[]
427
-
428
- const osm = processRows(rows, {
429
- idColumn: "osm_id",
430
- geometryColumn: "geom",
431
- tagsColumn: "properties",
432
- })
433
-
434
- expect(osm.nodes.size).toBe(1)
435
- const node = osm.nodes.getById(-1)
436
- expect(node?.tags?.["name"]).toBe("Custom columns")
437
- })
438
-
439
- it("should infer type from geometry when type column is missing", () => {
440
- // Simple polygon without holes - should only create a way, not a relation
441
- const simplePolygonRows: GeoParquetRow[] = [
442
- {
443
- type: undefined as unknown as "way", // Missing type
444
- id: 123n,
445
- geometry: createPolygonWkb([
446
- [
447
- [-122.4194, 37.7749],
448
- [-122.4094, 37.7749],
449
- [-122.4094, 37.7849],
450
- [-122.4194, 37.7849],
451
- [-122.4194, 37.7749],
452
- ],
453
- ]),
454
- tags: { building: "yes" },
455
- bbox: [-122.4194, 37.7749, -122.4094, 37.7849],
456
- },
457
- ]
458
-
459
- const osm1 = processRows(simplePolygonRows)
460
-
461
- // Should create just a way, no relation
462
- expect(osm1.ways.size).toBe(1)
463
- expect(osm1.relations.size).toBe(0)
464
-
465
- // Way should have the feature ID and tags
466
- const way = osm1.ways.getById(123)
467
- expect(way).toBeDefined()
468
- expect(way?.tags?.["building"]).toBe("yes")
469
- expect(way?.tags?.["area"]).toBe("yes")
470
- })
471
-
472
- it("should infer relation type for polygon with holes when type is missing", () => {
473
- // Polygon with hole - should create a relation
474
- const polygonWithHoleRows: GeoParquetRow[] = [
475
- {
476
- type: undefined as unknown as "relation", // Missing type
477
- id: 456n,
478
- geometry: createPolygonWkb([
479
- // Outer ring
480
- [
481
- [-122.4194, 37.7749],
482
- [-122.4094, 37.7749],
483
- [-122.4094, 37.7849],
484
- [-122.4194, 37.7849],
485
- [-122.4194, 37.7749],
486
- ],
487
- // Hole
488
- [
489
- [-122.4164, 37.7779],
490
- [-122.4144, 37.7779],
491
- [-122.4144, 37.7799],
492
- [-122.4164, 37.7799],
493
- [-122.4164, 37.7779],
494
- ],
495
- ]),
496
- tags: { building: "yes" },
497
- bbox: [-122.4194, 37.7749, -122.4094, 37.7849],
498
- },
499
- ]
500
-
501
- const osm2 = processRows(polygonWithHoleRows)
502
-
503
- // Should create 2 ways (outer + hole) and 1 relation
504
- expect(osm2.ways.size).toBe(2)
505
- expect(osm2.relations.size).toBe(1)
506
-
507
- // Relation should have the feature ID and tags
508
- const relation = osm2.relations.getById(456)
509
- expect(relation).toBeDefined()
510
- expect(relation?.tags?.["type"]).toBe("multipolygon")
511
- expect(relation?.tags?.["building"]).toBe("yes")
512
- expect(relation?.members).toHaveLength(2)
513
- expect(relation?.members[0]?.role).toBe("outer")
514
- expect(relation?.members[1]?.role).toBe("inner")
515
-
516
- // Ways should not have the feature ID (456 goes to relation)
517
- const outerWay = osm2.ways.getByIndex(0)
518
- expect(outerWay?.id).not.toBe(456)
519
- })
520
- })
@@ -1,249 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { getFixturePath } from "@osmix/shared/test/fixtures"
3
- import { fromGeoParquet, GeoParquetOsmBuilder } from "../src"
4
-
5
- /**
6
- * Integration tests using the Monaco highways GeoParquet fixture.
7
- *
8
- * The fixture was downloaded from LayerCake (https://openstreetmap.us/our-work/layercake/)
9
- * using DuckDB with a bounding box filter for Monaco. The fixture has a flattened
10
- * structure where each tag column is separate (highway, name, surface, etc.) rather
11
- * than nested in a tags struct.
12
- *
13
- * Note: hyparquet automatically parses GeoParquet WKB geometry into GeoJSON objects.
14
- *
15
- * Monaco bounding box:
16
- * - xmin: 7.409205, xmax: 7.448637
17
- * - ymin: 43.72335, ymax: 43.75169
18
- */
19
-
20
- describe("@osmix/geoparquet: Monaco highways fixture", () => {
21
- const fixtureFile = () => Bun.file(getFixturePath("monaco.parquet"))
22
- const getOsm = async () => fromGeoParquet(await fixtureFile().arrayBuffer())
23
-
24
- it("should load the monaco.parquet fixture", async () => {
25
- const file = fixtureFile()
26
- expect(await file.exists()).toBe(true)
27
-
28
- const size = file.size
29
- expect(size).toBeGreaterThan(0)
30
- expect(size).toBeLessThan(1_000_000) // Should be a small fixture
31
- })
32
-
33
- it("should read parquet rows with correct structure", async () => {
34
- const builder = new GeoParquetOsmBuilder({ id: "monaco" }, { rowEnd: 5 })
35
- const rows = await builder.readParquetRows(
36
- await fixtureFile().arrayBuffer(),
37
- )
38
-
39
- expect(rows.length).toBe(5)
40
-
41
- // Verify row structure
42
- const firstRow = rows[0]!
43
- expect(firstRow["id"]).toBeDefined()
44
- expect(firstRow["geometry"]).toBeDefined()
45
- // expect(firstRow.osm_type).toBeDefined()
46
-
47
- // hyparquet parses GeoParquet WKB into GeoJSON automatically
48
- expect(firstRow["geometry"].type).toBeDefined()
49
- // Verify it's a geometry with coordinates (not a GeometryCollection)
50
- expect(["Point", "LineString", "Polygon", "MultiPolygon"]).toContain(
51
- firstRow["geometry"].type,
52
- )
53
- })
54
-
55
- it("should convert highway features to OSM entities", async () => {
56
- const osm = await getOsm()
57
-
58
- // Should have created nodes and ways from the highway features
59
- expect(osm.nodes.size).toBeGreaterThan(0)
60
- expect(osm.ways.size).toBeGreaterThan(0)
61
-
62
- // Monaco is small, so we expect reasonable numbers
63
- // The highway extract should have a few hundred to a few thousand ways
64
- expect(osm.ways.size).toBeGreaterThan(100)
65
- expect(osm.ways.size).toBeLessThan(10_000)
66
- })
67
-
68
- it("should preserve highway tags from GeoParquet columns", async () => {
69
- const osm = await getOsm()
70
-
71
- // Find ways with highway tags
72
- let highwayCount = 0
73
- for (let i = 0; i < osm.ways.size; i++) {
74
- const way = osm.ways.getByIndex(i)
75
- if (way?.tags?.["highway"]) {
76
- highwayCount++
77
- }
78
- }
79
-
80
- // Most features should have highway tags since this is the highways layer
81
- expect(highwayCount).toBeGreaterThan(0)
82
- })
83
-
84
- it("should have valid node coordinates within Monaco bounds", async () => {
85
- const osm = await getOsm()
86
-
87
- // Monaco bounding box (slightly expanded for node positions)
88
- const minLon = 7.4
89
- const maxLon = 7.45
90
- const minLat = 43.72
91
- const maxLat = 43.76
92
-
93
- // Check that all nodes are within Monaco bounds
94
- let validNodes = 0
95
- for (let i = 0; i < osm.nodes.size; i++) {
96
- const node = osm.nodes.getByIndex(i)
97
- if (node) {
98
- expect(node.lon).toBeGreaterThanOrEqual(minLon)
99
- expect(node.lon).toBeLessThanOrEqual(maxLon)
100
- expect(node.lat).toBeGreaterThanOrEqual(minLat)
101
- expect(node.lat).toBeLessThanOrEqual(maxLat)
102
- validNodes++
103
- }
104
- }
105
-
106
- expect(validNodes).toBeGreaterThan(0)
107
- })
108
-
109
- it("should build spatial indexes after loading", async () => {
110
- const osm = await getOsm()
111
-
112
- // Indexes should be built
113
- expect(osm.isReady()).toBe(true)
114
- expect(osm.nodes.isReady()).toBe(true)
115
- expect(osm.ways.isReady()).toBe(true)
116
- })
117
-
118
- it("should handle ways with multiple nodes", async () => {
119
- const osm = await getOsm()
120
- // Count ways with enough nodes
121
- let validWays = 0
122
- for (let i = 0; i < osm.ways.size; i++) {
123
- const way = osm.ways.getByIndex(i)
124
- if (way && way.refs.length >= 2) {
125
- validWays++
126
- // All node refs should resolve to actual nodes
127
- for (const ref of way.refs) {
128
- const node = osm.nodes.getById(ref)
129
- expect(node).toBeDefined()
130
- }
131
- }
132
- }
133
-
134
- // Most ways should have at least 2 nodes (minimum for a LineString)
135
- expect(validWays).toBeGreaterThan(0)
136
- })
137
-
138
- it("should handle maxRows option to limit features", async () => {
139
- const builder = new GeoParquetOsmBuilder({ id: "monaco" }, { rowEnd: 10 })
140
- const rows = await builder.readParquetRows(
141
- await fixtureFile().arrayBuffer(),
142
- )
143
- builder.processGeoParquetRows(rows)
144
- const osm = builder.buildOsm()
145
-
146
- // Should only have processed 10 features
147
- expect(rows.length).toBe(10)
148
- // Note: Some rows may be Point geometries (crossings) which don't create ways
149
- expect(osm.nodes.size + osm.ways.size).toBeGreaterThan(0)
150
- })
151
-
152
- it("should find common highway types in Monaco", async () => {
153
- const osm = await getOsm()
154
-
155
- const highwayTypes = new Map<string, number>()
156
-
157
- for (let i = 0; i < osm.ways.size; i++) {
158
- const way = osm.ways.getByIndex(i)
159
- const highway = way?.tags?.["highway"]
160
- if (highway && typeof highway === "string") {
161
- highwayTypes.set(highway, (highwayTypes.get(highway) ?? 0) + 1)
162
- }
163
- }
164
-
165
- // Monaco should have some common highway types
166
- const allTypes = Array.from(highwayTypes.keys())
167
- expect(allTypes.length).toBeGreaterThan(0)
168
-
169
- // Common types that should exist in Monaco's dense urban area
170
- const commonTypes = ["residential", "footway", "service", "path", "steps"]
171
- const foundCommonTypes = commonTypes.filter((t) => highwayTypes.has(t))
172
-
173
- // At least some common types should be present
174
- expect(foundCommonTypes.length).toBeGreaterThan(0)
175
- })
176
-
177
- it("should handle surface and other highway attributes", async () => {
178
- const osm = await getOsm()
179
-
180
- // Check that some ways have additional attributes beyond just highway
181
- let waysWithSurface = 0
182
- let waysWithName = 0
183
-
184
- for (let i = 0; i < osm.ways.size; i++) {
185
- const way = osm.ways.getByIndex(i)
186
- if (way?.tags) {
187
- if (way.tags["surface"]) waysWithSurface++
188
- if (way.tags["name"]) waysWithName++
189
- }
190
- }
191
-
192
- // Some highways should have surface or name attributes
193
- // (not all will, so we just check that at least some do)
194
- expect(waysWithSurface + waysWithName).toBeGreaterThan(0)
195
- })
196
-
197
- it("should reuse nodes for connected highways", async () => {
198
- const osm = await getOsm()
199
-
200
- // Count how many ways reference the same nodes
201
- const nodeRefCounts = new Map<number, number>()
202
-
203
- for (let i = 0; i < osm.ways.size; i++) {
204
- const way = osm.ways.getByIndex(i)
205
- if (way) {
206
- for (const ref of way.refs) {
207
- nodeRefCounts.set(ref, (nodeRefCounts.get(ref) ?? 0) + 1)
208
- }
209
- }
210
- }
211
-
212
- // Some nodes should be shared between multiple ways (intersections)
213
- const sharedNodes = Array.from(nodeRefCounts.values()).filter((c) => c > 1)
214
- expect(sharedNodes.length).toBeGreaterThan(0)
215
- })
216
-
217
- it("should handle Point geometries (crossings)", async () => {
218
- const osm = await getOsm()
219
-
220
- // Find nodes with crossing tags (Point features become Nodes with tags)
221
- let crossingCount = 0
222
- for (let i = 0; i < osm.nodes.size; i++) {
223
- const node = osm.nodes.getByIndex(i)
224
- if (node?.tags?.["highway"] === "crossing") {
225
- crossingCount++
226
- }
227
- }
228
-
229
- // Monaco should have some pedestrian crossings
230
- expect(crossingCount).toBeGreaterThan(0)
231
- })
232
-
233
- it("should count geometry types in the fixture", async () => {
234
- const builder = new GeoParquetOsmBuilder({ id: "monaco" })
235
- const rows = await builder.readParquetRows(
236
- await fixtureFile().arrayBuffer(),
237
- )
238
-
239
- const typeCounts = new Map<string, number>()
240
- for (const row of rows) {
241
- const type = row["geometry"]?.type ?? "unknown"
242
- typeCounts.set(type, (typeCounts.get(type) ?? 0) + 1)
243
- }
244
-
245
- // Highways layer should have mostly LineStrings (roads) and some Points (crossings)
246
- expect(typeCounts.get("LineString") ?? 0).toBeGreaterThan(0)
247
- expect(typeCounts.get("Point") ?? 0).toBeGreaterThan(0)
248
- })
249
- })
package/test/wkb.test.ts DELETED
@@ -1,296 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { parseWkb } from "../src/wkb"
3
-
4
- /**
5
- * Helper to create a WKB Point with properly encoded float64 coordinates.
6
- */
7
- function createPointWkb(
8
- lon: number,
9
- lat: number,
10
- littleEndian = true,
11
- ): Uint8Array {
12
- const buffer = new ArrayBuffer(21)
13
- const view = new DataView(buffer)
14
- let offset = 0
15
-
16
- view.setUint8(offset, littleEndian ? 1 : 0)
17
- offset += 1
18
- view.setUint32(offset, 1, littleEndian) // Point type
19
- offset += 4
20
- view.setFloat64(offset, lon, littleEndian)
21
- offset += 8
22
- view.setFloat64(offset, lat, littleEndian)
23
-
24
- return new Uint8Array(buffer)
25
- }
26
-
27
- describe("@osmix/geoparquet: WKB Parser", () => {
28
- it("should parse a Point geometry (little endian)", () => {
29
- const wkb = createPointWkb(-122.4194, 37.7749, true)
30
-
31
- const geometry = parseWkb(wkb)
32
-
33
- expect(geometry.type).toBe("Point")
34
- if (geometry.type === "Point") {
35
- expect(geometry.coordinates[0]).toBeCloseTo(-122.4194, 4)
36
- expect(geometry.coordinates[1]).toBeCloseTo(37.7749, 4)
37
- }
38
- })
39
-
40
- it("should parse a Point geometry (big endian)", () => {
41
- const wkb = createPointWkb(-122.4194, 37.7749, false)
42
-
43
- const geometry = parseWkb(wkb)
44
-
45
- expect(geometry.type).toBe("Point")
46
- if (geometry.type === "Point") {
47
- expect(geometry.coordinates[0]).toBeCloseTo(-122.4194, 4)
48
- expect(geometry.coordinates[1]).toBeCloseTo(37.7749, 4)
49
- }
50
- })
51
-
52
- it("should parse a LineString geometry", () => {
53
- // WKB LineString with 3 points
54
- const buffer = new ArrayBuffer(1 + 4 + 4 + 3 * 16)
55
- const view = new DataView(buffer)
56
- let offset = 0
57
-
58
- // Byte order: little endian
59
- view.setUint8(offset, 1)
60
- offset += 1
61
-
62
- // Geometry type: LineString (2)
63
- view.setUint32(offset, 2, true)
64
- offset += 4
65
-
66
- // Number of points: 3
67
- view.setUint32(offset, 3, true)
68
- offset += 4
69
-
70
- // Point 1: -122.4194, 37.7749
71
- view.setFloat64(offset, -122.4194, true)
72
- offset += 8
73
- view.setFloat64(offset, 37.7749, true)
74
- offset += 8
75
-
76
- // Point 2: -122.4094, 37.7849
77
- view.setFloat64(offset, -122.4094, true)
78
- offset += 8
79
- view.setFloat64(offset, 37.7849, true)
80
- offset += 8
81
-
82
- // Point 3: -122.3994, 37.7949
83
- view.setFloat64(offset, -122.3994, true)
84
- offset += 8
85
- view.setFloat64(offset, 37.7949, true)
86
-
87
- const wkb = new Uint8Array(buffer)
88
- const geometry = parseWkb(wkb)
89
-
90
- expect(geometry.type).toBe("LineString")
91
- if (geometry.type === "LineString") {
92
- expect(geometry.coordinates).toHaveLength(3)
93
- expect(geometry.coordinates[0]?.[0]).toBeCloseTo(-122.4194, 4)
94
- expect(geometry.coordinates[0]?.[1]).toBeCloseTo(37.7749, 4)
95
- expect(geometry.coordinates[1]?.[0]).toBeCloseTo(-122.4094, 4)
96
- expect(geometry.coordinates[1]?.[1]).toBeCloseTo(37.7849, 4)
97
- expect(geometry.coordinates[2]?.[0]).toBeCloseTo(-122.3994, 4)
98
- expect(geometry.coordinates[2]?.[1]).toBeCloseTo(37.7949, 4)
99
- }
100
- })
101
-
102
- it("should parse a Polygon geometry", () => {
103
- // WKB Polygon with 1 ring of 5 points (closed)
104
- const buffer = new ArrayBuffer(1 + 4 + 4 + 4 + 5 * 16)
105
- const view = new DataView(buffer)
106
- let offset = 0
107
-
108
- // Byte order: little endian
109
- view.setUint8(offset, 1)
110
- offset += 1
111
-
112
- // Geometry type: Polygon (3)
113
- view.setUint32(offset, 3, true)
114
- offset += 4
115
-
116
- // Number of rings: 1
117
- view.setUint32(offset, 1, true)
118
- offset += 4
119
-
120
- // Number of points in ring: 5
121
- view.setUint32(offset, 5, true)
122
- offset += 4
123
-
124
- // Points (closed ring)
125
- const coords = [
126
- [-122.4194, 37.7749],
127
- [-122.4094, 37.7749],
128
- [-122.4094, 37.7849],
129
- [-122.4194, 37.7849],
130
- [-122.4194, 37.7749], // closed
131
- ]
132
-
133
- for (const [lon, lat] of coords) {
134
- view.setFloat64(offset, lon!, true)
135
- offset += 8
136
- view.setFloat64(offset, lat!, true)
137
- offset += 8
138
- }
139
-
140
- const wkb = new Uint8Array(buffer)
141
- const geometry = parseWkb(wkb)
142
-
143
- expect(geometry.type).toBe("Polygon")
144
- if (geometry.type === "Polygon") {
145
- expect(geometry.coordinates).toHaveLength(1)
146
- expect(geometry.coordinates[0]).toHaveLength(5)
147
- expect(geometry.coordinates[0]?.[0]?.[0]).toBeCloseTo(-122.4194, 4)
148
- expect(geometry.coordinates[0]?.[0]?.[1]).toBeCloseTo(37.7749, 4)
149
- }
150
- })
151
-
152
- it("should parse a MultiPolygon geometry", () => {
153
- // Helper to create polygon WKB
154
- const createPolygonWkb = (
155
- coords: [number, number][][],
156
- ): [ArrayBuffer, number] => {
157
- const totalPoints = coords.reduce((sum, ring) => sum + ring.length, 0)
158
- const size = 1 + 4 + 4 + coords.length * 4 + totalPoints * 16
159
- const buffer = new ArrayBuffer(size)
160
- const view = new DataView(buffer)
161
- let offset = 0
162
-
163
- // Byte order
164
- view.setUint8(offset, 1)
165
- offset += 1
166
-
167
- // Polygon type
168
- view.setUint32(offset, 3, true)
169
- offset += 4
170
-
171
- // Number of rings
172
- view.setUint32(offset, coords.length, true)
173
- offset += 4
174
-
175
- for (const ring of coords) {
176
- view.setUint32(offset, ring.length, true)
177
- offset += 4
178
- for (const [lon, lat] of ring) {
179
- view.setFloat64(offset, lon, true)
180
- offset += 8
181
- view.setFloat64(offset, lat, true)
182
- offset += 8
183
- }
184
- }
185
-
186
- return [buffer, size]
187
- }
188
-
189
- // Create MultiPolygon with 2 polygons
190
- const poly1Coords: [number, number][][] = [
191
- [
192
- [-122.4194, 37.7749],
193
- [-122.4094, 37.7749],
194
- [-122.4094, 37.7849],
195
- [-122.4194, 37.7849],
196
- [-122.4194, 37.7749],
197
- ],
198
- ]
199
-
200
- const poly2Coords: [number, number][][] = [
201
- [
202
- [-122.3994, 37.7649],
203
- [-122.3894, 37.7649],
204
- [-122.3894, 37.7749],
205
- [-122.3994, 37.7749],
206
- [-122.3994, 37.7649],
207
- ],
208
- ]
209
-
210
- const [poly1Buffer, poly1Size] = createPolygonWkb(poly1Coords)
211
- const [poly2Buffer, poly2Size] = createPolygonWkb(poly2Coords)
212
-
213
- // MultiPolygon header + 2 polygons
214
- const multiBuffer = new ArrayBuffer(1 + 4 + 4 + poly1Size + poly2Size)
215
- const multiView = new DataView(multiBuffer)
216
- let offset = 0
217
-
218
- // Byte order
219
- multiView.setUint8(offset, 1)
220
- offset += 1
221
-
222
- // MultiPolygon type (6)
223
- multiView.setUint32(offset, 6, true)
224
- offset += 4
225
-
226
- // Number of geometries
227
- multiView.setUint32(offset, 2, true)
228
- offset += 4
229
-
230
- // Copy polygon 1
231
- new Uint8Array(multiBuffer, offset, poly1Size).set(
232
- new Uint8Array(poly1Buffer),
233
- )
234
- offset += poly1Size
235
-
236
- // Copy polygon 2
237
- new Uint8Array(multiBuffer, offset, poly2Size).set(
238
- new Uint8Array(poly2Buffer),
239
- )
240
-
241
- const wkb = new Uint8Array(multiBuffer)
242
- const geometry = parseWkb(wkb)
243
-
244
- expect(geometry.type).toBe("MultiPolygon")
245
- if (geometry.type === "MultiPolygon") {
246
- expect(geometry.coordinates).toHaveLength(2)
247
- expect(geometry.coordinates[0]).toHaveLength(1)
248
- expect(geometry.coordinates[1]).toHaveLength(1)
249
- }
250
- })
251
-
252
- it("should handle EWKB with SRID flag", () => {
253
- // EWKB Point with SRID (SRID flag: 0x20000000)
254
- const buffer = new ArrayBuffer(1 + 4 + 4 + 16)
255
- const view = new DataView(buffer)
256
- let offset = 0
257
-
258
- // Byte order: little endian
259
- view.setUint8(offset, 1)
260
- offset += 1
261
-
262
- // Geometry type: Point (1) with SRID flag (0x20000001)
263
- view.setUint32(offset, 0x20000001, true)
264
- offset += 4
265
-
266
- // SRID: 4326
267
- view.setUint32(offset, 4326, true)
268
- offset += 4
269
-
270
- // Point: -122.4194, 37.7749
271
- view.setFloat64(offset, -122.4194, true)
272
- offset += 8
273
- view.setFloat64(offset, 37.7749, true)
274
-
275
- const wkb = new Uint8Array(buffer)
276
- const geometry = parseWkb(wkb)
277
-
278
- expect(geometry.type).toBe("Point")
279
- if (geometry.type === "Point") {
280
- expect(geometry.coordinates[0]).toBeCloseTo(-122.4194, 4)
281
- expect(geometry.coordinates[1]).toBeCloseTo(37.7749, 4)
282
- }
283
- })
284
-
285
- it("should throw on unsupported geometry type", () => {
286
- const wkb = new Uint8Array([
287
- 0x01, // little endian
288
- 0x64,
289
- 0x00,
290
- 0x00,
291
- 0x00, // Invalid type (100)
292
- ])
293
-
294
- expect(() => parseWkb(wkb)).toThrow("Unsupported WKB geometry type: 100")
295
- })
296
- })
@@ -1,5 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/tsconfig",
3
- "extends": "./tsconfig.json",
4
- "include": ["src"]
5
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/tsconfig",
3
- "extends": "@osmix/shared/tsconfig/base.json",
4
- "exclude": ["node_modules", "dist"],
5
- "include": ["src", "test"],
6
- "compilerOptions": {
7
- "outDir": "./dist"
8
- }
9
- }