@osmix/core 0.0.2 → 0.1.1

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 (104) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +118 -107
  3. package/dist/src/entities.d.ts +103 -9
  4. package/dist/src/entities.d.ts.map +1 -1
  5. package/dist/src/entities.js +101 -11
  6. package/dist/src/entities.js.map +1 -1
  7. package/dist/src/ids.d.ts +82 -13
  8. package/dist/src/ids.d.ts.map +1 -1
  9. package/dist/src/ids.js +103 -31
  10. package/dist/src/ids.js.map +1 -1
  11. package/dist/src/index.d.ts +31 -5
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +33 -5
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/nodes.d.ts +91 -21
  16. package/dist/src/nodes.d.ts.map +1 -1
  17. package/dist/src/nodes.js +191 -44
  18. package/dist/src/nodes.js.map +1 -1
  19. package/dist/src/osm.d.ts +99 -0
  20. package/dist/src/osm.d.ts.map +1 -0
  21. package/dist/src/osm.js +165 -0
  22. package/dist/src/osm.js.map +1 -0
  23. package/dist/src/relations.d.ts +112 -20
  24. package/dist/src/relations.d.ts.map +1 -1
  25. package/dist/src/relations.js +298 -35
  26. package/dist/src/relations.js.map +1 -1
  27. package/dist/src/stringtable.d.ts +78 -14
  28. package/dist/src/stringtable.d.ts.map +1 -1
  29. package/dist/src/stringtable.js +105 -26
  30. package/dist/src/stringtable.js.map +1 -1
  31. package/dist/src/tags.d.ts +117 -18
  32. package/dist/src/tags.d.ts.map +1 -1
  33. package/dist/src/tags.js +177 -30
  34. package/dist/src/tags.js.map +1 -1
  35. package/dist/src/typed-arrays.d.ts +85 -11
  36. package/dist/src/typed-arrays.d.ts.map +1 -1
  37. package/dist/src/typed-arrays.js +88 -9
  38. package/dist/src/typed-arrays.js.map +1 -1
  39. package/dist/src/utils.d.ts +35 -1
  40. package/dist/src/utils.d.ts.map +1 -1
  41. package/dist/src/utils.js +69 -10
  42. package/dist/src/utils.js.map +1 -1
  43. package/dist/src/ways.d.ts +91 -27
  44. package/dist/src/ways.d.ts.map +1 -1
  45. package/dist/src/ways.js +229 -69
  46. package/dist/src/ways.js.map +1 -1
  47. package/dist/test/memory.test.d.ts +7 -0
  48. package/dist/test/memory.test.d.ts.map +1 -0
  49. package/dist/test/memory.test.js +29 -0
  50. package/dist/test/memory.test.js.map +1 -0
  51. package/dist/test/mock-osm.d.ts +3 -3
  52. package/dist/test/mock-osm.d.ts.map +1 -1
  53. package/dist/test/mock-osm.js +3 -3
  54. package/dist/test/mock-osm.js.map +1 -1
  55. package/dist/test/tags-search.test.d.ts +2 -0
  56. package/dist/test/tags-search.test.d.ts.map +1 -0
  57. package/dist/test/tags-search.test.js +56 -0
  58. package/dist/test/tags-search.test.js.map +1 -0
  59. package/dist/test/typed-arrays.test.js +19 -17
  60. package/dist/test/typed-arrays.test.js.map +1 -1
  61. package/package.json +11 -13
  62. package/src/entities.ts +147 -22
  63. package/src/ids.ts +126 -45
  64. package/src/index.ts +34 -5
  65. package/src/nodes.ts +248 -58
  66. package/src/osm.ts +222 -0
  67. package/src/relations.ts +396 -54
  68. package/src/stringtable.ts +123 -36
  69. package/src/tags.ts +220 -46
  70. package/src/typed-arrays.ts +97 -10
  71. package/src/types/geoflatbush.d.ts +37 -0
  72. package/src/types/geokdbush.d.ts +37 -0
  73. package/src/types/kdbush.d.ts +24 -6
  74. package/src/utils.ts +80 -14
  75. package/src/ways.ts +260 -84
  76. package/test/memory.test.ts +38 -0
  77. package/test/mock-osm.ts +8 -8
  78. package/test/tags-search.test.ts +60 -0
  79. package/test/typed-arrays.test.ts +5 -6
  80. package/dist/src/osmix.d.ts +0 -88
  81. package/dist/src/osmix.d.ts.map +0 -1
  82. package/dist/src/osmix.js +0 -438
  83. package/dist/src/osmix.js.map +0 -1
  84. package/dist/test/extract.bench.d.ts +0 -2
  85. package/dist/test/extract.bench.d.ts.map +0 -1
  86. package/dist/test/extract.bench.js +0 -23
  87. package/dist/test/extract.bench.js.map +0 -1
  88. package/dist/test/extract.test.d.ts +0 -2
  89. package/dist/test/extract.test.d.ts.map +0 -1
  90. package/dist/test/extract.test.js +0 -127
  91. package/dist/test/extract.test.js.map +0 -1
  92. package/dist/test/read.test.d.ts +0 -2
  93. package/dist/test/read.test.d.ts.map +0 -1
  94. package/dist/test/read.test.js +0 -17
  95. package/dist/test/read.test.js.map +0 -1
  96. package/dist/test/write.test.d.ts +0 -2
  97. package/dist/test/write.test.d.ts.map +0 -1
  98. package/dist/test/write.test.js +0 -55
  99. package/dist/test/write.test.js.map +0 -1
  100. package/src/osmix.ts +0 -556
  101. package/test/extract.bench.ts +0 -29
  102. package/test/extract.test.ts +0 -161
  103. package/test/read.test.ts +0 -22
  104. package/test/write.test.ts +0 -69
package/src/nodes.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { OsmNode, OsmTags } from "@osmix/json"
2
1
  import type { OsmPbfBlock, OsmPbfDenseNodes } from "@osmix/pbf"
3
2
  import { assertValue } from "@osmix/shared/assert"
4
- import type { GeoBbox2D } from "@osmix/shared/types"
3
+ import { microToDegrees, toMicroDegrees } from "@osmix/shared/coordinates"
4
+ import type { GeoBbox2D, OsmNode, OsmTags } from "@osmix/shared/types"
5
+ import { around as geoAround } from "geokdbush"
5
6
  import KDBush from "kdbush"
6
7
  import { Entities, type EntitiesTransferables } from "./entities"
7
8
  import { type IdOrIndex, Ids } from "./ids"
@@ -10,15 +11,17 @@ import { Tags } from "./tags"
10
11
  import {
11
12
  BufferConstructor,
12
13
  type BufferType,
13
- CoordinateArrayType,
14
- ResizeableTypedArray,
14
+ ResizeableTypedArray as RTA,
15
15
  } from "./typed-arrays"
16
+ import type { ContentHasher } from "./utils"
16
17
 
17
- export interface NodesTransferables extends EntitiesTransferables {
18
- lons: BufferType
19
- lats: BufferType
18
+ export interface NodesTransferables<T extends BufferType = BufferType>
19
+ extends EntitiesTransferables<T> {
20
+ lons: T
21
+ lats: T
20
22
  bbox: GeoBbox2D
21
- spatialIndex: BufferType
23
+ /** Optional - can be rebuilt via buildSpatialIndex() */
24
+ spatialIndex?: T
22
25
  }
23
26
 
24
27
  export interface AddNodeOptions {
@@ -26,66 +29,96 @@ export interface AddNodeOptions {
26
29
  }
27
30
 
28
31
  export class Nodes extends Entities<OsmNode> {
29
- lons: ResizeableTypedArray<Float64Array>
30
- lats: ResizeableTypedArray<Float64Array>
31
- bbox: GeoBbox2D = [
32
- Number.POSITIVE_INFINITY,
33
- Number.POSITIVE_INFINITY,
34
- Number.NEGATIVE_INFINITY,
35
- Number.NEGATIVE_INFINITY,
32
+ /**
33
+ * Coordinates are stored as integer microdegrees (Int32Array).
34
+ * Use OSM_COORD_SCALE (1e7) to convert between degrees and microdegrees.
35
+ */
36
+ private lons: RTA<Int32Array>
37
+ private lats: RTA<Int32Array>
38
+ // Bbox stored in microdegrees internally
39
+ private bbox: [
40
+ minLon: number,
41
+ minLat: number,
42
+ maxLon: number,
43
+ maxLat: number,
44
+ ] = [
45
+ Number.MAX_SAFE_INTEGER,
46
+ Number.MAX_SAFE_INTEGER,
47
+ Number.MIN_SAFE_INTEGER,
48
+ Number.MIN_SAFE_INTEGER,
36
49
  ]
37
- spatialIndex: KDBush = new KDBush(0, 128, Float64Array, BufferConstructor)
38
-
39
- static from(stringTable: StringTable, nits: NodesTransferables) {
40
- const idIndex = Ids.from(nits)
41
- const tagIndex = Tags.from(stringTable, nits)
42
- const nodeIndex = new Nodes(stringTable, idIndex, tagIndex)
43
- nodeIndex.lons = ResizeableTypedArray.from(CoordinateArrayType, nits.lons)
44
- nodeIndex.lats = ResizeableTypedArray.from(CoordinateArrayType, nits.lats)
45
- nodeIndex.bbox = nits.bbox
46
- nodeIndex.spatialIndex = KDBush.from(nits.spatialIndex)
47
- return nodeIndex
48
- }
49
-
50
- constructor(stringTable: StringTable, idIndex?: Ids, tagIndex?: Tags) {
51
- super("node", stringTable, idIndex, tagIndex)
52
- this.lons = new ResizeableTypedArray(CoordinateArrayType)
53
- this.lats = new ResizeableTypedArray(CoordinateArrayType)
54
- }
50
+ // Spatial index stores coordinates in degrees (Float64Array) for geokdbush compatibility
51
+ private spatialIndex: KDBush = new KDBush(
52
+ 0,
53
+ 128,
54
+ Float64Array,
55
+ BufferConstructor,
56
+ )
57
+ // Track if spatial index was properly built (vs default empty)
58
+ private spatialIndexBuilt = false
55
59
 
56
- transferables(): NodesTransferables {
57
- return {
58
- lons: this.lons.array.buffer,
59
- lats: this.lats.array.buffer,
60
- bbox: this.bbox,
61
- spatialIndex: this.spatialIndex.data,
62
- ...this.ids.transferables(),
63
- ...this.tags.transferables(),
60
+ /**
61
+ * Create a new Nodes index.
62
+ */
63
+ constructor(stringTable: StringTable, transferables?: NodesTransferables) {
64
+ if (transferables) {
65
+ super(
66
+ "node",
67
+ new Ids(transferables),
68
+ new Tags(stringTable, transferables),
69
+ )
70
+ this.lons = RTA.from(Int32Array, transferables.lons)
71
+ this.lats = RTA.from(Int32Array, transferables.lats)
72
+ // Only load spatial index if provided (not stored in IndexedDB)
73
+ if (transferables.spatialIndex?.byteLength) {
74
+ this.spatialIndex = KDBush.from(transferables.spatialIndex)
75
+ this.spatialIndexBuilt = true
76
+ }
77
+ this.bbox = transferables.bbox
78
+ this.indexBuilt = true
79
+ } else {
80
+ super("node", new Ids(), new Tags(stringTable))
81
+ this.lons = new RTA(Int32Array)
82
+ this.lats = new RTA(Int32Array)
83
+ // Spatial index uses Float64Array with degrees for geokdbush compatibility
84
+ this.spatialIndex = new KDBush(0, 128, Float64Array, BufferConstructor)
64
85
  }
65
86
  }
66
87
 
67
- addNode(node: OsmNode) {
68
- this.ids.add(node.id)
69
- this.tags.addTags(node.tags)
88
+ /**
89
+ * Add a single node to the index.
90
+ */
91
+ addNode(node: OsmNode): number {
92
+ const nodeIndex = this.addEntity(node.id, node.tags ?? {})
70
93
 
71
- this.lons.push(node.lon)
72
- this.lats.push(node.lat)
94
+ const lonMicro = toMicroDegrees(node.lon)
95
+ const latMicro = toMicroDegrees(node.lat)
96
+
97
+ this.lons.push(lonMicro)
98
+ this.lats.push(latMicro)
73
99
 
74
100
  if (node.lon < this.bbox[0]) this.bbox[0] = node.lon
75
101
  if (node.lat < this.bbox[1]) this.bbox[1] = node.lat
76
102
  if (node.lon > this.bbox[2]) this.bbox[2] = node.lon
77
103
  if (node.lat > this.bbox[3]) this.bbox[3] = node.lat
104
+
105
+ return nodeIndex
78
106
  }
79
107
 
108
+ /**
109
+ * Add dense nodes from a PBF block.
110
+ */
80
111
  addDenseNodes(
81
112
  dense: OsmPbfDenseNodes,
82
113
  block: OsmPbfBlock,
83
- blockStringIndexMap: Map<number, number>,
114
+ blockStringIndexMap: Uint32Array,
84
115
  filter?: (node: OsmNode) => boolean,
85
116
  ): number {
117
+ // PBF block already has offsets and granularity converted to degrees
86
118
  const lon_offset = block.lon_offset ?? 0
87
119
  const lat_offset = block.lat_offset ?? 0
88
120
  const granularity = block.granularity ?? 1e7
121
+
89
122
  const delta = {
90
123
  id: 0,
91
124
  lat: 0,
@@ -99,7 +132,7 @@ export class Nodes extends Entities<OsmNode> {
99
132
  const getStringTableIndex = (keyIndex: number) => {
100
133
  const key = dense.keys_vals[keyIndex]
101
134
  assertValue(key, "Block string key is undefined")
102
- const index = blockStringIndexMap.get(key)
135
+ const index = blockStringIndexMap[key]
103
136
  assertValue(index, "Block string not found")
104
137
  return index
105
138
  }
@@ -118,9 +151,14 @@ export class Nodes extends Entities<OsmNode> {
118
151
  delta.lat += latSid
119
152
  delta.lon += lonSid
120
153
 
154
+ // Calculate degrees from PBF delta encoding
121
155
  const lon = lon_offset + delta.lon / granularity
122
156
  const lat = lat_offset + delta.lat / granularity
123
157
 
158
+ // Convert to microdegrees for storage
159
+ const lonMicro = toMicroDegrees(lon)
160
+ const latMicro = toMicroDegrees(lat)
161
+
124
162
  const tagKeys: number[] = []
125
163
  const tagValues: number[] = []
126
164
  if (dense.keys_vals.length > 0) {
@@ -146,46 +184,90 @@ export class Nodes extends Entities<OsmNode> {
146
184
  : true
147
185
  if (!shouldInclude) continue
148
186
 
149
- this.ids.add(delta.id)
150
- this.lons.push(lon)
151
- this.lats.push(lat)
187
+ this.addEntity(delta.id, tagKeys, tagValues)
188
+ this.lons.push(lonMicro)
189
+ this.lats.push(latMicro)
152
190
 
153
191
  if (lon < this.bbox[0]) this.bbox[0] = lon
154
192
  if (lat < this.bbox[1]) this.bbox[1] = lat
155
193
  if (lon > this.bbox[2]) this.bbox[2] = lon
156
194
  if (lat > this.bbox[3]) this.bbox[3] = lat
157
- this.tags.addTagKeysAndValues(tagKeys, tagValues)
158
195
  added++
159
196
  }
160
197
 
161
198
  return added
162
199
  }
163
200
 
201
+ /**
202
+ * Compact the internal arrays to free up memory.
203
+ */
164
204
  buildEntityIndex() {
165
205
  this.lons.compact()
166
206
  this.lats.compact()
167
207
  }
168
208
 
209
+ /**
210
+ * Build the spatial index for nodes.
211
+ * Spatial index stores degrees (Float64Array) for geokdbush compatibility.
212
+ */
169
213
  buildSpatialIndex() {
170
214
  console.time("NodeIndex.buildSpatialIndex")
215
+ // Use Float64Array with degrees for geokdbush geographic queries
171
216
  this.spatialIndex = new KDBush(
172
217
  this.size,
173
- 128,
218
+ 64,
174
219
  Float64Array,
175
220
  BufferConstructor,
176
221
  )
177
222
  for (let i = 0; i < this.size; i++) {
178
- this.spatialIndex.add(this.lons.at(i), this.lats.at(i))
223
+ // Convert microdegrees to degrees for spatial index
224
+ const lon = microToDegrees(this.lons.at(i))
225
+ const lat = microToDegrees(this.lats.at(i))
226
+ this.spatialIndex.add(lon, lat)
179
227
  }
180
228
  this.spatialIndex.finish()
229
+ this.spatialIndexBuilt = true
181
230
  console.timeEnd("NodeIndex.buildSpatialIndex")
182
231
  }
183
232
 
233
+ /**
234
+ * Check if the spatial index has been built.
235
+ */
236
+ hasSpatialIndex(): boolean {
237
+ return this.spatialIndexBuilt
238
+ }
239
+
240
+ /**
241
+ * Get the bounding box of all nodes.
242
+ */
243
+ getBbox(): GeoBbox2D {
244
+ return this.bbox
245
+ }
246
+
247
+ /**
248
+ * Get the bounding box of a specific node.
249
+ */
250
+ getEntityBbox(i: IdOrIndex): GeoBbox2D {
251
+ const index = "index" in i ? i.index : this.ids.idOrIndex(i)[0]
252
+ const lon = microToDegrees(this.lons.at(index))
253
+ const lat = microToDegrees(this.lats.at(index))
254
+ return [lon, lat, lon, lat] as GeoBbox2D
255
+ }
256
+
257
+ /**
258
+ * Get the longitude and latitude of a specific node.
259
+ */
184
260
  getNodeLonLat(i: IdOrIndex): [number, number] {
185
261
  const index = "index" in i ? i.index : this.ids.idOrIndex(i)[0]
186
- return [this.lons.at(index), this.lats.at(index)]
262
+ return [
263
+ microToDegrees(this.lons.at(index)),
264
+ microToDegrees(this.lats.at(index)),
265
+ ]
187
266
  }
188
267
 
268
+ /**
269
+ * Get the full node entity.
270
+ */
189
271
  getFullEntity(index: number, id: number, tags?: OsmTags): OsmNode {
190
272
  const [lon, lat] = this.getNodeLonLat({ index })
191
273
  if (tags) {
@@ -204,11 +286,119 @@ export class Nodes extends Entities<OsmNode> {
204
286
  }
205
287
 
206
288
  // Spatial operations
207
- withinBbox(bbox: GeoBbox2D): number[] {
289
+ /**
290
+ * Find node indexes within a bounding box.
291
+ */
292
+ findIndexesWithinBbox(bbox: GeoBbox2D): number[] {
293
+ // Spatial index stores degrees, so query with degrees directly
208
294
  return this.spatialIndex.range(bbox[0], bbox[1], bbox[2], bbox[3])
209
295
  }
210
296
 
211
- withinRadius(x: number, y: number, radius: number): number[] {
212
- return this.spatialIndex.within(x, y, radius)
297
+ /**
298
+ * Find node indexes within a radius of a point.
299
+ * Uses geokdbush for proper great-circle distance calculations.
300
+ * @param lon - Longitude in degrees.
301
+ * @param lat - Latitude in degrees.
302
+ * @param radiusKm - Radius in kilometers.
303
+ * @returns Array of node indexes within the radius.
304
+ */
305
+ findIndexesWithinRadius(
306
+ lon: number,
307
+ lat: number,
308
+ radiusKm: number,
309
+ ): number[] {
310
+ // Use geokdbush for proper geographic distance calculations
311
+ return geoAround(
312
+ this.spatialIndex,
313
+ lon,
314
+ lat,
315
+ Number.POSITIVE_INFINITY,
316
+ radiusKm,
317
+ )
318
+ }
319
+
320
+ /**
321
+ * Get nodes within a bounding box.
322
+ * @param bbox - The bounding box to search within.
323
+ * @param include - A function to filter nodes. If provided, only nodes for which the function returns true will be included.
324
+ * @returns An object containing the IDs and positions of the nodes within the bounding box.
325
+ */
326
+ withinBbox(
327
+ bbox: GeoBbox2D,
328
+ include?: (i: number) => boolean,
329
+ ): {
330
+ ids: Float64Array
331
+ positions: Float64Array
332
+ } {
333
+ console.time("Nodes.withinBbox")
334
+ const nodeCandidates = this.findIndexesWithinBbox(bbox)
335
+ const nodePositions = new Float64Array(nodeCandidates.length * 2)
336
+ const ids = new Float64Array(nodeCandidates.length)
337
+
338
+ let skipped = 0
339
+ nodeCandidates.forEach((nodeIndex, i) => {
340
+ if (include && !include(nodeIndex)) {
341
+ skipped++
342
+ return
343
+ }
344
+
345
+ const [lon, lat] = this.getNodeLonLat({ index: nodeIndex })
346
+ ids[i - skipped] = this.ids.at(nodeIndex)
347
+ nodePositions[(i - skipped) * 2] = lon
348
+ nodePositions[(i - skipped) * 2 + 1] = lat
349
+ })
350
+ console.timeEnd("Nodes.withinBbox")
351
+ return {
352
+ ids: ids.subarray(0, nodeCandidates.length - skipped),
353
+ positions: nodePositions.slice(0, (nodeCandidates.length - skipped) * 2),
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Get transferable objects for passing to another thread.
359
+ * Only includes spatialIndex if it has been built.
360
+ */
361
+ override transferables(): NodesTransferables {
362
+ const base = {
363
+ ...super.transferables(),
364
+ lons: this.lons.array.buffer,
365
+ lats: this.lats.array.buffer,
366
+ bbox: this.bbox,
367
+ }
368
+ // Only include spatial index if it was built
369
+ if (this.spatialIndexBuilt) {
370
+ return { ...base, spatialIndex: this.spatialIndex.data }
371
+ }
372
+ return base
373
+ }
374
+
375
+ /**
376
+ * Get the approximate memory requirements for a given number of nodes in bytes.
377
+ */
378
+ static getBytesRequired(count: number) {
379
+ if (count === 0) return 0
380
+ const indexBytes = (count < 65536 ? 2 : 4) * count
381
+ // Spatial index stores coordinates in degrees (Float64Array)
382
+ const coordsBytes = count * 2 * Float64Array.BYTES_PER_ELEMENT
383
+ const padding = (8 - (indexBytes % 8)) % 8
384
+ const spatialIndexBytes = 8 + indexBytes + coordsBytes + padding
385
+
386
+ return (
387
+ Ids.getBytesRequired(count) +
388
+ Tags.getBytesRequired(count) +
389
+ count * Int32Array.BYTES_PER_ELEMENT + // lons (stored in microdegrees)
390
+ count * Int32Array.BYTES_PER_ELEMENT + // lats (stored in microdegrees)
391
+ spatialIndexBytes
392
+ )
393
+ }
394
+
395
+ /**
396
+ * Update a ContentHasher with node-specific data (coordinates).
397
+ */
398
+ override updateHash(hasher: ContentHasher): ContentHasher {
399
+ return super
400
+ .updateHash(hasher)
401
+ .update(this.lons.array)
402
+ .update(this.lats.array)
213
403
  }
214
404
  }
package/src/osm.ts ADDED
@@ -0,0 +1,222 @@
1
+ import type { OsmPbfHeaderBlock } from "@osmix/pbf"
2
+ import type { GeoBbox2D } from "@osmix/shared/types"
3
+ import { Nodes, type NodesTransferables } from "./nodes"
4
+ import { Relations, type RelationsTransferables } from "./relations"
5
+ import StringTable, { type StringTableTransferables } from "./stringtable"
6
+ import type { BufferType } from "./typed-arrays"
7
+ import { ContentHasher } from "./utils"
8
+ import { Ways, type WaysTransferables } from "./ways"
9
+
10
+ export interface OsmTransferables<T extends BufferType = BufferType> {
11
+ id: string
12
+ header: OsmPbfHeaderBlock
13
+ contentHash: string
14
+ stringTable: StringTableTransferables<T>
15
+ nodes: NodesTransferables<T>
16
+ ways: WaysTransferables<T>
17
+ relations: RelationsTransferables<T>
18
+ }
19
+
20
+ export interface OsmInfo {
21
+ id: string
22
+ bbox: GeoBbox2D
23
+ header: OsmPbfHeaderBlock
24
+ stats: {
25
+ nodes: number
26
+ ways: number
27
+ relations: number
28
+ }
29
+ }
30
+
31
+ export interface OsmOptions {
32
+ id: string
33
+ header: OsmPbfHeaderBlock
34
+ }
35
+
36
+ /**
37
+ * OSM Entity Index.
38
+ */
39
+ export class Osm {
40
+ // Filename or ID of this OSM Entity index.
41
+ readonly id: string
42
+ readonly header: OsmPbfHeaderBlock
43
+
44
+ // Shared string lookup table for all nodes, ways, and relations
45
+ readonly stringTable: StringTable
46
+ readonly nodes: Nodes
47
+ readonly ways: Ways
48
+ readonly relations: Relations
49
+
50
+ private indexBuilt = false
51
+ private _contentHash = ""
52
+
53
+ /**
54
+ * Create a new OSM Entity index.
55
+ */
56
+ constructor(opts?: Partial<OsmOptions> | OsmTransferables | Osm) {
57
+ this.id = opts?.id ?? "unknown"
58
+ this.header = opts?.header ?? {
59
+ required_features: [],
60
+ optional_features: [],
61
+ }
62
+ if (opts && "stringTable" in opts) {
63
+ if (opts instanceof Osm) {
64
+ this.stringTable = new StringTable(opts.stringTable.transferables())
65
+ this.nodes = new Nodes(this.stringTable, opts.nodes.transferables())
66
+ this.ways = new Ways(
67
+ this.stringTable,
68
+ this.nodes,
69
+ opts.ways.transferables(),
70
+ )
71
+ this.relations = new Relations(
72
+ this.stringTable,
73
+ this.nodes,
74
+ this.ways,
75
+ opts.relations.transferables(),
76
+ )
77
+ this._contentHash = opts._contentHash
78
+ this.indexBuilt = true
79
+ } else {
80
+ this.stringTable = new StringTable(opts.stringTable)
81
+ this.nodes = new Nodes(this.stringTable, opts.nodes)
82
+ this.ways = new Ways(this.stringTable, this.nodes, opts.ways)
83
+ this.relations = new Relations(
84
+ this.stringTable,
85
+ this.nodes,
86
+ this.ways,
87
+ opts.relations,
88
+ )
89
+ this._contentHash = opts.contentHash
90
+ this.indexBuilt = true
91
+ }
92
+ } else {
93
+ this.stringTable = new StringTable()
94
+ this.nodes = new Nodes(this.stringTable)
95
+ this.ways = new Ways(this.stringTable, this.nodes)
96
+ this.relations = new Relations(this.stringTable, this.nodes, this.ways)
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Build the internal indexes for all entities.
102
+ * Also computes the content hash after indexes are built.
103
+ */
104
+ buildIndexes() {
105
+ this.stringTable.buildIndex()
106
+ this.nodes.buildIndex()
107
+ this.ways.buildIndex()
108
+ this.relations.buildIndex()
109
+ this.indexBuilt = true
110
+ // Compute content hash after indexes are built
111
+ this._contentHash = this.computeContentHash()
112
+ }
113
+
114
+ /**
115
+ * Check if the index is built and ready for use.
116
+ */
117
+ isReady() {
118
+ return (
119
+ this.nodes.isReady() &&
120
+ this.ways.isReady() &&
121
+ this.relations.isReady() &&
122
+ this.indexBuilt
123
+ )
124
+ }
125
+
126
+ /**
127
+ * Build spatial indexes for all entities.
128
+ */
129
+ buildSpatialIndexes() {
130
+ this.nodes.buildSpatialIndex()
131
+ this.ways.buildSpatialIndex()
132
+ this.relations.buildSpatialIndex()
133
+ }
134
+
135
+ /**
136
+ * Check if spatial indexes have been built for all entity types.
137
+ */
138
+ hasSpatialIndexes(): boolean {
139
+ return (
140
+ this.nodes.hasSpatialIndex() &&
141
+ this.ways.hasSpatialIndex() &&
142
+ this.relations.hasSpatialIndex()
143
+ )
144
+ }
145
+
146
+ /**
147
+ * Get the bounding box of all entities in the OSM index.
148
+ */
149
+ bbox(): GeoBbox2D {
150
+ return this.nodes.getBbox()
151
+ }
152
+
153
+ /**
154
+ * Get information about the OSM index.
155
+ */
156
+ info(): OsmInfo {
157
+ return {
158
+ id: this.id,
159
+ bbox: this.bbox(),
160
+ header: this.header,
161
+ stats: {
162
+ nodes: this.nodes.size,
163
+ ways: this.ways.size,
164
+ relations: this.relations.size,
165
+ },
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get transferable objects for passing to another thread.
171
+ */
172
+ transferables(): OsmTransferables {
173
+ return {
174
+ id: this.id,
175
+ header: this.header,
176
+ contentHash: this._contentHash,
177
+ stringTable: this.stringTable.transferables(),
178
+ nodes: this.nodes.transferables(),
179
+ ways: this.ways.transferables(),
180
+ relations: this.relations.transferables(),
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get the content hash of this OSM dataset.
186
+ * The hash is computed when indexes are built.
187
+ *
188
+ * @returns A hex string hash uniquely identifying the content.
189
+ */
190
+ contentHash(): string {
191
+ return this._contentHash
192
+ }
193
+
194
+ /**
195
+ * Check if this OSM dataset has identical content to another.
196
+ * Uses the pre-computed content hash for fast comparison.
197
+ *
198
+ * @param other - The other Osm instance to compare with.
199
+ * @returns True if both datasets have identical content.
200
+ */
201
+ isEqual(other: Osm | null | undefined): boolean {
202
+ if (!other) return false
203
+ return this._contentHash === other._contentHash
204
+ }
205
+
206
+ /**
207
+ * Compute a content hash of all underlying data.
208
+ * This hash uniquely identifies the dataset content regardless of metadata.
209
+ *
210
+ * @returns A hex string hash of the content.
211
+ */
212
+ private computeContentHash(): string {
213
+ const hasher = new ContentHasher()
214
+ // Hash string table first (shared by all entities)
215
+ this.stringTable.updateHash(hasher)
216
+ // Hash each entity collection
217
+ this.nodes.updateHash(hasher)
218
+ this.ways.updateHash(hasher)
219
+ this.relations.updateHash(hasher)
220
+ return hasher.digest()
221
+ }
222
+ }