@xyo-network/quadkey 2.36.10

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/src/Quadkey.ts ADDED
@@ -0,0 +1,335 @@
1
+ import { BigNumber } from '@xylabs/bignumber'
2
+ import { Buffer } from '@xylabs/buffer'
3
+ import { assertEx } from '@xylabs/sdk-js'
4
+ import {
5
+ GeoJson,
6
+ MercatorBoundingBox,
7
+ MercatorTile,
8
+ tileFromPoint,
9
+ tileFromQuadkey,
10
+ tilesFromBoundingBox,
11
+ tileToBoundingBox,
12
+ tileToQuadkey,
13
+ } from '@xyo-network/sdk-xyo-js'
14
+ import { LngLat, LngLatLike } from 'mapbox-gl'
15
+
16
+ import { bitShiftLeft, bitShiftRight, padHex } from './utils'
17
+
18
+ export * from './utils'
19
+
20
+ const MAX_ZOOM = 124
21
+
22
+ export const isQuadkey = (obj: { type: string }) => obj?.type === Quadkey.type
23
+
24
+ const RelativeDirectionConstantLookup: Record<string, number> = {
25
+ e: 1,
26
+ n: -2,
27
+ s: 2,
28
+ w: -1,
29
+ }
30
+
31
+ export class Quadkey {
32
+ public type = Quadkey.type
33
+
34
+ private key = Buffer.alloc(32)
35
+
36
+ constructor(key = Buffer.alloc(32)) {
37
+ key.copy(this.key, this.key.length - key.length)
38
+ }
39
+
40
+ public equals(obj: Quadkey): boolean {
41
+ return obj.toBase4HashLabel() == this.toBase4HashLabel()
42
+ }
43
+
44
+ static root = new Quadkey()
45
+
46
+ static type = 'Quadkey'
47
+
48
+ static fromBase4String(value?: string) {
49
+ if (value === 'fhr' || value === '') {
50
+ return Quadkey.root
51
+ }
52
+ if (value && value.length && value.length > 0) {
53
+ const quadkey = new Quadkey(Buffer.from(padHex(new BigNumber(value, 4).toString(16)), 'hex')).setZoom(value.length)
54
+ return quadkey.valid() ? quadkey : undefined
55
+ }
56
+ }
57
+
58
+ static fromBase10String(value: string) {
59
+ return new Quadkey(Buffer.from(padHex(new BigNumber(value, 10).toString(16)), 'hex'))
60
+ }
61
+
62
+ static fromBase16String(value: string) {
63
+ const valueToUse = value.startsWith('0x') ? value.slice(2) : value
64
+ return new Quadkey(Buffer.from(padHex(valueToUse), 'hex'))
65
+ }
66
+
67
+ static fromBuffer(value: Buffer) {
68
+ return Quadkey.fromBase16String(value.toString('hex'))
69
+ }
70
+
71
+ static fromString(zoom: number, id: string, base = 10) {
72
+ switch (base) {
73
+ case 10:
74
+ return Quadkey.fromBase10String(id)?.setZoom(zoom)
75
+ case 16:
76
+ return Quadkey.fromBase16String(id).setZoom(zoom)
77
+ default:
78
+ throw Error(`Invalid base [${base}]`)
79
+ }
80
+ }
81
+
82
+ public static from(zoom: number, id: Buffer) {
83
+ return new Quadkey().setId(id).setZoom(zoom)
84
+ }
85
+
86
+ public static fromLngLat(point: LngLatLike, zoom: number) {
87
+ const tile = tileFromPoint(LngLat.convert(point), zoom)
88
+ const quadkeyString = tileToQuadkey(tile)
89
+ return Quadkey.fromBase4String(quadkeyString)
90
+ }
91
+
92
+ public static fromTile(tile: MercatorTile) {
93
+ return Quadkey.fromBase4String(tileToQuadkey(tile))
94
+ }
95
+
96
+ public isInBoundingBox(boundingBox: MercatorBoundingBox) {
97
+ const tileBoundingBox = tileToBoundingBox(this.toTile())
98
+ return (
99
+ boundingBox.contains(tileBoundingBox.getNorthEast()) ||
100
+ boundingBox.contains(tileBoundingBox.getNorthWest()) ||
101
+ boundingBox.contains(tileBoundingBox.getSouthEast()) ||
102
+ boundingBox.contains(tileBoundingBox.getSouthWest())
103
+ )
104
+ }
105
+
106
+ public static fromBoundingBox(bbox: MercatorBoundingBox, zoom: number) {
107
+ const tiles = tilesFromBoundingBox(bbox, Math.floor(zoom))
108
+ const result: Quadkey[] = []
109
+ for (const tile of tiles) {
110
+ result.push(assertEx(Quadkey.fromTile(tile), 'Bad Quadkey'))
111
+ }
112
+
113
+ return result
114
+ }
115
+
116
+ public getGridBoundingBox(size: number) {
117
+ const hash = this.toBase4Hash()
118
+ let index = 0
119
+ let left = 0
120
+ let top = 0
121
+ let blockSize = size
122
+ while (index < hash.length) {
123
+ blockSize >>= 1
124
+ switch (hash[index]) {
125
+ case '1':
126
+ left += blockSize
127
+ break
128
+ case '2':
129
+ top += blockSize
130
+ break
131
+ case '3':
132
+ left += blockSize
133
+ top += blockSize
134
+ break
135
+ }
136
+ index++
137
+ }
138
+ if (blockSize < 2) {
139
+ blockSize = 2
140
+ }
141
+ return {
142
+ height: blockSize,
143
+ left,
144
+ top,
145
+ width: blockSize,
146
+ }
147
+ }
148
+
149
+ public setKey(zoom: number, id: Buffer) {
150
+ id.copy(this.key, this.key.length - id.length)
151
+ this.key.writeUInt8(zoom, 0)
152
+ return this
153
+ }
154
+
155
+ public setZoom(zoom: number) {
156
+ assertEx(zoom < MAX_ZOOM, `Invalid zoom [${zoom}] max=${MAX_ZOOM}`)
157
+ this.setKey(zoom, this.id())
158
+ return this
159
+ }
160
+
161
+ public parent() {
162
+ if (this.zoom() > 0) {
163
+ return new Quadkey().setId(bitShiftRight(bitShiftRight(this.id()))).setZoom(this.zoom() - 1)
164
+ }
165
+ }
166
+
167
+ public relative(direction: string) {
168
+ const directionConstant = assertEx(RelativeDirectionConstantLookup[direction], 'Invalid direction')
169
+ let quadkey = this.toBase4Hash()
170
+ if (quadkey.length === 0) {
171
+ return this
172
+ }
173
+ let index = quadkey.length - 1
174
+ while (index >= 0) {
175
+ let number = parseInt(quadkey.charAt(index))
176
+ number += directionConstant
177
+ if (number > 3) {
178
+ number -= 4
179
+ quadkey = quadkey.substring(0, index) + number.toString() + quadkey.substring(index + 1)
180
+ index--
181
+ } else if (number < 0) {
182
+ number += 4
183
+ quadkey = quadkey.substring(0, index) + number.toString() + quadkey.substring(index + 1)
184
+ index--
185
+ } else {
186
+ index = -1
187
+ }
188
+ }
189
+ return Quadkey.fromBase4String(quadkey)
190
+ }
191
+
192
+ public childrenByZoom(zoom: number) {
193
+ // if we are limiting by zoom, and we are already at that limit, just return this quadkey
194
+ if (zoom && zoom === this.zoom()) {
195
+ return [this]
196
+ }
197
+
198
+ // recursively get children
199
+ let deepResult: Quadkey[] = []
200
+ for (const quadkey of this.children()) {
201
+ deepResult = deepResult.concat(quadkey.childrenByZoom(zoom))
202
+ }
203
+ return deepResult
204
+ }
205
+
206
+ public children() {
207
+ assertEx(this.zoom() < MAX_ZOOM - 1, 'Can not get children of bottom tiles')
208
+ const result: Quadkey[] = []
209
+ const shiftedId = bitShiftLeft(bitShiftLeft(this.id()))
210
+ for (let i = 0; i < 4; i++) {
211
+ const currentLastByte = shiftedId.readUInt8(shiftedId.length - 1)
212
+ shiftedId.writeUInt8((currentLastByte & 0xfc) | i, shiftedId.length - 1)
213
+ result.push(new Quadkey().setId(shiftedId).setZoom(this.zoom() + 1))
214
+ }
215
+ return result
216
+ }
217
+
218
+ public siblings() {
219
+ const siblings = assertEx(this.parent()?.children(), `siblings: parentChildren ${this.toBase4Hash()}`)
220
+ const filteredeSiblinngs = siblings.filter((quadkey) => this.compareTo(quadkey) !== 0)
221
+ assertEx(filteredeSiblinngs.length === 3, `siblings: expected 3 [${filteredeSiblinngs.length}]`)
222
+ return filteredeSiblinngs
223
+ }
224
+
225
+ public clone() {
226
+ return Quadkey.fromBase10String(this.toBase10String())
227
+ }
228
+
229
+ public zoom() {
230
+ return this.toBuffer().readUInt8(0)
231
+ }
232
+
233
+ private _geoJson?: GeoJson
234
+
235
+ public geoJson() {
236
+ this._geoJson = this._geoJson ?? new GeoJson(this.toBase4Hash())
237
+ return this._geoJson
238
+ }
239
+
240
+ public setId(id: Buffer) {
241
+ this.setKey(this.zoom(), id)
242
+ return this
243
+ }
244
+
245
+ public toTile(): MercatorTile {
246
+ return tileFromQuadkey(this.toBase4Hash())
247
+ }
248
+
249
+ public toBoundingBox(): MercatorBoundingBox {
250
+ return tileToBoundingBox(this.toTile())
251
+ }
252
+
253
+ public getGridLocation() {
254
+ const tileData = tileFromQuadkey(this.toBase4Hash())
255
+
256
+ return {
257
+ col: 2 ** tileData[2] - tileData[1] - 1,
258
+ row: tileData[0],
259
+ zoom: tileData[2],
260
+ }
261
+ }
262
+
263
+ public valid() {
264
+ const zoom = this.zoom()
265
+ const shift = (MAX_ZOOM - zoom) * 2
266
+ const id = this.id()
267
+ let testId = id
268
+ for (let i = 0; i < shift; i++) {
269
+ testId = bitShiftLeft(testId)
270
+ }
271
+ for (let i = 0; i < shift; i++) {
272
+ testId = bitShiftRight(testId)
273
+ }
274
+ return testId.compare(id) === 0
275
+ }
276
+
277
+ public id() {
278
+ return this.toBuffer().slice(1)
279
+ }
280
+
281
+ public toBuffer() {
282
+ return this.key
283
+ }
284
+
285
+ public toBigNumber() {
286
+ return new BigNumber(`${this.key.toString('hex')}`, 'hex')
287
+ }
288
+
289
+ public toString() {
290
+ return `0x${padHex(this.toBigNumber().toString(16))}`
291
+ }
292
+
293
+ public toJSON(): string {
294
+ return this.toBase4HashLabel()
295
+ }
296
+
297
+ public toBase10String() {
298
+ return this.toBigNumber().toString(10)
299
+ }
300
+
301
+ public toBase4HashLabel() {
302
+ const hash = this.toBase4Hash()
303
+ return hash.length === 0 ? 'fhr' : hash
304
+ }
305
+
306
+ public toBase4Hash() {
307
+ const bn = new BigNumber(this.id().toString('hex'), 16)
308
+ const zoom = this.zoom()
309
+ if (zoom === 0) {
310
+ return ''
311
+ }
312
+ let quadkeySimple = bn.toString(4)
313
+ while (quadkeySimple.length < zoom) {
314
+ quadkeySimple = `0${quadkeySimple}`
315
+ }
316
+ return quadkeySimple
317
+ }
318
+
319
+ public toHex() {
320
+ return `0x${this.key.toString('hex')}`
321
+ }
322
+
323
+ public toShortString() {
324
+ const buffer = this.toBuffer()
325
+ const part1 = buffer.slice(0, 2)
326
+ const part2 = buffer.slice(buffer.length - 2, buffer.length)
327
+ return `${part1.toString('hex')}...${part2.toString('hex')}`
328
+ }
329
+
330
+ public compareTo(quadkey: Quadkey) {
331
+ return this.toBigNumber().cmp(quadkey.toBigNumber())
332
+ }
333
+
334
+ public static Zero = Quadkey.from(0, Buffer.alloc(31, 0))
335
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './Quadkey'
package/src/utils.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { Buffer } from 'buffer/'
2
+
3
+ export const padHex = (hex: string, byteCount?: number) => {
4
+ let result = hex
5
+ if (hex.length % 2 !== 0) {
6
+ result = `0${hex}`
7
+ }
8
+ if (byteCount) {
9
+ while (result.length / 2 < byteCount) {
10
+ result = `00${result}`
11
+ }
12
+ }
13
+ return result
14
+ }
15
+
16
+ export const bitShiftLeft = (buffer: Buffer) => {
17
+ const shifted = Buffer.alloc(buffer.length)
18
+ const last = buffer.length - 1
19
+ for (let index = 0; index < last; index++) {
20
+ shifted[index] = buffer[index] << 1
21
+ if (buffer[index + 1] & 0x80) {
22
+ shifted[index] += 0x01
23
+ }
24
+ }
25
+ shifted[last] = buffer[last] << 1
26
+ return shifted
27
+ }
28
+
29
+ export const bitShiftRight = (buffer: Buffer) => {
30
+ const shifted = Buffer.alloc(buffer.length)
31
+ const last = buffer.length - 1
32
+ for (let index = last; index > 0; index--) {
33
+ shifted[index] = buffer[index] >> 1
34
+ if (buffer[index - 1] & 0x01) {
35
+ shifted[index] += 0x80
36
+ }
37
+ }
38
+ shifted[0] = buffer[0] >> 1
39
+ return shifted
40
+ }