@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/relations.ts CHANGED
@@ -1,76 +1,122 @@
1
+ import type { OsmPbfRelation } from "@osmix/pbf"
2
+ import { assertValue } from "@osmix/shared/assert"
3
+ import {
4
+ buildRelationLineStrings,
5
+ collectRelationPoints,
6
+ isAreaRelation,
7
+ isLineRelation,
8
+ isPointRelation,
9
+ isSuperRelation,
10
+ resolveRelationMembers,
11
+ } from "@osmix/shared/relation-kind"
12
+ import { buildRelationRings } from "@osmix/shared/relation-multipolygon"
1
13
  import type {
14
+ GeoBbox2D,
15
+ LonLat,
2
16
  OsmEntityType,
3
17
  OsmRelation,
4
18
  OsmRelationMember,
5
19
  OsmTags,
6
- } from "@osmix/json"
7
- import type { OsmPbfRelation } from "@osmix/pbf"
8
- import { assertValue } from "@osmix/shared/assert"
20
+ } from "@osmix/shared/types"
21
+ import Flatbush from "flatbush"
22
+ import { around as geoAround } from "geoflatbush"
9
23
  import { Entities, type EntitiesTransferables } from "./entities"
10
- import { Ids } from "./ids"
24
+ import { type IdOrIndex, Ids } from "./ids"
25
+ import type { Nodes } from "./nodes"
11
26
  import type StringTable from "./stringtable"
12
27
  import { Tags } from "./tags"
13
28
  import {
29
+ BufferConstructor,
14
30
  type BufferType,
15
31
  IdArrayType,
16
- ResizeableTypedArray,
32
+ ResizeableTypedArray as RTA,
17
33
  } from "./typed-arrays"
34
+ import { bboxFromLonLats } from "./utils"
35
+ import type { Ways } from "./ways"
18
36
 
19
37
  const RELATION_MEMBER_TYPES: OsmEntityType[] = ["node", "way", "relation"]
20
38
 
21
- export interface RelationsTransferables extends EntitiesTransferables {
22
- memberStart: BufferType
23
- memberCount: BufferType
24
- memberRefs: BufferType
25
- memberTypes: BufferType
26
- memberRoles: BufferType
39
+ export interface RelationsTransferables<T extends BufferType = BufferType>
40
+ extends EntitiesTransferables<T> {
41
+ memberStart: T
42
+ memberCount: T
43
+ memberRefs: T
44
+ memberTypes: T
45
+ memberRoles: T
46
+ bbox: T
47
+ /** Optional - can be rebuilt via buildSpatialIndex() */
48
+ spatialIndex?: T
27
49
  }
28
50
 
29
51
  export class Relations extends Entities<OsmRelation> {
30
- memberStart: ResizeableTypedArray<Uint32Array>
31
- memberCount: ResizeableTypedArray<Uint16Array> // Maximum 65,535 members per relation
52
+ private stringTable: StringTable
53
+
54
+ private memberStart: RTA<Uint32Array>
55
+ private memberCount: RTA<Uint16Array> // Maximum 65,535 members per relation
32
56
 
33
57
  // Store the ID of the member because relations have other relations as members.
34
- memberRefs: ResizeableTypedArray<Float64Array>
35
- memberTypes: ResizeableTypedArray<Uint8Array>
36
- memberRoles: ResizeableTypedArray<Uint32Array>
37
-
38
- static from(stringTable: StringTable, rit: RelationsTransferables) {
39
- const idIndex = Ids.from(rit)
40
- const tagIndex = Tags.from(stringTable, rit)
41
- const ri = new Relations(stringTable, idIndex, tagIndex)
42
- ri.memberStart = ResizeableTypedArray.from(Uint32Array, rit.memberStart)
43
- ri.memberCount = ResizeableTypedArray.from(Uint16Array, rit.memberCount)
44
- ri.memberRefs = ResizeableTypedArray.from(IdArrayType, rit.memberRefs)
45
- ri.memberTypes = ResizeableTypedArray.from(Uint8Array, rit.memberTypes)
46
- ri.memberRoles = ResizeableTypedArray.from(Uint32Array, rit.memberRoles)
47
- return ri
48
- }
49
-
50
- constructor(stringTable: StringTable, idIndex?: Ids, tagIndex?: Tags) {
51
- super("relation", stringTable, idIndex, tagIndex)
52
- this.memberStart = new ResizeableTypedArray(Uint32Array)
53
- this.memberCount = new ResizeableTypedArray(Uint16Array)
54
- this.memberRefs = new ResizeableTypedArray(IdArrayType)
55
- this.memberTypes = new ResizeableTypedArray(Uint8Array)
56
- this.memberRoles = new ResizeableTypedArray(Uint32Array)
57
- }
58
-
59
- transferables(): RelationsTransferables {
60
- return {
61
- memberStart: this.memberStart.array.buffer,
62
- memberCount: this.memberCount.array.buffer,
63
- memberRefs: this.memberRefs.array.buffer,
64
- memberTypes: this.memberTypes.array.buffer,
65
- memberRoles: this.memberRoles.array.buffer,
66
- ...this.ids.transferables(),
67
- ...this.tags.transferables(),
58
+ private memberRefs: RTA<Float64Array>
59
+ private memberTypes: RTA<Uint8Array>
60
+ private memberRoles: RTA<Uint32Array>
61
+
62
+ // Spatial index
63
+ private spatialIndex: Flatbush = new Flatbush(1)
64
+ // Track if spatial index was properly built (vs default empty)
65
+ private spatialIndexBuilt = false
66
+
67
+ // Bounding box of the relation in geographic coordinates
68
+ private bbox: RTA<Float64Array>
69
+
70
+ // Node and Way indexes
71
+ private nodes: Nodes
72
+ private ways: Ways
73
+
74
+ /**
75
+ * Create a new Relations index.
76
+ */
77
+ constructor(
78
+ stringTable: StringTable,
79
+ nodes: Nodes,
80
+ ways: Ways,
81
+ transferables?: RelationsTransferables,
82
+ ) {
83
+ if (transferables) {
84
+ super(
85
+ "relation",
86
+ new Ids(transferables),
87
+ new Tags(stringTable, transferables),
88
+ )
89
+ this.memberStart = RTA.from(Uint32Array, transferables.memberStart)
90
+ this.memberCount = RTA.from(Uint16Array, transferables.memberCount)
91
+ this.memberRefs = RTA.from(IdArrayType, transferables.memberRefs)
92
+ this.memberTypes = RTA.from(Uint8Array, transferables.memberTypes)
93
+ this.memberRoles = RTA.from(Uint32Array, transferables.memberRoles)
94
+ this.bbox = RTA.from(Float64Array, transferables.bbox)
95
+ // Only load spatial index if provided (not stored in IndexedDB)
96
+ if (transferables.spatialIndex?.byteLength) {
97
+ this.spatialIndex = Flatbush.from(transferables.spatialIndex)
98
+ this.spatialIndexBuilt = true
99
+ }
100
+ this.indexBuilt = true
101
+ } else {
102
+ super("relation", new Ids(), new Tags(stringTable))
103
+ this.memberStart = new RTA(Uint32Array)
104
+ this.memberCount = new RTA(Uint16Array)
105
+ this.memberRefs = new RTA(IdArrayType)
106
+ this.memberTypes = new RTA(Uint8Array)
107
+ this.memberRoles = new RTA(Uint32Array)
108
+ this.bbox = new RTA(Float64Array)
68
109
  }
110
+ this.nodes = nodes
111
+ this.ways = ways
112
+ this.stringTable = stringTable
69
113
  }
70
114
 
115
+ /**
116
+ * Add a single relation to the index.
117
+ */
71
118
  addRelation(relation: OsmRelation) {
72
- this.ids.add(relation.id)
73
- this.tags.addTags(relation.tags)
119
+ const relationIndex = this.addEntity(relation.id, relation.tags ?? {})
74
120
  this.memberStart.push(this.memberRefs.length)
75
121
  this.memberCount.push(relation.members.length)
76
122
  for (const member of relation.members) {
@@ -78,15 +124,19 @@ export class Relations extends Entities<OsmRelation> {
78
124
  this.memberTypes.push(RELATION_MEMBER_TYPES.indexOf(member.type))
79
125
  this.memberRoles.push(this.stringTable.add(member.role ?? ""))
80
126
  }
127
+ return relationIndex
81
128
  }
82
129
 
130
+ /**
131
+ * Bulk add relations directly from a PBF PrimitiveBlock.
132
+ */
83
133
  addRelations(
84
134
  relations: OsmPbfRelation[],
85
- blockStringIndexMap: Map<number, number>,
135
+ blockStringIndexMap: Uint32Array,
86
136
  filter?: (relation: OsmRelation) => OsmRelation | null,
87
137
  ): number {
88
138
  const blockToStringTable = (k: number) => {
89
- const index = blockStringIndexMap.get(k)
139
+ const index = blockStringIndexMap[k]
90
140
  if (index === undefined) throw Error("Tag key not found")
91
141
  return index
92
142
  }
@@ -138,7 +188,7 @@ export class Relations extends Entities<OsmRelation> {
138
188
  if (filter && filteredRelation === null) continue
139
189
  added++
140
190
 
141
- this.ids.add(relation.id)
191
+ this.addEntity(relation.id, tagKeys, tagValues)
142
192
  this.memberStart.push(this.memberRefs.length)
143
193
  this.memberCount.push(
144
194
  filteredRelation?.members.length ?? memberRefs.length,
@@ -156,12 +206,13 @@ export class Relations extends Entities<OsmRelation> {
156
206
  this.stringTable.add(m.role ?? ""),
157
207
  ) ?? memberRoles,
158
208
  )
159
-
160
- this.tags.addTagKeysAndValues(tagKeys, tagValues)
161
209
  }
162
210
  return added
163
211
  }
164
212
 
213
+ /**
214
+ * Compact the internal arrays to free up memory.
215
+ */
165
216
  buildEntityIndex() {
166
217
  this.memberStart.compact()
167
218
  this.memberCount.compact()
@@ -170,6 +221,121 @@ export class Relations extends Entities<OsmRelation> {
170
221
  this.memberRoles.compact()
171
222
  }
172
223
 
224
+ /**
225
+ * Build the spatial index for relations.
226
+ * Handles nested relations by resolving all descendant nodes and ways.
227
+ * If bbox data already exists (e.g., loaded from storage), reuses it.
228
+ */
229
+ buildSpatialIndex() {
230
+ if (!this.nodes.isReady()) throw Error("Node index is not ready.")
231
+ if (!this.ways.isReady()) throw Error("Way index is not ready.")
232
+ if (this.size === 0) return this.spatialIndex
233
+ console.time("RelationIndex.buildSpatialIndex")
234
+
235
+ this.spatialIndex = new Flatbush(
236
+ this.size,
237
+ 128,
238
+ Float64Array,
239
+ BufferConstructor,
240
+ )
241
+
242
+ // If bbox already has data (loaded from storage), use it directly
243
+ const hasBboxData = this.bbox.length >= this.size * 4
244
+ for (let i = 0; i < this.size; i++) {
245
+ let minX: number
246
+ let minY: number
247
+ let maxX: number
248
+ let maxY: number
249
+
250
+ if (hasBboxData) {
251
+ // Use stored bbox values
252
+ minX = this.bbox.at(i * 4)
253
+ minY = this.bbox.at(i * 4 + 1)
254
+ maxX = this.bbox.at(i * 4 + 2)
255
+ maxY = this.bbox.at(i * 4 + 3)
256
+ } else {
257
+ // Calculate bbox from coordinates
258
+ const lls = this.collectRelationCoordinates(i)
259
+ const bbox = bboxFromLonLats(lls)
260
+ minX = bbox[0]
261
+ minY = bbox[1]
262
+ maxX = bbox[2]
263
+ maxY = bbox[3]
264
+ this.bbox.push(minX)
265
+ this.bbox.push(minY)
266
+ this.bbox.push(maxX)
267
+ this.bbox.push(maxY)
268
+ }
269
+ this.spatialIndex.add(minX, minY, maxX, maxY)
270
+ }
271
+ if (!hasBboxData) {
272
+ this.bbox.compact()
273
+ }
274
+ this.spatialIndex.finish()
275
+ this.spatialIndexBuilt = true
276
+ console.timeEnd("RelationIndex.buildSpatialIndex")
277
+ return this.spatialIndex
278
+ }
279
+
280
+ /**
281
+ * Check if the spatial index has been built.
282
+ */
283
+ hasSpatialIndex(): boolean {
284
+ return this.spatialIndexBuilt
285
+ }
286
+
287
+ /**
288
+ * Collect all coordinates from a relation, including nested relations.
289
+ * Used for building bounding boxes and spatial indexes.
290
+ */
291
+ private collectRelationCoordinates(index: number): LonLat[] {
292
+ const lls: LonLat[] = []
293
+ const relation = this.getByIndex(index)
294
+
295
+ // Resolve nested relations to get all descendant nodes and ways
296
+ const resolved = resolveRelationMembers(
297
+ relation,
298
+ (relId) => {
299
+ const relIndex = this.ids.getIndexFromId(relId)
300
+ if (relIndex === -1) return null
301
+ return this.getByIndex(relIndex)
302
+ },
303
+ 10, // max depth
304
+ )
305
+
306
+ // Collect coordinates from resolved nodes
307
+ for (const nodeId of resolved.nodes) {
308
+ const ll = this.nodes.getNodeLonLat({ id: nodeId })
309
+ if (ll) lls.push(ll)
310
+ }
311
+
312
+ // Collect coordinates from resolved ways
313
+ for (const wayId of resolved.ways) {
314
+ const wayIndex = this.ways.ids.getIndexFromId(wayId)
315
+ if (wayIndex === -1) continue
316
+ const wayPositions = this.ways.getCoordinates(wayIndex)
317
+ lls.push(...wayPositions)
318
+ }
319
+
320
+ return lls
321
+ }
322
+
323
+ /**
324
+ * Get the bounding box of a relation.
325
+ */
326
+ getEntityBbox(i: IdOrIndex): GeoBbox2D {
327
+ const index = "index" in i ? i.index : this.ids.idOrIndex(i)[0]
328
+ return [
329
+ this.bbox.at(index * 4),
330
+ this.bbox.at(index * 4 + 1),
331
+ this.bbox.at(index * 4 + 2),
332
+ this.bbox.at(index * 4 + 3),
333
+ ]
334
+ }
335
+
336
+ /**
337
+ * Get the full relation entity.
338
+ */
173
339
  getFullEntity(index: number, id: number, tags?: OsmTags): OsmRelation {
174
340
  return {
175
341
  id,
@@ -178,6 +344,9 @@ export class Relations extends Entities<OsmRelation> {
178
344
  }
179
345
  }
180
346
 
347
+ /**
348
+ * Get the members of a relation.
349
+ */
181
350
  getMembersByIndex(
182
351
  index: number,
183
352
  relationMemberTypes = RELATION_MEMBER_TYPES,
@@ -196,6 +365,9 @@ export class Relations extends Entities<OsmRelation> {
196
365
  return members
197
366
  }
198
367
 
368
+ /**
369
+ * Check if a relation includes a specific member.
370
+ */
199
371
  includesMember(
200
372
  index: number,
201
373
  memberRef: number,
@@ -218,4 +390,161 @@ export class Relations extends Entities<OsmRelation> {
218
390
  }
219
391
  return false
220
392
  }
393
+
394
+ /**
395
+ * Get all way IDs that are members of relations, including nested relations.
396
+ * Used to exclude these ways from individual rendering.
397
+ */
398
+ getWayMemberIds(): Set<number> {
399
+ const wayIds = new Set<number>()
400
+ for (let i = 0; i < this.size; i++) {
401
+ const relation = this.getByIndex(i)
402
+ const resolved = resolveRelationMembers(
403
+ relation,
404
+ (relId) => {
405
+ const relIndex = this.ids.getIndexFromId(relId)
406
+ if (relIndex === -1) return null
407
+ return this.getByIndex(relIndex)
408
+ },
409
+ 10, // max depth
410
+ )
411
+ for (const wayId of resolved.ways) {
412
+ wayIds.add(wayId)
413
+ }
414
+ }
415
+ return wayIds
416
+ }
417
+
418
+ /**
419
+ * Get relation geometry based on its kind.
420
+ * Returns coordinates suitable for rendering based on relation type.
421
+ * @param index - Relation index
422
+ * @returns Object with geometry data based on relation kind
423
+ */
424
+ getRelationGeometry(index: number): {
425
+ points?: LonLat[]
426
+ lineStrings?: LonLat[][]
427
+ rings?: LonLat[][][]
428
+ } {
429
+ const relation = this.getByIndex(index)
430
+ if (isAreaRelation(relation)) {
431
+ return {
432
+ rings: buildRelationRings(
433
+ relation,
434
+ (ref) => this.ways.getById(ref),
435
+ (id) => this.nodes.getNodeLonLat({ id }),
436
+ ),
437
+ }
438
+ }
439
+
440
+ // Point relations
441
+ if (isPointRelation(relation)) {
442
+ return {
443
+ points: collectRelationPoints(relation, (id) =>
444
+ this.nodes.getNodeLonLat({ id }),
445
+ ),
446
+ }
447
+ }
448
+
449
+ // Line relations
450
+ if (isLineRelation(relation)) {
451
+ return {
452
+ lineStrings: buildRelationLineStrings(
453
+ relation,
454
+ (ref) => this.ways.getById(ref),
455
+ (id) => this.nodes.getNodeLonLat({ id }),
456
+ ),
457
+ }
458
+ }
459
+
460
+ // Super relations are handled by individual relation geometry methods
461
+ if (isSuperRelation(relation)) {
462
+ return {}
463
+ }
464
+
465
+ // Area relations are handled by buildRelationRings in shared/relation-multipolygon
466
+ // This method doesn't duplicate that logic
467
+
468
+ return {}
469
+ }
470
+
471
+ /**
472
+ * Find relations that intersect a bounding box.
473
+ */
474
+ intersects(bbox: GeoBbox2D, filterFn?: (index: number) => boolean): number[] {
475
+ if (this.size === 0) return []
476
+ return this.spatialIndex.search(
477
+ bbox[0],
478
+ bbox[1],
479
+ bbox[2],
480
+ bbox[3],
481
+ filterFn,
482
+ )
483
+ }
484
+
485
+ /**
486
+ * Find relation indexes near a point using great-circle distance.
487
+ * @param lon - Longitude in degrees.
488
+ * @param lat - Latitude in degrees.
489
+ * @param maxResults - Maximum number of results to return.
490
+ * @param maxDistanceKm - Maximum distance in kilometers.
491
+ * @returns Array of relation indexes sorted by distance.
492
+ */
493
+ neighbors(
494
+ lon: number,
495
+ lat: number,
496
+ maxResults?: number,
497
+ maxDistanceKm?: number,
498
+ ): number[] {
499
+ if (this.size === 0) return []
500
+ // Use geoflatbush for proper geographic distance calculations
501
+ return geoAround(this.spatialIndex, lon, lat, maxResults, maxDistanceKm)
502
+ }
503
+
504
+ /**
505
+ * Get transferable objects for passing to another thread.
506
+ * Only includes spatialIndex if it has been built.
507
+ */
508
+ override transferables(): RelationsTransferables {
509
+ const base = {
510
+ ...super.transferables(),
511
+ memberStart: this.memberStart.array.buffer,
512
+ memberCount: this.memberCount.array.buffer,
513
+ memberRefs: this.memberRefs.array.buffer,
514
+ memberTypes: this.memberTypes.array.buffer,
515
+ memberRoles: this.memberRoles.array.buffer,
516
+ bbox: this.bbox.array.buffer,
517
+ }
518
+ // Only include spatial index if it was built
519
+ if (this.spatialIndexBuilt) {
520
+ return { ...base, spatialIndex: this.spatialIndex.data }
521
+ }
522
+ return base
523
+ }
524
+
525
+ /**
526
+ * Get the approximate memory requirements for a given number of relations in bytes.
527
+ */
528
+ static getBytesRequired(count: number) {
529
+ if (count === 0) return 0
530
+ // Approximate members per relation
531
+ let numNodes = count
532
+ let n = count
533
+ while (n !== 1) {
534
+ n = Math.ceil(n / 128)
535
+ numNodes += n
536
+ }
537
+ const indexBytes = (numNodes < 16384 ? 2 : 4) * numNodes
538
+ const boxesBytes = numNodes * 4 * Float64Array.BYTES_PER_ELEMENT
539
+ const spatialIndexBytes = 8 + indexBytes + boxesBytes
540
+
541
+ return (
542
+ Ids.getBytesRequired(count) +
543
+ Tags.getBytesRequired(count) +
544
+ count * Uint32Array.BYTES_PER_ELEMENT + // memberStart
545
+ count * Uint16Array.BYTES_PER_ELEMENT + // memberCount
546
+ count * 4 * Float64Array.BYTES_PER_ELEMENT + // bbox
547
+ spatialIndexBytes
548
+ )
549
+ }
221
550
  }