@math.gl/dggs 5.0.0-alpha.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/LICENSE +140 -0
- package/README.md +7 -0
- package/dist/a5-decoder.d.ts +15 -0
- package/dist/a5-decoder.d.ts.map +1 -0
- package/dist/a5-decoder.js +36 -0
- package/dist/a5-decoder.js.map +1 -0
- package/dist/a5.cjs +134 -0
- package/dist/a5.cjs.map +6 -0
- package/dist/a5.d.ts +2 -0
- package/dist/a5.d.ts.map +1 -0
- package/dist/a5.js +5 -0
- package/dist/a5.js.map +1 -0
- package/dist/converters/s2-to-obb-points.d.ts +15 -0
- package/dist/converters/s2-to-obb-points.d.ts.map +1 -0
- package/dist/converters/s2-to-obb-points.js +60 -0
- package/dist/converters/s2-to-obb-points.js.map +1 -0
- package/dist/converters/s2-to-region.d.ts +6 -0
- package/dist/converters/s2-to-region.d.ts.map +1 -0
- package/dist/converters/s2-to-region.js +27 -0
- package/dist/converters/s2-to-region.js.map +1 -0
- package/dist/dggs-decoder.d.ts +46 -0
- package/dist/dggs-decoder.d.ts.map +1 -0
- package/dist/dggs-decoder.js +52 -0
- package/dist/dggs-decoder.js.map +1 -0
- package/dist/geohash-decoder.d.ts +17 -0
- package/dist/geohash-decoder.d.ts.map +1 -0
- package/dist/geohash-decoder.js +88 -0
- package/dist/geohash-decoder.js.map +1 -0
- package/dist/geohash.cjs +181 -0
- package/dist/geohash.cjs.map +6 -0
- package/dist/geohash.d.ts +2 -0
- package/dist/geohash.d.ts.map +1 -0
- package/dist/geohash.js +5 -0
- package/dist/geohash.js.map +1 -0
- package/dist/h3-decoder.d.ts +15 -0
- package/dist/h3-decoder.d.ts.map +1 -0
- package/dist/h3-decoder.js +46 -0
- package/dist/h3-decoder.js.map +1 -0
- package/dist/h3.cjs +144 -0
- package/dist/h3.cjs.map +6 -0
- package/dist/h3.d.ts +2 -0
- package/dist/h3.d.ts.map +1 -0
- package/dist/h3.js +5 -0
- package/dist/h3.js.map +1 -0
- package/dist/index.cjs +863 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/plus-code-decoder.d.ts +16 -0
- package/dist/plus-code-decoder.d.ts.map +1 -0
- package/dist/plus-code-decoder.js +50 -0
- package/dist/plus-code-decoder.js.map +1 -0
- package/dist/plus-code.cjs +161 -0
- package/dist/plus-code.cjs.map +6 -0
- package/dist/plus-code.d.ts +2 -0
- package/dist/plus-code.d.ts.map +1 -0
- package/dist/plus-code.js +5 -0
- package/dist/plus-code.js.map +1 -0
- package/dist/quadkey-decoder.d.ts +14 -0
- package/dist/quadkey-decoder.d.ts.map +1 -0
- package/dist/quadkey-decoder.js +86 -0
- package/dist/quadkey-decoder.js.map +1 -0
- package/dist/quadkey.cjs +179 -0
- package/dist/quadkey.cjs.map +6 -0
- package/dist/quadkey.d.ts +2 -0
- package/dist/quadkey.d.ts.map +1 -0
- package/dist/quadkey.js +5 -0
- package/dist/quadkey.js.map +1 -0
- package/dist/s2-decoder.d.ts +36 -0
- package/dist/s2-decoder.d.ts.map +1 -0
- package/dist/s2-decoder.js +144 -0
- package/dist/s2-decoder.js.map +1 -0
- package/dist/s2-geometry/s2-geometry.d.ts +33 -0
- package/dist/s2-geometry/s2-geometry.d.ts.map +1 -0
- package/dist/s2-geometry/s2-geometry.js +249 -0
- package/dist/s2-geometry/s2-geometry.js.map +1 -0
- package/dist/s2-geometry/s2-to-boundary.d.ts +14 -0
- package/dist/s2-geometry/s2-to-boundary.d.ts.map +1 -0
- package/dist/s2-geometry/s2-to-boundary.js +60 -0
- package/dist/s2-geometry/s2-to-boundary.js.map +1 -0
- package/dist/s2-geometry/s2-token.d.ts +43 -0
- package/dist/s2-geometry/s2-token.d.ts.map +1 -0
- package/dist/s2-geometry/s2-token.js +127 -0
- package/dist/s2-geometry/s2-token.js.map +1 -0
- package/dist/s2.cjs +543 -0
- package/dist/s2.cjs.map +6 -0
- package/dist/s2.d.ts +5 -0
- package/dist/s2.d.ts.map +1 -0
- package/dist/s2.js +7 -0
- package/dist/s2.js.map +1 -0
- 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 +80 -0
- package/src/a5-decoder.ts +47 -0
- package/src/a5.ts +5 -0
- package/src/converters/s2-to-obb-points.ts +78 -0
- package/src/converters/s2-to-region.ts +30 -0
- package/src/dggs-decoder.ts +108 -0
- package/src/geohash-decoder.ts +104 -0
- package/src/geohash.ts +5 -0
- package/src/h3-decoder.ts +58 -0
- package/src/h3.ts +5 -0
- package/src/index.ts +72 -0
- package/src/open-location-code.d.ts +19 -0
- package/src/plus-code-decoder.ts +61 -0
- package/src/plus-code.ts +5 -0
- package/src/quadkey-decoder.ts +103 -0
- package/src/quadkey.ts +5 -0
- package/src/s2-decoder.ts +157 -0
- package/src/s2-geometry/s2-geometry.ts +303 -0
- package/src/s2-geometry/s2-to-boundary.ts +76 -0
- package/src/s2-geometry/s2-token.ts +139 -0
- package/src/s2.ts +15 -0
- package/src/unwrap-dggs-boundary.ts +54 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {type Bounds2D} from '@math.gl/types';
|
|
6
|
+
import {
|
|
7
|
+
withDGGSBoundaryOptions,
|
|
8
|
+
type DGGSCell,
|
|
9
|
+
type DGGSDecoder,
|
|
10
|
+
getDGGSCellToken
|
|
11
|
+
} from './dggs-decoder';
|
|
12
|
+
|
|
13
|
+
const TILE_SIZE = 512;
|
|
14
|
+
|
|
15
|
+
/** Decoder for the quadkey DGGS */
|
|
16
|
+
export const QuadkeyDecoder = withDGGSBoundaryOptions({
|
|
17
|
+
name: 'quadkey',
|
|
18
|
+
hasNumericRepresentation: false,
|
|
19
|
+
cellColumnNames: ['quadkey', 'quadkeyId', 'quadkey_id'],
|
|
20
|
+
cellToLngLat: (cell: DGGSCell): [number, number] =>
|
|
21
|
+
getQuadkeyLngLat(getDGGSCellToken(cell, 'Quadkey')),
|
|
22
|
+
cellToBoundary: (cell: DGGSCell): [number, number][] =>
|
|
23
|
+
getQuadkeyBoundary(getDGGSCellToken(cell, 'Quadkey')),
|
|
24
|
+
cellToBoundaryFlat: (cell: DGGSCell): number[] =>
|
|
25
|
+
getQuadkeyBoundaryFlat(getDGGSCellToken(cell, 'Quadkey')),
|
|
26
|
+
cellToBounds: (cell: DGGSCell): Bounds2D => getQuadkeyBounds(getDGGSCellToken(cell, 'Quadkey'))
|
|
27
|
+
} as const satisfies DGGSDecoder);
|
|
28
|
+
|
|
29
|
+
function getQuadkeyLngLat(quadkey: string): [number, number] {
|
|
30
|
+
const [topLeft, bottomRight] = quadkeyToWorldBounds(quadkey);
|
|
31
|
+
const [w, n] = worldToLngLat(topLeft);
|
|
32
|
+
const [e, s] = worldToLngLat(bottomRight);
|
|
33
|
+
return [(e + w) / 2, (s + n) / 2];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getQuadkeyBoundary(quadkey: string): [number, number][] {
|
|
37
|
+
const [topLeft, bottomRight] = quadkeyToWorldBounds(quadkey);
|
|
38
|
+
const [w, n] = worldToLngLat(topLeft);
|
|
39
|
+
const [e, s] = worldToLngLat(bottomRight);
|
|
40
|
+
return [
|
|
41
|
+
[e, n],
|
|
42
|
+
[e, s],
|
|
43
|
+
[w, s],
|
|
44
|
+
[w, n],
|
|
45
|
+
[e, n]
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getQuadkeyBoundaryFlat(quadkey: string): number[] {
|
|
50
|
+
const [topLeft, bottomRight] = quadkeyToWorldBounds(quadkey);
|
|
51
|
+
const [w, n] = worldToLngLat(topLeft);
|
|
52
|
+
const [e, s] = worldToLngLat(bottomRight);
|
|
53
|
+
return [e, n, e, s, w, s, w, n, e, n];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getQuadkeyBounds(quadkey: string): Bounds2D {
|
|
57
|
+
const [topLeft, bottomRight] = quadkeyToWorldBounds(quadkey);
|
|
58
|
+
const [w, n] = worldToLngLat(topLeft);
|
|
59
|
+
const [e, s] = worldToLngLat(bottomRight);
|
|
60
|
+
return [
|
|
61
|
+
[w, s],
|
|
62
|
+
[e, n]
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function quadkeyToWorldBounds(quadkey: string): [[number, number], [number, number]] {
|
|
67
|
+
let x = 0;
|
|
68
|
+
let y = 0;
|
|
69
|
+
let mask = 1 << quadkey.length;
|
|
70
|
+
const scale = mask / TILE_SIZE;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < quadkey.length; i++) {
|
|
73
|
+
mask >>= 1;
|
|
74
|
+
const q = parseInt(quadkey[i], 10);
|
|
75
|
+
if (q % 2) x |= mask;
|
|
76
|
+
if (q > 1) y |= mask;
|
|
77
|
+
}
|
|
78
|
+
return [
|
|
79
|
+
[x / scale, TILE_SIZE - y / scale],
|
|
80
|
+
[(x + 0.99) / scale, TILE_SIZE - (y + 0.99) / scale]
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// CONSTANTS
|
|
85
|
+
const PI = Math.PI;
|
|
86
|
+
const PI_4 = PI / 4;
|
|
87
|
+
const RADIANS_TO_DEGREES = 180 / PI;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Unproject world point [x,y] on map onto {lat, lon} on sphere
|
|
91
|
+
*
|
|
92
|
+
* @param xy - array with [x,y] members
|
|
93
|
+
* representing point on projected map plane
|
|
94
|
+
* @return - array with [x,y] of point on sphere.
|
|
95
|
+
* Has toArray method if you need a GeoJSON Array.
|
|
96
|
+
* Per cartographic tradition, lat and lon are specified as degrees.
|
|
97
|
+
*/
|
|
98
|
+
function worldToLngLat(xy: number[]): [number, number] {
|
|
99
|
+
const [x, y] = xy;
|
|
100
|
+
const lambda2 = (x / TILE_SIZE) * (2 * PI) - PI;
|
|
101
|
+
const phi2 = 2 * (Math.atan(Math.exp((y / TILE_SIZE) * (2 * PI) - PI)) - PI_4);
|
|
102
|
+
return [lambda2 * RADIANS_TO_DEGREES, phi2 * RADIANS_TO_DEGREES];
|
|
103
|
+
}
|
package/src/quadkey.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {type Bounds2D} from '@math.gl/types';
|
|
6
|
+
import {withDGGSBoundaryOptions, type DGGSCell, type DGGSDecoder} from './dggs-decoder';
|
|
7
|
+
|
|
8
|
+
import {getS2IndexFromToken, getS2TokenFromIndex} from './s2-geometry/s2-token';
|
|
9
|
+
import {getS2GeoBounds} from './s2-geometry/s2-to-boundary';
|
|
10
|
+
import {getS2Cell, IJToST, STToUV, FaceUVToXYZ, XYZToLngLat} from './s2-geometry/s2-geometry';
|
|
11
|
+
|
|
12
|
+
/** Decoder for the S2 DGGS */
|
|
13
|
+
export const S2Decoder = withDGGSBoundaryOptions({
|
|
14
|
+
name: 's2',
|
|
15
|
+
hasNumericRepresentation: true,
|
|
16
|
+
cellColumnNames: ['s2', 's2Token', 's2_token', 's2Cell', 's2_cell', 's2CellId', 's2_cell_id'],
|
|
17
|
+
tokenToCell: (s2Token: string): bigint => getS2IndexFromToken(s2Token),
|
|
18
|
+
cellToToken: (cell: DGGSCell): string => getS2TokenFromIndex(getS2CellIndex(cell)),
|
|
19
|
+
cellToLngLat: (cell: DGGSCell): [number, number] => getS2LngLat(cell),
|
|
20
|
+
cellToBoundary: (cell: DGGSCell): [number, number][] => getS2Boundary(cell),
|
|
21
|
+
cellToBoundaryFlat: (cell: DGGSCell): number[] => getS2BoundaryFlat(cell),
|
|
22
|
+
cellToBounds: (cell: DGGSCell): Bounds2D => getS2Bounds(cell)
|
|
23
|
+
} as const satisfies DGGSDecoder);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Retrieve S2 geometry center
|
|
27
|
+
*/
|
|
28
|
+
export function getS2LngLat(cell: DGGSCell): [number, number] {
|
|
29
|
+
const s2Index = getS2CellIndex(cell);
|
|
30
|
+
const s2Cell = getS2Cell(s2Index);
|
|
31
|
+
|
|
32
|
+
const st = IJToST(s2Cell.ij, s2Cell.level, [0.5, 0.5]);
|
|
33
|
+
const uv = STToUV(st);
|
|
34
|
+
const xyz = FaceUVToXYZ(s2Cell.face, uv);
|
|
35
|
+
const lngLat = XYZToLngLat(xyz);
|
|
36
|
+
|
|
37
|
+
return lngLat as [number, number];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a polygon with corner coordinates for an s2 cell
|
|
42
|
+
* @param - This can be an S2 key or token
|
|
43
|
+
* @return {Float64Array} - a simple polygon in flat array format: [lng0, lat0, lng1, lat1, ...]
|
|
44
|
+
* - the polygon is closed, i.e. last coordinate is a copy of the first coordinate
|
|
45
|
+
*/
|
|
46
|
+
export function getS2BoundaryFlat(cell: DGGSCell): number[] {
|
|
47
|
+
const s2Index = getS2CellIndex(cell);
|
|
48
|
+
const float64Array = getS2GeoBounds(s2Index);
|
|
49
|
+
// TODO - inefficient
|
|
50
|
+
return Array.from(float64Array);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getS2Boundary(cell: DGGSCell): [number, number][] {
|
|
54
|
+
const flatBoundary = getS2BoundaryFlat(cell);
|
|
55
|
+
const boundary: [number, number][] = [];
|
|
56
|
+
for (let i = 0; i < flatBoundary.length; i += 2) {
|
|
57
|
+
boundary.push([flatBoundary[i], flatBoundary[i + 1]]);
|
|
58
|
+
}
|
|
59
|
+
return boundary;
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
+
}
|
|
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);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const angularEpsilon = (2 * Number.EPSILON * 180) / Math.PI;
|
|
111
|
+
return [
|
|
112
|
+
[west - angularEpsilon, Math.max(-90, south - angularEpsilon)],
|
|
113
|
+
[east + angularEpsilon, Math.min(90, north + angularEpsilon)]
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getS2CellIndex(cell: DGGSCell): bigint {
|
|
118
|
+
return typeof cell === 'bigint' ? cell : getS2IndexFromToken(cell);
|
|
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
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT and ISC
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
// math.gl, MIT license
|
|
6
|
+
/*
|
|
7
|
+
Adapted from s2-geometry under ISC License (ISC)
|
|
8
|
+
Copyright (c) 2012-2016, Jon Atkins <github@jonatkins.com>
|
|
9
|
+
Copyright (c) 2016, AJ ONeal <aj@daplie.com>
|
|
10
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
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
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {isS2IndexValid} from './s2-token';
|
|
15
|
+
|
|
16
|
+
export type S2Cell = {
|
|
17
|
+
face: number;
|
|
18
|
+
ij: [number, number];
|
|
19
|
+
level: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function getS2Cell(s2Index: bigint): S2Cell {
|
|
23
|
+
if (!isS2IndexValid(s2Index)) {
|
|
24
|
+
throw new Error(`Invalid S2 index: ${s2Index}`);
|
|
25
|
+
}
|
|
26
|
+
const key = toHilbertQuadkey(s2Index);
|
|
27
|
+
const s2cell = fromHilbertQuadKey(key);
|
|
28
|
+
return s2cell;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//
|
|
32
|
+
// Functional Style
|
|
33
|
+
//
|
|
34
|
+
const FACE_BITS = 3;
|
|
35
|
+
const MAX_LEVEL = 30;
|
|
36
|
+
const POS_BITS = 2 * MAX_LEVEL + 1; // 61 (60 bits of data, 1 bit lsb marker)
|
|
37
|
+
const RADIAN_TO_DEGREE = 180 / Math.PI;
|
|
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
|
+
|
|
71
|
+
/*
|
|
72
|
+
Original function taken from deck.gl doesn't support the case of (face <= 5)
|
|
73
|
+
It's fixed here.
|
|
74
|
+
*/
|
|
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
|
+
}
|
|
79
|
+
const parts = hilbertQuadkey.split('/');
|
|
80
|
+
const face = parseInt(parts[0], 10);
|
|
81
|
+
const position = parts[1] || '';
|
|
82
|
+
const maxLevel = position.length;
|
|
83
|
+
let level = 0;
|
|
84
|
+
|
|
85
|
+
const point = [0, 0] as [number, number];
|
|
86
|
+
|
|
87
|
+
for (let i = maxLevel - 1; i >= 0; i--) {
|
|
88
|
+
level = maxLevel - i;
|
|
89
|
+
const bit = position[i];
|
|
90
|
+
let rx = 0;
|
|
91
|
+
let ry = 0;
|
|
92
|
+
if (bit === '1') {
|
|
93
|
+
ry = 1;
|
|
94
|
+
} else if (bit === '2') {
|
|
95
|
+
rx = 1;
|
|
96
|
+
ry = 1;
|
|
97
|
+
} else if (bit === '3') {
|
|
98
|
+
rx = 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const val = Math.pow(2, level - 1);
|
|
102
|
+
rotateAndFlipQuadrant(val, point, rx, ry);
|
|
103
|
+
|
|
104
|
+
point[0] += val * rx;
|
|
105
|
+
point[1] += val * ry;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (face % 2 === 1) {
|
|
109
|
+
const t = point[0];
|
|
110
|
+
point[0] = point[1];
|
|
111
|
+
point[1] = t;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {face, ij: point, level};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function toHilbertQuadkey(id: bigint): string {
|
|
118
|
+
if (!isS2IndexValid(id)) {
|
|
119
|
+
throw new Error(`Invalid S2 index: ${id}`);
|
|
120
|
+
}
|
|
121
|
+
let bin = id.toString(2);
|
|
122
|
+
|
|
123
|
+
while (bin.length < FACE_BITS + POS_BITS) {
|
|
124
|
+
// eslint-disable-next-line prefer-template
|
|
125
|
+
bin = '0' + bin;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MUST come AFTER binstr has been left-padded with '0's
|
|
129
|
+
const lsbIndex = bin.lastIndexOf('1');
|
|
130
|
+
// substr(start, len)
|
|
131
|
+
// substring(start, end) // includes start, does not include end
|
|
132
|
+
const faceB = bin.substring(0, 3);
|
|
133
|
+
// posB will always be a multiple of 2 (or it's invalid)
|
|
134
|
+
const posB = bin.substring(3, lsbIndex);
|
|
135
|
+
const levelN = posB.length / 2;
|
|
136
|
+
|
|
137
|
+
const faceS = BigInt(`0b${faceB}`).toString(10);
|
|
138
|
+
|
|
139
|
+
let posS = '';
|
|
140
|
+
if (levelN !== 0) {
|
|
141
|
+
posS = BigInt(`0b${posB}`).toString(4);
|
|
142
|
+
|
|
143
|
+
while (posS.length < levelN) {
|
|
144
|
+
// eslint-disable-next-line prefer-template
|
|
145
|
+
posS = '0' + posS;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return `${faceS}/${posS}`;
|
|
149
|
+
}
|
|
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
|
+
|
|
231
|
+
export function IJToST(
|
|
232
|
+
ij: [number, number],
|
|
233
|
+
order: number,
|
|
234
|
+
offsets: [number, number]
|
|
235
|
+
): [number, number] {
|
|
236
|
+
const maxSize = 1 << order;
|
|
237
|
+
|
|
238
|
+
return [(ij[0] + offsets[0]) / maxSize, (ij[1] + offsets[1]) / maxSize];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function singleSTtoUV(st: number): number {
|
|
242
|
+
if (st >= 0.5) {
|
|
243
|
+
return (1 / 3.0) * (4 * st * st - 1);
|
|
244
|
+
}
|
|
245
|
+
return (1 / 3.0) * (1 - 4 * (1 - st) * (1 - st));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function STToUV(st: [number, number]): [number, number] {
|
|
249
|
+
return [singleSTtoUV(st[0]), singleSTtoUV(st[1])];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function FaceUVToXYZ(face: number, [u, v]: [number, number]): [number, number, number] {
|
|
253
|
+
switch (face) {
|
|
254
|
+
case 0:
|
|
255
|
+
return [1, u, v];
|
|
256
|
+
case 1:
|
|
257
|
+
return [-u, 1, v];
|
|
258
|
+
case 2:
|
|
259
|
+
return [-u, -v, 1];
|
|
260
|
+
case 3:
|
|
261
|
+
return [-1, -v, -u];
|
|
262
|
+
case 4:
|
|
263
|
+
return [v, -1, -u];
|
|
264
|
+
case 5:
|
|
265
|
+
return [v, u, -1];
|
|
266
|
+
default:
|
|
267
|
+
throw new Error('Invalid face');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function XYZToLngLat([x, y, z]: [number, number, number]): [number, number] {
|
|
272
|
+
const lat = Math.atan2(z, Math.sqrt(x * x + y * y));
|
|
273
|
+
const lng = Math.atan2(y, x);
|
|
274
|
+
|
|
275
|
+
return [lng * RADIAN_TO_DEGREE, lat * RADIAN_TO_DEGREE];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function rotateAndFlipQuadrant(n: number, point: [number, number], rx: number, ry: number): void {
|
|
279
|
+
if (ry === 0) {
|
|
280
|
+
if (rx === 1) {
|
|
281
|
+
point[0] = n - 1 - point[0];
|
|
282
|
+
point[1] = n - 1 - point[1];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const x = point[0];
|
|
286
|
+
point[0] = point[1];
|
|
287
|
+
point[1] = x;
|
|
288
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {IJToST, STToUV, FaceUVToXYZ, XYZToLngLat, getS2Cell} from './s2-geometry';
|
|
6
|
+
|
|
7
|
+
const MAX_RESOLUTION = 100;
|
|
8
|
+
|
|
9
|
+
export function getS2GeoBounds(s2Index: bigint): Float64Array {
|
|
10
|
+
const s2Cell = getS2Cell(s2Index);
|
|
11
|
+
return getS2GeoBoundsFromCell(s2Cell);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The S2 cell edge is curved: http://s2geometry.io/
|
|
16
|
+
* This is more prominent at lower levels
|
|
17
|
+
* resolution is the number of segments to generate per edge.
|
|
18
|
+
* We exponentially reduce resolution as level increases so it doesn't affect perf
|
|
19
|
+
* when there are a large number of cells
|
|
20
|
+
*/
|
|
21
|
+
// eslint-disable-next-line max-statements
|
|
22
|
+
export function getS2GeoBoundsFromCell({
|
|
23
|
+
face,
|
|
24
|
+
ij,
|
|
25
|
+
level
|
|
26
|
+
}: {
|
|
27
|
+
face: number;
|
|
28
|
+
ij: [number, number];
|
|
29
|
+
level: number;
|
|
30
|
+
}): Float64Array {
|
|
31
|
+
const offsets = [
|
|
32
|
+
[0, 0],
|
|
33
|
+
[0, 1],
|
|
34
|
+
[1, 1],
|
|
35
|
+
[1, 0],
|
|
36
|
+
[0, 0]
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const resolution = Math.max(1, Math.ceil(MAX_RESOLUTION * Math.pow(2, -level)));
|
|
40
|
+
const result = new Float64Array(4 * resolution * 2 + 2);
|
|
41
|
+
let ptIndex = 0;
|
|
42
|
+
let prevLng = 0;
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < 4; i++) {
|
|
45
|
+
const offset = offsets[i].slice(0) as [number, number];
|
|
46
|
+
const nextOffset = offsets[i + 1];
|
|
47
|
+
const stepI = (nextOffset[0] - offset[0]) / resolution;
|
|
48
|
+
const stepJ = (nextOffset[1] - offset[1]) / resolution;
|
|
49
|
+
|
|
50
|
+
for (let j = 0; j < resolution; j++) {
|
|
51
|
+
offset[0] += stepI;
|
|
52
|
+
offset[1] += stepJ;
|
|
53
|
+
// Cell can be represented by coordinates IJ, ST, UV, XYZ
|
|
54
|
+
// http://s2geometry.io/devguide/s2cell_hierarchy#coordinate-systems
|
|
55
|
+
const st = IJToST(ij, level, offset);
|
|
56
|
+
const uv = STToUV(st);
|
|
57
|
+
const xyz = FaceUVToXYZ(face, uv);
|
|
58
|
+
const lngLat = XYZToLngLat(xyz);
|
|
59
|
+
|
|
60
|
+
// Adjust longitude for Web Mercator projection
|
|
61
|
+
if (Math.abs(lngLat[1]) > 89.999) {
|
|
62
|
+
lngLat[0] = prevLng;
|
|
63
|
+
}
|
|
64
|
+
const deltaLng = lngLat[0] - prevLng;
|
|
65
|
+
lngLat[0] += deltaLng > 180 ? -360 : deltaLng < -180 ? 360 : 0;
|
|
66
|
+
|
|
67
|
+
result[ptIndex++] = lngLat[0];
|
|
68
|
+
result[ptIndex++] = lngLat[1];
|
|
69
|
+
prevLng = lngLat[0];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// close the loop
|
|
73
|
+
result[ptIndex++] = result[0];
|
|
74
|
+
result[ptIndex++] = result[1];
|
|
75
|
+
return result;
|
|
76
|
+
}
|