@math.gl/dggs 5.0.0-alpha.5 → 5.0.0-alpha.8
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/README.md +1 -1
- package/dist/a5-decoder.d.ts +3 -3
- package/dist/a5-decoder.d.ts.map +1 -1
- package/dist/a5-decoder.js +3 -3
- package/dist/a5-decoder.js.map +1 -1
- package/dist/a5.cjs +59 -2
- package/dist/a5.cjs.map +3 -3
- package/dist/dggs-decoder.d.ts +12 -3
- package/dist/dggs-decoder.d.ts.map +1 -1
- package/dist/dggs-decoder.js +24 -0
- package/dist/dggs-decoder.js.map +1 -1
- package/dist/geohash-decoder.d.ts +3 -3
- package/dist/geohash-decoder.d.ts.map +1 -1
- package/dist/geohash-decoder.js +3 -3
- package/dist/geohash-decoder.js.map +1 -1
- package/dist/geohash.cjs +75 -2
- package/dist/geohash.cjs.map +2 -2
- package/dist/h3-decoder.d.ts +3 -3
- package/dist/h3-decoder.d.ts.map +1 -1
- package/dist/h3-decoder.js +3 -3
- package/dist/h3-decoder.js.map +1 -1
- package/dist/h3.cjs +59 -2
- package/dist/h3.cjs.map +3 -3
- package/dist/index.cjs +281 -33
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/plus-code-decoder.d.ts +3 -3
- package/dist/plus-code-decoder.d.ts.map +1 -1
- package/dist/plus-code-decoder.js +3 -3
- package/dist/plus-code-decoder.js.map +1 -1
- package/dist/plus-code.cjs +75 -2
- package/dist/plus-code.cjs.map +2 -2
- package/dist/quadkey-decoder.d.ts +3 -3
- package/dist/quadkey-decoder.d.ts.map +1 -1
- package/dist/quadkey-decoder.js +3 -3
- package/dist/quadkey-decoder.js.map +1 -1
- package/dist/quadkey.cjs +75 -2
- package/dist/quadkey.cjs.map +2 -2
- package/dist/s2-decoder.d.ts +13 -3
- package/dist/s2-decoder.d.ts.map +1 -1
- package/dist/s2-decoder.js +89 -21
- package/dist/s2-decoder.js.map +1 -1
- package/dist/s2-geometry/s2-geometry.d.ts +20 -0
- package/dist/s2-geometry/s2-geometry.d.ts.map +1 -1
- package/dist/s2-geometry/s2-geometry.js +123 -0
- package/dist/s2-geometry/s2-geometry.js.map +1 -1
- package/dist/s2-geometry/s2-token.d.ts +30 -0
- package/dist/s2-geometry/s2-token.d.ts.map +1 -1
- package/dist/s2-geometry/s2-token.js +70 -4
- package/dist/s2-geometry/s2-token.js.map +1 -1
- package/dist/s2.cjs +288 -23
- package/dist/s2.cjs.map +2 -2
- package/dist/s2.d.ts +4 -2
- package/dist/s2.d.ts.map +1 -1
- package/dist/s2.js +3 -2
- package/dist/s2.js.map +1 -1
- package/dist/unwrap-dggs-boundary.d.ts +12 -0
- package/dist/unwrap-dggs-boundary.d.ts.map +1 -0
- package/dist/unwrap-dggs-boundary.js +49 -0
- package/dist/unwrap-dggs-boundary.js.map +1 -0
- package/package.json +3 -3
- package/src/a5-decoder.ts +8 -3
- package/src/dggs-decoder.ts +40 -3
- package/src/geohash-decoder.ts +8 -3
- package/src/h3-decoder.ts +8 -3
- package/src/index.ts +13 -2
- package/src/plus-code-decoder.ts +8 -3
- package/src/quadkey-decoder.ts +8 -3
- package/src/s2-decoder.ts +90 -20
- package/src/s2-geometry/s2-geometry.ts +137 -0
- package/src/s2-geometry/s2-token.ts +73 -4
- package/src/s2.ts +11 -2
- package/src/unwrap-dggs-boundary.ts +54 -0
package/src/geohash-decoder.ts
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
5
|
import {type Bounds2D} from '@math.gl/types';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
withDGGSBoundaryOptions,
|
|
8
|
+
type DGGSCell,
|
|
9
|
+
type DGGSDecoder,
|
|
10
|
+
getDGGSCellToken
|
|
11
|
+
} from './dggs-decoder';
|
|
7
12
|
|
|
8
13
|
/** Decoder for the geohash dggs */
|
|
9
|
-
export const GeohashDecoder = {
|
|
14
|
+
export const GeohashDecoder = withDGGSBoundaryOptions({
|
|
10
15
|
name: 'geohash',
|
|
11
16
|
hasNumericRepresentation: false,
|
|
12
17
|
cellColumnNames: ['geohash', 'geohashId', 'geohash_id'],
|
|
@@ -17,7 +22,7 @@ export const GeohashDecoder = {
|
|
|
17
22
|
cellToBoundaryFlat: (cell: DGGSCell): number[] =>
|
|
18
23
|
getGeohashBoundaryFlat(getDGGSCellToken(cell, 'GeoHash')),
|
|
19
24
|
cellToBounds: (cell: DGGSCell): Bounds2D => getGeohashBounds(getDGGSCellToken(cell, 'GeoHash'))
|
|
20
|
-
} as const satisfies DGGSDecoder;
|
|
25
|
+
} as const satisfies DGGSDecoder);
|
|
21
26
|
|
|
22
27
|
const BASE32_CODES = '0123456789bcdefghjkmnpqrstuvwxyz';
|
|
23
28
|
const BASE32_CODES_DICT: Record<string, number> = {};
|
package/src/h3-decoder.ts
CHANGED
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import {cellToBoundary, cellToLatLng} from 'h3-js';
|
|
6
6
|
import {type Bounds2D} from '@math.gl/types';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
withDGGSBoundaryOptions,
|
|
9
|
+
type DGGSCell,
|
|
10
|
+
type DGGSDecoder,
|
|
11
|
+
getDGGSCellBounds
|
|
12
|
+
} from './dggs-decoder';
|
|
8
13
|
|
|
9
14
|
/** Lightweight cell geometry decoder for the H3 hexagonal DGGS. */
|
|
10
|
-
export const H3Decoder = {
|
|
15
|
+
export const H3Decoder = withDGGSBoundaryOptions({
|
|
11
16
|
name: 'h3',
|
|
12
17
|
hasNumericRepresentation: true,
|
|
13
18
|
cellColumnNames: [
|
|
@@ -26,7 +31,7 @@ export const H3Decoder = {
|
|
|
26
31
|
cellToBoundary: (cell: DGGSCell): [number, number][] => getH3Boundary(cell),
|
|
27
32
|
cellToBoundaryFlat: (cell: DGGSCell): number[] => getH3Boundary(cell).flat(),
|
|
28
33
|
cellToBounds: (cell: DGGSCell): Bounds2D => getDGGSCellBounds(getH3Boundary(cell))
|
|
29
|
-
} as const satisfies DGGSDecoder;
|
|
34
|
+
} as const satisfies DGGSDecoder);
|
|
30
35
|
|
|
31
36
|
function getH3Token(cell: DGGSCell): string {
|
|
32
37
|
return typeof cell === 'bigint' ? cell.toString(16) : removeHexPrefix(cell);
|
package/src/index.ts
CHANGED
|
@@ -10,14 +10,25 @@ import {PlusCodeDecoder} from './plus-code-decoder';
|
|
|
10
10
|
import {QuadkeyDecoder} from './quadkey-decoder';
|
|
11
11
|
import {S2Decoder} from './s2-decoder';
|
|
12
12
|
|
|
13
|
+
export {type DGGSBoundaryOptions} from './dggs-decoder';
|
|
13
14
|
export {type DGGSCell, type DGGSCellColumn, type DGGSDecoder};
|
|
15
|
+
export {unwrapDGGSBoundary} from './unwrap-dggs-boundary';
|
|
14
16
|
export {A5Decoder};
|
|
15
17
|
export {GeohashDecoder};
|
|
16
18
|
export {H3Decoder};
|
|
17
19
|
export {PlusCodeDecoder};
|
|
18
20
|
export {QuadkeyDecoder, quadkeyToWorldBounds} from './quadkey-decoder';
|
|
19
|
-
export {S2Decoder};
|
|
20
|
-
export {
|
|
21
|
+
export {S2Decoder, getS2Bounds, getS2LngLat, getS2BoundaryFlat} from './s2-decoder';
|
|
22
|
+
export {
|
|
23
|
+
getS2ChildIndex,
|
|
24
|
+
getS2IndexFromToken,
|
|
25
|
+
getS2Level,
|
|
26
|
+
getS2TokenFromIndex,
|
|
27
|
+
isS2IndexValid,
|
|
28
|
+
isS2TokenValid
|
|
29
|
+
} from './s2-geometry/s2-token';
|
|
30
|
+
export {getS2DescendantIndex, getS2IndexFromCell} from './s2-geometry/s2-geometry';
|
|
31
|
+
export type {S2Cell} from './s2-geometry/s2-geometry';
|
|
21
32
|
|
|
22
33
|
/** Lightweight decoders bundled with this module. */
|
|
23
34
|
export const DGGS_DECODERS: readonly DGGSDecoder[] = [
|
package/src/plus-code-decoder.ts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import OpenLocationCodePackage, {type OpenLocationCodeArea} from 'open-location-code';
|
|
6
6
|
import {type Bounds2D} from '@math.gl/types';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
withDGGSBoundaryOptions,
|
|
9
|
+
type DGGSCell,
|
|
10
|
+
type DGGSDecoder,
|
|
11
|
+
getDGGSCellToken
|
|
12
|
+
} from './dggs-decoder';
|
|
8
13
|
|
|
9
14
|
const openLocationCode = new OpenLocationCodePackage.OpenLocationCode();
|
|
10
15
|
|
|
@@ -12,7 +17,7 @@ const openLocationCode = new OpenLocationCodePackage.OpenLocationCode();
|
|
|
12
17
|
* Lightweight geometry decoder for full Google Plus Codes (Open Location Codes).
|
|
13
18
|
* Short codes require a reference location and are intentionally unsupported.
|
|
14
19
|
*/
|
|
15
|
-
export const PlusCodeDecoder = {
|
|
20
|
+
export const PlusCodeDecoder = withDGGSBoundaryOptions({
|
|
16
21
|
name: 'plus-code',
|
|
17
22
|
hasNumericRepresentation: false,
|
|
18
23
|
cellColumnNames: ['plusCode', 'plus_code', 'openLocationCode', 'open_location_code', 'olc'],
|
|
@@ -29,7 +34,7 @@ export const PlusCodeDecoder = {
|
|
|
29
34
|
[area.longitudeHi, area.latitudeHi]
|
|
30
35
|
];
|
|
31
36
|
}
|
|
32
|
-
} as const satisfies DGGSDecoder;
|
|
37
|
+
} as const satisfies DGGSDecoder);
|
|
33
38
|
|
|
34
39
|
function getPlusCodeLngLat(cell: DGGSCell): [number, number] {
|
|
35
40
|
const area = decodePlusCode(cell);
|
package/src/quadkey-decoder.ts
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
5
|
import {type Bounds2D} from '@math.gl/types';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
withDGGSBoundaryOptions,
|
|
8
|
+
type DGGSCell,
|
|
9
|
+
type DGGSDecoder,
|
|
10
|
+
getDGGSCellToken
|
|
11
|
+
} from './dggs-decoder';
|
|
7
12
|
|
|
8
13
|
const TILE_SIZE = 512;
|
|
9
14
|
|
|
10
15
|
/** Decoder for the quadkey DGGS */
|
|
11
|
-
export const QuadkeyDecoder = {
|
|
16
|
+
export const QuadkeyDecoder = withDGGSBoundaryOptions({
|
|
12
17
|
name: 'quadkey',
|
|
13
18
|
hasNumericRepresentation: false,
|
|
14
19
|
cellColumnNames: ['quadkey', 'quadkeyId', 'quadkey_id'],
|
|
@@ -19,7 +24,7 @@ export const QuadkeyDecoder = {
|
|
|
19
24
|
cellToBoundaryFlat: (cell: DGGSCell): number[] =>
|
|
20
25
|
getQuadkeyBoundaryFlat(getDGGSCellToken(cell, 'Quadkey')),
|
|
21
26
|
cellToBounds: (cell: DGGSCell): Bounds2D => getQuadkeyBounds(getDGGSCellToken(cell, 'Quadkey'))
|
|
22
|
-
} as const satisfies DGGSDecoder;
|
|
27
|
+
} as const satisfies DGGSDecoder);
|
|
23
28
|
|
|
24
29
|
function getQuadkeyLngLat(quadkey: string): [number, number] {
|
|
25
30
|
const [topLeft, bottomRight] = quadkeyToWorldBounds(quadkey);
|
package/src/s2-decoder.ts
CHANGED
|
@@ -3,25 +3,24 @@
|
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
5
|
import {type Bounds2D} from '@math.gl/types';
|
|
6
|
-
import {type DGGSCell, type DGGSDecoder} from './dggs-decoder';
|
|
6
|
+
import {withDGGSBoundaryOptions, type DGGSCell, type DGGSDecoder} from './dggs-decoder';
|
|
7
7
|
|
|
8
8
|
import {getS2IndexFromToken, getS2TokenFromIndex} from './s2-geometry/s2-token';
|
|
9
9
|
import {getS2GeoBounds} from './s2-geometry/s2-to-boundary';
|
|
10
10
|
import {getS2Cell, IJToST, STToUV, FaceUVToXYZ, XYZToLngLat} from './s2-geometry/s2-geometry';
|
|
11
11
|
|
|
12
12
|
/** Decoder for the S2 DGGS */
|
|
13
|
-
export const S2Decoder = {
|
|
13
|
+
export const S2Decoder = withDGGSBoundaryOptions({
|
|
14
14
|
name: 's2',
|
|
15
15
|
hasNumericRepresentation: true,
|
|
16
16
|
cellColumnNames: ['s2', 's2Token', 's2_token', 's2Cell', 's2_cell', 's2CellId', 's2_cell_id'],
|
|
17
17
|
tokenToCell: (s2Token: string): bigint => getS2IndexFromToken(s2Token),
|
|
18
|
-
cellToToken: (cell: DGGSCell): string =>
|
|
19
|
-
typeof cell === 'string' ? cell : getS2TokenFromIndex(cell),
|
|
18
|
+
cellToToken: (cell: DGGSCell): string => getS2TokenFromIndex(getS2CellIndex(cell)),
|
|
20
19
|
cellToLngLat: (cell: DGGSCell): [number, number] => getS2LngLat(cell),
|
|
21
20
|
cellToBoundary: (cell: DGGSCell): [number, number][] => getS2Boundary(cell),
|
|
22
21
|
cellToBoundaryFlat: (cell: DGGSCell): number[] => getS2BoundaryFlat(cell),
|
|
23
22
|
cellToBounds: (cell: DGGSCell): Bounds2D => getS2Bounds(cell)
|
|
24
|
-
} as const satisfies DGGSDecoder;
|
|
23
|
+
} as const satisfies DGGSDecoder);
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* Retrieve S2 geometry center
|
|
@@ -60,28 +59,99 @@ function getS2Boundary(cell: DGGSCell): [number, number][] {
|
|
|
60
59
|
return boundary;
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Returns conservative longitude/latitude bounds for an S2 cell.
|
|
64
|
+
*
|
|
65
|
+
* Longitudes are unwrapped so `east >= west`; either endpoint may lie outside `[-180, 180]` for
|
|
66
|
+
* antimeridian cells. The two level-zero polar faces span a complete 360-degree longitude range.
|
|
67
|
+
*
|
|
68
|
+
* @param cell - S2 token or 64-bit cell index.
|
|
69
|
+
* @returns Bounds in `[[west, south], [east, north]]` order and degrees.
|
|
70
|
+
*/
|
|
71
|
+
export function getS2Bounds(cell: DGGSCell): Bounds2D {
|
|
72
|
+
const s2Cell = getS2Cell(getS2CellIndex(cell));
|
|
73
|
+
if (s2Cell.level === 0) {
|
|
74
|
+
return getS2RootBounds(s2Cell.face);
|
|
75
|
+
}
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
// For non-root S2 cells, latitude and longitude extrema occur at vertices. Root cells require
|
|
78
|
+
// the special cases above because their curved edges or interiors contain the extrema.
|
|
79
|
+
// https://github.com/google/s2geometry/blob/master/src/s2/s2cell.cc
|
|
80
|
+
const offsets: readonly [number, number][] = [
|
|
81
|
+
[0, 0],
|
|
82
|
+
[0, 1],
|
|
83
|
+
[1, 1],
|
|
84
|
+
[1, 0]
|
|
85
|
+
];
|
|
86
|
+
let west = Infinity;
|
|
87
|
+
let south = Infinity;
|
|
88
|
+
let east = -Infinity;
|
|
89
|
+
let north = -Infinity;
|
|
90
|
+
let previousLongitude: number | undefined;
|
|
91
|
+
for (const offset of offsets) {
|
|
92
|
+
const st = IJToST(s2Cell.ij, s2Cell.level, offset);
|
|
93
|
+
const uv = STToUV(st);
|
|
94
|
+
const [x, y, z] = FaceUVToXYZ(s2Cell.face, uv);
|
|
95
|
+
let [longitude, latitude] = XYZToLngLat([x, y, z]);
|
|
96
|
+
if (x === 0 && y === 0 && previousLongitude !== undefined) {
|
|
97
|
+
longitude = previousLongitude;
|
|
98
|
+
}
|
|
99
|
+
if (previousLongitude !== undefined) {
|
|
100
|
+
const longitudeDelta = longitude - previousLongitude;
|
|
101
|
+
longitude += longitudeDelta > 180 ? -360 : longitudeDelta < -180 ? 360 : 0;
|
|
102
|
+
}
|
|
103
|
+
previousLongitude = longitude;
|
|
104
|
+
west = Math.min(west, longitude);
|
|
105
|
+
south = Math.min(south, latitude);
|
|
106
|
+
east = Math.max(east, longitude);
|
|
107
|
+
north = Math.max(north, latitude);
|
|
77
108
|
}
|
|
78
109
|
|
|
110
|
+
const angularEpsilon = (2 * Number.EPSILON * 180) / Math.PI;
|
|
79
111
|
return [
|
|
80
|
-
[
|
|
81
|
-
[
|
|
112
|
+
[west - angularEpsilon, Math.max(-90, south - angularEpsilon)],
|
|
113
|
+
[east + angularEpsilon, Math.min(90, north + angularEpsilon)]
|
|
82
114
|
];
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
function getS2CellIndex(cell: DGGSCell): bigint {
|
|
86
118
|
return typeof cell === 'bigint' ? cell : getS2IndexFromToken(cell);
|
|
87
119
|
}
|
|
120
|
+
|
|
121
|
+
function getS2RootBounds(face: number): Bounds2D {
|
|
122
|
+
const polarLatitude = (Math.asin(Math.sqrt(1 / 3)) * 180) / Math.PI;
|
|
123
|
+
switch (face) {
|
|
124
|
+
case 0:
|
|
125
|
+
return [
|
|
126
|
+
[-45, -45],
|
|
127
|
+
[45, 45]
|
|
128
|
+
];
|
|
129
|
+
case 1:
|
|
130
|
+
return [
|
|
131
|
+
[45, -45],
|
|
132
|
+
[135, 45]
|
|
133
|
+
];
|
|
134
|
+
case 2:
|
|
135
|
+
return [
|
|
136
|
+
[-180, polarLatitude],
|
|
137
|
+
[180, 90]
|
|
138
|
+
];
|
|
139
|
+
case 3:
|
|
140
|
+
return [
|
|
141
|
+
[135, -45],
|
|
142
|
+
[225, 45]
|
|
143
|
+
];
|
|
144
|
+
case 4:
|
|
145
|
+
return [
|
|
146
|
+
[-135, -45],
|
|
147
|
+
[-45, 45]
|
|
148
|
+
];
|
|
149
|
+
case 5:
|
|
150
|
+
return [
|
|
151
|
+
[-180, -90],
|
|
152
|
+
[180, -polarLatitude]
|
|
153
|
+
];
|
|
154
|
+
default:
|
|
155
|
+
throw new Error(`Invalid S2 face: ${face}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -11,6 +11,8 @@ Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
|
11
11
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import {isS2IndexValid} from './s2-token';
|
|
15
|
+
|
|
14
16
|
export type S2Cell = {
|
|
15
17
|
face: number;
|
|
16
18
|
ij: [number, number];
|
|
@@ -18,6 +20,9 @@ export type S2Cell = {
|
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
export function getS2Cell(s2Index: bigint): S2Cell {
|
|
23
|
+
if (!isS2IndexValid(s2Index)) {
|
|
24
|
+
throw new Error(`Invalid S2 index: ${s2Index}`);
|
|
25
|
+
}
|
|
21
26
|
const key = toHilbertQuadkey(s2Index);
|
|
22
27
|
const s2cell = fromHilbertQuadKey(key);
|
|
23
28
|
return s2cell;
|
|
@@ -31,11 +36,46 @@ const MAX_LEVEL = 30;
|
|
|
31
36
|
const POS_BITS = 2 * MAX_LEVEL + 1; // 61 (60 bits of data, 1 bit lsb marker)
|
|
32
37
|
const RADIAN_TO_DEGREE = 180 / Math.PI;
|
|
33
38
|
|
|
39
|
+
type HilbertOrientation = 'a' | 'b' | 'c' | 'd';
|
|
40
|
+
|
|
41
|
+
const HILBERT_MAP: Record<
|
|
42
|
+
HilbertOrientation,
|
|
43
|
+
readonly (readonly [position: number, orientation: HilbertOrientation])[]
|
|
44
|
+
> = {
|
|
45
|
+
a: [
|
|
46
|
+
[0, 'd'],
|
|
47
|
+
[1, 'a'],
|
|
48
|
+
[3, 'b'],
|
|
49
|
+
[2, 'a']
|
|
50
|
+
],
|
|
51
|
+
b: [
|
|
52
|
+
[2, 'b'],
|
|
53
|
+
[1, 'b'],
|
|
54
|
+
[3, 'a'],
|
|
55
|
+
[0, 'c']
|
|
56
|
+
],
|
|
57
|
+
c: [
|
|
58
|
+
[2, 'c'],
|
|
59
|
+
[3, 'd'],
|
|
60
|
+
[1, 'c'],
|
|
61
|
+
[0, 'b']
|
|
62
|
+
],
|
|
63
|
+
d: [
|
|
64
|
+
[0, 'a'],
|
|
65
|
+
[3, 'c'],
|
|
66
|
+
[1, 'd'],
|
|
67
|
+
[2, 'd']
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
|
|
34
71
|
/*
|
|
35
72
|
Original function taken from deck.gl doesn't support the case of (face <= 5)
|
|
36
73
|
It's fixed here.
|
|
37
74
|
*/
|
|
38
75
|
export function fromHilbertQuadKey(hilbertQuadkey: string): S2Cell {
|
|
76
|
+
if (!/^[0-5]\/[0-3]{0,30}$/.test(hilbertQuadkey)) {
|
|
77
|
+
throw new Error(`Invalid S2 Hilbert quadkey: ${hilbertQuadkey}`);
|
|
78
|
+
}
|
|
39
79
|
const parts = hilbertQuadkey.split('/');
|
|
40
80
|
const face = parseInt(parts[0], 10);
|
|
41
81
|
const position = parts[1] || '';
|
|
@@ -75,6 +115,9 @@ export function fromHilbertQuadKey(hilbertQuadkey: string): S2Cell {
|
|
|
75
115
|
}
|
|
76
116
|
|
|
77
117
|
export function toHilbertQuadkey(id: bigint): string {
|
|
118
|
+
if (!isS2IndexValid(id)) {
|
|
119
|
+
throw new Error(`Invalid S2 index: ${id}`);
|
|
120
|
+
}
|
|
78
121
|
let bin = id.toString(2);
|
|
79
122
|
|
|
80
123
|
while (bin.length < FACE_BITS + POS_BITS) {
|
|
@@ -105,6 +148,86 @@ export function toHilbertQuadkey(id: bigint): string {
|
|
|
105
148
|
return `${faceS}/${posS}`;
|
|
106
149
|
}
|
|
107
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Converts face-local integer coordinates into an S2 cell index.
|
|
153
|
+
*
|
|
154
|
+
* @param s2Cell - Face, `(i, j)` coordinates, and level of an S2 cell.
|
|
155
|
+
* @returns The corresponding 64-bit S2 cell index.
|
|
156
|
+
*/
|
|
157
|
+
export function getS2IndexFromCell(s2Cell: S2Cell): bigint {
|
|
158
|
+
const {face, ij, level} = s2Cell;
|
|
159
|
+
if (!Number.isInteger(face) || face < 0 || face > 5) {
|
|
160
|
+
throw new Error(`Invalid S2 face: ${face}`);
|
|
161
|
+
}
|
|
162
|
+
if (!Number.isInteger(level) || level < 0 || level > MAX_LEVEL) {
|
|
163
|
+
throw new Error(`Invalid S2 level: ${level}`);
|
|
164
|
+
}
|
|
165
|
+
const maximumCoordinate = 2 ** level;
|
|
166
|
+
if (
|
|
167
|
+
!Number.isInteger(ij[0]) ||
|
|
168
|
+
!Number.isInteger(ij[1]) ||
|
|
169
|
+
ij[0] < 0 ||
|
|
170
|
+
ij[1] < 0 ||
|
|
171
|
+
ij[0] >= maximumCoordinate ||
|
|
172
|
+
ij[1] >= maximumCoordinate
|
|
173
|
+
) {
|
|
174
|
+
throw new Error(`Invalid S2 IJ coordinates: ${ij[0]}, ${ij[1]}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const positions = getHilbertQuadList(ij[0], ij[1], level, face);
|
|
178
|
+
let s2Index = BigInt(face) << 61n;
|
|
179
|
+
for (let positionIndex = 0; positionIndex < positions.length; positionIndex++) {
|
|
180
|
+
const bitOffset = 59 - positionIndex * 2;
|
|
181
|
+
s2Index |= BigInt(positions[positionIndex]) << BigInt(bitOffset);
|
|
182
|
+
}
|
|
183
|
+
s2Index |= 1n << BigInt(60 - level * 2);
|
|
184
|
+
return s2Index;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Returns a spatially addressed descendant of an S2 cell.
|
|
189
|
+
*
|
|
190
|
+
* Unlike {@link getS2ChildIndex}, `x` and `y` address the descendant along increasing face-local
|
|
191
|
+
* `i` and `j` axes. This accounts for Hilbert orientation changes across faces and levels.
|
|
192
|
+
*
|
|
193
|
+
* @param rootIndex - Valid S2 cell index at the root of the relative grid.
|
|
194
|
+
* @param relativeLevel - Number of subdivision levels below the root.
|
|
195
|
+
* @param x - Descendant coordinate along the root cell's increasing `i` axis.
|
|
196
|
+
* @param y - Descendant coordinate along the root cell's increasing `j` axis.
|
|
197
|
+
* @returns The descendant S2 cell index.
|
|
198
|
+
*/
|
|
199
|
+
export function getS2DescendantIndex(
|
|
200
|
+
rootIndex: bigint,
|
|
201
|
+
relativeLevel: number,
|
|
202
|
+
x: number,
|
|
203
|
+
y: number
|
|
204
|
+
): bigint {
|
|
205
|
+
const rootCell = getS2Cell(rootIndex);
|
|
206
|
+
if (
|
|
207
|
+
!Number.isInteger(relativeLevel) ||
|
|
208
|
+
relativeLevel < 0 ||
|
|
209
|
+
rootCell.level + relativeLevel > MAX_LEVEL
|
|
210
|
+
) {
|
|
211
|
+
throw new Error(`Invalid S2 relative level: ${relativeLevel}`);
|
|
212
|
+
}
|
|
213
|
+
const divisionCount = 2 ** relativeLevel;
|
|
214
|
+
if (
|
|
215
|
+
!Number.isInteger(x) ||
|
|
216
|
+
!Number.isInteger(y) ||
|
|
217
|
+
x < 0 ||
|
|
218
|
+
y < 0 ||
|
|
219
|
+
x >= divisionCount ||
|
|
220
|
+
y >= divisionCount
|
|
221
|
+
) {
|
|
222
|
+
throw new Error(`Invalid S2 descendant coordinates: ${x}, ${y}`);
|
|
223
|
+
}
|
|
224
|
+
return getS2IndexFromCell({
|
|
225
|
+
face: rootCell.face,
|
|
226
|
+
ij: [rootCell.ij[0] * divisionCount + x, rootCell.ij[1] * divisionCount + y],
|
|
227
|
+
level: rootCell.level + relativeLevel
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
108
231
|
export function IJToST(
|
|
109
232
|
ij: [number, number],
|
|
110
233
|
order: number,
|
|
@@ -164,3 +287,17 @@ function rotateAndFlipQuadrant(n: number, point: [number, number], rx: number, r
|
|
|
164
287
|
point[1] = x;
|
|
165
288
|
}
|
|
166
289
|
}
|
|
290
|
+
|
|
291
|
+
function getHilbertQuadList(x: number, y: number, level: number, face: number): number[] {
|
|
292
|
+
let orientation: HilbertOrientation = face % 2 === 1 ? 'd' : 'a';
|
|
293
|
+
const positions: number[] = [];
|
|
294
|
+
for (let bit = level - 1; bit >= 0; bit--) {
|
|
295
|
+
const mask = 1 << bit;
|
|
296
|
+
const quadrantX = (x & mask) !== 0 ? 1 : 0;
|
|
297
|
+
const quadrantY = (y & mask) !== 0 ? 1 : 0;
|
|
298
|
+
const [position, nextOrientation] = HILBERT_MAP[orientation][quadrantX * 2 + quadrantY];
|
|
299
|
+
positions.push(position);
|
|
300
|
+
orientation = nextOrientation;
|
|
301
|
+
}
|
|
302
|
+
return positions;
|
|
303
|
+
}
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
// s2-geometry is a pure JavaScript port of Google/Niantic's S2 Geometry library
|
|
6
6
|
// which is perfect since it works in the browser.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const MAXIMUM_TOKEN_LENGTH = 16;
|
|
9
|
+
const MAXIMUM_S2_INDEX = (1n << 64n) - 1n;
|
|
10
|
+
const VALID_LOWEST_ON_BITS = 0x1555555555555555n;
|
|
11
|
+
const MAXIMUM_LEVEL = 30;
|
|
9
12
|
|
|
10
13
|
// INDEX CALCULATIONS
|
|
11
14
|
|
|
@@ -16,10 +19,13 @@
|
|
|
16
19
|
*/
|
|
17
20
|
export function getS2IndexFromToken(token: string): bigint {
|
|
18
21
|
if (token === 'X') {
|
|
19
|
-
|
|
22
|
+
return 0n;
|
|
23
|
+
}
|
|
24
|
+
if (!isS2TokenValid(token)) {
|
|
25
|
+
throw new Error(`Invalid S2 token: ${token}`);
|
|
20
26
|
}
|
|
21
27
|
// pad token with zeros to make the length 16
|
|
22
|
-
const paddedToken = token.padEnd(
|
|
28
|
+
const paddedToken = token.padEnd(MAXIMUM_TOKEN_LENGTH, '0');
|
|
23
29
|
return BigInt(`0x${paddedToken}`);
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -31,6 +37,9 @@ export function getS2TokenFromIndex(cellId: bigint): string {
|
|
|
31
37
|
if (cellId === 0n) {
|
|
32
38
|
return 'X';
|
|
33
39
|
}
|
|
40
|
+
if (!isS2IndexValid(cellId)) {
|
|
41
|
+
throw new Error(`Invalid S2 index: ${cellId}`);
|
|
42
|
+
}
|
|
34
43
|
let numZeroDigits = countTrailingZeros(cellId);
|
|
35
44
|
|
|
36
45
|
const remainder = numZeroDigits % 4;
|
|
@@ -44,7 +53,67 @@ export function getS2TokenFromIndex(cellId: bigint): string {
|
|
|
44
53
|
return zeroString + hexString;
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Returns whether a value is a valid non-empty 64-bit S2 cell index.
|
|
58
|
+
*
|
|
59
|
+
* @param s2Index - Candidate S2 cell index.
|
|
60
|
+
* @returns `true` when the face and level marker encode a valid S2 cell.
|
|
61
|
+
*/
|
|
62
|
+
export function isS2IndexValid(s2Index: unknown): s2Index is bigint {
|
|
63
|
+
if (typeof s2Index !== 'bigint' || s2Index <= 0n || s2Index > MAXIMUM_S2_INDEX) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const face = s2Index >> 61n;
|
|
67
|
+
const lowestOnBit = s2Index & -s2Index;
|
|
68
|
+
return face < 6n && (lowestOnBit & VALID_LOWEST_ON_BITS) !== 0n;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns whether a string is a canonical token for a non-empty S2 cell.
|
|
73
|
+
*
|
|
74
|
+
* The empty-cell sentinel `X` is supported by the conversion functions but is not a valid cell.
|
|
75
|
+
*
|
|
76
|
+
* @param token - Candidate hexadecimal S2 token.
|
|
77
|
+
* @returns `true` when the token canonically encodes a valid S2 cell.
|
|
78
|
+
*/
|
|
79
|
+
export function isS2TokenValid(token: unknown): token is string {
|
|
80
|
+
if (typeof token !== 'string' || !/^[0-9a-fA-F]{1,16}$/.test(token) || token.endsWith('0')) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const paddedToken = token.padEnd(MAXIMUM_TOKEN_LENGTH, '0');
|
|
84
|
+
return isS2IndexValid(BigInt(`0x${paddedToken}`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns the subdivision level encoded by an S2 cell index.
|
|
89
|
+
*
|
|
90
|
+
* @param s2Index - Valid non-empty S2 cell index.
|
|
91
|
+
* @returns The S2 level in the range `[0, 30]`.
|
|
92
|
+
*/
|
|
93
|
+
export function getS2Level(s2Index: bigint): number {
|
|
94
|
+
if (!isS2IndexValid(s2Index)) {
|
|
95
|
+
throw new Error(`Invalid S2 index: ${s2Index}`);
|
|
96
|
+
}
|
|
97
|
+
return (60 - countTrailingZeros(s2Index)) / 2;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns one of the four Hilbert-ordered children of an S2 cell.
|
|
102
|
+
*
|
|
103
|
+
* @param s2Index - Valid S2 cell index below level 30.
|
|
104
|
+
* @param index - Hilbert child position in the range `[0, 3]`.
|
|
105
|
+
* @returns The child S2 cell index.
|
|
106
|
+
*/
|
|
47
107
|
export function getS2ChildIndex(s2Index: bigint, index: number): bigint {
|
|
108
|
+
if (!isS2IndexValid(s2Index)) {
|
|
109
|
+
throw new Error(`Invalid S2 index: ${s2Index}`);
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isInteger(index) || index < 0 || index > 3) {
|
|
112
|
+
throw new Error(`Invalid S2 child index: ${index}`);
|
|
113
|
+
}
|
|
114
|
+
if (getS2Level(s2Index) === MAXIMUM_LEVEL) {
|
|
115
|
+
throw new Error('Cannot subdivide an S2 leaf cell');
|
|
116
|
+
}
|
|
48
117
|
// Shift sentinel bit 2 positions to the right.
|
|
49
118
|
const newLsb = lsb(s2Index) >> 2n;
|
|
50
119
|
// Insert child index before the sentinel bit.
|
|
@@ -57,7 +126,7 @@ export function getS2ChildIndex(s2Index: bigint, index: number): bigint {
|
|
|
57
126
|
* @private
|
|
58
127
|
*/
|
|
59
128
|
function lsb(cellId: bigint): bigint {
|
|
60
|
-
return cellId &
|
|
129
|
+
return cellId & -cellId; // eslint-disable-line
|
|
61
130
|
}
|
|
62
131
|
|
|
63
132
|
function countTrailingZeros(n: bigint): number {
|
package/src/s2.ts
CHANGED
|
@@ -2,5 +2,14 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
-
export {S2Decoder} from './s2-decoder';
|
|
6
|
-
export {
|
|
5
|
+
export {S2Decoder, getS2Bounds, getS2LngLat, getS2BoundaryFlat} from './s2-decoder';
|
|
6
|
+
export {
|
|
7
|
+
getS2ChildIndex,
|
|
8
|
+
getS2IndexFromToken,
|
|
9
|
+
getS2Level,
|
|
10
|
+
getS2TokenFromIndex,
|
|
11
|
+
isS2IndexValid,
|
|
12
|
+
isS2TokenValid
|
|
13
|
+
} from './s2-geometry/s2-token';
|
|
14
|
+
export {getS2DescendantIndex, getS2IndexFromCell} from './s2-geometry/s2-geometry';
|
|
15
|
+
export type {S2Cell} from './s2-geometry/s2-geometry';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Copy a cell boundary, unwrapping longitudes across the antimeridian.
|
|
7
|
+
* @param boundary Open or closed longitude/latitude pairs in degrees.
|
|
8
|
+
* @param referenceLongitude Optional longitude near which to place the first vertex.
|
|
9
|
+
* @returns Fresh pairs with each longitude nearest the preceding longitude. Exact
|
|
10
|
+
* 180-degree ties retain their direction. Latitudes and vertex order are preserved.
|
|
11
|
+
* @remarks Explicit boundaries spanning at least 360 degrees and closed rings with
|
|
12
|
+
* net longitude winding are copied unchanged. Polar cells need additional topology
|
|
13
|
+
* handling. This helper neither splits polygons nor calculates geodesic edges.
|
|
14
|
+
*/
|
|
15
|
+
export function unwrapDGGSBoundary(
|
|
16
|
+
boundary: ReadonlyArray<readonly [number, number]>,
|
|
17
|
+
referenceLongitude?: number
|
|
18
|
+
): [number, number][] {
|
|
19
|
+
const result: [number, number][] = boundary.map(([longitude, latitude]) => [longitude, latitude]);
|
|
20
|
+
if (result.length === 0) {
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let west = Infinity;
|
|
25
|
+
let east = -Infinity;
|
|
26
|
+
for (const [longitude] of boundary) {
|
|
27
|
+
west = Math.min(west, longitude);
|
|
28
|
+
east = Math.max(east, longitude);
|
|
29
|
+
}
|
|
30
|
+
if (east - west >= 360) {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let previous = referenceLongitude ?? result[0][0];
|
|
35
|
+
for (const point of result) {
|
|
36
|
+
const delta = point[0] - previous;
|
|
37
|
+
if (delta > 180) {
|
|
38
|
+
point[0] -= 360 * Math.ceil((delta - 180) / 360);
|
|
39
|
+
} else if (delta < -180) {
|
|
40
|
+
point[0] += 360 * Math.ceil((-delta - 180) / 360);
|
|
41
|
+
}
|
|
42
|
+
previous = point[0];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const first = boundary[0];
|
|
46
|
+
const last = boundary[boundary.length - 1];
|
|
47
|
+
if (first[0] === last[0] && first[1] === last[1]) {
|
|
48
|
+
if (Math.abs(result[result.length - 1][0] - result[0][0]) > 180) {
|
|
49
|
+
return boundary.map(([longitude, latitude]) => [longitude, latitude]);
|
|
50
|
+
}
|
|
51
|
+
result[result.length - 1] = [...result[0]];
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|