@math.gl/dggs 5.0.0-alpha.2 → 5.0.0-alpha.20

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.
Files changed (110) hide show
  1. package/LICENSE +54 -87
  2. package/README.md +6 -1
  3. package/THIRD-PARTY-NOTICES.md +18 -0
  4. package/dist/a5-decoder.d.ts +4 -4
  5. package/dist/a5-decoder.d.ts.map +1 -1
  6. package/dist/a5-decoder.js +4 -4
  7. package/dist/a5-decoder.js.map +1 -1
  8. package/dist/a5.cjs +59 -2
  9. package/dist/a5.cjs.map +3 -3
  10. package/dist/a5.js +1 -1
  11. package/dist/a5.js.map +1 -1
  12. package/dist/converters/s2-to-obb-points.js +1 -1
  13. package/dist/converters/s2-to-obb-points.js.map +1 -1
  14. package/dist/converters/s2-to-region.js +1 -1
  15. package/dist/converters/s2-to-region.js.map +1 -1
  16. package/dist/dggs-decoder.d.ts +13 -4
  17. package/dist/dggs-decoder.d.ts.map +1 -1
  18. package/dist/dggs-decoder.js +25 -1
  19. package/dist/dggs-decoder.js.map +1 -1
  20. package/dist/expressions.cjs +637 -0
  21. package/dist/expressions.cjs.map +6 -0
  22. package/dist/expressions.d.ts +7 -0
  23. package/dist/expressions.d.ts.map +1 -0
  24. package/dist/expressions.js +32 -0
  25. package/dist/expressions.js.map +1 -0
  26. package/dist/geohash-decoder.d.ts +4 -4
  27. package/dist/geohash-decoder.d.ts.map +1 -1
  28. package/dist/geohash-decoder.js +4 -4
  29. package/dist/geohash-decoder.js.map +1 -1
  30. package/dist/geohash.cjs +75 -2
  31. package/dist/geohash.cjs.map +2 -2
  32. package/dist/geohash.js +1 -1
  33. package/dist/geohash.js.map +1 -1
  34. package/dist/h3-decoder.d.ts +4 -4
  35. package/dist/h3-decoder.d.ts.map +1 -1
  36. package/dist/h3-decoder.js +4 -4
  37. package/dist/h3-decoder.js.map +1 -1
  38. package/dist/h3.cjs +59 -2
  39. package/dist/h3.cjs.map +3 -3
  40. package/dist/h3.js +1 -1
  41. package/dist/h3.js.map +1 -1
  42. package/dist/index.cjs +281 -33
  43. package/dist/index.cjs.map +3 -3
  44. package/dist/index.d.ts +6 -3
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +5 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/plus-code-decoder.d.ts +4 -4
  49. package/dist/plus-code-decoder.d.ts.map +1 -1
  50. package/dist/plus-code-decoder.js +4 -4
  51. package/dist/plus-code-decoder.js.map +1 -1
  52. package/dist/plus-code.cjs +75 -2
  53. package/dist/plus-code.cjs.map +2 -2
  54. package/dist/plus-code.js +1 -1
  55. package/dist/plus-code.js.map +1 -1
  56. package/dist/quadkey-decoder.d.ts +4 -4
  57. package/dist/quadkey-decoder.d.ts.map +1 -1
  58. package/dist/quadkey-decoder.js +4 -4
  59. package/dist/quadkey-decoder.js.map +1 -1
  60. package/dist/quadkey.cjs +75 -2
  61. package/dist/quadkey.cjs.map +2 -2
  62. package/dist/quadkey.js +1 -1
  63. package/dist/quadkey.js.map +1 -1
  64. package/dist/s2-decoder.d.ts +14 -4
  65. package/dist/s2-decoder.d.ts.map +1 -1
  66. package/dist/s2-decoder.js +90 -22
  67. package/dist/s2-decoder.js.map +1 -1
  68. package/dist/s2-geometry/s2-geometry.d.ts +20 -0
  69. package/dist/s2-geometry/s2-geometry.d.ts.map +1 -1
  70. package/dist/s2-geometry/s2-geometry.js +128 -2
  71. package/dist/s2-geometry/s2-geometry.js.map +1 -1
  72. package/dist/s2-geometry/s2-to-boundary.js +1 -1
  73. package/dist/s2-geometry/s2-to-boundary.js.map +1 -1
  74. package/dist/s2-geometry/s2-token.d.ts +30 -0
  75. package/dist/s2-geometry/s2-token.d.ts.map +1 -1
  76. package/dist/s2-geometry/s2-token.js +74 -6
  77. package/dist/s2-geometry/s2-token.js.map +1 -1
  78. package/dist/s2.cjs +288 -23
  79. package/dist/s2.cjs.map +2 -2
  80. package/dist/s2.d.ts +4 -2
  81. package/dist/s2.d.ts.map +1 -1
  82. package/dist/s2.js +4 -3
  83. package/dist/s2.js.map +1 -1
  84. package/dist/unwrap-dggs-boundary.d.ts +12 -0
  85. package/dist/unwrap-dggs-boundary.d.ts.map +1 -0
  86. package/dist/unwrap-dggs-boundary.js +49 -0
  87. package/dist/unwrap-dggs-boundary.js.map +1 -0
  88. package/package.json +13 -6
  89. package/src/a5-decoder.ts +10 -5
  90. package/src/a5.ts +1 -1
  91. package/src/converters/s2-to-obb-points.ts +2 -2
  92. package/src/converters/s2-to-region.ts +1 -1
  93. package/src/dggs-decoder.ts +42 -5
  94. package/src/expressions.ts +37 -0
  95. package/src/geohash-decoder.ts +10 -5
  96. package/src/geohash.ts +1 -1
  97. package/src/h3-decoder.ts +10 -5
  98. package/src/h3.ts +1 -1
  99. package/src/index.ts +14 -3
  100. package/src/open-location-code.d.ts +3 -0
  101. package/src/plus-code-decoder.ts +10 -5
  102. package/src/plus-code.ts +1 -1
  103. package/src/quadkey-decoder.ts +10 -5
  104. package/src/quadkey.ts +1 -1
  105. package/src/s2-decoder.ts +92 -22
  106. package/src/s2-geometry/s2-geometry.ts +142 -2
  107. package/src/s2-geometry/s2-to-boundary.ts +1 -1
  108. package/src/s2-geometry/s2-token.ts +77 -6
  109. package/src/s2.ts +12 -3
  110. package/src/unwrap-dggs-boundary.ts +54 -0
@@ -1,8 +1,17 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
- import {type Bounds2D} from '@math.gl/types';
5
+ import {type Bounds2D} from '@math.gl/core/types';
6
+ import {unwrapDGGSBoundary} from './unwrap-dggs-boundary';
7
+
8
+ /** Optional continuous-longitude geometry output for DGGS decoders. */
9
+ export type DGGSBoundaryOptions = {
10
+ /** Unwrap ordinary antimeridian crossings. Defaults to false. */
11
+ unwrap?: boolean;
12
+ /** Place the first longitude near this value, in degrees. Used only with unwrap. */
13
+ referenceLongitude?: number;
14
+ };
6
15
 
7
16
  /** Cell identifiers used by the lightweight decoder API. */
8
17
  export type DGGSCell = string | bigint;
@@ -28,12 +37,12 @@ export type DGGSDecoder = {
28
37
  /** Return the center of the cell as `[longitude, latitude]`. */
29
38
  cellToLngLat: (cell: DGGSCell) => [number, number];
30
39
  /** Return the closed cell boundary as longitude/latitude pairs. */
31
- cellToBoundary: (cell: DGGSCell) => [number, number][];
40
+ cellToBoundary: (cell: DGGSCell, options?: DGGSBoundaryOptions) => [number, number][];
32
41
 
33
42
  /** @returns the boundary of the cell, as a flat array */
34
- cellToBoundaryFlat: (cell: DGGSCell) => number[];
43
+ cellToBoundaryFlat: (cell: DGGSCell, options?: DGGSBoundaryOptions) => number[];
35
44
  /** @returns the longitude/latitude bounds of the cell */
36
- cellToBounds: (cell: DGGSCell) => Bounds2D;
45
+ cellToBounds: (cell: DGGSCell, options?: DGGSBoundaryOptions) => Bounds2D;
37
46
  };
38
47
 
39
48
  /** A cell column and the decoder selected for it. */
@@ -69,3 +78,31 @@ export function getDGGSCellBounds(boundary: [number, number][]): Bounds2D {
69
78
  [east, north]
70
79
  ];
71
80
  }
81
+
82
+ /** @internal Apply the shared opt-in geometry policy to a decoder. */
83
+ export function withDGGSBoundaryOptions<T extends DGGSDecoder>(
84
+ decoder: T
85
+ ): Omit<T, 'cellToBoundary' | 'cellToBoundaryFlat' | 'cellToBounds'> &
86
+ Pick<DGGSDecoder, 'cellToBoundary' | 'cellToBoundaryFlat' | 'cellToBounds'> {
87
+ const cellToBoundary = (cell: DGGSCell, options?: DGGSBoundaryOptions): [number, number][] => {
88
+ const boundary = decoder.cellToBoundary(cell);
89
+ return options?.unwrap ? unwrapDGGSBoundary(boundary, options.referenceLongitude) : boundary;
90
+ };
91
+ return {
92
+ ...decoder,
93
+ cellToBoundary,
94
+ cellToBoundaryFlat: (cell, options) =>
95
+ options?.unwrap ? cellToBoundary(cell, options).flat() : decoder.cellToBoundaryFlat(cell),
96
+ cellToBounds: (cell, options) => {
97
+ const bounds = decoder.cellToBounds(cell);
98
+ if (!options?.unwrap || bounds[1][0] - bounds[0][0] >= 360) {
99
+ return bounds;
100
+ }
101
+ const unwrapped = getDGGSCellBounds(cellToBoundary(cell, options));
102
+ // Latitude extrema may occur inside the cell or along curved edges.
103
+ unwrapped[0][1] = bounds[0][1];
104
+ unwrapped[1][1] = bounds[1][1];
105
+ return unwrapped;
106
+ }
107
+ };
108
+ }
@@ -0,0 +1,37 @@
1
+ // math.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
5
+
6
+ import {GeohashDecoder} from './geohash-decoder';
7
+ import {QuadkeyDecoder, quadkeyToWorldBounds} from './quadkey-decoder';
8
+ import {S2Decoder} from './s2-decoder';
9
+ import {getS2ChildIndex} from './s2-geometry/s2-token';
10
+ type ExpressionFunctionLibrary = Record<string, (...args: any[]) => any>;
11
+
12
+ export const GEOHASH_FUNCTION_LIBRARY: ExpressionFunctionLibrary = {
13
+ getGeohashBoundary: GeohashDecoder.cellToBoundary,
14
+ getGeohashBoundaryFlat: GeohashDecoder.cellToBoundaryFlat,
15
+ getGeohashBounds: GeohashDecoder.cellToBounds,
16
+ getGeohashLngLat: GeohashDecoder.cellToLngLat
17
+ };
18
+
19
+ export const QUADKEY_FUNCTION_LIBRARY: ExpressionFunctionLibrary = {
20
+ getQuadkeyBoundary: QuadkeyDecoder.cellToBoundary,
21
+ getQuadkeyBoundaryFlat: QuadkeyDecoder.cellToBoundaryFlat,
22
+ getQuadkeyLngLat: QuadkeyDecoder.cellToLngLat,
23
+ quadkeyToWorldBounds
24
+ };
25
+
26
+ export const S2_FUNCTION_LIBRARY: ExpressionFunctionLibrary = {
27
+ getS2BoundaryFlat: S2Decoder.cellToBoundaryFlat,
28
+ getS2ChildIndex,
29
+ getS2IndexFromToken: S2Decoder.tokenToCell,
30
+ getS2TokenFromIndex: S2Decoder.cellToToken
31
+ };
32
+
33
+ export const DGGS_FUNCTION_LIBRARY: ExpressionFunctionLibrary = {
34
+ ...GEOHASH_FUNCTION_LIBRARY,
35
+ ...QUADKEY_FUNCTION_LIBRARY,
36
+ ...S2_FUNCTION_LIBRARY
37
+ };
@@ -1,12 +1,17 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
- import {type Bounds2D} from '@math.gl/types';
6
- import {type DGGSCell, type DGGSDecoder, getDGGSCellToken} from './dggs-decoder';
5
+ import {type Bounds2D} from '@math.gl/core/types';
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/geohash.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  export {GeohashDecoder, getGeohashBounds} from './geohash-decoder';
package/src/h3-decoder.ts CHANGED
@@ -1,13 +1,18 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  import {cellToBoundary, cellToLatLng} from 'h3-js';
6
- import {type Bounds2D} from '@math.gl/types';
7
- import {type DGGSCell, type DGGSDecoder, getDGGSCellBounds} from './dggs-decoder';
6
+ import {type Bounds2D} from '@math.gl/core/types';
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/h3.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  export {H3Decoder} from './h3-decoder';
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  import {type DGGSCell, type DGGSCellColumn, type DGGSDecoder} from './dggs-decoder';
6
6
  import {A5Decoder} from './a5-decoder';
@@ -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 {getS2ChildIndex} from './s2-geometry/s2-token';
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[] = [
@@ -1,3 +1,6 @@
1
+ // math.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
1
4
  declare module 'open-location-code' {
2
5
  export type OpenLocationCodeArea = {
3
6
  latitudeLo: number;
@@ -1,10 +1,15 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  import OpenLocationCodePackage, {type OpenLocationCodeArea} from 'open-location-code';
6
- import {type Bounds2D} from '@math.gl/types';
7
- import {type DGGSCell, type DGGSDecoder, getDGGSCellToken} from './dggs-decoder';
6
+ import {type Bounds2D} from '@math.gl/core/types';
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/plus-code.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  export {PlusCodeDecoder} from './plus-code-decoder';
@@ -1,14 +1,19 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
- import {type Bounds2D} from '@math.gl/types';
6
- import {type DGGSCell, type DGGSDecoder, getDGGSCellToken} from './dggs-decoder';
5
+ import {type Bounds2D} from '@math.gl/core/types';
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/quadkey.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  export {QuadkeyDecoder, quadkeyToWorldBounds} from './quadkey-decoder';
package/src/s2-decoder.ts CHANGED
@@ -1,27 +1,26 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
- import {type Bounds2D} from '@math.gl/types';
6
- import {type DGGSCell, type DGGSDecoder} from './dggs-decoder';
5
+ import {type Bounds2D} from '@math.gl/core/types';
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
- function getS2Bounds(cell: DGGSCell): Bounds2D {
64
- const flatBoundary = getS2Boundary(cell);
65
-
66
- // We know that we have at least one point, no Infinity will be returned.
67
- let xmin = Infinity;
68
- let ymin = Infinity;
69
- let xmax = -Infinity;
70
- let ymax = -Infinity;
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
- for (const [x, y] of flatBoundary) {
73
- if (x < xmin) xmin = x;
74
- if (x > xmax) xmax = x;
75
- if (y < ymin) ymin = y;
76
- if (y > ymax) ymax = y;
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
- [xmin, ymin],
81
- [xmax, ymax]
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
+ }
@@ -1,6 +1,9 @@
1
1
  // math.gl
2
- // SPDX-License-Identifier: MIT and ISC
3
- // Copyright (c) vis.gl contributors
2
+ // SPDX-License-Identifier: MIT AND ISC
3
+ // SPDX-FileCopyrightText: Copyright (c) 2012-2016, Jon Atkins <github@jonatkins.com>
4
+ // SPDX-FileCopyrightText: Copyright (c) 2016, AJ ONeal <aj@daplie.com>
5
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
6
+ // SPDX-FileComment: Adapted from s2-geometry. Original ISC notice retained below.
4
7
 
5
8
  // math.gl, MIT license
6
9
  /*
@@ -11,6 +14,8 @@ Permission to use, copy, modify, and/or distribute this software for any purpose
11
14
  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
15
  */
13
16
 
17
+ import {isS2IndexValid} from './s2-token';
18
+
14
19
  export type S2Cell = {
15
20
  face: number;
16
21
  ij: [number, number];
@@ -18,6 +23,9 @@ export type S2Cell = {
18
23
  };
19
24
 
20
25
  export function getS2Cell(s2Index: bigint): S2Cell {
26
+ if (!isS2IndexValid(s2Index)) {
27
+ throw new Error(`Invalid S2 index: ${s2Index}`);
28
+ }
21
29
  const key = toHilbertQuadkey(s2Index);
22
30
  const s2cell = fromHilbertQuadKey(key);
23
31
  return s2cell;
@@ -31,11 +39,46 @@ const MAX_LEVEL = 30;
31
39
  const POS_BITS = 2 * MAX_LEVEL + 1; // 61 (60 bits of data, 1 bit lsb marker)
32
40
  const RADIAN_TO_DEGREE = 180 / Math.PI;
33
41
 
42
+ type HilbertOrientation = 'a' | 'b' | 'c' | 'd';
43
+
44
+ const HILBERT_MAP: Record<
45
+ HilbertOrientation,
46
+ readonly (readonly [position: number, orientation: HilbertOrientation])[]
47
+ > = {
48
+ a: [
49
+ [0, 'd'],
50
+ [1, 'a'],
51
+ [3, 'b'],
52
+ [2, 'a']
53
+ ],
54
+ b: [
55
+ [2, 'b'],
56
+ [1, 'b'],
57
+ [3, 'a'],
58
+ [0, 'c']
59
+ ],
60
+ c: [
61
+ [2, 'c'],
62
+ [3, 'd'],
63
+ [1, 'c'],
64
+ [0, 'b']
65
+ ],
66
+ d: [
67
+ [0, 'a'],
68
+ [3, 'c'],
69
+ [1, 'd'],
70
+ [2, 'd']
71
+ ]
72
+ };
73
+
34
74
  /*
35
75
  Original function taken from deck.gl doesn't support the case of (face <= 5)
36
76
  It's fixed here.
37
77
  */
38
78
  export function fromHilbertQuadKey(hilbertQuadkey: string): S2Cell {
79
+ if (!/^[0-5]\/[0-3]{0,30}$/.test(hilbertQuadkey)) {
80
+ throw new Error(`Invalid S2 Hilbert quadkey: ${hilbertQuadkey}`);
81
+ }
39
82
  const parts = hilbertQuadkey.split('/');
40
83
  const face = parseInt(parts[0], 10);
41
84
  const position = parts[1] || '';
@@ -75,6 +118,9 @@ export function fromHilbertQuadKey(hilbertQuadkey: string): S2Cell {
75
118
  }
76
119
 
77
120
  export function toHilbertQuadkey(id: bigint): string {
121
+ if (!isS2IndexValid(id)) {
122
+ throw new Error(`Invalid S2 index: ${id}`);
123
+ }
78
124
  let bin = id.toString(2);
79
125
 
80
126
  while (bin.length < FACE_BITS + POS_BITS) {
@@ -105,6 +151,86 @@ export function toHilbertQuadkey(id: bigint): string {
105
151
  return `${faceS}/${posS}`;
106
152
  }
107
153
 
154
+ /**
155
+ * Converts face-local integer coordinates into an S2 cell index.
156
+ *
157
+ * @param s2Cell - Face, `(i, j)` coordinates, and level of an S2 cell.
158
+ * @returns The corresponding 64-bit S2 cell index.
159
+ */
160
+ export function getS2IndexFromCell(s2Cell: S2Cell): bigint {
161
+ const {face, ij, level} = s2Cell;
162
+ if (!Number.isInteger(face) || face < 0 || face > 5) {
163
+ throw new Error(`Invalid S2 face: ${face}`);
164
+ }
165
+ if (!Number.isInteger(level) || level < 0 || level > MAX_LEVEL) {
166
+ throw new Error(`Invalid S2 level: ${level}`);
167
+ }
168
+ const maximumCoordinate = 2 ** level;
169
+ if (
170
+ !Number.isInteger(ij[0]) ||
171
+ !Number.isInteger(ij[1]) ||
172
+ ij[0] < 0 ||
173
+ ij[1] < 0 ||
174
+ ij[0] >= maximumCoordinate ||
175
+ ij[1] >= maximumCoordinate
176
+ ) {
177
+ throw new Error(`Invalid S2 IJ coordinates: ${ij[0]}, ${ij[1]}`);
178
+ }
179
+
180
+ const positions = getHilbertQuadList(ij[0], ij[1], level, face);
181
+ let s2Index = BigInt(face) << 61n;
182
+ for (let positionIndex = 0; positionIndex < positions.length; positionIndex++) {
183
+ const bitOffset = 59 - positionIndex * 2;
184
+ s2Index |= BigInt(positions[positionIndex]) << BigInt(bitOffset);
185
+ }
186
+ s2Index |= 1n << BigInt(60 - level * 2);
187
+ return s2Index;
188
+ }
189
+
190
+ /**
191
+ * Returns a spatially addressed descendant of an S2 cell.
192
+ *
193
+ * Unlike {@link getS2ChildIndex}, `x` and `y` address the descendant along increasing face-local
194
+ * `i` and `j` axes. This accounts for Hilbert orientation changes across faces and levels.
195
+ *
196
+ * @param rootIndex - Valid S2 cell index at the root of the relative grid.
197
+ * @param relativeLevel - Number of subdivision levels below the root.
198
+ * @param x - Descendant coordinate along the root cell's increasing `i` axis.
199
+ * @param y - Descendant coordinate along the root cell's increasing `j` axis.
200
+ * @returns The descendant S2 cell index.
201
+ */
202
+ export function getS2DescendantIndex(
203
+ rootIndex: bigint,
204
+ relativeLevel: number,
205
+ x: number,
206
+ y: number
207
+ ): bigint {
208
+ const rootCell = getS2Cell(rootIndex);
209
+ if (
210
+ !Number.isInteger(relativeLevel) ||
211
+ relativeLevel < 0 ||
212
+ rootCell.level + relativeLevel > MAX_LEVEL
213
+ ) {
214
+ throw new Error(`Invalid S2 relative level: ${relativeLevel}`);
215
+ }
216
+ const divisionCount = 2 ** relativeLevel;
217
+ if (
218
+ !Number.isInteger(x) ||
219
+ !Number.isInteger(y) ||
220
+ x < 0 ||
221
+ y < 0 ||
222
+ x >= divisionCount ||
223
+ y >= divisionCount
224
+ ) {
225
+ throw new Error(`Invalid S2 descendant coordinates: ${x}, ${y}`);
226
+ }
227
+ return getS2IndexFromCell({
228
+ face: rootCell.face,
229
+ ij: [rootCell.ij[0] * divisionCount + x, rootCell.ij[1] * divisionCount + y],
230
+ level: rootCell.level + relativeLevel
231
+ });
232
+ }
233
+
108
234
  export function IJToST(
109
235
  ij: [number, number],
110
236
  order: number,
@@ -164,3 +290,17 @@ function rotateAndFlipQuadrant(n: number, point: [number, number], rx: number, r
164
290
  point[1] = x;
165
291
  }
166
292
  }
293
+
294
+ function getHilbertQuadList(x: number, y: number, level: number, face: number): number[] {
295
+ let orientation: HilbertOrientation = face % 2 === 1 ? 'd' : 'a';
296
+ const positions: number[] = [];
297
+ for (let bit = level - 1; bit >= 0; bit--) {
298
+ const mask = 1 << bit;
299
+ const quadrantX = (x & mask) !== 0 ? 1 : 0;
300
+ const quadrantY = (y & mask) !== 0 ? 1 : 0;
301
+ const [position, nextOrientation] = HILBERT_MAP[orientation][quadrantX * 2 + quadrantY];
302
+ positions.push(position);
303
+ orientation = nextOrientation;
304
+ }
305
+ return positions;
306
+ }
@@ -1,6 +1,6 @@
1
1
  // math.gl
2
2
  // SPDX-License-Identifier: MIT
3
- // Copyright (c) vis.gl contributors
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  import {IJToST, STToUV, FaceUVToXYZ, XYZToLngLat, getS2Cell} from './s2-geometry';
6
6