@osmix/core 0.1.10 → 0.3.0
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 +56 -0
- package/LICENSE +21 -0
- package/README.md +136 -72
- package/dist/contracts.d.ts +27 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -0
- package/dist/entities.d.ts +22 -2
- package/dist/entities.d.ts.map +1 -1
- package/dist/entities.js +54 -10
- package/dist/entities.js.map +1 -1
- package/dist/ids.d.ts +20 -5
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +87 -29
- package/dist/ids.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/indirect-kd-index.d.ts +28 -0
- package/dist/indirect-kd-index.d.ts.map +1 -0
- package/dist/indirect-kd-index.js +163 -0
- package/dist/indirect-kd-index.js.map +1 -0
- package/dist/limits.d.ts +30 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +51 -0
- package/dist/limits.js.map +1 -0
- package/dist/mocks.d.ts.map +1 -1
- package/dist/mocks.js.map +1 -1
- package/dist/nodes.d.ts +36 -15
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +185 -62
- package/dist/nodes.js.map +1 -1
- package/dist/osm.d.ts +60 -4
- package/dist/osm.d.ts.map +1 -1
- package/dist/osm.js +32 -6
- package/dist/osm.js.map +1 -1
- package/dist/relations.d.ts +11 -4
- package/dist/relations.d.ts.map +1 -1
- package/dist/relations.js +73 -29
- package/dist/relations.js.map +1 -1
- package/dist/renumber.d.ts +23 -0
- package/dist/renumber.d.ts.map +1 -0
- package/dist/renumber.js +64 -0
- package/dist/renumber.js.map +1 -0
- package/dist/stringtable.d.ts +2 -1
- package/dist/stringtable.d.ts.map +1 -1
- package/dist/stringtable.js +9 -14
- package/dist/stringtable.js.map +1 -1
- package/dist/tags.d.ts +45 -12
- package/dist/tags.d.ts.map +1 -1
- package/dist/tags.js +153 -27
- package/dist/tags.js.map +1 -1
- package/dist/typed-arrays.d.ts +63 -48
- package/dist/typed-arrays.d.ts.map +1 -1
- package/dist/typed-arrays.js +184 -107
- package/dist/typed-arrays.js.map +1 -1
- package/dist/ways.d.ts +52 -3
- package/dist/ways.d.ts.map +1 -1
- package/dist/ways.js +223 -46
- package/dist/ways.js.map +1 -1
- package/package.json +45 -31
- package/src/contracts.ts +29 -0
- package/src/entities.ts +242 -194
- package/src/ids.ts +278 -235
- package/src/index.ts +22 -13
- package/src/indirect-kd-index.ts +212 -0
- package/src/limits.ts +73 -0
- package/src/mocks.ts +114 -113
- package/src/nodes.ts +538 -397
- package/src/osm.ts +282 -207
- package/src/relations.ts +580 -558
- package/src/renumber.ts +80 -0
- package/src/stringtable.ts +189 -184
- package/src/tags.ts +406 -272
- package/src/typed-arrays.ts +307 -205
- package/src/types/geoflatbush.d.ts +28 -33
- package/src/ways.ts +577 -406
- package/test/entity-index-errors.test.ts +41 -0
- package/test/ids.bench.ts +33 -0
- package/test/ids.test.ts +122 -0
- package/test/limits.test.ts +83 -0
- package/test/memory.test.ts +29 -30
- package/test/no-shared-array-buffer.test.ts +41 -0
- package/test/nodes-spatial-index.test.ts +261 -0
- package/test/osm-bbox.test.ts +30 -0
- package/test/relations.bench.ts +32 -0
- package/test/relations.test.ts +124 -0
- package/test/renumber.test.ts +54 -0
- package/test/spatial-index-rebuild.test.ts +33 -0
- package/test/storage-compaction.test.ts +269 -0
- package/test/tags-search.test.ts +89 -56
- package/test/typed-arrays.test.ts +252 -132
- package/tsconfig.build.json +6 -6
- package/tsconfig.json +7 -7
- package/src/types/geokdbush.d.ts +0 -37
- package/src/types/kdbush.d.ts +0 -47
package/src/ways.ts
CHANGED
|
@@ -1,410 +1,581 @@
|
|
|
1
|
-
import type { OsmPbfWay } from "@osmix/pbf"
|
|
2
|
-
import type { ContentHasher } from "@osmix/shared/content-hasher"
|
|
3
|
-
import type { GeoBbox2D, LonLat, OsmTags, OsmWay } from "@osmix/
|
|
4
|
-
import Flatbush from "flatbush"
|
|
5
|
-
import { around as geoAround } from "geoflatbush"
|
|
6
|
-
|
|
7
|
-
import { type
|
|
8
|
-
import type
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
import type { OsmPbfWay } from "@osmix/pbf";
|
|
2
|
+
import type { ContentHasher } from "@osmix/shared/content-hasher";
|
|
3
|
+
import type { GeoBbox2D, LonLat, OsmTags, OsmWay } from "@osmix/types";
|
|
4
|
+
import Flatbush from "flatbush";
|
|
5
|
+
import { around as geoAround } from "geoflatbush";
|
|
6
|
+
|
|
7
|
+
import { Entities, type EntitiesTransferables, isValidSpatialBbox } from "./entities.ts";
|
|
8
|
+
import { type IdOrIndex, Ids } from "./ids.ts";
|
|
9
|
+
import { MAX_UINT32_OFFSET, MAX_WAY_REFS, assertCapacity } from "./limits.ts";
|
|
10
|
+
import type { Nodes } from "./nodes.ts";
|
|
11
|
+
import type StringTable from "./stringtable.ts";
|
|
12
|
+
import { Tags } from "./tags.ts";
|
|
13
|
+
import { BufferConstructor, type BufferType, ResizeableTypedArray as RTA } from "./typed-arrays.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Node-index sentinel stored for way refs whose node is not present in the dataset.
|
|
17
|
+
* Returned by {@link Ways.getRefIndexes} in place of a node index.
|
|
18
|
+
*/
|
|
19
|
+
export const MISSING_NODE_INDEX = 0xffffffff;
|
|
20
|
+
const HASH_REF_CHUNK_SIZE = 8192;
|
|
21
|
+
|
|
22
|
+
export interface WaysTransferables<
|
|
23
|
+
T extends BufferType = BufferType,
|
|
24
|
+
> extends EntitiesTransferables<T> {
|
|
25
|
+
refStart: T;
|
|
26
|
+
refCount: T;
|
|
27
|
+
/** Node indexes, with `0xffffffff` marking an unresolved reference. */
|
|
28
|
+
refs: T;
|
|
29
|
+
/** Sorted positions in `refs` whose node ID could not be resolved. */
|
|
30
|
+
missingRefPositions: T;
|
|
31
|
+
/** OSM node IDs parallel to `missingRefPositions`. */
|
|
32
|
+
missingRefIds: T;
|
|
33
|
+
bbox: T;
|
|
34
|
+
/** Optional - can be rebuilt via buildSpatialIndex() */
|
|
35
|
+
spatialIndex?: T;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
export class Ways extends Entities<OsmWay> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
39
|
+
private spatialIndex: Flatbush = new Flatbush(1);
|
|
40
|
+
// Track if spatial index was properly built (vs default empty)
|
|
41
|
+
private spatialIndexBuilt = false;
|
|
42
|
+
|
|
43
|
+
private refStart: RTA<Uint32Array>;
|
|
44
|
+
private refCount: RTA<Uint16Array>; // Maximum MAX_WAY_REFS (65,535) nodes per way
|
|
45
|
+
|
|
46
|
+
// Node indexes. Missing node IDs are preserved in sparse parallel arrays.
|
|
47
|
+
private refs: RTA<Uint32Array>;
|
|
48
|
+
private missingRefPositions: RTA<Uint32Array>;
|
|
49
|
+
private missingRefIds: RTA<Float64Array>;
|
|
50
|
+
|
|
51
|
+
// Temporary OSM node IDs used when nodes are not indexed during ingestion.
|
|
52
|
+
private pendingRefIds: RTA<Float64Array> | null;
|
|
53
|
+
|
|
54
|
+
// Bounding box of the way in geographic coordinates
|
|
55
|
+
private bbox: RTA<Float64Array>;
|
|
56
|
+
|
|
57
|
+
// Node reference index
|
|
58
|
+
private nodes: Nodes;
|
|
59
|
+
|
|
60
|
+
private getNodeLonLat(refPosition: number): [number, number] {
|
|
61
|
+
const coordinate = this.tryGetNodeLonLat(refPosition);
|
|
62
|
+
if (!coordinate) {
|
|
63
|
+
throw Error(`Node ${this.getMissingRefId(refPosition)} not found for way geometry`);
|
|
64
|
+
}
|
|
65
|
+
return coordinate;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resolve a reference position to coordinates, or null when the node is absent. */
|
|
69
|
+
private tryGetNodeLonLat(refPosition: number): [number, number] | null {
|
|
70
|
+
const nodeIndex = this.refs.at(refPosition);
|
|
71
|
+
if (nodeIndex !== MISSING_NODE_INDEX) {
|
|
72
|
+
return this.nodes.getNodeLonLat({ index: nodeIndex });
|
|
73
|
+
}
|
|
74
|
+
// A missing ref may still resolve if the node was added after finalization.
|
|
75
|
+
return this.nodes.getNodeLonLat({ id: this.getMissingRefId(refPosition) });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a new Ways index.
|
|
80
|
+
*/
|
|
81
|
+
constructor(stringTable: StringTable, nodes: Nodes, transferables?: WaysTransferables) {
|
|
82
|
+
if (transferables) {
|
|
83
|
+
super("way", new Ids(transferables), new Tags(stringTable, transferables));
|
|
84
|
+
this.refStart = RTA.from(Uint32Array, transferables.refStart);
|
|
85
|
+
this.refCount = RTA.from(Uint16Array, transferables.refCount);
|
|
86
|
+
this.refs = RTA.from(Uint32Array, transferables.refs);
|
|
87
|
+
this.missingRefPositions = RTA.from(Uint32Array, transferables.missingRefPositions);
|
|
88
|
+
this.missingRefIds = RTA.from(Float64Array, transferables.missingRefIds);
|
|
89
|
+
this.pendingRefIds = null;
|
|
90
|
+
this.bbox = RTA.from(Float64Array, transferables.bbox);
|
|
91
|
+
// Only load spatial index if provided (not stored in IndexedDB)
|
|
92
|
+
if (transferables.spatialIndex?.byteLength) {
|
|
93
|
+
this.spatialIndex = Flatbush.from(transferables.spatialIndex);
|
|
94
|
+
this.spatialIndexBuilt = true;
|
|
95
|
+
}
|
|
96
|
+
this.indexBuilt = true;
|
|
97
|
+
} else {
|
|
98
|
+
super("way", new Ids(), new Tags(stringTable));
|
|
99
|
+
this.refStart = new RTA(Uint32Array);
|
|
100
|
+
this.refCount = new RTA(Uint16Array);
|
|
101
|
+
this.refs = new RTA(Uint32Array);
|
|
102
|
+
this.missingRefPositions = new RTA(Uint32Array);
|
|
103
|
+
this.missingRefIds = new RTA(Float64Array);
|
|
104
|
+
this.pendingRefIds = new RTA(Float64Array);
|
|
105
|
+
this.bbox = new RTA(Float64Array);
|
|
106
|
+
}
|
|
107
|
+
this.nodes = nodes;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Add a single way to the index.
|
|
112
|
+
*/
|
|
113
|
+
addWay(way: OsmWay) {
|
|
114
|
+
this.assertRefCapacity(way.id, way.refs.length);
|
|
115
|
+
const wayIndex = this.addEntity(way.id, way.tags ?? {});
|
|
116
|
+
this.refStart.push(this.refLength);
|
|
117
|
+
this.refCount.push(way.refs.length);
|
|
118
|
+
this.appendRefIds(way.refs);
|
|
119
|
+
return wayIndex;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Bulk add ways directly from a PBF PrimitiveBlock.
|
|
124
|
+
*/
|
|
125
|
+
addWays(
|
|
126
|
+
ways: OsmPbfWay[],
|
|
127
|
+
blockStringIndexMap: Uint32Array,
|
|
128
|
+
filter?: (way: OsmWay) => OsmWay | null,
|
|
129
|
+
nodeIdToIndex?: (nodeId: number) => number,
|
|
130
|
+
) {
|
|
131
|
+
let added = 0;
|
|
132
|
+
for (const way of ways) {
|
|
133
|
+
let prevRefId = 0;
|
|
134
|
+
const refs = way.refs.map((refId) => {
|
|
135
|
+
prevRefId += refId;
|
|
136
|
+
return prevRefId;
|
|
137
|
+
});
|
|
138
|
+
const tagKeys: number[] = way.keys.map((key) => {
|
|
139
|
+
const index = blockStringIndexMap[key];
|
|
140
|
+
if (index === undefined) throw Error("Tag key not found");
|
|
141
|
+
return index;
|
|
142
|
+
});
|
|
143
|
+
const tagValues: number[] = way.vals.map((val) => {
|
|
144
|
+
const index = blockStringIndexMap[val];
|
|
145
|
+
if (index === undefined) throw Error("Tag value not found");
|
|
146
|
+
return index;
|
|
147
|
+
});
|
|
148
|
+
const filteredWay = filter
|
|
149
|
+
? filter({
|
|
150
|
+
id: way.id,
|
|
151
|
+
refs,
|
|
152
|
+
tags: this.tags.getTagsFromIndices(tagKeys, tagValues),
|
|
153
|
+
})
|
|
154
|
+
: null;
|
|
155
|
+
if (filter && filteredWay === null) continue;
|
|
156
|
+
|
|
157
|
+
const addedRefs = filteredWay?.refs ?? refs;
|
|
158
|
+
this.assertRefCapacity(way.id, addedRefs.length);
|
|
159
|
+
this.addEntity(way.id, tagKeys, tagValues);
|
|
160
|
+
this.refStart.push(this.refLength);
|
|
161
|
+
this.refCount.push(addedRefs.length);
|
|
162
|
+
this.appendRefIds(addedRefs, nodeIdToIndex);
|
|
163
|
+
added++;
|
|
164
|
+
}
|
|
165
|
+
return added;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compact the internal arrays to free up memory.
|
|
170
|
+
*
|
|
171
|
+
* Must run after the node ID index is built (`Osm.buildIndexes()` orders
|
|
172
|
+
* nodes before ways) so pending OSM node IDs resolve to compact `Uint32`
|
|
173
|
+
* node indexes. If the node index is not ready, every pending ref degrades
|
|
174
|
+
* to the larger missing-ref representation (sentinel + sorted ID lookup).
|
|
175
|
+
*/
|
|
176
|
+
buildEntityIndex() {
|
|
177
|
+
this.refStart.compact();
|
|
178
|
+
this.refCount.compact();
|
|
179
|
+
if (this.pendingRefIds && this.pendingRefIds.length > 0) {
|
|
180
|
+
if (this.refs.length > 0) throw Error("Mixed pending and indexed way references.");
|
|
181
|
+
const refsBuffer = new BufferConstructor(
|
|
182
|
+
this.pendingRefIds.length * Uint32Array.BYTES_PER_ELEMENT,
|
|
183
|
+
);
|
|
184
|
+
this.refs = RTA.from(Uint32Array, refsBuffer);
|
|
185
|
+
const canResolveNodes = this.nodes.ids.isReady();
|
|
186
|
+
if (!canResolveNodes) {
|
|
187
|
+
console.warn(
|
|
188
|
+
"Ways.buildEntityIndex ran before the node index was ready; all way references will use the missing-ref representation.",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
for (let i = 0; i < this.pendingRefIds.length; i++) {
|
|
192
|
+
const refId = this.pendingRefIds.at(i);
|
|
193
|
+
const nodeIndex = canResolveNodes ? this.nodes.ids.getIndexFromId(refId) : -1;
|
|
194
|
+
this.setIndexedRef(i, refId, nodeIndex);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.pendingRefIds = null;
|
|
198
|
+
this.refs.compact();
|
|
199
|
+
this.missingRefPositions.compact();
|
|
200
|
+
this.missingRefIds.compact();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Build the spatial index for ways.
|
|
205
|
+
* If bbox data already exists (e.g., loaded from storage), reuses it.
|
|
206
|
+
* Returns the existing index when it is already built.
|
|
207
|
+
*/
|
|
208
|
+
buildSpatialIndex() {
|
|
209
|
+
if (!this.nodes.isReady()) throw Error("Node index is not ready.");
|
|
210
|
+
if (this.spatialIndexBuilt) return this.spatialIndex;
|
|
211
|
+
if (this.size === 0) {
|
|
212
|
+
this.spatialIndex = new Flatbush(1, 128, Float64Array, BufferConstructor);
|
|
213
|
+
this.spatialIndexBuilt = true;
|
|
214
|
+
return this.spatialIndex;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.spatialIndex = new Flatbush(this.size, 128, Float64Array, BufferConstructor);
|
|
218
|
+
|
|
219
|
+
// If bbox already has data (loaded from storage), use it directly
|
|
220
|
+
const hasBboxData = this.bbox.length >= this.size * 4;
|
|
221
|
+
for (let i = 0; i < this.size; i++) {
|
|
222
|
+
let minX: number;
|
|
223
|
+
let minY: number;
|
|
224
|
+
let maxX: number;
|
|
225
|
+
let maxY: number;
|
|
226
|
+
|
|
227
|
+
if (hasBboxData) {
|
|
228
|
+
// Use stored bbox values
|
|
229
|
+
minX = this.bbox.at(i * 4);
|
|
230
|
+
minY = this.bbox.at(i * 4 + 1);
|
|
231
|
+
maxX = this.bbox.at(i * 4 + 2);
|
|
232
|
+
maxY = this.bbox.at(i * 4 + 3);
|
|
233
|
+
} else {
|
|
234
|
+
// Calculate bbox from resolvable node coordinates. Missing refs are
|
|
235
|
+
// preserved losslessly in storage but contribute no geometry; a way
|
|
236
|
+
// with no resolvable refs keeps an inverted bbox that never matches.
|
|
237
|
+
minX = Number.POSITIVE_INFINITY;
|
|
238
|
+
minY = Number.POSITIVE_INFINITY;
|
|
239
|
+
maxX = Number.NEGATIVE_INFINITY;
|
|
240
|
+
maxY = Number.NEGATIVE_INFINITY;
|
|
241
|
+
const start = this.refStart.at(i);
|
|
242
|
+
const count = this.refCount.at(i);
|
|
243
|
+
for (let j = start; j < start + count; j++) {
|
|
244
|
+
const coordinate = this.tryGetNodeLonLat(j);
|
|
245
|
+
if (!coordinate) continue;
|
|
246
|
+
const [lon, lat] = coordinate;
|
|
247
|
+
if (lon < minX) minX = lon;
|
|
248
|
+
if (lon > maxX) maxX = lon;
|
|
249
|
+
if (lat < minY) minY = lat;
|
|
250
|
+
if (lat > maxY) maxY = lat;
|
|
251
|
+
}
|
|
252
|
+
this.bbox.push(minX);
|
|
253
|
+
this.bbox.push(minY);
|
|
254
|
+
this.bbox.push(maxX);
|
|
255
|
+
this.bbox.push(maxY);
|
|
256
|
+
}
|
|
257
|
+
this.spatialIndex.add(minX, minY, maxX, maxY);
|
|
258
|
+
}
|
|
259
|
+
// Compact bbox if we just calculated it
|
|
260
|
+
if (!hasBboxData) {
|
|
261
|
+
this.bbox.compact();
|
|
262
|
+
}
|
|
263
|
+
this.spatialIndex.finish();
|
|
264
|
+
this.spatialIndexBuilt = true;
|
|
265
|
+
return this.spatialIndex;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if the spatial index has been built.
|
|
270
|
+
*/
|
|
271
|
+
hasSpatialIndex(): boolean {
|
|
272
|
+
return this.spatialIndexBuilt;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get the full way entity.
|
|
277
|
+
*/
|
|
278
|
+
getFullEntity(index: number, id: number, tags?: OsmTags): OsmWay {
|
|
279
|
+
return {
|
|
280
|
+
id,
|
|
281
|
+
refs: [...this.getRefIds(index)],
|
|
282
|
+
tags,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get the node IDs referenced by a way.
|
|
288
|
+
*/
|
|
289
|
+
getRefIds(index: number): number[] {
|
|
290
|
+
const start = this.refStart.at(index);
|
|
291
|
+
const count = this.refCount.at(index);
|
|
292
|
+
const refs = Array.from<number>({ length: count });
|
|
293
|
+
for (let i = 0; i < count; i++) refs[i] = this.getRefId(start + i);
|
|
294
|
+
return refs;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get the node indexes referenced by a way without allocating.
|
|
299
|
+
*
|
|
300
|
+
* Returns a view into internal storage; do not mutate it or hold it across writes. Refs to
|
|
301
|
+
* nodes absent from the dataset are {@link MISSING_NODE_INDEX}. Requires `buildEntityIndex()`.
|
|
302
|
+
*/
|
|
303
|
+
getRefIndexes(index: number): Uint32Array {
|
|
304
|
+
if (this.pendingRefIds !== null) throw Error("Way index is not built.");
|
|
305
|
+
const start = this.refStart.at(index);
|
|
306
|
+
return this.refs.array.subarray(start, start + this.refCount.at(index));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get the bounding box of a way.
|
|
311
|
+
*/
|
|
312
|
+
getEntityBbox(idOrIndex: IdOrIndex): GeoBbox2D {
|
|
313
|
+
const index = "index" in idOrIndex ? idOrIndex.index : this.ids.idOrIndex(idOrIndex)[0];
|
|
314
|
+
return [
|
|
315
|
+
this.bbox.at(index * 4),
|
|
316
|
+
this.bbox.at(index * 4 + 1),
|
|
317
|
+
this.bbox.at(index * 4 + 2),
|
|
318
|
+
this.bbox.at(index * 4 + 3),
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get the coordinates of a way as a flat array.
|
|
324
|
+
*/
|
|
325
|
+
getLine(index: number) {
|
|
326
|
+
const count = this.refCount.at(index);
|
|
327
|
+
const start = this.refStart.at(index);
|
|
328
|
+
const line = new Float64Array(count * 2);
|
|
329
|
+
for (let i = 0; i < count; i++) {
|
|
330
|
+
const [lon, lat] = this.getNodeLonLat(start + i);
|
|
331
|
+
line[i * 2] = lon;
|
|
332
|
+
line[i * 2 + 1] = lat;
|
|
333
|
+
}
|
|
334
|
+
return line;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get the coordinates of a way as an array of [lon, lat] pairs.
|
|
339
|
+
* Throws when a referenced node cannot be resolved; use
|
|
340
|
+
* `getResolvedCoordinates()` for tolerant consumers such as bbox building.
|
|
341
|
+
*/
|
|
342
|
+
getCoordinates(index: number): LonLat[] {
|
|
343
|
+
const count = this.refCount.at(index);
|
|
344
|
+
const start = this.refStart.at(index);
|
|
345
|
+
const coords: [number, number][] = [];
|
|
346
|
+
for (let refIndex = start; refIndex < start + count; refIndex++) {
|
|
347
|
+
coords.push(this.getNodeLonLat(refIndex));
|
|
348
|
+
}
|
|
349
|
+
return coords;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get the resolvable coordinates of a way, skipping missing node refs.
|
|
354
|
+
* Preferred over `getCoordinates()` when partial geometry is acceptable,
|
|
355
|
+
* e.g. computing bounding boxes over referentially incomplete extracts.
|
|
356
|
+
*/
|
|
357
|
+
getResolvedCoordinates(index: number): LonLat[] {
|
|
358
|
+
const count = this.refCount.at(index);
|
|
359
|
+
const start = this.refStart.at(index);
|
|
360
|
+
const coords: [number, number][] = [];
|
|
361
|
+
for (let refIndex = start; refIndex < start + count; refIndex++) {
|
|
362
|
+
const coordinate = this.tryGetNodeLonLat(refIndex);
|
|
363
|
+
if (coordinate) coords.push(coordinate);
|
|
364
|
+
}
|
|
365
|
+
return coords;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Find way indexes that intersect a bounding box.
|
|
370
|
+
*/
|
|
371
|
+
intersects(bbox: GeoBbox2D, filterFn?: (index: number) => boolean): number[] {
|
|
372
|
+
if (this.size === 0) return [];
|
|
373
|
+
// A way with no resolvable refs stores an inverted bbox. Flatbush's
|
|
374
|
+
// contained-node fast path skips per-leaf intersection tests, so filter
|
|
375
|
+
// inverted boxes out explicitly.
|
|
376
|
+
return this.spatialIndex.search(bbox[0], bbox[1], bbox[2], bbox[3], (index, x0, y0, x1, y1) => {
|
|
377
|
+
if (!isValidSpatialBbox(x0, y0, x1, y1)) return false;
|
|
378
|
+
return filterFn ? filterFn(index) : true;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Find way indexes near a point using great-circle distance.
|
|
384
|
+
* @param lon - Longitude in degrees.
|
|
385
|
+
* @param lat - Latitude in degrees.
|
|
386
|
+
* @param maxResults - Maximum number of results to return.
|
|
387
|
+
* @param maxDistanceKm - Maximum distance in kilometers.
|
|
388
|
+
* @returns Array of way indexes sorted by distance.
|
|
389
|
+
*/
|
|
390
|
+
neighbors(lon: number, lat: number, maxResults?: number, maxDistanceKm?: number): number[] {
|
|
391
|
+
if (this.size === 0) return [];
|
|
392
|
+
// Use geoflatbush for proper geographic distance calculations
|
|
393
|
+
return geoAround(this.spatialIndex, lon, lat, maxResults, maxDistanceKm, (index) => {
|
|
394
|
+
const offset = index * 4;
|
|
395
|
+
return isValidSpatialBbox(
|
|
396
|
+
this.bbox.at(offset),
|
|
397
|
+
this.bbox.at(offset + 1),
|
|
398
|
+
this.bbox.at(offset + 2),
|
|
399
|
+
this.bbox.at(offset + 3),
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get ways within a bounding box.
|
|
406
|
+
*/
|
|
407
|
+
withinBbox(
|
|
408
|
+
bbox: GeoBbox2D,
|
|
409
|
+
include?: (index: number) => boolean,
|
|
410
|
+
): {
|
|
411
|
+
ids: Float64Array;
|
|
412
|
+
positions: Float64Array;
|
|
413
|
+
startIndices: Uint32Array;
|
|
414
|
+
} {
|
|
415
|
+
const wayCandidates = this.intersects(bbox, include);
|
|
416
|
+
const ids = new Float64Array(wayCandidates.length);
|
|
417
|
+
const wayPositions: Float64Array[] = [];
|
|
418
|
+
const wayStartIndices = new Uint32Array(wayCandidates.length + 1);
|
|
419
|
+
wayStartIndices[0] = 0;
|
|
420
|
+
|
|
421
|
+
let size = 0;
|
|
422
|
+
wayCandidates.forEach((wayIndex, i) => {
|
|
423
|
+
ids[i] = this.ids.at(wayIndex);
|
|
424
|
+
const way = this.getLine(wayIndex);
|
|
425
|
+
size += way.length;
|
|
426
|
+
wayPositions.push(way);
|
|
427
|
+
const prevIndex = wayStartIndices[i];
|
|
428
|
+
if (prevIndex === undefined) throw Error("Previous index is undefined");
|
|
429
|
+
wayStartIndices[i + 1] = prevIndex + way.length / 2;
|
|
430
|
+
});
|
|
431
|
+
const wayPositionsArray = new Float64Array(size);
|
|
432
|
+
let pIndex = 0;
|
|
433
|
+
for (const way of wayPositions) {
|
|
434
|
+
wayPositionsArray.set(way, pIndex);
|
|
435
|
+
pIndex += way.length;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
ids,
|
|
440
|
+
positions: wayPositionsArray,
|
|
441
|
+
startIndices: wayStartIndices,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get transferable objects for passing to another thread.
|
|
447
|
+
* Only includes spatialIndex if it has been built.
|
|
448
|
+
*/
|
|
449
|
+
override transferables(): WaysTransferables {
|
|
450
|
+
const base = {
|
|
451
|
+
...super.transferables(),
|
|
452
|
+
refStart: this.refStart.array.buffer,
|
|
453
|
+
refCount: this.refCount.array.buffer,
|
|
454
|
+
refs: this.refs.array.buffer,
|
|
455
|
+
missingRefPositions: this.missingRefPositions.array.buffer,
|
|
456
|
+
missingRefIds: this.missingRefIds.array.buffer,
|
|
457
|
+
bbox: this.bbox.array.buffer,
|
|
458
|
+
};
|
|
459
|
+
// Only include spatial index if it was built
|
|
460
|
+
if (this.spatialIndexBuilt) {
|
|
461
|
+
return { ...base, spatialIndex: this.spatialIndex.data };
|
|
462
|
+
}
|
|
463
|
+
return base;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Get the approximate memory requirements for a given number of ways in bytes.
|
|
468
|
+
*/
|
|
469
|
+
static getBytesRequired(count: number, refCount = 0, missingRefCount = 0, tagCount = 0) {
|
|
470
|
+
if (count === 0) return 0;
|
|
471
|
+
// Approximate nodes per way
|
|
472
|
+
let numNodes = count;
|
|
473
|
+
let n = count;
|
|
474
|
+
while (n !== 1) {
|
|
475
|
+
n = Math.ceil(n / 128);
|
|
476
|
+
numNodes += n;
|
|
477
|
+
}
|
|
478
|
+
const indexBytes = (numNodes < 16384 ? 2 : 4) * numNodes;
|
|
479
|
+
const boxesBytes = numNodes * 4 * Float64Array.BYTES_PER_ELEMENT;
|
|
480
|
+
const spatialIndexBytes = 8 + indexBytes + boxesBytes;
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
Ids.getBytesRequired(count) +
|
|
484
|
+
Tags.getBytesRequired(count, count, tagCount) +
|
|
485
|
+
count * Uint32Array.BYTES_PER_ELEMENT + // refStart
|
|
486
|
+
count * Uint16Array.BYTES_PER_ELEMENT + // refCount
|
|
487
|
+
refCount * Uint32Array.BYTES_PER_ELEMENT + // node indexes
|
|
488
|
+
missingRefCount * (Uint32Array.BYTES_PER_ELEMENT + Float64Array.BYTES_PER_ELEMENT) + // missing refs
|
|
489
|
+
count * 4 * Float64Array.BYTES_PER_ELEMENT + // bbox
|
|
490
|
+
spatialIndexBytes
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Update a ContentHasher with way-specific data (node references).
|
|
496
|
+
*/
|
|
497
|
+
override updateHash(hasher: ContentHasher): ContentHasher {
|
|
498
|
+
super.updateHash(hasher).update(this.refCount.array);
|
|
499
|
+
const chunk = new Float64Array(Math.min(HASH_REF_CHUNK_SIZE, this.refs.length));
|
|
500
|
+
let chunkLength = 0;
|
|
501
|
+
for (let i = 0; i < this.refs.length; i++) {
|
|
502
|
+
chunk[chunkLength++] = this.getRefId(i);
|
|
503
|
+
if (chunkLength === chunk.length) {
|
|
504
|
+
hasher.update(chunk);
|
|
505
|
+
chunkLength = 0;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (chunkLength > 0) hasher.update(chunk.subarray(0, chunkLength));
|
|
509
|
+
return hasher;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Logical reference length during either pending-ID or indexed ingestion. */
|
|
513
|
+
/** Reject a way whose ref count would wrap `refCount` or the `refStart` offsets. */
|
|
514
|
+
private assertRefCapacity(wayId: number, refCount: number) {
|
|
515
|
+
assertCapacity("way-refs", refCount, MAX_WAY_REFS, () => `Way ${wayId}`);
|
|
516
|
+
assertCapacity(
|
|
517
|
+
"total-way-refs",
|
|
518
|
+
this.refLength + refCount,
|
|
519
|
+
MAX_UINT32_OFFSET,
|
|
520
|
+
() => `The dataset (at way ${wayId})`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private get refLength(): number {
|
|
525
|
+
return this.pendingRefIds?.length ?? this.refs.length;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Append OSM node IDs, optionally resolving them directly to node indexes. */
|
|
529
|
+
private appendRefIds(refIds: number[], nodeIdToIndex?: (nodeId: number) => number) {
|
|
530
|
+
if (nodeIdToIndex) {
|
|
531
|
+
if ((this.pendingRefIds?.length ?? 0) > 0) {
|
|
532
|
+
throw Error("Cannot mix unresolved and directly indexed way references.");
|
|
533
|
+
}
|
|
534
|
+
this.pendingRefIds = null;
|
|
535
|
+
for (const refId of refIds) {
|
|
536
|
+
this.setIndexedRef(this.refs.length, refId, nodeIdToIndex(refId));
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (!this.pendingRefIds) {
|
|
541
|
+
throw Error("Directly indexed way references require a node ID resolver.");
|
|
542
|
+
}
|
|
543
|
+
this.pendingRefIds.pushMany(refIds);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Store one resolved or losslessly preserved missing reference. */
|
|
547
|
+
private setIndexedRef(position: number, refId: number, nodeIndex: number) {
|
|
548
|
+
if (nodeIndex < 0) {
|
|
549
|
+
this.refs.set(position, MISSING_NODE_INDEX);
|
|
550
|
+
this.missingRefPositions.push(position);
|
|
551
|
+
this.missingRefIds.push(refId);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (!Number.isInteger(nodeIndex) || nodeIndex >= MISSING_NODE_INDEX) {
|
|
555
|
+
throw Error(`Invalid node index ${nodeIndex} for reference ${refId}`);
|
|
556
|
+
}
|
|
557
|
+
this.refs.set(position, nodeIndex);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Reconstruct the public OSM node ID for one internal reference position. */
|
|
561
|
+
private getRefId(position: number): number {
|
|
562
|
+
const nodeIndex = this.refs.at(position);
|
|
563
|
+
return nodeIndex === MISSING_NODE_INDEX
|
|
564
|
+
? this.getMissingRefId(position)
|
|
565
|
+
: this.nodes.ids.at(nodeIndex);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Find the OSM node ID for a sentinel reference position. */
|
|
569
|
+
private getMissingRefId(position: number): number {
|
|
570
|
+
let low = 0;
|
|
571
|
+
let high = this.missingRefPositions.length - 1;
|
|
572
|
+
while (low <= high) {
|
|
573
|
+
const middle = (low + high) >>> 1;
|
|
574
|
+
const candidate = this.missingRefPositions.at(middle);
|
|
575
|
+
if (candidate === position) return this.missingRefIds.at(middle);
|
|
576
|
+
if (candidate < position) low = middle + 1;
|
|
577
|
+
else high = middle - 1;
|
|
578
|
+
}
|
|
579
|
+
throw Error(`Missing node ID not found for way reference position ${position}`);
|
|
580
|
+
}
|
|
410
581
|
}
|