@osmix/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +160 -0
- package/package.json +53 -0
- package/src/entities.ts +95 -0
- package/src/ids.ts +210 -0
- package/src/index.ts +7 -0
- package/src/nodes.ts +214 -0
- package/src/osmix.ts +556 -0
- package/src/relations.ts +221 -0
- package/src/stringtable.ts +120 -0
- package/src/tags.ts +122 -0
- package/src/typed-arrays.ts +164 -0
- package/src/types/kdbush.d.ts +29 -0
- package/src/utils.ts +29 -0
- package/src/ways.ts +241 -0
- package/test/extract.bench.ts +29 -0
- package/test/extract.test.ts +156 -0
- package/test/mock-osm.ts +140 -0
- package/test/read.test.ts +22 -0
- package/test/typed-arrays.test.ts +135 -0
- package/test/write.test.ts +66 -0
- package/tsconfig.json +9 -0
package/src/relations.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OsmEntityType,
|
|
3
|
+
OsmRelation,
|
|
4
|
+
OsmRelationMember,
|
|
5
|
+
OsmTags,
|
|
6
|
+
} from "@osmix/json"
|
|
7
|
+
import type { OsmPbfRelation } from "@osmix/pbf"
|
|
8
|
+
import { assertValue } from "@osmix/shared/assert"
|
|
9
|
+
import { Entities, type EntitiesTransferables } from "./entities"
|
|
10
|
+
import { Ids } from "./ids"
|
|
11
|
+
import type StringTable from "./stringtable"
|
|
12
|
+
import { Tags } from "./tags"
|
|
13
|
+
import {
|
|
14
|
+
type BufferType,
|
|
15
|
+
IdArrayType,
|
|
16
|
+
ResizeableTypedArray,
|
|
17
|
+
} from "./typed-arrays"
|
|
18
|
+
|
|
19
|
+
const RELATION_MEMBER_TYPES: OsmEntityType[] = ["node", "way", "relation"]
|
|
20
|
+
|
|
21
|
+
export interface RelationsTransferables extends EntitiesTransferables {
|
|
22
|
+
memberStart: BufferType
|
|
23
|
+
memberCount: BufferType
|
|
24
|
+
memberRefs: BufferType
|
|
25
|
+
memberTypes: BufferType
|
|
26
|
+
memberRoles: BufferType
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Relations extends Entities<OsmRelation> {
|
|
30
|
+
memberStart: ResizeableTypedArray<Uint32Array>
|
|
31
|
+
memberCount: ResizeableTypedArray<Uint16Array> // Maximum 65,535 members per relation
|
|
32
|
+
|
|
33
|
+
// Store the ID of the member because relations have other relations as members.
|
|
34
|
+
memberRefs: ResizeableTypedArray<Float64Array>
|
|
35
|
+
memberTypes: ResizeableTypedArray<Uint8Array>
|
|
36
|
+
memberRoles: ResizeableTypedArray<Uint32Array>
|
|
37
|
+
|
|
38
|
+
static from(stringTable: StringTable, rit: RelationsTransferables) {
|
|
39
|
+
const idIndex = Ids.from(rit)
|
|
40
|
+
const tagIndex = Tags.from(stringTable, rit)
|
|
41
|
+
const ri = new Relations(stringTable, idIndex, tagIndex)
|
|
42
|
+
ri.memberStart = ResizeableTypedArray.from(Uint32Array, rit.memberStart)
|
|
43
|
+
ri.memberCount = ResizeableTypedArray.from(Uint16Array, rit.memberCount)
|
|
44
|
+
ri.memberRefs = ResizeableTypedArray.from(IdArrayType, rit.memberRefs)
|
|
45
|
+
ri.memberTypes = ResizeableTypedArray.from(Uint8Array, rit.memberTypes)
|
|
46
|
+
ri.memberRoles = ResizeableTypedArray.from(Uint32Array, rit.memberRoles)
|
|
47
|
+
return ri
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
constructor(stringTable: StringTable, idIndex?: Ids, tagIndex?: Tags) {
|
|
51
|
+
super("relation", stringTable, idIndex, tagIndex)
|
|
52
|
+
this.memberStart = new ResizeableTypedArray(Uint32Array)
|
|
53
|
+
this.memberCount = new ResizeableTypedArray(Uint16Array)
|
|
54
|
+
this.memberRefs = new ResizeableTypedArray(IdArrayType)
|
|
55
|
+
this.memberTypes = new ResizeableTypedArray(Uint8Array)
|
|
56
|
+
this.memberRoles = new ResizeableTypedArray(Uint32Array)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
transferables(): RelationsTransferables {
|
|
60
|
+
return {
|
|
61
|
+
memberStart: this.memberStart.array.buffer,
|
|
62
|
+
memberCount: this.memberCount.array.buffer,
|
|
63
|
+
memberRefs: this.memberRefs.array.buffer,
|
|
64
|
+
memberTypes: this.memberTypes.array.buffer,
|
|
65
|
+
memberRoles: this.memberRoles.array.buffer,
|
|
66
|
+
...this.ids.transferables(),
|
|
67
|
+
...this.tags.transferables(),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
addRelation(relation: OsmRelation) {
|
|
72
|
+
this.ids.add(relation.id)
|
|
73
|
+
this.tags.addTags(relation.tags)
|
|
74
|
+
this.memberStart.push(this.memberRefs.length)
|
|
75
|
+
this.memberCount.push(relation.members.length)
|
|
76
|
+
for (const member of relation.members) {
|
|
77
|
+
this.memberRefs.push(member.ref)
|
|
78
|
+
this.memberTypes.push(RELATION_MEMBER_TYPES.indexOf(member.type))
|
|
79
|
+
this.memberRoles.push(this.stringTable.add(member.role ?? ""))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addRelations(
|
|
84
|
+
relations: OsmPbfRelation[],
|
|
85
|
+
blockStringIndexMap: Map<number, number>,
|
|
86
|
+
filter?: (relation: OsmRelation) => OsmRelation | null,
|
|
87
|
+
): number {
|
|
88
|
+
const blockToStringTable = (k: number) => {
|
|
89
|
+
const index = blockStringIndexMap.get(k)
|
|
90
|
+
if (index === undefined) throw Error("Tag key not found")
|
|
91
|
+
return index
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let added = 0
|
|
95
|
+
for (const relation of relations) {
|
|
96
|
+
const members: OsmRelationMember[] = []
|
|
97
|
+
const memberRefs: number[] = []
|
|
98
|
+
const memberTypes: number[] = []
|
|
99
|
+
const memberRoles: number[] = []
|
|
100
|
+
|
|
101
|
+
let refId = 0
|
|
102
|
+
for (let i = 0; i < relation.memids.length; i++) {
|
|
103
|
+
const memid = relation.memids[i]
|
|
104
|
+
const roleSid = relation.roles_sid[i]
|
|
105
|
+
const typeIndex = relation.types[i]
|
|
106
|
+
assertValue(memid, "Relation member ID is undefined")
|
|
107
|
+
assertValue(roleSid, "Relation member role SID is undefined")
|
|
108
|
+
assertValue(typeIndex, "Relation member type is undefined")
|
|
109
|
+
|
|
110
|
+
refId += memid
|
|
111
|
+
const roleIndex = blockToStringTable(roleSid)
|
|
112
|
+
const type = RELATION_MEMBER_TYPES[typeIndex]
|
|
113
|
+
assertValue(type, "Relation member type not found")
|
|
114
|
+
|
|
115
|
+
if (filter) {
|
|
116
|
+
members.push({
|
|
117
|
+
type,
|
|
118
|
+
ref: refId,
|
|
119
|
+
role: this.stringTable.get(roleIndex),
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
memberRefs.push(refId)
|
|
124
|
+
memberTypes.push(typeIndex)
|
|
125
|
+
memberRoles.push(roleIndex)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const tagKeys: number[] = relation.keys.map(blockToStringTable)
|
|
129
|
+
const tagValues: number[] = relation.vals.map(blockToStringTable)
|
|
130
|
+
|
|
131
|
+
const filteredRelation = filter
|
|
132
|
+
? filter({
|
|
133
|
+
id: relation.id,
|
|
134
|
+
members,
|
|
135
|
+
tags: this.tags.getTagsFromIndices(tagKeys, tagValues),
|
|
136
|
+
})
|
|
137
|
+
: null
|
|
138
|
+
if (filter && filteredRelation === null) continue
|
|
139
|
+
added++
|
|
140
|
+
|
|
141
|
+
this.ids.add(relation.id)
|
|
142
|
+
this.memberStart.push(this.memberRefs.length)
|
|
143
|
+
this.memberCount.push(
|
|
144
|
+
filteredRelation?.members.length ?? memberRefs.length,
|
|
145
|
+
)
|
|
146
|
+
this.memberRefs.pushMany(
|
|
147
|
+
filteredRelation?.members.map((m) => m.ref) ?? memberRefs,
|
|
148
|
+
)
|
|
149
|
+
this.memberTypes.pushMany(
|
|
150
|
+
filteredRelation?.members.map((m) =>
|
|
151
|
+
RELATION_MEMBER_TYPES.indexOf(m.type),
|
|
152
|
+
) ?? memberTypes,
|
|
153
|
+
)
|
|
154
|
+
this.memberRoles.pushMany(
|
|
155
|
+
filteredRelation?.members.map((m) =>
|
|
156
|
+
this.stringTable.add(m.role ?? ""),
|
|
157
|
+
) ?? memberRoles,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
this.tags.addTagKeysAndValues(tagKeys, tagValues)
|
|
161
|
+
}
|
|
162
|
+
return added
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
buildEntityIndex() {
|
|
166
|
+
this.memberStart.compact()
|
|
167
|
+
this.memberCount.compact()
|
|
168
|
+
this.memberRefs.compact()
|
|
169
|
+
this.memberTypes.compact()
|
|
170
|
+
this.memberRoles.compact()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getFullEntity(index: number, id: number, tags?: OsmTags): OsmRelation {
|
|
174
|
+
return {
|
|
175
|
+
id,
|
|
176
|
+
members: this.getMembersByIndex(index),
|
|
177
|
+
tags,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getMembersByIndex(
|
|
182
|
+
index: number,
|
|
183
|
+
relationMemberTypes = RELATION_MEMBER_TYPES,
|
|
184
|
+
) {
|
|
185
|
+
const start = this.memberStart.at(index)
|
|
186
|
+
const count = this.memberCount.at(index)
|
|
187
|
+
const members: OsmRelationMember[] = []
|
|
188
|
+
for (let i = start; i < start + count; i++) {
|
|
189
|
+
const ref = this.memberRefs.at(i)
|
|
190
|
+
const type = RELATION_MEMBER_TYPES[this.memberTypes.at(i)]
|
|
191
|
+
if (type === undefined) throw Error(`Member type not found: ${i}`)
|
|
192
|
+
if (!relationMemberTypes.includes(type)) continue
|
|
193
|
+
const role = this.stringTable.get(this.memberRoles.at(i))
|
|
194
|
+
members.push({ ref, type, role })
|
|
195
|
+
}
|
|
196
|
+
return members
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
includesMember(
|
|
200
|
+
index: number,
|
|
201
|
+
memberRef: number,
|
|
202
|
+
memberType: OsmEntityType,
|
|
203
|
+
memberRole?: string,
|
|
204
|
+
) {
|
|
205
|
+
const start = this.memberStart.at(index)
|
|
206
|
+
const count = this.memberCount.at(index)
|
|
207
|
+
for (let i = start; i < start + count; i++) {
|
|
208
|
+
const type = RELATION_MEMBER_TYPES[this.memberTypes.at(i)]
|
|
209
|
+
if (type !== memberType) continue
|
|
210
|
+
const ref = this.memberRefs.at(i)
|
|
211
|
+
if (ref !== memberRef) continue
|
|
212
|
+
if (
|
|
213
|
+
memberRole !== undefined &&
|
|
214
|
+
this.stringTable.get(this.memberRoles.at(i)) !== memberRole
|
|
215
|
+
)
|
|
216
|
+
continue
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { OsmPbfBlock, OsmPbfStringTable } from "@osmix/pbf"
|
|
2
|
+
import { type BufferType, ResizeableTypedArray } from "./typed-arrays"
|
|
3
|
+
|
|
4
|
+
export interface StringTableTransferables {
|
|
5
|
+
bytes: BufferType
|
|
6
|
+
start: BufferType
|
|
7
|
+
count: BufferType
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* StringTable that can be used to store and retrieve strings as bytes.
|
|
12
|
+
*/
|
|
13
|
+
export default class StringTable {
|
|
14
|
+
private enc = new TextEncoder()
|
|
15
|
+
private dec = new TextDecoder() // UTF-8
|
|
16
|
+
|
|
17
|
+
// Serializable state
|
|
18
|
+
private bytes: ResizeableTypedArray<Uint8Array>
|
|
19
|
+
private start: ResizeableTypedArray<Uint32Array>
|
|
20
|
+
private count: ResizeableTypedArray<Uint16Array>
|
|
21
|
+
|
|
22
|
+
// Builder state
|
|
23
|
+
private stringToIndex = new Map<string, number>()
|
|
24
|
+
|
|
25
|
+
// Retrieval state
|
|
26
|
+
private indexToString = new Map<number, string>()
|
|
27
|
+
|
|
28
|
+
static from({ bytes, start, count }: StringTableTransferables): StringTable {
|
|
29
|
+
const builder = new StringTable()
|
|
30
|
+
builder.bytes = ResizeableTypedArray.from(Uint8Array, bytes)
|
|
31
|
+
builder.start = ResizeableTypedArray.from(Uint32Array, start)
|
|
32
|
+
builder.count = ResizeableTypedArray.from(Uint16Array, count)
|
|
33
|
+
return builder
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
this.bytes = new ResizeableTypedArray(Uint8Array)
|
|
38
|
+
this.start = new ResizeableTypedArray(Uint32Array)
|
|
39
|
+
this.count = new ResizeableTypedArray(Uint16Array)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
transferables(): StringTableTransferables {
|
|
43
|
+
return {
|
|
44
|
+
bytes: this.bytes.array.buffer,
|
|
45
|
+
start: this.start.array.buffer,
|
|
46
|
+
count: this.count.array.buffer,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
add(str: string): number {
|
|
51
|
+
const existingIndex = this.stringToIndex.get(str)
|
|
52
|
+
if (existingIndex !== undefined) return existingIndex
|
|
53
|
+
const startIndex = this.start.length
|
|
54
|
+
const encoded = this.enc.encode(str)
|
|
55
|
+
this.start.push(this.bytes.length)
|
|
56
|
+
this.bytes.pushMany(encoded)
|
|
57
|
+
this.count.push(encoded.length)
|
|
58
|
+
this.stringToIndex.set(str, startIndex)
|
|
59
|
+
return startIndex
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decode all the strings in a primitive block and add them to the string table.
|
|
64
|
+
* Return a mapping of block index -> string table index
|
|
65
|
+
*/
|
|
66
|
+
createBlockIndexMap(block: OsmPbfBlock) {
|
|
67
|
+
const map = new Map<number, number>()
|
|
68
|
+
for (let i = 0; i < block.stringtable.length; i++) {
|
|
69
|
+
const bytesString = block.stringtable[i]
|
|
70
|
+
const str = this.dec.decode(bytesString)
|
|
71
|
+
const index = this.add(str)
|
|
72
|
+
map.set(i, index)
|
|
73
|
+
}
|
|
74
|
+
return map
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* TextDecoder.decode() does not support SharedArrayBuffer, so we need to first copy to a normal ArrayBuffer before decoding.
|
|
79
|
+
*/
|
|
80
|
+
get(index: number): string {
|
|
81
|
+
const string = this.indexToString.get(index)
|
|
82
|
+
if (string) return string
|
|
83
|
+
const bytes = this.getBytes(index)
|
|
84
|
+
const tempBuffer = new ArrayBuffer(bytes.byteLength)
|
|
85
|
+
const tempBytes = new Uint8Array(tempBuffer)
|
|
86
|
+
tempBytes.set(bytes)
|
|
87
|
+
const decoded = this.dec.decode(tempBytes)
|
|
88
|
+
this.indexToString.set(index, decoded)
|
|
89
|
+
return decoded
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getBytes(index: number): Uint8Array {
|
|
93
|
+
if (index < 0 || index >= this.length)
|
|
94
|
+
throw Error(`String index out of range: ${index}`)
|
|
95
|
+
const start = this.start.at(index)
|
|
96
|
+
const count = this.count.at(index)
|
|
97
|
+
// Important: subarray creates a *view*, not a copy; TextDecoder cannot read from it directly.
|
|
98
|
+
return this.bytes.array.subarray(start, start + count)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get length() {
|
|
102
|
+
return this.start.length
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
buildIndex() {
|
|
106
|
+
this.bytes.compact()
|
|
107
|
+
this.start.compact()
|
|
108
|
+
this.count.compact()
|
|
109
|
+
|
|
110
|
+
this.stringToIndex.clear()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
toOsmPbfStringTable(): OsmPbfStringTable {
|
|
114
|
+
const stringTable: OsmPbfStringTable = []
|
|
115
|
+
for (let i = 0; i < this.length; i++) {
|
|
116
|
+
stringTable.push(this.getBytes(i))
|
|
117
|
+
}
|
|
118
|
+
return stringTable
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/tags.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { OsmTags } from "@osmix/json"
|
|
2
|
+
import type StringTable from "./stringtable"
|
|
3
|
+
import { type BufferType, ResizeableTypedArray } from "./typed-arrays"
|
|
4
|
+
|
|
5
|
+
export interface TagsTransferables {
|
|
6
|
+
tagStart: BufferType
|
|
7
|
+
tagCount: BufferType
|
|
8
|
+
tagKeys: BufferType
|
|
9
|
+
tagVals: BufferType
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Tags {
|
|
13
|
+
private stringTable: StringTable
|
|
14
|
+
private tagStart: ResizeableTypedArray<Uint32Array>
|
|
15
|
+
private tagCount: ResizeableTypedArray<Uint8Array>
|
|
16
|
+
private tagKeys: ResizeableTypedArray<Uint32Array>
|
|
17
|
+
private tagVals: ResizeableTypedArray<Uint32Array>
|
|
18
|
+
|
|
19
|
+
private indexBuilt = false
|
|
20
|
+
|
|
21
|
+
static from(
|
|
22
|
+
stringTable: StringTable,
|
|
23
|
+
{ tagStart, tagCount, tagKeys, tagVals }: TagsTransferables,
|
|
24
|
+
) {
|
|
25
|
+
const tagIndex = new Tags(stringTable)
|
|
26
|
+
tagIndex.tagStart = ResizeableTypedArray.from(Uint32Array, tagStart)
|
|
27
|
+
tagIndex.tagCount = ResizeableTypedArray.from(Uint8Array, tagCount)
|
|
28
|
+
tagIndex.tagKeys = ResizeableTypedArray.from(Uint32Array, tagKeys)
|
|
29
|
+
tagIndex.tagVals = ResizeableTypedArray.from(Uint32Array, tagVals)
|
|
30
|
+
tagIndex.indexBuilt = true
|
|
31
|
+
return tagIndex
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
constructor(stringTable: StringTable) {
|
|
35
|
+
this.stringTable = stringTable
|
|
36
|
+
this.tagStart = new ResizeableTypedArray(Uint32Array)
|
|
37
|
+
this.tagCount = new ResizeableTypedArray(Uint8Array)
|
|
38
|
+
this.tagKeys = new ResizeableTypedArray(Uint32Array)
|
|
39
|
+
this.tagVals = new ResizeableTypedArray(Uint32Array)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
transferables() {
|
|
43
|
+
return {
|
|
44
|
+
tagStart: this.tagStart.array.buffer,
|
|
45
|
+
tagCount: this.tagCount.array.buffer,
|
|
46
|
+
tagKeys: this.tagKeys.array.buffer,
|
|
47
|
+
tagVals: this.tagVals.array.buffer,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addTags(tags?: OsmTags) {
|
|
52
|
+
const tagKeys: number[] = []
|
|
53
|
+
const tagValues: number[] = []
|
|
54
|
+
|
|
55
|
+
if (tags) {
|
|
56
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
57
|
+
tagKeys.push(this.stringTable.add(key))
|
|
58
|
+
tagValues.push(this.stringTable.add(String(value)))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.addTagKeysAndValues(tagKeys, tagValues)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
addTagKeysAndValues(keys: number[], values: number[]) {
|
|
66
|
+
this.tagStart.push(this.tagKeys.length)
|
|
67
|
+
this.tagCount.push(keys.length)
|
|
68
|
+
this.tagKeys.pushMany(keys)
|
|
69
|
+
this.tagVals.pushMany(values)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
buildIndex() {
|
|
73
|
+
this.tagStart.compact()
|
|
74
|
+
this.tagCount.compact()
|
|
75
|
+
this.tagKeys.compact()
|
|
76
|
+
this.tagVals.compact()
|
|
77
|
+
this.indexBuilt = true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get isReady() {
|
|
81
|
+
return this.indexBuilt
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
cardinality(index: number): number {
|
|
85
|
+
return this.tagCount.at(index) ?? 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getTags(index: number): OsmTags | undefined {
|
|
89
|
+
const tagCount = this.tagCount.at(index) ?? 0
|
|
90
|
+
if (tagCount === 0) return
|
|
91
|
+
const tagStart = this.tagStart.at(index) ?? 0
|
|
92
|
+
const tagKeyIndexes = this.tagKeys.array.slice(
|
|
93
|
+
tagStart,
|
|
94
|
+
tagStart + tagCount,
|
|
95
|
+
)
|
|
96
|
+
const tagValIndexes = this.tagVals.array.slice(
|
|
97
|
+
tagStart,
|
|
98
|
+
tagStart + tagCount,
|
|
99
|
+
)
|
|
100
|
+
const tags: OsmTags = {}
|
|
101
|
+
for (let i = 0; i < tagCount; i++) {
|
|
102
|
+
const keyIndex = tagKeyIndexes[i]
|
|
103
|
+
const valIndex = tagValIndexes[i]
|
|
104
|
+
if (keyIndex === undefined || valIndex === undefined)
|
|
105
|
+
throw Error("Tag key or value not found")
|
|
106
|
+
tags[this.stringTable.get(keyIndex)] = this.stringTable.get(valIndex)
|
|
107
|
+
}
|
|
108
|
+
return tags
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getTagsFromIndices(keys: number[], values: number[]): OsmTags {
|
|
112
|
+
const tags: OsmTags = {}
|
|
113
|
+
for (let i = 0; i < keys.length; i++) {
|
|
114
|
+
const keyIndex = keys[i]
|
|
115
|
+
const valIndex = values[i]
|
|
116
|
+
if (keyIndex === undefined || valIndex === undefined)
|
|
117
|
+
throw Error("Tag key or value not found")
|
|
118
|
+
tags[this.stringTable.get(keyIndex)] = this.stringTable.get(valIndex)
|
|
119
|
+
}
|
|
120
|
+
return tags
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use SharedArrayBuffer if the runtime supports it, otherwise fall back to ArrayBuffer.
|
|
3
|
+
*/
|
|
4
|
+
export const BufferConstructor =
|
|
5
|
+
typeof SharedArrayBuffer !== "undefined"
|
|
6
|
+
? (SharedArrayBuffer as SharedArrayBufferConstructor)
|
|
7
|
+
: (ArrayBuffer as ArrayBufferConstructor)
|
|
8
|
+
export type BufferType = InstanceType<typeof BufferConstructor>
|
|
9
|
+
|
|
10
|
+
export type TypedArray<B extends BufferType = BufferType> =
|
|
11
|
+
| Int8Array<B>
|
|
12
|
+
| Uint8Array<B>
|
|
13
|
+
| Uint8ClampedArray<B>
|
|
14
|
+
| Int16Array<B>
|
|
15
|
+
| Uint16Array<B>
|
|
16
|
+
| Int32Array<B>
|
|
17
|
+
| Uint32Array<B>
|
|
18
|
+
| Float32Array<B>
|
|
19
|
+
| Float64Array<B>
|
|
20
|
+
|
|
21
|
+
export interface TypedArrayConstructor<
|
|
22
|
+
T extends TypedArray<BufferType> = TypedArray<BufferType>,
|
|
23
|
+
> {
|
|
24
|
+
new (buffer: BufferType): T
|
|
25
|
+
readonly BYTES_PER_ELEMENT: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* OSM IDs can be stored as 64-bit floating point numbers.
|
|
30
|
+
*/
|
|
31
|
+
export const IdArrayType = Float64Array
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* When we are storing coordinates, we need to be able to store 64-bit floating point numbers.
|
|
35
|
+
* However, for benchmarking it is handy to test 32-bit floating point numbers.
|
|
36
|
+
*/
|
|
37
|
+
export const CoordinateArrayType = Float64Array
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* When we are storing indexes into other arrays, we never need an index to exceed 2^32.
|
|
41
|
+
*/
|
|
42
|
+
export const IndexArrayType = Uint32Array
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default buffer size.
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_BUFFER_SIZE = 2 ** 20 // 1 MiB
|
|
48
|
+
|
|
49
|
+
export class ResizeableTypedArray<TA extends TypedArray> {
|
|
50
|
+
ArrayType: TypedArrayConstructor<TA>
|
|
51
|
+
array: TA
|
|
52
|
+
items = 0
|
|
53
|
+
|
|
54
|
+
buffer: BufferType
|
|
55
|
+
bufferSize: number
|
|
56
|
+
maxByteLength: number
|
|
57
|
+
|
|
58
|
+
BC: SharedArrayBufferConstructor | ArrayBufferConstructor
|
|
59
|
+
|
|
60
|
+
static from<TA extends TypedArray>(
|
|
61
|
+
ArrayType: TypedArrayConstructor<TA>,
|
|
62
|
+
buffer: BufferType,
|
|
63
|
+
) {
|
|
64
|
+
const rta = new ResizeableTypedArray<TA>(
|
|
65
|
+
ArrayType,
|
|
66
|
+
buffer instanceof SharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer,
|
|
67
|
+
)
|
|
68
|
+
rta.buffer = buffer
|
|
69
|
+
rta.array = new ArrayType(buffer)
|
|
70
|
+
rta.items = rta.array.length
|
|
71
|
+
return rta
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
ArrayType: TypedArrayConstructor<TA>,
|
|
76
|
+
BC:
|
|
77
|
+
| SharedArrayBufferConstructor
|
|
78
|
+
| ArrayBufferConstructor = BufferConstructor,
|
|
79
|
+
) {
|
|
80
|
+
this.ArrayType = ArrayType
|
|
81
|
+
this.bufferSize = DEFAULT_BUFFER_SIZE
|
|
82
|
+
this.maxByteLength = DEFAULT_BUFFER_SIZE * 2
|
|
83
|
+
this.BC = BC
|
|
84
|
+
this.buffer = new BC(this.bufferSize, {
|
|
85
|
+
maxByteLength: this.maxByteLength,
|
|
86
|
+
})
|
|
87
|
+
this.array = new this.ArrayType(this.buffer)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[Symbol.iterator](): ArrayIterator<number> {
|
|
91
|
+
return this.array[Symbol.iterator]()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
expandArray() {
|
|
95
|
+
this.bufferSize *= 2
|
|
96
|
+
if (this.bufferSize > this.buffer.maxByteLength) {
|
|
97
|
+
this.maxByteLength *= 2
|
|
98
|
+
const newBuffer = new this.BC(this.bufferSize, {
|
|
99
|
+
maxByteLength: this.maxByteLength,
|
|
100
|
+
})
|
|
101
|
+
const newArray = new this.ArrayType(newBuffer)
|
|
102
|
+
newArray.set(this.array)
|
|
103
|
+
this.buffer = newBuffer
|
|
104
|
+
this.array = newArray
|
|
105
|
+
} else {
|
|
106
|
+
if (this.buffer instanceof SharedArrayBuffer && this.buffer.growable) {
|
|
107
|
+
this.buffer.grow(this.bufferSize)
|
|
108
|
+
} else if (this.buffer instanceof ArrayBuffer && this.buffer.resizable) {
|
|
109
|
+
this.buffer.resize(this.bufferSize)
|
|
110
|
+
} else {
|
|
111
|
+
throw Error("Buffer is not growable or resizable")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handles negative numbers
|
|
118
|
+
*/
|
|
119
|
+
at(index: number): number {
|
|
120
|
+
if (index < -this.length || index >= this.length)
|
|
121
|
+
throw Error(`Index out of bounds: ${index}. Length: ${this.length}`)
|
|
122
|
+
if (index < 0) return this.at(this.length + index)
|
|
123
|
+
const result = this.array[index]
|
|
124
|
+
if (result === undefined) throw Error(`No value at index: ${index}`)
|
|
125
|
+
return result
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
slice(start: number, end: number) {
|
|
129
|
+
return this.array.slice(start, end)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get length() {
|
|
133
|
+
return this.items
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
push(value: number): number {
|
|
137
|
+
if (this.length >= this.array.length) {
|
|
138
|
+
this.expandArray()
|
|
139
|
+
}
|
|
140
|
+
this.array[this.items++] = value
|
|
141
|
+
return this.length - 1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pushMany(values: number[] | TypedArray) {
|
|
145
|
+
while (this.length + values.length > this.array.length) this.expandArray()
|
|
146
|
+
this.array.set(values, this.length)
|
|
147
|
+
this.items += values.length
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
compact() {
|
|
151
|
+
if (this.buffer instanceof SharedArrayBuffer) {
|
|
152
|
+
this.buffer = this.buffer.slice(
|
|
153
|
+
0,
|
|
154
|
+
this.length * this.ArrayType.BYTES_PER_ELEMENT,
|
|
155
|
+
)
|
|
156
|
+
} else {
|
|
157
|
+
this.buffer = this.buffer.transferToFixedLength(
|
|
158
|
+
this.length * this.ArrayType.BYTES_PER_ELEMENT,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
this.array = new this.ArrayType(this.buffer)
|
|
162
|
+
return this.array
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare module "kdbush" {
|
|
2
|
+
import type { BufferConstructor, BufferType } from "../typed-arrays"
|
|
3
|
+
export default class KDBush {
|
|
4
|
+
constructor(
|
|
5
|
+
length: number,
|
|
6
|
+
nodeSize: number,
|
|
7
|
+
ArrayType: Float64ArrayConstructor,
|
|
8
|
+
BufferConstructor: BufferConstructor,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
/** Serialized index data buffer */
|
|
12
|
+
data: BufferType
|
|
13
|
+
|
|
14
|
+
/** Add a point before calling finish */
|
|
15
|
+
add(x: number, y: number): void
|
|
16
|
+
|
|
17
|
+
/** Finalize the index after adding all points */
|
|
18
|
+
finish(): void
|
|
19
|
+
|
|
20
|
+
/** Query points within a bounding box; returns internal point indexes */
|
|
21
|
+
range(minX: number, minY: number, maxX: number, maxY: number): number[]
|
|
22
|
+
|
|
23
|
+
/** Query points within a radius from (x, y); returns internal point indexes */
|
|
24
|
+
within(x: number, y: number, radius: number): number[]
|
|
25
|
+
|
|
26
|
+
/** Reconstruct an index from a serialized buffer */
|
|
27
|
+
static from(data: BufferType): KDBush
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GeoBbox2D, LonLat } from "@osmix/shared/types"
|
|
2
|
+
|
|
3
|
+
export function throttle<T extends unknown[]>(
|
|
4
|
+
func: (...args: T) => void,
|
|
5
|
+
timeFrame: number,
|
|
6
|
+
) {
|
|
7
|
+
let lastTime = 0
|
|
8
|
+
return (...args: T) => {
|
|
9
|
+
const now = Date.now()
|
|
10
|
+
if (now - lastTime >= timeFrame) {
|
|
11
|
+
func(...args)
|
|
12
|
+
lastTime = now
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function bboxFromLonLats(lonLats: LonLat[]): GeoBbox2D {
|
|
18
|
+
let minLon = Number.POSITIVE_INFINITY
|
|
19
|
+
let minLat = Number.POSITIVE_INFINITY
|
|
20
|
+
let maxLon = Number.NEGATIVE_INFINITY
|
|
21
|
+
let maxLat = Number.NEGATIVE_INFINITY
|
|
22
|
+
for (const [lon, lat] of lonLats) {
|
|
23
|
+
if (lon < minLon) minLon = lon
|
|
24
|
+
if (lat < minLat) minLat = lat
|
|
25
|
+
if (lon > maxLon) maxLon = lon
|
|
26
|
+
if (lat > maxLat) maxLat = lat
|
|
27
|
+
}
|
|
28
|
+
return [minLon, minLat, maxLon, maxLat]
|
|
29
|
+
}
|