@osmix/core 0.0.2 → 0.0.6

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