@osmix/core 0.0.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.
- package/CHANGELOG.md +11 -0
- package/README.md +160 -0
- package/package.json +53 -0
- package/src/entities.ts +95 -0
- package/src/ids.ts +210 -0
- package/src/index.ts +7 -0
- package/src/nodes.ts +214 -0
- package/src/osmix.ts +556 -0
- package/src/relations.ts +221 -0
- package/src/stringtable.ts +120 -0
- package/src/tags.ts +122 -0
- package/src/typed-arrays.ts +164 -0
- package/src/types/kdbush.d.ts +29 -0
- package/src/utils.ts +29 -0
- package/src/ways.ts +241 -0
- package/test/extract.bench.ts +29 -0
- package/test/extract.test.ts +156 -0
- package/test/mock-osm.ts +140 -0
- package/test/read.test.ts +22 -0
- package/test/typed-arrays.test.ts +135 -0
- package/test/write.test.ts +66 -0
- package/tsconfig.json +9 -0
package/src/osmix.ts
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNode,
|
|
3
|
+
isRelation,
|
|
4
|
+
isWay,
|
|
5
|
+
nodeToFeature,
|
|
6
|
+
type OsmEntity,
|
|
7
|
+
type OsmEntityType,
|
|
8
|
+
type OsmEntityTypeMap,
|
|
9
|
+
type OsmixGeoJSONFeature,
|
|
10
|
+
OsmJsonToBlocksTransformStream,
|
|
11
|
+
type OsmNode,
|
|
12
|
+
type OsmRelation,
|
|
13
|
+
type OsmWay,
|
|
14
|
+
relationToFeature,
|
|
15
|
+
wayToFeature,
|
|
16
|
+
} from "@osmix/json"
|
|
17
|
+
import {
|
|
18
|
+
type AsyncGeneratorValue,
|
|
19
|
+
OsmBlocksToPbfBytesTransformStream,
|
|
20
|
+
type OsmPbfBlock,
|
|
21
|
+
type OsmPbfHeaderBlock,
|
|
22
|
+
readOsmPbf,
|
|
23
|
+
} from "@osmix/pbf"
|
|
24
|
+
import type { GeoBbox2D, LonLat } from "@osmix/shared/types"
|
|
25
|
+
import { Nodes, type NodesTransferables } from "./nodes"
|
|
26
|
+
import { Relations, type RelationsTransferables } from "./relations"
|
|
27
|
+
import StringTable, { type StringTableTransferables } from "./stringtable"
|
|
28
|
+
import { IdArrayType } from "./typed-arrays"
|
|
29
|
+
import { bboxFromLonLats, throttle } from "./utils"
|
|
30
|
+
import { Ways, type WaysTransferables } from "./ways"
|
|
31
|
+
|
|
32
|
+
export interface OsmixTransferables {
|
|
33
|
+
id: string
|
|
34
|
+
header: OsmPbfHeaderBlock
|
|
35
|
+
stringTable: StringTableTransferables
|
|
36
|
+
nodes: NodesTransferables
|
|
37
|
+
ways: WaysTransferables
|
|
38
|
+
relations: RelationsTransferables
|
|
39
|
+
parsingTimeMs: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OsmixOptions {
|
|
43
|
+
id: string
|
|
44
|
+
extractBbox: GeoBbox2D
|
|
45
|
+
logger: (message: string, type?: LogLevel) => void
|
|
46
|
+
filter<T extends OsmEntityType>(
|
|
47
|
+
type: T,
|
|
48
|
+
entity: OsmEntityTypeMap[T],
|
|
49
|
+
osmix: Osmix,
|
|
50
|
+
): boolean
|
|
51
|
+
header: OsmPbfHeaderBlock
|
|
52
|
+
buildSpatialIndexes: OsmEntityType[]
|
|
53
|
+
|
|
54
|
+
// Future options
|
|
55
|
+
// include: OsmEntityType[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type LogLevel = "debug" | "info" | "warn" | "error"
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* OSM Entity Index.
|
|
62
|
+
*/
|
|
63
|
+
export class Osmix {
|
|
64
|
+
// Filename or ID of this OSM Entity index.
|
|
65
|
+
id = "unknown"
|
|
66
|
+
header: OsmPbfHeaderBlock = {
|
|
67
|
+
required_features: [],
|
|
68
|
+
optional_features: [],
|
|
69
|
+
}
|
|
70
|
+
blocksGenerator: AsyncGenerator<OsmPbfBlock> | null = null
|
|
71
|
+
|
|
72
|
+
// Shared string lookup table for all nodes, ways, and relations
|
|
73
|
+
stringTable: StringTable
|
|
74
|
+
nodes: Nodes
|
|
75
|
+
ways: Ways
|
|
76
|
+
relations: Relations
|
|
77
|
+
|
|
78
|
+
log = (message: string, type?: LogLevel) =>
|
|
79
|
+
type === "error" ? console.error(message) : console.log(message)
|
|
80
|
+
|
|
81
|
+
#indexBuilt = false
|
|
82
|
+
#startTime = performance.now()
|
|
83
|
+
buildTimeMs = 0
|
|
84
|
+
|
|
85
|
+
static from({
|
|
86
|
+
id,
|
|
87
|
+
header,
|
|
88
|
+
stringTable,
|
|
89
|
+
nodes,
|
|
90
|
+
ways,
|
|
91
|
+
relations,
|
|
92
|
+
parsingTimeMs,
|
|
93
|
+
}: OsmixTransferables) {
|
|
94
|
+
const osm = new Osmix({ id, header })
|
|
95
|
+
osm.stringTable = StringTable.from(stringTable)
|
|
96
|
+
osm.nodes = Nodes.from(osm.stringTable, nodes)
|
|
97
|
+
osm.ways = Ways.from(osm.stringTable, ways)
|
|
98
|
+
osm.relations = Relations.from(osm.stringTable, relations)
|
|
99
|
+
osm.buildTimeMs = parsingTimeMs
|
|
100
|
+
osm.#indexBuilt = true
|
|
101
|
+
return osm
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Easiest way to get started with Osmix. Reads a PBF file into an Osmix index. Logs progress to the console.
|
|
106
|
+
*/
|
|
107
|
+
static async fromPbf(
|
|
108
|
+
data: AsyncGeneratorValue<ArrayBufferLike>,
|
|
109
|
+
options: Partial<OsmixOptions> = {},
|
|
110
|
+
) {
|
|
111
|
+
const osm = new Osmix(options)
|
|
112
|
+
await osm.readPbf(data, options)
|
|
113
|
+
|
|
114
|
+
// By default, build all spatial indexes.
|
|
115
|
+
if (!Array.isArray(options.buildSpatialIndexes)) {
|
|
116
|
+
osm.buildSpatialIndexes()
|
|
117
|
+
} else if (options.buildSpatialIndexes.includes("node")) {
|
|
118
|
+
osm.nodes.buildSpatialIndex()
|
|
119
|
+
} else if (options.buildSpatialIndexes.includes("way")) {
|
|
120
|
+
osm.ways.buildSpatialIndex(osm.nodes)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return osm
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
constructor(options: Partial<OsmixOptions> = {}) {
|
|
127
|
+
if (options.header) this.header = options.header
|
|
128
|
+
if (options.id) this.id = options.id
|
|
129
|
+
if (options.logger) this.log = options.logger
|
|
130
|
+
this.stringTable = new StringTable()
|
|
131
|
+
this.nodes = new Nodes(this.stringTable)
|
|
132
|
+
this.ways = new Ways(this.stringTable)
|
|
133
|
+
this.relations = new Relations(this.stringTable)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setLogger(listener: (message: string, type?: LogLevel) => void) {
|
|
137
|
+
this.log = listener
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
createThrottledLog(interval: number) {
|
|
141
|
+
return throttle(this.log, interval)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
buildIndexes() {
|
|
145
|
+
if (!this.nodes.isReady) this.nodes.buildIndex()
|
|
146
|
+
if (!this.ways.isReady) this.ways.buildIndex()
|
|
147
|
+
if (!this.relations.isReady) this.relations.buildIndex()
|
|
148
|
+
this.stringTable.buildIndex()
|
|
149
|
+
this.#indexBuilt = true
|
|
150
|
+
this.buildTimeMs = performance.now() - this.#startTime
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
isReady() {
|
|
154
|
+
return this.#indexBuilt
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
transferables(): OsmixTransferables {
|
|
158
|
+
return {
|
|
159
|
+
id: this.id,
|
|
160
|
+
header: this.header,
|
|
161
|
+
stringTable: this.stringTable.transferables(),
|
|
162
|
+
nodes: this.nodes.transferables(),
|
|
163
|
+
ways: this.ways.transferables(),
|
|
164
|
+
relations: this.relations.transferables(),
|
|
165
|
+
parsingTimeMs: this.buildTimeMs,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async readPbf(
|
|
170
|
+
data: AsyncGeneratorValue<ArrayBufferLike>,
|
|
171
|
+
options: Partial<OsmixOptions> = {},
|
|
172
|
+
) {
|
|
173
|
+
if (this.#indexBuilt) throw Error("Osmix built. Create new instance.")
|
|
174
|
+
const { extractBbox } = options
|
|
175
|
+
const { header, blocks } = await readOsmPbf(data)
|
|
176
|
+
this.header = header
|
|
177
|
+
if (extractBbox) {
|
|
178
|
+
this.header.bbox = {
|
|
179
|
+
left: extractBbox[0],
|
|
180
|
+
bottom: extractBbox[1],
|
|
181
|
+
right: extractBbox[2],
|
|
182
|
+
top: extractBbox[3],
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const logEverySecond = this.createThrottledLog(1_000)
|
|
186
|
+
|
|
187
|
+
for await (const block of blocks) {
|
|
188
|
+
const blockStringIndexMap = this.stringTable.createBlockIndexMap(block)
|
|
189
|
+
|
|
190
|
+
for (const group of block.primitivegroup) {
|
|
191
|
+
const { nodes, ways, relations, dense } = group
|
|
192
|
+
if (nodes && nodes.length > 0) {
|
|
193
|
+
throw Error("Nodes must be dense!")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (dense) {
|
|
197
|
+
this.nodes.addDenseNodes(
|
|
198
|
+
dense,
|
|
199
|
+
block,
|
|
200
|
+
blockStringIndexMap,
|
|
201
|
+
extractBbox
|
|
202
|
+
? (node: OsmNode) => {
|
|
203
|
+
return (
|
|
204
|
+
node.lon >= extractBbox[0] &&
|
|
205
|
+
node.lon <= extractBbox[2] &&
|
|
206
|
+
node.lat >= extractBbox[1] &&
|
|
207
|
+
node.lat <= extractBbox[3]
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
: undefined,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
logEverySecond(`${this.nodes.size.toLocaleString()} nodes added`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (ways.length > 0) {
|
|
217
|
+
// Nodes are finished, build their index.
|
|
218
|
+
if (!this.nodes.isReady) this.nodes.buildIndex()
|
|
219
|
+
this.ways.addWays(
|
|
220
|
+
ways,
|
|
221
|
+
blockStringIndexMap,
|
|
222
|
+
extractBbox
|
|
223
|
+
? (way: OsmWay) => {
|
|
224
|
+
const refs = way.refs.filter((ref) => this.nodes.ids.has(ref))
|
|
225
|
+
if (refs.length === 0) return null
|
|
226
|
+
return {
|
|
227
|
+
...way,
|
|
228
|
+
refs,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
: undefined,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
logEverySecond(`${this.ways.size.toLocaleString()} ways added`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (relations.length > 0) {
|
|
238
|
+
if (!this.ways.isReady) this.ways.buildIndex()
|
|
239
|
+
this.relations.addRelations(
|
|
240
|
+
relations,
|
|
241
|
+
blockStringIndexMap,
|
|
242
|
+
extractBbox
|
|
243
|
+
? (relation: OsmRelation) => {
|
|
244
|
+
const members = relation.members.filter((member) => {
|
|
245
|
+
if (member.type === "node")
|
|
246
|
+
return this.nodes.ids.has(member.ref)
|
|
247
|
+
if (member.type === "way")
|
|
248
|
+
return this.ways.ids.has(member.ref)
|
|
249
|
+
return false
|
|
250
|
+
})
|
|
251
|
+
if (members.length === 0) return null
|
|
252
|
+
return {
|
|
253
|
+
...relation,
|
|
254
|
+
members,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
: undefined,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
logEverySecond(
|
|
261
|
+
`${this.relations.size.toLocaleString()} relations added`,
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.log(
|
|
268
|
+
`${this.nodes.size.toLocaleString()} nodes, ${this.ways.size.toLocaleString()} ways, and ${this.relations.size.toLocaleString()} relations added.`,
|
|
269
|
+
)
|
|
270
|
+
this.log("Building remaining id and tag indexes...")
|
|
271
|
+
this.buildIndexes()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
buildSpatialIndexes() {
|
|
275
|
+
this.nodes.buildSpatialIndex()
|
|
276
|
+
this.ways.buildSpatialIndex(this.nodes)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
extract(bbox: GeoBbox2D) {
|
|
280
|
+
if (!this.#indexBuilt) this.buildIndexes()
|
|
281
|
+
|
|
282
|
+
const [minLon, minLat, maxLon, maxLat] = bbox
|
|
283
|
+
const extracted = new Osmix({
|
|
284
|
+
id: this.id,
|
|
285
|
+
header: {
|
|
286
|
+
...this.header,
|
|
287
|
+
bbox: {
|
|
288
|
+
left: minLon,
|
|
289
|
+
bottom: minLat,
|
|
290
|
+
right: maxLon,
|
|
291
|
+
top: maxLat,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
this.log("Selecting nodes within bounding box...")
|
|
297
|
+
this.buildSpatialIndexes()
|
|
298
|
+
for (const node of this.nodes.sorted()) {
|
|
299
|
+
if (
|
|
300
|
+
node.lon >= minLon &&
|
|
301
|
+
node.lon <= maxLon &&
|
|
302
|
+
node.lat >= minLat &&
|
|
303
|
+
node.lat <= maxLat
|
|
304
|
+
) {
|
|
305
|
+
extracted.nodes.addNode(node)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
extracted.nodes.buildIndex()
|
|
309
|
+
|
|
310
|
+
this.log("Selecting ways within bounding box...")
|
|
311
|
+
for (const way of this.ways.sorted()) {
|
|
312
|
+
const refs = way.refs.filter((ref) => extracted.nodes.ids.has(ref))
|
|
313
|
+
if (refs.length > 0) {
|
|
314
|
+
extracted.ways.addWay({
|
|
315
|
+
...way,
|
|
316
|
+
refs,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
extracted.ways.buildIndex()
|
|
321
|
+
|
|
322
|
+
this.log("Selecting relations within bounding box...")
|
|
323
|
+
for (const relation of this.relations.sorted()) {
|
|
324
|
+
const members = relation.members.filter((m) => {
|
|
325
|
+
if (m.type === "node") return extracted.nodes.ids.has(m.ref)
|
|
326
|
+
if (m.type === "way") return extracted.ways.ids.has(m.ref)
|
|
327
|
+
return false
|
|
328
|
+
})
|
|
329
|
+
if (members.length > 0) {
|
|
330
|
+
extracted.relations.addRelation({
|
|
331
|
+
...relation,
|
|
332
|
+
members,
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
extracted.buildIndexes()
|
|
338
|
+
return extracted
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
get<T extends OsmEntityType>(
|
|
342
|
+
type: T,
|
|
343
|
+
id: number,
|
|
344
|
+
): OsmEntityTypeMap[T] | undefined {
|
|
345
|
+
if (type === "node") return this.nodes.get({ id }) as OsmEntityTypeMap[T]
|
|
346
|
+
if (type === "way") return this.ways.get({ id }) as OsmEntityTypeMap[T]
|
|
347
|
+
if (type === "relation")
|
|
348
|
+
return this.relations.get({ id }) as OsmEntityTypeMap[T]
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
getById(eid: string): OsmEntity | null {
|
|
352
|
+
const [type, sid] = eid.split("/")
|
|
353
|
+
const id = Number(sid)
|
|
354
|
+
switch (type) {
|
|
355
|
+
case "node":
|
|
356
|
+
return this.nodes.getById(id)
|
|
357
|
+
case "way":
|
|
358
|
+
return this.ways.getById(id)
|
|
359
|
+
case "relation":
|
|
360
|
+
return this.relations.getById(id)
|
|
361
|
+
default: {
|
|
362
|
+
return null
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getNodesInBbox(bbox: GeoBbox2D, allNodes = false) {
|
|
368
|
+
if (!this.#indexBuilt) throw new Error("Osm not finished")
|
|
369
|
+
console.time("Osm.getNodesInBbox")
|
|
370
|
+
const nodeCandidates = this.nodes.withinBbox(bbox)
|
|
371
|
+
const nodePositions = new Float64Array(nodeCandidates.length * 2)
|
|
372
|
+
const ids = new IdArrayType(nodeCandidates.length)
|
|
373
|
+
|
|
374
|
+
let skipped = 0
|
|
375
|
+
nodeCandidates.forEach((nodeIndex, i) => {
|
|
376
|
+
// Skip nodes with no tags, likely just a way node
|
|
377
|
+
if (!allNodes && this.nodes.tags.cardinality(nodeIndex) === 0) {
|
|
378
|
+
skipped++
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const [lon, lat] = this.nodes.getNodeLonLat({ index: nodeIndex })
|
|
383
|
+
ids[i - skipped] = this.nodes.ids.at(nodeIndex)
|
|
384
|
+
nodePositions[(i - skipped) * 2] = lon
|
|
385
|
+
nodePositions[(i - skipped) * 2 + 1] = lat
|
|
386
|
+
})
|
|
387
|
+
console.timeEnd("Osm.getNodesInBbox")
|
|
388
|
+
return {
|
|
389
|
+
ids: ids.subarray(0, nodeCandidates.length - skipped),
|
|
390
|
+
positions: nodePositions.slice(0, (nodeCandidates.length - skipped) * 2),
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
getWaysInBbox(bbox: GeoBbox2D) {
|
|
395
|
+
console.time("Osm.getWaysInBbox")
|
|
396
|
+
const wayCandidates = this.ways.intersects(bbox)
|
|
397
|
+
const ids = new IdArrayType(wayCandidates.length)
|
|
398
|
+
const wayPositions: Float64Array[] = []
|
|
399
|
+
const wayStartIndices = new Uint32Array(wayCandidates.length + 1)
|
|
400
|
+
wayStartIndices[0] = 0
|
|
401
|
+
|
|
402
|
+
console.time("Osm.getWaysInBbox.loop")
|
|
403
|
+
let size = 0
|
|
404
|
+
wayCandidates.forEach((wayIndex, i) => {
|
|
405
|
+
ids[i] = this.ways.ids.at(wayIndex)
|
|
406
|
+
const way = this.ways.getLine(wayIndex, this.nodes)
|
|
407
|
+
size += way.length
|
|
408
|
+
wayPositions.push(way)
|
|
409
|
+
const prevIndex = wayStartIndices[i]
|
|
410
|
+
if (prevIndex === undefined) throw Error("Previous index is undefined")
|
|
411
|
+
wayStartIndices[i + 1] = prevIndex + way.length / 2
|
|
412
|
+
})
|
|
413
|
+
console.timeEnd("Osm.getWaysInBbox.loop")
|
|
414
|
+
const wayPositionsArray = new Float64Array(size)
|
|
415
|
+
let pIndex = 0
|
|
416
|
+
for (const way of wayPositions) {
|
|
417
|
+
wayPositionsArray.set(way, pIndex)
|
|
418
|
+
pIndex += way.length
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.timeEnd("Osm.getWaysInBbox")
|
|
422
|
+
return {
|
|
423
|
+
ids,
|
|
424
|
+
positions: wayPositionsArray,
|
|
425
|
+
startIndices: wayStartIndices,
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
getEntityGeoJson(
|
|
430
|
+
entity: OsmNode | OsmWay | OsmRelation,
|
|
431
|
+
): OsmixGeoJSONFeature<
|
|
432
|
+
GeoJSON.Point | GeoJSON.LineString | GeoJSON.Polygon | GeoJSON.MultiPolygon
|
|
433
|
+
> {
|
|
434
|
+
if (isNode(entity)) {
|
|
435
|
+
return nodeToFeature(entity)
|
|
436
|
+
}
|
|
437
|
+
if (isWay(entity)) {
|
|
438
|
+
return wayToFeature(entity, (ref) =>
|
|
439
|
+
this.nodes.getNodeLonLat({ id: ref }),
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
if (isRelation(entity)) {
|
|
443
|
+
return relationToFeature(entity, (ref) =>
|
|
444
|
+
this.nodes.getNodeLonLat({ id: ref }),
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
throw new Error("Unknown entity type")
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
getEntityBbox(entity: OsmNode | OsmWay | OsmRelation): GeoBbox2D {
|
|
451
|
+
if (isNode(entity)) {
|
|
452
|
+
const [lon, lat] = this.nodes.getNodeLonLat({ id: entity.id })
|
|
453
|
+
return [lon, lat, lon, lat] as GeoBbox2D
|
|
454
|
+
}
|
|
455
|
+
if (isWay(entity)) {
|
|
456
|
+
return this.ways.getBbox({ id: entity.id })
|
|
457
|
+
}
|
|
458
|
+
if (isRelation(entity)) {
|
|
459
|
+
const relation = this.relations.getById(entity.id)
|
|
460
|
+
if (!relation) throw Error("Relation not found")
|
|
461
|
+
const lls: LonLat[] = []
|
|
462
|
+
for (const member of relation.members) {
|
|
463
|
+
if (member.type === "node") {
|
|
464
|
+
const ll = this.nodes.getNodeLonLat({ id: member.ref })
|
|
465
|
+
lls.push(ll)
|
|
466
|
+
} else if (member.type === "way") {
|
|
467
|
+
const wayIndex = this.ways.ids.getIndexFromId(member.ref)
|
|
468
|
+
if (wayIndex === -1) throw Error("Way not found")
|
|
469
|
+
const wayPositions = this.ways.getCoordinates(wayIndex, this.nodes)
|
|
470
|
+
lls.push(...wayPositions)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return bboxFromLonLats(lls)
|
|
474
|
+
}
|
|
475
|
+
throw Error("Unknown entity type")
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get the bounding box of all entities in the OSM index.
|
|
480
|
+
*/
|
|
481
|
+
bbox(): GeoBbox2D {
|
|
482
|
+
return this.nodes.bbox
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Create a generator that yields all entities in the OSM index, sorted by type and id.
|
|
487
|
+
*/
|
|
488
|
+
*allEntitiesSorted(): Generator<OsmEntity> {
|
|
489
|
+
for (const node of this.nodes.sorted()) {
|
|
490
|
+
yield node
|
|
491
|
+
}
|
|
492
|
+
for (const way of this.ways.sorted()) {
|
|
493
|
+
yield way
|
|
494
|
+
}
|
|
495
|
+
for (const relation of this.relations.sorted()) {
|
|
496
|
+
yield relation
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Convert the OSM index to a `ReadableStream<OsmPbfHeaderBlock | OsmEntity>`.
|
|
502
|
+
*/
|
|
503
|
+
toEntityStream() {
|
|
504
|
+
let headerEnqueued = false
|
|
505
|
+
const entityGenerator = this.allEntitiesSorted()
|
|
506
|
+
return new ReadableStream<OsmPbfHeaderBlock | OsmEntity>({
|
|
507
|
+
pull: async (controller) => {
|
|
508
|
+
if (!headerEnqueued) {
|
|
509
|
+
controller.enqueue({
|
|
510
|
+
...this.header,
|
|
511
|
+
writingprogram: "@osmix/core",
|
|
512
|
+
osmosis_replication_timestamp: Date.now(),
|
|
513
|
+
})
|
|
514
|
+
headerEnqueued = true
|
|
515
|
+
}
|
|
516
|
+
const block = entityGenerator.next()
|
|
517
|
+
if (block.done) {
|
|
518
|
+
controller.close()
|
|
519
|
+
} else {
|
|
520
|
+
controller.enqueue(block.value)
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Convert the OSM index to a `ReadableStream<Uint8Array>` of PBF bytes.
|
|
528
|
+
*/
|
|
529
|
+
toPbfStream() {
|
|
530
|
+
return this.toEntityStream()
|
|
531
|
+
.pipeThrough(new OsmJsonToBlocksTransformStream())
|
|
532
|
+
.pipeThrough(new OsmBlocksToPbfBytesTransformStream())
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Convert the OSM index to an in memory PBF ArrayBuffer.
|
|
537
|
+
*/
|
|
538
|
+
async toPbfBuffer() {
|
|
539
|
+
const chunks: Uint8Array[] = []
|
|
540
|
+
let byteLength = 0
|
|
541
|
+
const writable = new WritableStream<Uint8Array>({
|
|
542
|
+
write(chunk) {
|
|
543
|
+
chunks.push(chunk)
|
|
544
|
+
byteLength += chunk.byteLength
|
|
545
|
+
},
|
|
546
|
+
})
|
|
547
|
+
await this.toPbfStream().pipeTo(writable)
|
|
548
|
+
const combined = new Uint8Array(byteLength)
|
|
549
|
+
let offset = 0
|
|
550
|
+
for (const chunk of chunks) {
|
|
551
|
+
combined.set(chunk, offset)
|
|
552
|
+
offset += chunk.byteLength
|
|
553
|
+
}
|
|
554
|
+
return combined.buffer
|
|
555
|
+
}
|
|
556
|
+
}
|