@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/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# @osmix/core
|
|
2
|
+
|
|
3
|
+
@osmix/core wraps the `Osmix` index: a typed-array OpenStreetMap engine that reads `.osm.pbf` streams, builds spatial indexes, and emits JSON, PBF, or raster tiles without leaving modern JavaScript runtimes.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- Ingest `.osm.pbf` sources into node, way, and relation stores backed by transferable typed arrays using readers from [`@osmix/pbf`](../pbf/README.md).
|
|
8
|
+
- Run fast bounding-box searches with KDBush/Flatbush and convert matches straight to GeoJSON.
|
|
9
|
+
- Trim extracts, write new PBF buffers, or stream entities to downstream tooling with [`@osmix/json`](../json/README.md) or [`@osmix/pbf`](../pbf/README.md).
|
|
10
|
+
- Pair with [`@osmix/change`](../change/README.md) when you need deduplication, intersection, or merge pipelines.
|
|
11
|
+
- Ship fully indexed datasets across workers via `transferables()` + `Osmix.from`.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @osmix/core
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Load a PBF in one call
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Osmix } from "@osmix/core"
|
|
25
|
+
import { readFile } from "node:fs/promises"
|
|
26
|
+
|
|
27
|
+
const osm = await Osmix.fromPbf(readFile("example.osm.pbf"))
|
|
28
|
+
|
|
29
|
+
console.log(osm.nodes.size, osm.ways.size, osm.relations.size)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`fromPbf` accepts `ArrayBufferLike`, async (iterable) chunks, or Web `ReadableStream` inputs.
|
|
33
|
+
|
|
34
|
+
### Stream with progress and extraction
|
|
35
|
+
|
|
36
|
+
Pass a logger or set one later to receive throttled progress messages. Provide `extractBbox` to keep only features intersecting the desired bounds while parsing.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { Osmix } from "@osmix/core"
|
|
40
|
+
import { createReadStream } from "node:fs"
|
|
41
|
+
|
|
42
|
+
const osm = new Osmix({ id: "planet-seattle" })
|
|
43
|
+
osm.setLogger((message) => console.log(message))
|
|
44
|
+
|
|
45
|
+
await osm.readPbf(createReadStream("planet.osm.pbf"), {
|
|
46
|
+
extractBbox: [-122.5, 47.45, -122.2, 47.75],
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Build synthetic fixtures
|
|
51
|
+
|
|
52
|
+
When generating test data, insert entities directly and call `buildIndexes()` before querying.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const osm = new Osmix({ id: "fixture" })
|
|
56
|
+
osm.nodes.addNode({ id: 1, lon: -0.1, lat: 51.5, tags: { amenity: "cafe" } })
|
|
57
|
+
osm.ways.addWay({ id: 10, refs: [1], tags: { highway: "service" } })
|
|
58
|
+
osm.buildIndexes()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Query the index
|
|
62
|
+
|
|
63
|
+
- `osm.get("node", 123)` or `osm.getById("n123")` fetches specific entities.
|
|
64
|
+
- `osm.nodes.sorted()` / `osm.ways.sorted()` iterate ids in ascending order.
|
|
65
|
+
- `osm.getEntityGeoJson(entity)` returns a GeoJSON feature with coordinates resolved from the
|
|
66
|
+
indexed nodes.
|
|
67
|
+
|
|
68
|
+
Spatial lookups emit compact typed arrays that can be transferred across Web Workers and used directly in `Deck.gl`:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const bbox = [-122.34, 47.57, -122.30, 47.61]
|
|
72
|
+
const { ids, positions } = osm.getNodesInBbox(bbox)
|
|
73
|
+
|
|
74
|
+
const { ids: wayIds, positions: wayCoords, startIndices } = osm.getWaysInBbox(bbox)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`positions` stores `[lon, lat]` pairs; `startIndices` splits `wayCoords` into per-way sequences.
|
|
78
|
+
|
|
79
|
+
## Extract and export data
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const downtown = osm.extract([-122.35, 47.60, -122.32, 47.62])
|
|
83
|
+
|
|
84
|
+
const entityStream = downtown.toEntityStream() // header + entities
|
|
85
|
+
const pbfStream = downtown.toPbfStream() // Uint8Array chunks
|
|
86
|
+
const pbfBuffer = await downtown.toPbfBuffer()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`extract` returns a new, fully indexed `Osmix` instance with the header bbox updated.
|
|
90
|
+
|
|
91
|
+
## Changesets and merging
|
|
92
|
+
|
|
93
|
+
Change orchestration now lives in the dedicated [`@osmix/change`](../change/README.md) package. Install it alongside
|
|
94
|
+
`@osmix/core` when you want to deduplicate entities, generate direct merges, or script intersection creation:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { Osmix } from "@osmix/core"
|
|
98
|
+
import { merge, OsmixChangeset } from "@osmix/change"
|
|
99
|
+
import { readFile } from "node:fs/promises"
|
|
100
|
+
|
|
101
|
+
const base = await Osmix.fromPbf(readFile("base.osm.pbf"))
|
|
102
|
+
const patch = await Osmix.fromPbf(readFile("patch.osm.pbf"))
|
|
103
|
+
|
|
104
|
+
const changeset = new OsmixChangeset(base)
|
|
105
|
+
changeset.deduplicateWays(base.ways)
|
|
106
|
+
|
|
107
|
+
const merged = await merge(base, patch, { directMerge: true })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Refer to the [`@osmix/change` README](../change/README.md) for the full API surface.
|
|
111
|
+
|
|
112
|
+
## Transfer between threads
|
|
113
|
+
|
|
114
|
+
Typed arrays keep the index transferable.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// main thread
|
|
118
|
+
const payload = osm.transferables()
|
|
119
|
+
const buffers = []
|
|
120
|
+
const collect = (value: unknown) => {
|
|
121
|
+
if (value instanceof ArrayBuffer) buffers.push(value)
|
|
122
|
+
else if (ArrayBuffer.isView(value)) buffers.push(value.buffer)
|
|
123
|
+
else if (Array.isArray(value)) value.forEach(collect)
|
|
124
|
+
else if (value && typeof value === "object")
|
|
125
|
+
Object.values(value).forEach(collect)
|
|
126
|
+
return buffers
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
worker.postMessage(payload, { transfer: collect(payload) })
|
|
130
|
+
|
|
131
|
+
// worker
|
|
132
|
+
import { Osmix } from "@osmix/core"
|
|
133
|
+
|
|
134
|
+
self.addEventListener("message", ({ data }) => {
|
|
135
|
+
const osm = Osmix.from(data)
|
|
136
|
+
// ready for queries immediately
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## API overview
|
|
141
|
+
|
|
142
|
+
- `Osmix` – ingest PBF sources, build indexes, query entities, extract subsets, and emit JSON/PBF.
|
|
143
|
+
- `Nodes` / `Ways` / `Relations` – typed-array backed stores exposed for advanced workflows.
|
|
144
|
+
- `OsmixRasterTile` – rasterize indexed data into tile-sized buffers with lon/lat helpers.
|
|
145
|
+
- Utilities – `throttle`, plus the shared type definitions used across the package.
|
|
146
|
+
- Changesets and merge helpers now live in [`@osmix/change`](../change/README.md).
|
|
147
|
+
|
|
148
|
+
## Environment and limitations
|
|
149
|
+
|
|
150
|
+
- Requires runtimes with Web Streams, `TextEncoder`/`TextDecoder`, and zlib-compatible `CompressionStream`/`DecompressionStream` support (Bun, Node 20+, modern browsers).
|
|
151
|
+
- `readPbf` expects dense-node blocks; PBFs that omit dense encodings are currently unsupported.
|
|
152
|
+
- Filtering during ingest depends on node membership; emit nodes, then ways, then relations when supplying custom entity generators.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
- `bun run test packages/core`
|
|
157
|
+
- `bun run lint packages/core`
|
|
158
|
+
- `bun run typecheck packages/core`
|
|
159
|
+
|
|
160
|
+
Run `bun run check` at the repo root before publishing to ensure formatting, lint, and type coverage.
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
3
|
+
"name": "@osmix/core",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"description": "An OSM PBF reader, writer, and merge tool written in TypeScript",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/conveyal/osmix.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/conveyal/osmix#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/conveyal/osmix/issues"
|
|
32
|
+
},
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"bench": "vitest bench",
|
|
37
|
+
"test": "vitest",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/geojson": "catalog:",
|
|
42
|
+
"typescript": "catalog:",
|
|
43
|
+
"vitest": "catalog:"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@osmix/json": "workspace:*",
|
|
47
|
+
"@osmix/pbf": "workspace:*",
|
|
48
|
+
"@osmix/shared": "workspace:*",
|
|
49
|
+
"flatbush": "^4.4.1",
|
|
50
|
+
"kdbush": "mourner/kdbush#16416cd",
|
|
51
|
+
"sweepline-intersections": "^1.5.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/entities.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { OsmEntity, OsmEntityType, OsmTags } from "@osmix/json"
|
|
2
|
+
import { type IdOrIndex, Ids, type IdsTransferables } from "./ids"
|
|
3
|
+
import type StringTable from "./stringtable"
|
|
4
|
+
import { Tags, type TagsTransferables } from "./tags"
|
|
5
|
+
|
|
6
|
+
export interface EntitiesTransferables
|
|
7
|
+
extends IdsTransferables,
|
|
8
|
+
TagsTransferables {}
|
|
9
|
+
|
|
10
|
+
export abstract class Entities<T extends OsmEntity> {
|
|
11
|
+
indexType: OsmEntityType
|
|
12
|
+
stringTable: StringTable
|
|
13
|
+
ids: Ids
|
|
14
|
+
tags: Tags
|
|
15
|
+
|
|
16
|
+
private indexBuilt = false
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
indexType: OsmEntityType,
|
|
20
|
+
stringTable: StringTable,
|
|
21
|
+
ids?: Ids,
|
|
22
|
+
tags?: Tags,
|
|
23
|
+
) {
|
|
24
|
+
this.stringTable = stringTable
|
|
25
|
+
this.indexType = indexType
|
|
26
|
+
this.ids = ids ?? new Ids()
|
|
27
|
+
this.tags = tags ?? new Tags(stringTable)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get isReady() {
|
|
31
|
+
return this.ids.isReady && this.tags.isReady && this.indexBuilt
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get size() {
|
|
35
|
+
return this.ids.size
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
abstract buildEntityIndex(): void
|
|
39
|
+
|
|
40
|
+
buildIndex() {
|
|
41
|
+
console.time(`${this.indexType}Index.finish`)
|
|
42
|
+
this.ids.buildIndex()
|
|
43
|
+
this.tags.buildIndex()
|
|
44
|
+
this.buildEntityIndex()
|
|
45
|
+
this.indexBuilt = true
|
|
46
|
+
console.timeEnd(`${this.indexType}Index.finish`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
abstract getFullEntity(index: number, id: number, tags?: OsmTags): T
|
|
50
|
+
|
|
51
|
+
get(idOrIndex: IdOrIndex): T | null {
|
|
52
|
+
const [index, id] = this.ids.idOrIndex(idOrIndex)
|
|
53
|
+
if (index === -1) return null
|
|
54
|
+
return this.getFullEntity(index, id, this.tags.getTags(index))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getByIndex(index: number): T {
|
|
58
|
+
return this.getFullEntity(
|
|
59
|
+
index,
|
|
60
|
+
this.ids.at(index),
|
|
61
|
+
this.tags.getTags(index),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getEntitiesByIndex(indexes: number[]): T[] {
|
|
66
|
+
const entities: T[] = []
|
|
67
|
+
for (const index of indexes) {
|
|
68
|
+
const entity = this.getByIndex(index)
|
|
69
|
+
if (entity) entities.push(entity)
|
|
70
|
+
else throw Error(`Entity not found at index ${index}`)
|
|
71
|
+
}
|
|
72
|
+
return entities
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
*[Symbol.iterator](): Generator<T> {
|
|
76
|
+
for (let i = 0; i < this.size; i++) {
|
|
77
|
+
yield this.getFullEntity(i, this.ids.at(i), this.tags.getTags(i))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getById(id: number): T | null {
|
|
82
|
+
const index = this.ids.getIndexFromId(id)
|
|
83
|
+
if (index !== -1)
|
|
84
|
+
return this.getFullEntity(index, id, this.tags.getTags(index))
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
*sorted(): Generator<T> {
|
|
89
|
+
for (const id of this.ids.sorted) {
|
|
90
|
+
const index = this.ids.getIndexFromId(id)
|
|
91
|
+
if (index === -1) throw Error(`Entity not found at id ${id}`)
|
|
92
|
+
yield this.getFullEntity(index, id, this.tags.getTags(index))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/ids.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { assertValue } from "@osmix/shared/assert"
|
|
2
|
+
import {
|
|
3
|
+
BufferConstructor,
|
|
4
|
+
type BufferType,
|
|
5
|
+
ResizeableTypedArray,
|
|
6
|
+
} from "./typed-arrays"
|
|
7
|
+
|
|
8
|
+
export type IdOrIndex = { id: number } | { index: number }
|
|
9
|
+
|
|
10
|
+
const BLOCK_SIZE = 256
|
|
11
|
+
|
|
12
|
+
export interface IdsTransferables {
|
|
13
|
+
ids: BufferType
|
|
14
|
+
sortedIds: BufferType
|
|
15
|
+
sortedIdPositionToIndex: BufferType
|
|
16
|
+
anchors: BufferType
|
|
17
|
+
idsAreSorted: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Efficiently store and lookup IDs.
|
|
22
|
+
*
|
|
23
|
+
* This is used to store IDs in a sorted array and then use binary search to find the index of an ID.
|
|
24
|
+
* Maps max out at 2^32 IDs.
|
|
25
|
+
*/
|
|
26
|
+
export class Ids {
|
|
27
|
+
private ids: ResizeableTypedArray<Float64Array>
|
|
28
|
+
private indexBuilt = false
|
|
29
|
+
private idsAreSorted = true
|
|
30
|
+
private idsSorted: Float64Array
|
|
31
|
+
private sortedIdPositionToIndex: Uint32Array
|
|
32
|
+
private anchors: Float64Array
|
|
33
|
+
|
|
34
|
+
static from({
|
|
35
|
+
ids,
|
|
36
|
+
sortedIds,
|
|
37
|
+
sortedIdPositionToIndex,
|
|
38
|
+
anchors,
|
|
39
|
+
idsAreSorted,
|
|
40
|
+
}: IdsTransferables) {
|
|
41
|
+
const idIndex = new Ids()
|
|
42
|
+
idIndex.ids = ResizeableTypedArray.from(Float64Array, ids)
|
|
43
|
+
idIndex.idsSorted = new Float64Array(sortedIds)
|
|
44
|
+
idIndex.sortedIdPositionToIndex = new Uint32Array(sortedIdPositionToIndex)
|
|
45
|
+
idIndex.anchors = new Float64Array(anchors)
|
|
46
|
+
idIndex.indexBuilt = true
|
|
47
|
+
idIndex.idsAreSorted = idsAreSorted
|
|
48
|
+
return idIndex
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
this.ids = new ResizeableTypedArray(Float64Array)
|
|
53
|
+
this.idsSorted = new Float64Array(new BufferConstructor(0))
|
|
54
|
+
this.sortedIdPositionToIndex = new Uint32Array(new BufferConstructor(0))
|
|
55
|
+
this.anchors = new Float64Array(new BufferConstructor(0))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
transferables(): IdsTransferables {
|
|
59
|
+
return {
|
|
60
|
+
ids: this.ids.array.buffer,
|
|
61
|
+
sortedIds: this.idsSorted.buffer,
|
|
62
|
+
sortedIdPositionToIndex: this.sortedIdPositionToIndex.buffer,
|
|
63
|
+
anchors: this.anchors.buffer,
|
|
64
|
+
idsAreSorted: this.idsAreSorted,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get size() {
|
|
69
|
+
return this.ids.length
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get isReady() {
|
|
73
|
+
return this.indexBuilt
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
isSorted() {
|
|
77
|
+
return this.idsAreSorted
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
add(id: number): void {
|
|
81
|
+
if (this.indexBuilt) throw Error("ID index already built.")
|
|
82
|
+
if (this.ids.length > 0 && id < this.ids.at(-1)) this.idsAreSorted = false
|
|
83
|
+
this.ids.push(id)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
at(index: number): number {
|
|
87
|
+
return this.ids.at(index)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
has(id: number): boolean {
|
|
91
|
+
return this.getIndexFromId(id) !== -1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build the index of IDs to positions.
|
|
96
|
+
*
|
|
97
|
+
* If the IDs are not sorted, we need to sort them and build a new index.
|
|
98
|
+
* If the IDs are sorted, we can use the existing index.
|
|
99
|
+
*/
|
|
100
|
+
buildIndex() {
|
|
101
|
+
if (this.indexBuilt) throw Error("ID index already build.")
|
|
102
|
+
this.ids.compact()
|
|
103
|
+
if (!this.idsAreSorted) {
|
|
104
|
+
console.warn("IDs were not sorted. Sorting now...")
|
|
105
|
+
// Build the sorted index
|
|
106
|
+
const idsBuffer = new BufferConstructor(
|
|
107
|
+
this.size * Float64Array.BYTES_PER_ELEMENT,
|
|
108
|
+
)
|
|
109
|
+
const posBuffer = new BufferConstructor(
|
|
110
|
+
this.size * Uint32Array.BYTES_PER_ELEMENT,
|
|
111
|
+
)
|
|
112
|
+
this.idsSorted = new Float64Array(idsBuffer)
|
|
113
|
+
this.sortedIdPositionToIndex = new Uint32Array(posBuffer)
|
|
114
|
+
|
|
115
|
+
// Fill and sort with positions.
|
|
116
|
+
for (let i = 0; i < this.size; i++) {
|
|
117
|
+
this.idsSorted[i] = this.ids.at(i)
|
|
118
|
+
this.sortedIdPositionToIndex[i] = i
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Sort by id, carrying position; use native sort on chunks or a custom radix/merge for stability.
|
|
122
|
+
// For simplicity:
|
|
123
|
+
const tmp: { id: number; pos: number }[] = Array.from(
|
|
124
|
+
{ length: this.size },
|
|
125
|
+
(_, i) => ({
|
|
126
|
+
id: this.idsSorted[i] as number,
|
|
127
|
+
pos: this.sortedIdPositionToIndex[i] as number,
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
tmp.sort((a, b) => a.id - b.id)
|
|
131
|
+
tmp.forEach(({ id, pos }, i) => {
|
|
132
|
+
this.idsSorted[i] = id
|
|
133
|
+
this.sortedIdPositionToIndex[i] = pos
|
|
134
|
+
})
|
|
135
|
+
} else {
|
|
136
|
+
// Point to the same array
|
|
137
|
+
this.idsSorted = this.ids.array
|
|
138
|
+
// Create the sortedIdPositionToIndex array
|
|
139
|
+
const posBuffer = new BufferConstructor(
|
|
140
|
+
this.size * Uint32Array.BYTES_PER_ELEMENT,
|
|
141
|
+
)
|
|
142
|
+
this.sortedIdPositionToIndex = new Uint32Array(posBuffer)
|
|
143
|
+
for (let i = 0; i < this.size; i++) {
|
|
144
|
+
this.sortedIdPositionToIndex[i] = i
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Build anchors (every blockSize-th key)
|
|
149
|
+
const aLen = Math.ceil(this.size / BLOCK_SIZE)
|
|
150
|
+
const sab = new BufferConstructor(aLen * Float64Array.BYTES_PER_ELEMENT)
|
|
151
|
+
this.anchors = new Float64Array(sab, 0, aLen)
|
|
152
|
+
for (let j = 0; j < aLen; j++) {
|
|
153
|
+
const id = this.idsSorted[Math.min(j * BLOCK_SIZE, this.size - 1)]
|
|
154
|
+
assertValue(id, "ID is undefined")
|
|
155
|
+
this.anchors[j] = id
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.indexBuilt = true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Lookup id → index
|
|
162
|
+
getIndexFromId(id: number): number {
|
|
163
|
+
if (!this.indexBuilt) throw Error("IdIndex not built.")
|
|
164
|
+
|
|
165
|
+
// binary search anchors
|
|
166
|
+
let lo = 0
|
|
167
|
+
let hi = this.anchors.length - 1
|
|
168
|
+
while (lo < hi) {
|
|
169
|
+
const mid = (lo + hi + 1) >>> 1
|
|
170
|
+
const anchor = this.anchors[mid]
|
|
171
|
+
assertValue(anchor, "Anchor is undefined")
|
|
172
|
+
if (anchor <= id) lo = mid
|
|
173
|
+
else hi = mid - 1
|
|
174
|
+
}
|
|
175
|
+
const start = lo * BLOCK_SIZE
|
|
176
|
+
const end = Math.min(start + BLOCK_SIZE, this.idsSorted.length)
|
|
177
|
+
|
|
178
|
+
// binary search within block
|
|
179
|
+
let l = start
|
|
180
|
+
let r = end - 1
|
|
181
|
+
while (l <= r) {
|
|
182
|
+
const m = (l + r) >>> 1
|
|
183
|
+
const v = this.idsSorted[m]
|
|
184
|
+
assertValue(v, "Value is undefined")
|
|
185
|
+
if (v === id) {
|
|
186
|
+
if (this.idsAreSorted) return m
|
|
187
|
+
const index = this.sortedIdPositionToIndex[m]
|
|
188
|
+
assertValue(index, "Position is undefined")
|
|
189
|
+
return index
|
|
190
|
+
}
|
|
191
|
+
if (v < id) l = m + 1
|
|
192
|
+
else r = m - 1
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ID not found
|
|
196
|
+
return -1
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Pass an ID or an index, get both.
|
|
201
|
+
*/
|
|
202
|
+
idOrIndex(i: IdOrIndex): [index: number, id: number] {
|
|
203
|
+
if ("id" in i) return [this.getIndexFromId(i.id), i.id]
|
|
204
|
+
return [i.index, this.at(i.index)]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
get sorted() {
|
|
208
|
+
return this.idsSorted
|
|
209
|
+
}
|
|
210
|
+
}
|