@osmix/core 0.2.0 → 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 +31 -0
- package/README.md +54 -7
- package/dist/contracts.d.ts +2 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/entities.d.ts +21 -1
- package/dist/entities.d.ts.map +1 -1
- package/dist/entities.js +52 -3
- package/dist/entities.js.map +1 -1
- package/dist/ids.d.ts +18 -5
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +68 -18
- package/dist/ids.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- 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/nodes.d.ts +28 -13
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +181 -51
- package/dist/nodes.js.map +1 -1
- package/dist/osm.d.ts +59 -3
- package/dist/osm.d.ts.map +1 -1
- package/dist/osm.js +31 -2
- package/dist/osm.js.map +1 -1
- package/dist/relations.d.ts +10 -4
- package/dist/relations.d.ts.map +1 -1
- package/dist/relations.js +66 -28
- 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 +5 -1
- package/dist/stringtable.js.map +1 -1
- package/dist/tags.d.ts +44 -11
- 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 +50 -2
- package/dist/ways.d.ts.map +1 -1
- package/dist/ways.js +212 -35
- package/dist/ways.js.map +1 -1
- package/package.json +11 -12
- package/src/contracts.ts +2 -1
- package/src/entities.ts +67 -4
- package/src/ids.ts +72 -22
- package/src/index.ts +13 -7
- package/src/indirect-kd-index.ts +212 -0
- package/src/limits.ts +73 -0
- package/src/nodes.ts +228 -54
- package/src/osm.ts +90 -4
- package/src/relations.ts +80 -30
- package/src/renumber.ts +80 -0
- package/src/stringtable.ts +15 -1
- package/src/tags.ts +173 -31
- package/src/typed-arrays.ts +225 -113
- package/src/ways.ts +233 -38
- package/test/entity-index-errors.test.ts +41 -0
- package/test/ids.bench.ts +11 -7
- package/test/ids.test.ts +38 -0
- package/test/limits.test.ts +83 -0
- 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 +9 -4
- package/test/relations.test.ts +52 -29
- 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 +13 -0
- package/test/typed-arrays.test.ts +178 -59
- package/src/types/geokdbush.d.ts +0 -32
- package/src/types/kdbush.d.ts +0 -47
package/src/ids.ts
CHANGED
|
@@ -30,10 +30,10 @@ const BLOCK_SIZE = 256;
|
|
|
30
30
|
export interface IdsTransferables<T extends BufferType = BufferType> {
|
|
31
31
|
/** Entity IDs in insertion order. */
|
|
32
32
|
ids: T;
|
|
33
|
-
/** Sorted IDs for binary search
|
|
34
|
-
sortedIds
|
|
35
|
-
/** Maps sorted position → original insertion index. */
|
|
36
|
-
sortedIdPositionToIndex
|
|
33
|
+
/** Sorted IDs for binary search. Omitted when `idsAreSorted` is true. */
|
|
34
|
+
sortedIds?: T;
|
|
35
|
+
/** Maps sorted position → original insertion index. Omitted for sorted IDs. */
|
|
36
|
+
sortedIdPositionToIndex?: T;
|
|
37
37
|
/** Anchor array: every BLOCK_SIZE-th sorted ID. */
|
|
38
38
|
anchors: T;
|
|
39
39
|
/** True if IDs were inserted in ascending order. */
|
|
@@ -67,8 +67,16 @@ export class Ids {
|
|
|
67
67
|
constructor(transferables?: IdsTransferables) {
|
|
68
68
|
if (transferables) {
|
|
69
69
|
this.ids = RTA.from(Float64Array, transferables.ids);
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
if (transferables.idsAreSorted) {
|
|
71
|
+
this.idsSorted = this.ids.array;
|
|
72
|
+
this.sortedIdPositionToIndex = new Uint32Array(new BufferConstructor(0));
|
|
73
|
+
} else {
|
|
74
|
+
if (!transferables.sortedIds || !transferables.sortedIdPositionToIndex) {
|
|
75
|
+
throw Error("Unsorted IDs require sorted lookup buffers.");
|
|
76
|
+
}
|
|
77
|
+
this.idsSorted = new Float64Array(transferables.sortedIds);
|
|
78
|
+
this.sortedIdPositionToIndex = new Uint32Array(transferables.sortedIdPositionToIndex);
|
|
79
|
+
}
|
|
72
80
|
this.anchors = new Float64Array(transferables.anchors);
|
|
73
81
|
this.idsAreSorted = transferables.idsAreSorted;
|
|
74
82
|
this.indexBuilt = true;
|
|
@@ -157,12 +165,8 @@ export class Ids {
|
|
|
157
165
|
} else {
|
|
158
166
|
// Point to the same array
|
|
159
167
|
this.idsSorted = this.ids.array;
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
this.sortedIdPositionToIndex = new Uint32Array(posBuffer);
|
|
163
|
-
for (let i = 0; i < this.size; i++) {
|
|
164
|
-
this.sortedIdPositionToIndex[i] = i;
|
|
165
|
-
}
|
|
168
|
+
// Sorted position is already the insertion index; no identity map is needed.
|
|
169
|
+
this.sortedIdPositionToIndex = new Uint32Array(new BufferConstructor(0));
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
// Build anchors (every blockSize-th key)
|
|
@@ -238,37 +242,83 @@ export class Ids {
|
|
|
238
242
|
/** @internal Iterate sorted IDs with their original storage positions. */
|
|
239
243
|
*sortedEntries(): Generator<readonly [id: number, index: number]> {
|
|
240
244
|
for (let i = 0; i < this.idsSorted.length; i++) {
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
yield this.sortedEntry(i);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Iterate in canonical OSM file order: negative IDs first by increasing
|
|
251
|
+
* absolute value, followed by non-negative IDs in ascending order.
|
|
252
|
+
*
|
|
253
|
+
* @internal
|
|
254
|
+
*/
|
|
255
|
+
*osmSortedEntries(): Generator<readonly [id: number, index: number]> {
|
|
256
|
+
let firstNonNegative = 0;
|
|
257
|
+
while (firstNonNegative < this.idsSorted.length && this.idsSorted[firstNonNegative]! < 0) {
|
|
258
|
+
firstNonNegative++;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Numeric sorting places negative IDs in the opposite of canonical OSM
|
|
262
|
+
// order. Reverse ID groups while preserving duplicate insertion order.
|
|
263
|
+
let groupEnd = firstNonNegative;
|
|
264
|
+
while (groupEnd > 0) {
|
|
265
|
+
const id = this.idsSorted[groupEnd - 1];
|
|
243
266
|
assertValue(id, "Sorted ID is undefined");
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
let groupStart = groupEnd - 1;
|
|
268
|
+
while (groupStart > 0 && this.idsSorted[groupStart - 1] === id) groupStart--;
|
|
269
|
+
for (let position = groupStart; position < groupEnd; position++) {
|
|
270
|
+
yield this.sortedEntry(position);
|
|
271
|
+
}
|
|
272
|
+
groupEnd = groupStart;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (let position = firstNonNegative; position < this.idsSorted.length; position++) {
|
|
276
|
+
yield this.sortedEntry(position);
|
|
246
277
|
}
|
|
247
278
|
}
|
|
248
279
|
|
|
280
|
+
private sortedEntry(position: number): readonly [id: number, index: number] {
|
|
281
|
+
const id = this.idsSorted[position];
|
|
282
|
+
assertValue(id, "Sorted ID is undefined");
|
|
283
|
+
if (this.idsAreSorted) return [id, position];
|
|
284
|
+
const index = this.sortedIdPositionToIndex[position];
|
|
285
|
+
assertValue(index, "Sorted position is undefined");
|
|
286
|
+
return [id, index];
|
|
287
|
+
}
|
|
288
|
+
|
|
249
289
|
/**
|
|
250
290
|
* Get transferable buffers for passing to another thread.
|
|
251
291
|
* @returns Serializable representation of this index.
|
|
252
292
|
*/
|
|
253
293
|
transferables(): IdsTransferables {
|
|
254
|
-
|
|
294
|
+
const base: IdsTransferables = {
|
|
255
295
|
ids: this.ids.array.buffer,
|
|
256
|
-
sortedIds: this.idsSorted.buffer,
|
|
257
|
-
sortedIdPositionToIndex: this.sortedIdPositionToIndex.buffer,
|
|
258
296
|
anchors: this.anchors.buffer,
|
|
259
297
|
idsAreSorted: this.idsAreSorted,
|
|
260
298
|
};
|
|
299
|
+
if (this.idsAreSorted) return base;
|
|
300
|
+
return {
|
|
301
|
+
...base,
|
|
302
|
+
sortedIds: this.idsSorted.buffer,
|
|
303
|
+
sortedIdPositionToIndex: this.sortedIdPositionToIndex.buffer,
|
|
304
|
+
};
|
|
261
305
|
}
|
|
262
306
|
|
|
263
307
|
/**
|
|
264
308
|
* Get the approximate memory requirements for a given number of IDs in bytes.
|
|
309
|
+
*
|
|
310
|
+
* `idsAreSorted` defaults to true because PBF entity collections arrive in
|
|
311
|
+
* ascending ID order and retain no separate sort buffers. Pass false when
|
|
312
|
+
* estimating a programmatically built (unsorted) collection, which also
|
|
313
|
+
* retains sorted IDs plus a position map.
|
|
265
314
|
*/
|
|
266
|
-
static getBytesRequired(count: number) {
|
|
315
|
+
static getBytesRequired(count: number, idsAreSorted = true) {
|
|
267
316
|
if (count === 0) return 0;
|
|
268
317
|
return (
|
|
269
318
|
count * Float64Array.BYTES_PER_ELEMENT + // ids
|
|
270
|
-
|
|
271
|
-
|
|
319
|
+
(idsAreSorted
|
|
320
|
+
? 0
|
|
321
|
+
: count * (Float64Array.BYTES_PER_ELEMENT + Uint32Array.BYTES_PER_ELEMENT)) + // sort index
|
|
272
322
|
Math.ceil(count / BLOCK_SIZE) * Float64Array.BYTES_PER_ELEMENT // anchors
|
|
273
323
|
);
|
|
274
324
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Efficiently stores and queries OpenStreetMap entities (nodes, ways, relations).
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
|
-
* - **Memory-efficient**: Uses typed arrays
|
|
8
|
-
* - **Spatial indexing**:
|
|
7
|
+
* - **Memory-efficient**: Uses typed arrays; coordinates are Int32 at 1e-7 degrees.
|
|
8
|
+
* - **Spatial indexing**: Compact indirect KD trees (points) and Flatbush (bboxes).
|
|
9
9
|
* - **Worker-ready**: Zero-copy transfer via `transferables()`.
|
|
10
10
|
* - **Tag indexing**: Fast reverse lookup by tag key.
|
|
11
11
|
*
|
|
@@ -22,19 +22,25 @@
|
|
|
22
22
|
* @module @osmix/core
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
// oxlint-disable-next-line typescript/triple-slash-reference -- ambient module declarations for untyped packages
|
|
26
|
-
/// <reference path="./types/kdbush.d.ts" />
|
|
27
|
-
// oxlint-disable-next-line typescript/triple-slash-reference -- ambient module declarations for untyped packages
|
|
28
|
-
/// <reference path="./types/geokdbush.d.ts" />
|
|
29
25
|
// oxlint-disable-next-line typescript/triple-slash-reference -- ambient module declarations for untyped packages
|
|
30
26
|
/// <reference path="./types/geoflatbush.d.ts" />
|
|
31
27
|
|
|
32
28
|
export type { IdOrIndex } from "./ids.ts";
|
|
33
29
|
export type { OsmReader, OsmWriter } from "./contracts.ts";
|
|
30
|
+
export { OsmEntityIndexBuildError, type OsmEntityIndexComponent } from "./entities.ts";
|
|
31
|
+
export * from "./limits.ts";
|
|
34
32
|
export * from "./nodes.ts";
|
|
35
33
|
export * from "./osm.ts";
|
|
36
34
|
export * from "./relations.ts";
|
|
35
|
+
export { negativeIdMap, renumberNegativeIds, type OsmIdMap } from "./renumber.ts";
|
|
37
36
|
export * from "./stringtable.ts";
|
|
38
37
|
export * from "./tags.ts";
|
|
39
|
-
export {
|
|
38
|
+
export {
|
|
39
|
+
BufferConstructor,
|
|
40
|
+
ResizeableTypedArray,
|
|
41
|
+
TypedBufferAllocationError,
|
|
42
|
+
type BufferType,
|
|
43
|
+
type TypedBufferAllocationOperation,
|
|
44
|
+
type TypedBufferType,
|
|
45
|
+
} from "./typed-arrays.ts";
|
|
40
46
|
export * from "./ways.ts";
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A compact static KD index over coordinate columns owned by another object.
|
|
3
|
+
*
|
|
4
|
+
* The selection and traversal algorithms are adapted from KDBush 4.1.0,
|
|
5
|
+
* Copyright (c) 2018 Vladimir Agafonkin, distributed under the ISC License.
|
|
6
|
+
* Unlike KDBush, this index stores only a Uint32 permutation. Coordinates are
|
|
7
|
+
* dereferenced from the existing Int32 microdegree columns.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { BufferType } from "./typed-arrays.ts";
|
|
11
|
+
import { BufferConstructor } from "./typed-arrays.ts";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_NODE_SIZE = 64;
|
|
14
|
+
|
|
15
|
+
export class IndirectKdIndex {
|
|
16
|
+
readonly indexes: Uint32Array;
|
|
17
|
+
private readonly lons: Int32Array;
|
|
18
|
+
private readonly lats: Int32Array;
|
|
19
|
+
private readonly nodeSize: number;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
lons: Int32Array,
|
|
23
|
+
lats: Int32Array,
|
|
24
|
+
indexes: Uint32Array,
|
|
25
|
+
nodeSize = DEFAULT_NODE_SIZE,
|
|
26
|
+
) {
|
|
27
|
+
this.lons = lons;
|
|
28
|
+
this.lats = lats;
|
|
29
|
+
this.indexes = indexes;
|
|
30
|
+
this.nodeSize = nodeSize;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static build(
|
|
34
|
+
lons: Int32Array,
|
|
35
|
+
lats: Int32Array,
|
|
36
|
+
count: number,
|
|
37
|
+
fill: (indexes: Uint32Array) => void,
|
|
38
|
+
): IndirectKdIndex {
|
|
39
|
+
const buffer = new BufferConstructor(count * Uint32Array.BYTES_PER_ELEMENT);
|
|
40
|
+
const indexes = new Uint32Array(buffer);
|
|
41
|
+
fill(indexes);
|
|
42
|
+
sortKd(indexes, lons, lats, DEFAULT_NODE_SIZE, 0, indexes.length - 1, 0);
|
|
43
|
+
return new IndirectKdIndex(lons, lats, indexes);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static from(lons: Int32Array, lats: Int32Array, buffer: BufferType): IndirectKdIndex {
|
|
47
|
+
if (buffer.byteLength % Uint32Array.BYTES_PER_ELEMENT !== 0) {
|
|
48
|
+
throw new Error("Indirect KD index buffer length must be divisible by four");
|
|
49
|
+
}
|
|
50
|
+
return new IndirectKdIndex(lons, lats, new Uint32Array(buffer));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get buffer(): BufferType {
|
|
54
|
+
return this.indexes.buffer as BufferType;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find entity indexes within an inclusive bounding box.
|
|
59
|
+
*
|
|
60
|
+
* Bounds are **integer microdegrees** (the units of the underlying Int32
|
|
61
|
+
* coordinate columns), not degrees. `Nodes.findIndexesWithinBbox` performs
|
|
62
|
+
* the degree → microdegree conversion.
|
|
63
|
+
*/
|
|
64
|
+
range(minLon: number, minLat: number, maxLon: number, maxLat: number): number[] {
|
|
65
|
+
const stack = [0, this.indexes.length - 1, 0];
|
|
66
|
+
const result: number[] = [];
|
|
67
|
+
|
|
68
|
+
while (stack.length > 0) {
|
|
69
|
+
const axis = stack.pop();
|
|
70
|
+
const right = stack.pop();
|
|
71
|
+
const left = stack.pop();
|
|
72
|
+
if (axis === undefined || right === undefined || left === undefined) {
|
|
73
|
+
throw new Error("Indirect KD index traversal stack is corrupt");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (right - left <= this.nodeSize) {
|
|
77
|
+
for (let i = left; i <= right; i++) {
|
|
78
|
+
const entityIndex = entityAt(this.indexes, i);
|
|
79
|
+
const lon = coordinate(this.lons, this.lats, entityIndex, 0);
|
|
80
|
+
const lat = coordinate(this.lons, this.lats, entityIndex, 1);
|
|
81
|
+
if (lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat) {
|
|
82
|
+
result.push(entityIndex);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const middle = (left + right) >> 1;
|
|
89
|
+
const entityIndex = entityAt(this.indexes, middle);
|
|
90
|
+
const lon = coordinate(this.lons, this.lats, entityIndex, 0);
|
|
91
|
+
const lat = coordinate(this.lons, this.lats, entityIndex, 1);
|
|
92
|
+
|
|
93
|
+
if (lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat) {
|
|
94
|
+
result.push(entityIndex);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const splitCoordinate = axis === 0 ? lon : lat;
|
|
98
|
+
const minimum = axis === 0 ? minLon : minLat;
|
|
99
|
+
const maximum = axis === 0 ? maxLon : maxLat;
|
|
100
|
+
if (minimum <= splitCoordinate) stack.push(left, middle - 1, 1 - axis);
|
|
101
|
+
if (maximum >= splitCoordinate) stack.push(middle + 1, right, 1 - axis);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sortKd(
|
|
109
|
+
indexes: Uint32Array,
|
|
110
|
+
lons: Int32Array,
|
|
111
|
+
lats: Int32Array,
|
|
112
|
+
nodeSize: number,
|
|
113
|
+
left: number,
|
|
114
|
+
right: number,
|
|
115
|
+
axis: number,
|
|
116
|
+
) {
|
|
117
|
+
if (right - left <= nodeSize) return;
|
|
118
|
+
const middle = (left + right) >> 1;
|
|
119
|
+
select(indexes, lons, lats, middle, left, right, axis);
|
|
120
|
+
sortKd(indexes, lons, lats, nodeSize, left, middle - 1, 1 - axis);
|
|
121
|
+
sortKd(indexes, lons, lats, nodeSize, middle + 1, right, 1 - axis);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Floyd-Rivest selection adapted to dereference coordinates indirectly. */
|
|
125
|
+
function select(
|
|
126
|
+
indexes: Uint32Array,
|
|
127
|
+
lons: Int32Array,
|
|
128
|
+
lats: Int32Array,
|
|
129
|
+
k: number,
|
|
130
|
+
initialLeft: number,
|
|
131
|
+
initialRight: number,
|
|
132
|
+
axis: number,
|
|
133
|
+
) {
|
|
134
|
+
let left = initialLeft;
|
|
135
|
+
let right = initialRight;
|
|
136
|
+
|
|
137
|
+
while (right > left) {
|
|
138
|
+
if (right - left > 600) {
|
|
139
|
+
const n = right - left + 1;
|
|
140
|
+
const m = k - left + 1;
|
|
141
|
+
const z = Math.log(n);
|
|
142
|
+
const s = 0.5 * Math.exp((2 * z) / 3);
|
|
143
|
+
const sd = 0.5 * Math.sqrt((z * s * (n - s)) / n) * (m - n / 2 < 0 ? -1 : 1);
|
|
144
|
+
const newLeft = Math.max(left, Math.floor(k - (m * s) / n + sd));
|
|
145
|
+
const newRight = Math.min(right, Math.floor(k + ((n - m) * s) / n + sd));
|
|
146
|
+
select(indexes, lons, lats, k, newLeft, newRight, axis);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const pivotIndex = entityAt(indexes, k);
|
|
150
|
+
const pivot = coordinate(lons, lats, pivotIndex, axis);
|
|
151
|
+
let i = left;
|
|
152
|
+
let j = right;
|
|
153
|
+
|
|
154
|
+
swap(indexes, left, k);
|
|
155
|
+
if (compareCoordinate(lons, lats, entityAt(indexes, right), pivotIndex, axis) > 0) {
|
|
156
|
+
swap(indexes, left, right);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
while (i < j) {
|
|
160
|
+
swap(indexes, i, j);
|
|
161
|
+
i++;
|
|
162
|
+
j--;
|
|
163
|
+
while (i <= right && coordinate(lons, lats, entityAt(indexes, i), axis) < pivot) i++;
|
|
164
|
+
while (j >= left && coordinate(lons, lats, entityAt(indexes, j), axis) > pivot) j--;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (coordinate(lons, lats, entityAt(indexes, left), axis) === pivot) {
|
|
168
|
+
swap(indexes, left, j);
|
|
169
|
+
} else {
|
|
170
|
+
j++;
|
|
171
|
+
swap(indexes, j, right);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (j <= k) left = j + 1;
|
|
175
|
+
if (k <= j) right = j - 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read the permutation entry at `position`, throwing on an out-of-bounds read.
|
|
181
|
+
* A silent skip here would leave the permutation subtly unsorted and produce
|
|
182
|
+
* wrong query results, so fail loudly instead.
|
|
183
|
+
*/
|
|
184
|
+
function entityAt(indexes: Uint32Array, position: number): number {
|
|
185
|
+
const value = indexes[position];
|
|
186
|
+
if (value === undefined) {
|
|
187
|
+
throw new Error(`Indirect KD index read out of bounds at position ${position}`);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function coordinate(lons: Int32Array, lats: Int32Array, entityIndex: number, axis: number): number {
|
|
193
|
+
const value = axis === 0 ? lons[entityIndex] : lats[entityIndex];
|
|
194
|
+
if (value === undefined) throw new Error(`Missing coordinate for node index ${entityIndex}`);
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function compareCoordinate(
|
|
199
|
+
lons: Int32Array,
|
|
200
|
+
lats: Int32Array,
|
|
201
|
+
a: number,
|
|
202
|
+
b: number,
|
|
203
|
+
axis: number,
|
|
204
|
+
): number {
|
|
205
|
+
return coordinate(lons, lats, a, axis) - coordinate(lons, lats, b, axis);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function swap(indexes: Uint32Array, a: number, b: number) {
|
|
209
|
+
const valueA = entityAt(indexes, a);
|
|
210
|
+
indexes[a] = entityAt(indexes, b);
|
|
211
|
+
indexes[b] = valueA;
|
|
212
|
+
}
|
package/src/limits.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard capacity limits set by the typed-array layout of `Osm` storage.
|
|
3
|
+
*
|
|
4
|
+
* See `docs/limits.md` for the full list, including limits that come from the
|
|
5
|
+
* JavaScript engine rather than from this layout.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Largest number of node refs one way can hold (`Ways.refCount` is a `Uint16Array`). */
|
|
9
|
+
export const MAX_WAY_REFS = 0xffff;
|
|
10
|
+
|
|
11
|
+
/** Largest number of members one relation can hold (`Relations.memberCount` is a `Uint16Array`). */
|
|
12
|
+
export const MAX_RELATION_MEMBERS = 0xffff;
|
|
13
|
+
|
|
14
|
+
/** Largest UTF-8 byte length of one string (`StringTable.count` is a `Uint16Array`). */
|
|
15
|
+
export const MAX_STRING_BYTES = 0xffff;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Largest element offset a `Uint32Array` offset column can store. Bounds the total
|
|
19
|
+
* way refs, relation members and string-table bytes in one dataset.
|
|
20
|
+
*/
|
|
21
|
+
export const MAX_UINT32_OFFSET = 0xffffffff;
|
|
22
|
+
|
|
23
|
+
/** The storage limit an {@link OsmCapacityError} reports. */
|
|
24
|
+
export type OsmCapacityLimit =
|
|
25
|
+
| "way-refs"
|
|
26
|
+
| "relation-members"
|
|
27
|
+
| "string-bytes"
|
|
28
|
+
| "total-way-refs"
|
|
29
|
+
| "total-relation-members"
|
|
30
|
+
| "total-string-bytes";
|
|
31
|
+
|
|
32
|
+
/** Thrown when an entity or dataset is larger than the storage layout can hold. */
|
|
33
|
+
export class OsmCapacityError extends Error {
|
|
34
|
+
readonly code = "OSM_CAPACITY_EXCEEDED";
|
|
35
|
+
readonly limit: OsmCapacityLimit;
|
|
36
|
+
readonly value: number;
|
|
37
|
+
readonly max: number;
|
|
38
|
+
|
|
39
|
+
constructor(limit: OsmCapacityLimit, value: number, max: number, subject: string) {
|
|
40
|
+
super(
|
|
41
|
+
`${subject} has ${value.toLocaleString("en-US")} ${capacityUnit(limit)}; ` +
|
|
42
|
+
`Osmix stores at most ${max.toLocaleString("en-US")}.`,
|
|
43
|
+
);
|
|
44
|
+
this.name = "OsmCapacityError";
|
|
45
|
+
this.limit = limit;
|
|
46
|
+
this.value = value;
|
|
47
|
+
this.max = max;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function capacityUnit(limit: OsmCapacityLimit): string {
|
|
52
|
+
switch (limit) {
|
|
53
|
+
case "way-refs":
|
|
54
|
+
case "total-way-refs":
|
|
55
|
+
return "node refs";
|
|
56
|
+
case "relation-members":
|
|
57
|
+
case "total-relation-members":
|
|
58
|
+
return "members";
|
|
59
|
+
case "string-bytes":
|
|
60
|
+
case "total-string-bytes":
|
|
61
|
+
return "UTF-8 bytes";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Throw {@link OsmCapacityError} when `value` is above `max`. */
|
|
66
|
+
export function assertCapacity(
|
|
67
|
+
limit: OsmCapacityLimit,
|
|
68
|
+
value: number,
|
|
69
|
+
max: number,
|
|
70
|
+
subject: () => string,
|
|
71
|
+
): void {
|
|
72
|
+
if (value > max) throw new OsmCapacityError(limit, value, max, subject());
|
|
73
|
+
}
|