@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/nodes.ts
CHANGED
|
@@ -1,404 +1,545 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import type { GeoBbox2D, OsmNode, OsmTags } from "@osmix/
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import type StringTable from "./stringtable.ts"
|
|
11
|
-
import { Tags } from "./tags.ts"
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
import { microToDegrees, OSM_COORD_SCALE, toMicroDegrees } from "@osmix/geo/coordinates";
|
|
2
|
+
import type { OsmPbfBlock, OsmPbfDenseNodes } from "@osmix/pbf";
|
|
3
|
+
import { assertValue } from "@osmix/shared/assert";
|
|
4
|
+
import type { ContentHasher } from "@osmix/shared/content-hasher";
|
|
5
|
+
import type { GeoBbox2D, OsmNode, OsmTags } from "@osmix/types";
|
|
6
|
+
|
|
7
|
+
import { Entities, type EntitiesTransferables } from "./entities.ts";
|
|
8
|
+
import { type IdOrIndex, Ids } from "./ids.ts";
|
|
9
|
+
import { IndirectKdIndex } from "./indirect-kd-index.ts";
|
|
10
|
+
import type StringTable from "./stringtable.ts";
|
|
11
|
+
import { Tags } from "./tags.ts";
|
|
12
|
+
import { type BufferType, ResizeableTypedArray as RTA } from "./typed-arrays.ts";
|
|
13
|
+
|
|
14
|
+
const EARTH_RADIUS_KM = 6371.0088;
|
|
15
|
+
const COORDINATE_PADDING_DEGREES = 1 / OSM_COORD_SCALE;
|
|
16
|
+
|
|
17
|
+
export type NodeSpatialIndexKind = "all" | "tagged";
|
|
18
|
+
|
|
19
|
+
export class SpatialIndexNotBuiltError extends Error {
|
|
20
|
+
readonly code = "SPATIAL_INDEX_NOT_BUILT";
|
|
21
|
+
readonly entityType = "node";
|
|
22
|
+
readonly indexKind: NodeSpatialIndexKind;
|
|
23
|
+
|
|
24
|
+
constructor(indexKind: NodeSpatialIndexKind) {
|
|
25
|
+
super(`The ${indexKind} node spatial index has not been built.`);
|
|
26
|
+
this.name = "SpatialIndexNotBuiltError";
|
|
27
|
+
this.indexKind = indexKind;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NodesTransferables<
|
|
32
|
+
T extends BufferType = BufferType,
|
|
33
|
+
> extends EntitiesTransferables<T> {
|
|
34
|
+
lons: T;
|
|
35
|
+
lats: T;
|
|
36
|
+
bbox: GeoBbox2D;
|
|
37
|
+
/** Optional all-node Uint32 permutation; can be rebuilt via buildSpatialIndex("all"). */
|
|
38
|
+
allSpatialIndex?: T;
|
|
39
|
+
/** Optional tagged-node Uint32 permutation; can be rebuilt via buildSpatialIndex("tagged"). */
|
|
40
|
+
taggedSpatialIndex?: T;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export interface AddNodeOptions {
|
|
28
|
-
|
|
44
|
+
filter?: (node: OsmNode) => boolean;
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
export class Nodes extends Entities<OsmNode> {
|
|
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
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Coordinates are stored as integer microdegrees (Int32Array).
|
|
50
|
+
* Use OSM_COORD_SCALE (1e7) to convert between degrees and microdegrees.
|
|
51
|
+
*/
|
|
52
|
+
private lons: RTA<Int32Array>;
|
|
53
|
+
private lats: RTA<Int32Array>;
|
|
54
|
+
// Bounding box is exposed and transferred in degrees.
|
|
55
|
+
private bbox: [minLon: number, minLat: number, maxLon: number, maxLat: number] = [
|
|
56
|
+
Number.MAX_SAFE_INTEGER,
|
|
57
|
+
Number.MAX_SAFE_INTEGER,
|
|
58
|
+
Number.MIN_SAFE_INTEGER,
|
|
59
|
+
Number.MIN_SAFE_INTEGER,
|
|
60
|
+
];
|
|
61
|
+
private allSpatialIndex?: IndirectKdIndex;
|
|
62
|
+
private taggedSpatialIndex?: IndirectKdIndex;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a new Nodes index.
|
|
66
|
+
*/
|
|
67
|
+
constructor(stringTable: StringTable, transferables?: NodesTransferables) {
|
|
68
|
+
if (transferables) {
|
|
69
|
+
super("node", new Ids(transferables), new Tags(stringTable, transferables));
|
|
70
|
+
this.lons = RTA.from(Int32Array, transferables.lons);
|
|
71
|
+
this.lats = RTA.from(Int32Array, transferables.lats);
|
|
72
|
+
if (transferables.allSpatialIndex !== undefined) {
|
|
73
|
+
this.allSpatialIndex = IndirectKdIndex.from(
|
|
74
|
+
this.lons.array,
|
|
75
|
+
this.lats.array,
|
|
76
|
+
transferables.allSpatialIndex,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (transferables.taggedSpatialIndex !== undefined) {
|
|
80
|
+
this.taggedSpatialIndex = IndirectKdIndex.from(
|
|
81
|
+
this.lons.array,
|
|
82
|
+
this.lats.array,
|
|
83
|
+
transferables.taggedSpatialIndex,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
this.bbox = transferables.bbox;
|
|
87
|
+
this.indexBuilt = true;
|
|
88
|
+
} else {
|
|
89
|
+
super("node", new Ids(), new Tags(stringTable));
|
|
90
|
+
this.lons = new RTA(Int32Array);
|
|
91
|
+
this.lats = new RTA(Int32Array);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add a single node to the index.
|
|
97
|
+
*/
|
|
98
|
+
addNode(node: OsmNode): number {
|
|
99
|
+
const nodeIndex = this.addEntity(node.id, node.tags ?? {});
|
|
100
|
+
|
|
101
|
+
const lonMicro = toMicroDegrees(node.lon);
|
|
102
|
+
const latMicro = toMicroDegrees(node.lat);
|
|
103
|
+
|
|
104
|
+
this.lons.push(lonMicro);
|
|
105
|
+
this.lats.push(latMicro);
|
|
106
|
+
|
|
107
|
+
if (node.lon < this.bbox[0]) this.bbox[0] = node.lon;
|
|
108
|
+
if (node.lat < this.bbox[1]) this.bbox[1] = node.lat;
|
|
109
|
+
if (node.lon > this.bbox[2]) this.bbox[2] = node.lon;
|
|
110
|
+
if (node.lat > this.bbox[3]) this.bbox[3] = node.lat;
|
|
111
|
+
|
|
112
|
+
return nodeIndex;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Add dense nodes from a PBF block.
|
|
117
|
+
*/
|
|
118
|
+
addDenseNodes(
|
|
119
|
+
dense: OsmPbfDenseNodes,
|
|
120
|
+
block: OsmPbfBlock,
|
|
121
|
+
blockStringIndexMap: Uint32Array,
|
|
122
|
+
filter?: (node: OsmNode) => boolean,
|
|
123
|
+
): number {
|
|
124
|
+
// PBF block already has offsets and granularity converted to degrees
|
|
125
|
+
const lon_offset = block.lon_offset ?? 0;
|
|
126
|
+
const lat_offset = block.lat_offset ?? 0;
|
|
127
|
+
const granularity = block.granularity ?? 1e7;
|
|
128
|
+
|
|
129
|
+
const delta = {
|
|
130
|
+
id: 0,
|
|
131
|
+
lat: 0,
|
|
132
|
+
lon: 0,
|
|
133
|
+
timestamp: 0,
|
|
134
|
+
changeset: 0,
|
|
135
|
+
uid: 0,
|
|
136
|
+
user_sid: 0,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const getStringTableIndex = (keyIndex: number) => {
|
|
140
|
+
const key = dense.keys_vals[keyIndex];
|
|
141
|
+
assertValue(key, "Block string key is undefined");
|
|
142
|
+
const index = blockStringIndexMap[key];
|
|
143
|
+
assertValue(index, "Block string not found");
|
|
144
|
+
return index;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
let keysValsIndex = 0;
|
|
148
|
+
let added = 0;
|
|
149
|
+
for (let i = 0; i < dense.id.length; i++) {
|
|
150
|
+
const idSid = dense.id[i];
|
|
151
|
+
const latSid = dense.lat[i];
|
|
152
|
+
const lonSid = dense.lon[i];
|
|
153
|
+
assertValue(idSid, "ID SID is undefined");
|
|
154
|
+
assertValue(latSid, "Latitude SID is undefined");
|
|
155
|
+
assertValue(lonSid, "Longitude SID is undefined");
|
|
156
|
+
|
|
157
|
+
delta.id += idSid;
|
|
158
|
+
delta.lat += latSid;
|
|
159
|
+
delta.lon += lonSid;
|
|
160
|
+
|
|
161
|
+
// Calculate degrees from PBF delta encoding
|
|
162
|
+
const lon = lon_offset + delta.lon / granularity;
|
|
163
|
+
const lat = lat_offset + delta.lat / granularity;
|
|
164
|
+
|
|
165
|
+
// Convert to microdegrees for storage
|
|
166
|
+
const lonMicro = toMicroDegrees(lon);
|
|
167
|
+
const latMicro = toMicroDegrees(lat);
|
|
168
|
+
|
|
169
|
+
const tagKeys: number[] = [];
|
|
170
|
+
const tagValues: number[] = [];
|
|
171
|
+
if (dense.keys_vals.length > 0) {
|
|
172
|
+
while (dense.keys_vals[keysValsIndex] !== 0) {
|
|
173
|
+
const key = getStringTableIndex(keysValsIndex);
|
|
174
|
+
const val = getStringTableIndex(keysValsIndex + 1);
|
|
175
|
+
if (key && val) {
|
|
176
|
+
tagKeys.push(key);
|
|
177
|
+
tagValues.push(val);
|
|
178
|
+
}
|
|
179
|
+
keysValsIndex += 2;
|
|
180
|
+
}
|
|
181
|
+
keysValsIndex++;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const shouldInclude = filter
|
|
185
|
+
? filter({
|
|
186
|
+
id: delta.id,
|
|
187
|
+
lon,
|
|
188
|
+
lat,
|
|
189
|
+
tags: this.tags.getTagsFromIndices(tagKeys, tagValues),
|
|
190
|
+
})
|
|
191
|
+
: true;
|
|
192
|
+
if (!shouldInclude) continue;
|
|
193
|
+
|
|
194
|
+
this.addEntity(delta.id, tagKeys, tagValues);
|
|
195
|
+
this.lons.push(lonMicro);
|
|
196
|
+
this.lats.push(latMicro);
|
|
197
|
+
|
|
198
|
+
if (lon < this.bbox[0]) this.bbox[0] = lon;
|
|
199
|
+
if (lat < this.bbox[1]) this.bbox[1] = lat;
|
|
200
|
+
if (lon > this.bbox[2]) this.bbox[2] = lon;
|
|
201
|
+
if (lat > this.bbox[3]) this.bbox[3] = lat;
|
|
202
|
+
added++;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return added;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Compact the internal arrays to free up memory.
|
|
210
|
+
*/
|
|
211
|
+
buildEntityIndex() {
|
|
212
|
+
this.lons.compact();
|
|
213
|
+
this.lats.compact();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Build one of the independent node spatial indexes.
|
|
218
|
+
*/
|
|
219
|
+
buildSpatialIndex(kind: NodeSpatialIndexKind = "all") {
|
|
220
|
+
if (this.hasSpatialIndex(kind)) return;
|
|
221
|
+
|
|
222
|
+
console.time(`NodeIndex.buildSpatialIndex.${kind}`);
|
|
223
|
+
if (kind === "all") {
|
|
224
|
+
this.allSpatialIndex = IndirectKdIndex.build(
|
|
225
|
+
this.lons.array,
|
|
226
|
+
this.lats.array,
|
|
227
|
+
this.size,
|
|
228
|
+
(indexes) => {
|
|
229
|
+
for (let i = 0; i < indexes.length; i++) indexes[i] = i;
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
this.taggedSpatialIndex = IndirectKdIndex.build(
|
|
234
|
+
this.lons.array,
|
|
235
|
+
this.lats.array,
|
|
236
|
+
this.taggedSize,
|
|
237
|
+
(indexes) => {
|
|
238
|
+
let position = 0;
|
|
239
|
+
for (const entityIndex of this.tags.taggedEntityIndexes()) {
|
|
240
|
+
indexes[position++] = entityIndex;
|
|
241
|
+
}
|
|
242
|
+
if (position !== indexes.length) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Expected ${indexes.length} tagged nodes while building index, received ${position}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
console.timeEnd(`NodeIndex.buildSpatialIndex.${kind}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if the spatial index has been built.
|
|
255
|
+
*/
|
|
256
|
+
hasSpatialIndex(kind: NodeSpatialIndexKind = "all"): boolean {
|
|
257
|
+
return kind === "all"
|
|
258
|
+
? this.allSpatialIndex !== undefined
|
|
259
|
+
: this.taggedSpatialIndex !== undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Number of nodes that carry at least one tag. */
|
|
263
|
+
get taggedSize(): number {
|
|
264
|
+
return this.tags.taggedEntityCount;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the bounding box of all nodes, or `null` when there are no nodes.
|
|
269
|
+
*/
|
|
270
|
+
getBbox(): GeoBbox2D | null {
|
|
271
|
+
// The bbox starts inverted and only becomes valid once a node extends it.
|
|
272
|
+
if (this.bbox[0] > this.bbox[2] || this.bbox[1] > this.bbox[3]) return null;
|
|
273
|
+
return this.bbox;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get the bounding box of a specific node.
|
|
278
|
+
*/
|
|
279
|
+
getEntityBbox(i: IdOrIndex): GeoBbox2D {
|
|
280
|
+
const index = "index" in i ? i.index : this.ids.idOrIndex(i)[0];
|
|
281
|
+
const lon = microToDegrees(this.lons.at(index));
|
|
282
|
+
const lat = microToDegrees(this.lats.at(index));
|
|
283
|
+
return [lon, lat, lon, lat] as GeoBbox2D;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get the longitude and latitude of a specific node.
|
|
288
|
+
* ID lookups return `null` when the node is not present; index lookups remain strict.
|
|
289
|
+
*/
|
|
290
|
+
getNodeLonLat(i: { index: number }): [number, number];
|
|
291
|
+
getNodeLonLat(i: { id: number }): [number, number] | null;
|
|
292
|
+
getNodeLonLat(i: IdOrIndex): [number, number] | null {
|
|
293
|
+
const index = "index" in i ? i.index : this.ids.idOrIndex(i)[0];
|
|
294
|
+
if (index === -1) return null;
|
|
295
|
+
return [microToDegrees(this.lons.at(index)), microToDegrees(this.lats.at(index))];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the full node entity.
|
|
300
|
+
*/
|
|
301
|
+
getFullEntity(index: number, id: number, tags?: OsmTags): OsmNode {
|
|
302
|
+
const [lon, lat] = this.getNodeLonLat({ index });
|
|
303
|
+
if (tags) {
|
|
304
|
+
return {
|
|
305
|
+
id,
|
|
306
|
+
lat,
|
|
307
|
+
lon,
|
|
308
|
+
tags,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
id,
|
|
313
|
+
lat,
|
|
314
|
+
lon,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Spatial operations
|
|
319
|
+
/**
|
|
320
|
+
* Find node indexes within a bounding box.
|
|
321
|
+
*/
|
|
322
|
+
findIndexesWithinBbox(bbox: GeoBbox2D): number[] {
|
|
323
|
+
return findIndexesWithinBbox(this.getSpatialIndex("all"), bbox);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Find tagged node indexes within a bounding box. */
|
|
327
|
+
findTaggedIndexesWithinBbox(bbox: GeoBbox2D): number[] {
|
|
328
|
+
return findIndexesWithinBbox(this.getSpatialIndex("tagged"), bbox);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Find node indexes within a radius of a point.
|
|
333
|
+
* Uses an exact haversine-distance filter over a conservative KD bbox query.
|
|
334
|
+
* @param lon - Longitude in degrees.
|
|
335
|
+
* @param lat - Latitude in degrees.
|
|
336
|
+
* @param radiusKm - Radius in kilometers.
|
|
337
|
+
* @returns Array of node indexes within the radius.
|
|
338
|
+
*/
|
|
339
|
+
findIndexesWithinRadius(lon: number, lat: number, radiusKm: number): number[] {
|
|
340
|
+
const spatialIndex = this.getSpatialIndex("all");
|
|
341
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
|
|
342
|
+
throw new RangeError("Longitude and latitude must be finite numbers");
|
|
343
|
+
}
|
|
344
|
+
if (lat < -90 || lat > 90) throw new RangeError("Latitude must be between -90 and 90");
|
|
345
|
+
if (Number.isNaN(radiusKm) || radiusKm < 0) return [];
|
|
346
|
+
|
|
347
|
+
const normalizedLon = normalizeLongitude(lon);
|
|
348
|
+
const bbox = radiusBoundingBox(normalizedLon, lat, radiusKm);
|
|
349
|
+
const candidates = findIndexesWithinBbox(spatialIndex, bbox);
|
|
350
|
+
const matches: { distance: number; index: number }[] = [];
|
|
351
|
+
for (const index of candidates) {
|
|
352
|
+
const nodeLon = microToDegrees(this.lons.at(index));
|
|
353
|
+
const nodeLat = microToDegrees(this.lats.at(index));
|
|
354
|
+
const distance = haversineDistanceKm(normalizedLon, lat, nodeLon, nodeLat);
|
|
355
|
+
if (distance <= radiusKm) matches.push({ distance, index });
|
|
356
|
+
}
|
|
357
|
+
matches.sort((a, b) => a.distance - b.distance || a.index - b.index);
|
|
358
|
+
return matches.map(({ index }) => index);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get nodes within a bounding box.
|
|
363
|
+
* @param bbox - The bounding box to search within.
|
|
364
|
+
* @param include - A function to filter nodes. If provided, only nodes for which the function returns true will be included.
|
|
365
|
+
* @returns An object containing the IDs and positions of the nodes within the bounding box.
|
|
366
|
+
*/
|
|
367
|
+
withinBbox(
|
|
368
|
+
bbox: GeoBbox2D,
|
|
369
|
+
include?: (i: number) => boolean,
|
|
370
|
+
): {
|
|
371
|
+
ids: Float64Array;
|
|
372
|
+
positions: Float64Array;
|
|
373
|
+
} {
|
|
374
|
+
console.time("Nodes.withinBbox");
|
|
375
|
+
const nodeCandidates = this.findIndexesWithinBbox(bbox);
|
|
376
|
+
const nodePositions = new Float64Array(nodeCandidates.length * 2);
|
|
377
|
+
const ids = new Float64Array(nodeCandidates.length);
|
|
378
|
+
|
|
379
|
+
let skipped = 0;
|
|
380
|
+
nodeCandidates.forEach((nodeIndex, i) => {
|
|
381
|
+
if (include && !include(nodeIndex)) {
|
|
382
|
+
skipped++;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const [lon, lat] = this.getNodeLonLat({ index: nodeIndex });
|
|
387
|
+
ids[i - skipped] = this.ids.at(nodeIndex);
|
|
388
|
+
nodePositions[(i - skipped) * 2] = lon;
|
|
389
|
+
nodePositions[(i - skipped) * 2 + 1] = lat;
|
|
390
|
+
});
|
|
391
|
+
console.timeEnd("Nodes.withinBbox");
|
|
392
|
+
return {
|
|
393
|
+
ids: ids.subarray(0, nodeCandidates.length - skipped),
|
|
394
|
+
positions: nodePositions.slice(0, (nodeCandidates.length - skipped) * 2),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get transferable objects for passing to another thread.
|
|
400
|
+
* Only includes spatial indexes that have been built.
|
|
401
|
+
*/
|
|
402
|
+
override transferables(): NodesTransferables {
|
|
403
|
+
const base = {
|
|
404
|
+
...super.transferables(),
|
|
405
|
+
lons: this.lons.array.buffer,
|
|
406
|
+
lats: this.lats.array.buffer,
|
|
407
|
+
bbox: this.bbox,
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
...base,
|
|
411
|
+
...(this.allSpatialIndex ? { allSpatialIndex: this.allSpatialIndex.buffer } : {}),
|
|
412
|
+
...(this.taggedSpatialIndex ? { taggedSpatialIndex: this.taggedSpatialIndex.buffer } : {}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get the approximate memory requirements for a given number of nodes in bytes.
|
|
418
|
+
*/
|
|
419
|
+
static getBytesRequired(count: number, taggedCount = count, tagCount = 0) {
|
|
420
|
+
if (count === 0) return 0;
|
|
421
|
+
|
|
422
|
+
return (
|
|
423
|
+
Ids.getBytesRequired(count) +
|
|
424
|
+
Tags.getBytesRequired(count, taggedCount, tagCount) +
|
|
425
|
+
count * Int32Array.BYTES_PER_ELEMENT + // lons (stored in microdegrees)
|
|
426
|
+
count * Int32Array.BYTES_PER_ELEMENT + // lats (stored in microdegrees)
|
|
427
|
+
Nodes.getSpatialIndexBytesRequired(count) +
|
|
428
|
+
Nodes.getSpatialIndexBytesRequired(taggedCount)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Exact bytes in an indirect node spatial-index permutation. */
|
|
433
|
+
static getSpatialIndexBytesRequired(count: number): number {
|
|
434
|
+
return count * Uint32Array.BYTES_PER_ELEMENT;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Update a ContentHasher with node-specific data (coordinates).
|
|
439
|
+
*/
|
|
440
|
+
override updateHash(hasher: ContentHasher): ContentHasher {
|
|
441
|
+
return super.updateHash(hasher).update(this.lons.array).update(this.lats.array);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private getSpatialIndex(kind: NodeSpatialIndexKind): IndirectKdIndex {
|
|
445
|
+
const index = kind === "all" ? this.allSpatialIndex : this.taggedSpatialIndex;
|
|
446
|
+
if (!index) throw new SpatialIndexNotBuiltError(kind);
|
|
447
|
+
return index;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Round-trip float error tolerance for degree → microdegree conversion.
|
|
453
|
+
* `microToDegrees(x) * OSM_COORD_SCALE` can differ from `x` by a few ulps
|
|
454
|
+
* (~1e-6 at ±1.8e9), so ceil/floor without a tolerance could exclude a node
|
|
455
|
+
* whose exact stored coordinate sits on the query boundary.
|
|
456
|
+
*/
|
|
457
|
+
const MICRO_EPSILON = 1e-5;
|
|
458
|
+
|
|
459
|
+
/** Smallest microdegree integer inside an inclusive degree lower bound. */
|
|
460
|
+
function microLowerBound(degrees: number): number {
|
|
461
|
+
return Math.ceil(degrees * OSM_COORD_SCALE - MICRO_EPSILON);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Largest microdegree integer inside an inclusive degree upper bound. */
|
|
465
|
+
function microUpperBound(degrees: number): number {
|
|
466
|
+
return Math.floor(degrees * OSM_COORD_SCALE + MICRO_EPSILON);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function findIndexesWithinBbox(index: IndirectKdIndex, bbox: GeoBbox2D): number[] {
|
|
470
|
+
const [minLon, minLat, maxLon, maxLat] = bbox;
|
|
471
|
+
if (minLat > maxLat) return [];
|
|
472
|
+
|
|
473
|
+
const microMinLat = microLowerBound(minLat);
|
|
474
|
+
const microMaxLat = microUpperBound(maxLat);
|
|
475
|
+
if (microMinLat > microMaxLat) return [];
|
|
476
|
+
|
|
477
|
+
let result: number[];
|
|
478
|
+
if (minLon <= maxLon) {
|
|
479
|
+
result = index.range(
|
|
480
|
+
microLowerBound(minLon),
|
|
481
|
+
microMinLat,
|
|
482
|
+
microUpperBound(maxLon),
|
|
483
|
+
microMaxLat,
|
|
484
|
+
);
|
|
485
|
+
} else {
|
|
486
|
+
result = index
|
|
487
|
+
.range(microLowerBound(minLon), microMinLat, 180 * OSM_COORD_SCALE, microMaxLat)
|
|
488
|
+
.concat(
|
|
489
|
+
index.range(-180 * OSM_COORD_SCALE, microMinLat, microUpperBound(maxLon), microMaxLat),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function radiusBoundingBox(lon: number, lat: number, radiusKm: number): GeoBbox2D {
|
|
496
|
+
if (!Number.isFinite(radiusKm)) return [-180, -90, 180, 90];
|
|
497
|
+
const angularRadius = radiusKm / EARTH_RADIUS_KM;
|
|
498
|
+
if (angularRadius >= Math.PI) return [-180, -90, 180, 90];
|
|
499
|
+
|
|
500
|
+
const latRadians = degreesToRadians(lat);
|
|
501
|
+
const minLatRadians = Math.max(-Math.PI / 2, latRadians - angularRadius);
|
|
502
|
+
const maxLatRadians = Math.min(Math.PI / 2, latRadians + angularRadius);
|
|
503
|
+
const minLat = Math.max(-90, radiansToDegrees(minLatRadians) - COORDINATE_PADDING_DEGREES);
|
|
504
|
+
const maxLat = Math.min(90, radiansToDegrees(maxLatRadians) + COORDINATE_PADDING_DEGREES);
|
|
505
|
+
|
|
506
|
+
if (
|
|
507
|
+
angularRadius >= Math.PI / 2 ||
|
|
508
|
+
minLatRadians <= -Math.PI / 2 ||
|
|
509
|
+
maxLatRadians >= Math.PI / 2
|
|
510
|
+
) {
|
|
511
|
+
return [-180, minLat, 180, maxLat];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const ratio = Math.sin(angularRadius) / Math.cos(latRadians);
|
|
515
|
+
const deltaLon =
|
|
516
|
+
radiansToDegrees(Math.asin(Math.min(1, Math.max(-1, ratio)))) + COORDINATE_PADDING_DEGREES;
|
|
517
|
+
const rawMinLon = lon - deltaLon;
|
|
518
|
+
const rawMaxLon = lon + deltaLon;
|
|
519
|
+
return [normalizeLongitude(rawMinLon), minLat, normalizeLongitude(rawMaxLon), maxLat];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function haversineDistanceKm(lon1: number, lat1: number, lon2: number, lat2: number): number {
|
|
523
|
+
const latDelta = degreesToRadians(lat2 - lat1);
|
|
524
|
+
const lonDelta = degreesToRadians(lon2 - lon1);
|
|
525
|
+
const lat1Radians = degreesToRadians(lat1);
|
|
526
|
+
const lat2Radians = degreesToRadians(lat2);
|
|
527
|
+
const a =
|
|
528
|
+
Math.sin(latDelta / 2) ** 2 +
|
|
529
|
+
Math.sin(lonDelta / 2) ** 2 * Math.cos(lat1Radians) * Math.cos(lat2Radians);
|
|
530
|
+
return 2 * EARTH_RADIUS_KM * Math.atan2(Math.sqrt(a), Math.sqrt(Math.max(0, 1 - a)));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function normalizeLongitude(lon: number): number {
|
|
534
|
+
if (lon >= -180 && lon <= 180) return lon;
|
|
535
|
+
const normalized = ((((lon + 180) % 360) + 360) % 360) - 180;
|
|
536
|
+
return normalized === -180 && lon > 0 ? 180 : normalized;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function degreesToRadians(value: number): number {
|
|
540
|
+
return value * (Math.PI / 180);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function radiansToDegrees(value: number): number {
|
|
544
|
+
return value * (180 / Math.PI);
|
|
404
545
|
}
|