@math.gl/geospatial 5.0.0-alpha.11 → 5.0.0-alpha.13

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 (59) hide show
  1. package/LICENSE +7 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +4 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/ellipsoid-helpers/ellipsoid-transform.d.ts.map +1 -1
  6. package/dist/ellipsoid-helpers/ellipsoid-transform.js +46 -15
  7. package/dist/ellipsoid-helpers/ellipsoid-transform.js.map +1 -1
  8. package/dist/ellipsoid-helpers/scale-to-geodetic-surface.d.ts +2 -0
  9. package/dist/ellipsoid-helpers/scale-to-geodetic-surface.d.ts.map +1 -1
  10. package/dist/ellipsoid-helpers/scale-to-geodetic-surface.js +46 -13
  11. package/dist/ellipsoid-helpers/scale-to-geodetic-surface.js.map +1 -1
  12. package/dist/ellipsoid-occluder.d.ts +28 -0
  13. package/dist/ellipsoid-occluder.d.ts.map +1 -0
  14. package/dist/ellipsoid-occluder.js +112 -0
  15. package/dist/ellipsoid-occluder.js.map +1 -0
  16. package/dist/ellipsoid-tangent-plane.d.ts.map +1 -1
  17. package/dist/ellipsoid-tangent-plane.js +4 -2
  18. package/dist/ellipsoid-tangent-plane.js.map +1 -1
  19. package/dist/ellipsoid.d.ts +11 -0
  20. package/dist/ellipsoid.d.ts.map +1 -1
  21. package/dist/ellipsoid.js +77 -7
  22. package/dist/ellipsoid.js.map +1 -1
  23. package/dist/geographic-tiles.d.ts +24 -0
  24. package/dist/geographic-tiles.d.ts.map +1 -0
  25. package/dist/geographic-tiles.js +72 -0
  26. package/dist/geographic-tiles.js.map +1 -0
  27. package/dist/globe-horizon-bounds.d.ts +13 -0
  28. package/dist/globe-horizon-bounds.d.ts.map +1 -0
  29. package/dist/globe-horizon-bounds.js +59 -0
  30. package/dist/globe-horizon-bounds.js.map +1 -0
  31. package/dist/index.cjs +557 -40
  32. package/dist/index.cjs.map +3 -3
  33. package/dist/index.d.ts +7 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +4 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/lng-lat-rectangle.d.ts.map +1 -1
  38. package/dist/lng-lat-rectangle.js +4 -2
  39. package/dist/lng-lat-rectangle.js.map +1 -1
  40. package/dist/make-obb-from-region.d.ts.map +1 -1
  41. package/dist/make-obb-from-region.js +4 -2
  42. package/dist/make-obb-from-region.js.map +1 -1
  43. package/dist/type-utils.d.ts +2 -0
  44. package/dist/type-utils.d.ts.map +1 -1
  45. package/dist/type-utils.js +25 -2
  46. package/dist/type-utils.js.map +1 -1
  47. package/package.json +5 -5
  48. package/src/constants.ts +4 -2
  49. package/src/ellipsoid-helpers/ellipsoid-transform.ts +62 -15
  50. package/src/ellipsoid-helpers/scale-to-geodetic-surface.ts +48 -14
  51. package/src/ellipsoid-occluder.ts +135 -0
  52. package/src/ellipsoid-tangent-plane.ts +4 -2
  53. package/src/ellipsoid.ts +91 -7
  54. package/src/geographic-tiles.ts +87 -0
  55. package/src/globe-horizon-bounds.ts +68 -0
  56. package/src/index.ts +15 -1
  57. package/src/lng-lat-rectangle.ts +4 -2
  58. package/src/make-obb-from-region.ts +4 -2
  59. package/src/type-utils.ts +30 -2
@@ -0,0 +1,135 @@
1
+ // math.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
+ // SPDX-FileComment: Original scaled-space analytic geometry. Consumer requirements: https://github.com/keplergl/kepler.gl/pull/3557 and https://github.com/keplergl/kepler.gl/pull/3593. No implementation code copied.
5
+
6
+ /** The limb is center + axis1*cos(t) + axis2*sin(t), in input Cartesian units. */
7
+ export type EllipsoidHorizon = {
8
+ center: number[];
9
+ axis1: number[];
10
+ axis2: number[];
11
+ };
12
+
13
+ /** Axis-aligned ellipsoid queries, independent of viewport and vector classes. */
14
+ export class EllipsoidOccluder {
15
+ readonly radii: readonly number[];
16
+ readonly center: readonly number[];
17
+
18
+ constructor(radii: Readonly<ArrayLike<number>>, center: Readonly<ArrayLike<number>> = [0, 0, 0]) {
19
+ validateVector(radii);
20
+ validateVector(center);
21
+ if (Array.from(radii).some(value => value <= 0)) throw new RangeError('Radii must be positive');
22
+ this.radii = Object.freeze(Array.from(radii));
23
+ this.center = Object.freeze(Array.from(center));
24
+ }
25
+
26
+ /** Nonnegative ray parameter interval, including tangency and inside-origin rays.
27
+ * Direction need not be normalized. Undefined is a true miss; never clamps a miss
28
+ * to the limb. Result is untouched on miss. Distances equal parameters only for
29
+ * unit directions. Zero direction is a miss.
30
+ */
31
+ intersectRay(
32
+ origin: Readonly<ArrayLike<number>>,
33
+ direction: Readonly<ArrayLike<number>>,
34
+ result: number[] = [0, 0]
35
+ ): number[] | undefined {
36
+ validateVector(origin);
37
+ validateVector(direction);
38
+ const [x, y, z] = this.scalePoint(origin);
39
+ const dx = direction[0] / this.radii[0];
40
+ const dy = direction[1] / this.radii[1];
41
+ const dz = direction[2] / this.radii[2];
42
+ // Normalize in scaled space: avoid a tiny quadratic coefficient for ECEF rays.
43
+ const length = Math.hypot(dx, dy, dz);
44
+ if (length === 0 || !Number.isFinite(length)) return undefined;
45
+ const ux = dx / length,
46
+ uy = dy / length,
47
+ uz = dz / length;
48
+ const along = -(x * ux + y * uy + z * uz);
49
+ const perpendicular = Math.hypot(y * uz - z * uy, z * ux - x * uz, x * uy - y * ux);
50
+ if (perpendicular > 1) return undefined;
51
+ const half = Math.sqrt((1 - perpendicular) * (1 + perpendicular));
52
+ let near = along - half;
53
+ let far = along + half;
54
+ // Recover the small root by the root product rather than subtracting large terms.
55
+ const radius = Math.hypot(x, y, z);
56
+ if (!Number.isFinite(radius)) throw new RangeError('Scaled origin magnitude must be finite');
57
+ if (half > 0 && along > 0 && far !== 0) near = ((radius - 1) / far) * (radius + 1);
58
+ if (half > 0 && along < 0 && near !== 0) far = ((radius - 1) / near) * (radius + 1);
59
+ if (far < 0) return undefined;
60
+ result[0] = Math.max(0, Math.min(near, far)) / length;
61
+ result[1] = Math.max(0, far) / length;
62
+ return result;
63
+ }
64
+
65
+ /** True if the finite camera-to-point segment enters the solid interior.
66
+ * Surface contact/tangency alone is not occlusion. Works for elevated points;
67
+ * unlike a facing-normal test it does not discard visible points beyond the
68
+ * surface horizon. tolerance is a nonnegative scaled-space squared-radius margin.
69
+ */
70
+ isPointOccluded(
71
+ camera: Readonly<ArrayLike<number>>,
72
+ point: Readonly<ArrayLike<number>>,
73
+ tolerance = 1e-12
74
+ ): boolean {
75
+ validateVector(camera);
76
+ validateVector(point);
77
+ if (!Number.isFinite(tolerance) || tolerance < 0 || tolerance >= 1)
78
+ throw new RangeError('Tolerance must be in [0, 1)');
79
+ const a = this.scalePoint(camera);
80
+ const b = this.scalePoint(point);
81
+ const dx = b[0] - a[0],
82
+ dy = b[1] - a[1],
83
+ dz = b[2] - a[2];
84
+ const length = Math.hypot(dx, dy, dz);
85
+ if (!Number.isFinite(length)) throw new RangeError('Segment magnitude must be finite');
86
+ if (length === 0) return Math.hypot(...a) ** 2 < 1 - tolerance;
87
+ const ux = dx / length,
88
+ uy = dy / length,
89
+ uz = dz / length;
90
+ const distance = Math.max(0, Math.min(length, -(a[0] * ux + a[1] * uy + a[2] * uz)));
91
+ return (
92
+ Math.hypot(a[0] + distance * ux, a[1] + distance * uy, a[2] + distance * uz) ** 2 <
93
+ 1 - tolerance
94
+ );
95
+ }
96
+
97
+ /** Exact apparent limb ellipse for an exterior camera; undefined on/inside surface. */
98
+ getHorizon(camera: Readonly<ArrayLike<number>>): EllipsoidHorizon | undefined {
99
+ validateVector(camera);
100
+ const scaled = this.scalePoint(camera);
101
+ const distance = Math.hypot(...scaled);
102
+ if (!Number.isFinite(distance)) throw new RangeError('Camera magnitude must be finite');
103
+ if (distance <= 1) return undefined;
104
+ const normal = scaled.map(value => value / distance);
105
+ // Choose a basis away from parallel to the normal, including polar views.
106
+ const reference = Math.abs(normal[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0];
107
+ const u = cross(normal, reference);
108
+ const uLength = Math.hypot(...u);
109
+ const v = cross(
110
+ normal,
111
+ u.map(value => value / uLength)
112
+ );
113
+ const radius = Math.sqrt((1 - 1 / distance) * (1 + 1 / distance));
114
+ return {
115
+ center: normal.map((value, i) => this.center[i] + (this.radii[i] * value) / distance),
116
+ axis1: u.map((value, i) => (this.radii[i] * radius * value) / uLength),
117
+ axis2: v.map((value, i) => this.radii[i] * radius * value)
118
+ };
119
+ }
120
+
121
+ private scalePoint(point: Readonly<ArrayLike<number>>): number[] {
122
+ const result = this.radii.map((radius, i) => (point[i] - this.center[i]) / radius);
123
+ if (!result.every(Number.isFinite)) throw new RangeError('Scaled coordinates must be finite');
124
+ return result;
125
+ }
126
+ }
127
+
128
+ function validateVector(vector: Readonly<ArrayLike<number>>): void {
129
+ if (vector.length !== 3 || !Array.from(vector).every(Number.isFinite))
130
+ throw new RangeError('Expected three finite Cartesian components');
131
+ }
132
+
133
+ function cross(a: number[], b: number[]): number[] {
134
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
135
+ }
@@ -1,6 +1,8 @@
1
1
  // math.gl
2
- // SPDX-License-Identifier: MIT and Apache-2.0
3
- // Copyright (c) vis.gl contributors
2
+ // SPDX-License-Identifier: MIT AND Apache-2.0
3
+ // SPDX-FileCopyrightText: Copyright 2011-2018 CesiumJS Contributors
4
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
5
+ // SPDX-FileComment: Derived from Cesium. See the repository LICENSE for upstream attribution and Apache-2.0 terms.
4
6
 
5
7
  // This file is derived from the Cesium library under Apache 2 license
6
8
  // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md
package/src/ellipsoid.ts CHANGED
@@ -1,21 +1,30 @@
1
1
  // math.gl
2
- // SPDX-License-Identifier: MIT and Apache-2.0
3
- // Copyright (c) vis.gl contributors
2
+ // SPDX-License-Identifier: MIT AND Apache-2.0
3
+ // SPDX-FileCopyrightText: Copyright 2011-2018 CesiumJS Contributors
4
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
5
+ // SPDX-FileComment: Derived from Cesium. See the repository LICENSE for upstream attribution and Apache-2.0 terms. Original math.gl follow-up uses stable atan2 latitude and numeric cartographic output.
4
6
 
5
7
  // This file is derived from the Cesium math library under Apache 2 license
6
8
  // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md
7
9
 
8
10
  /* eslint-disable */
9
11
  import {Vector3, Matrix4, assert, equals, _MathUtils, NumericArray} from '@math.gl/core';
12
+ import type {SpheroidParameters} from '@math.gl/types';
13
+ import {
14
+ spheroidToCartesian,
15
+ cartesianToSpheroid,
16
+ type SpheroidGeometry
17
+ } from '@math.gl/core/spheroid';
10
18
  import * as vec3 from '@math.gl/core/vec3';
11
19
 
12
20
  import {WGS84_RADIUS_X, WGS84_RADIUS_Y, WGS84_RADIUS_Z} from './constants';
13
- import {fromCartographicToRadians, toCartographicFromRadians} from './type-utils';
21
+ import {fromCartographicToRadians, toCartographicFromRadiansComponents} from './type-utils';
14
22
 
15
23
  import type {AxisDirection} from './ellipsoid-helpers/ellipsoid-transform';
16
24
  import {localFrameToFixedFrame} from './ellipsoid-helpers/ellipsoid-transform';
17
- import {scaleToGeodeticSurface} from './ellipsoid-helpers/scale-to-geodetic-surface';
25
+ import {scaleToGeodeticSurface, writeResult} from './ellipsoid-helpers/scale-to-geodetic-surface';
18
26
 
27
+ const spheroidScratch = {x: 0, y: 0, z: 0};
19
28
  const scratchVector = new Vector3();
20
29
  const scratchNormal = new Vector3();
21
30
  const scratchK = new Vector3();
@@ -32,6 +41,15 @@ export class Ellipsoid {
32
41
  /** An Ellipsoid instance initialized to the WGS84 standard. */
33
42
  static readonly WGS84: Ellipsoid = new Ellipsoid(WGS84_RADIUS_X, WGS84_RADIUS_Y, WGS84_RADIUS_Z);
34
43
 
44
+ /** Construct from two-axis geometry, including normalized projection ellipsoids.
45
+ * The equatorial X/Y radii are equal; no CRS or datum information is inferred.
46
+ */
47
+ static fromSpheroid(parameters: SpheroidParameters): Ellipsoid {
48
+ const {semiMajorAxis, semiMinorAxis} = parameters;
49
+ validateSpheroid(semiMajorAxis, semiMinorAxis);
50
+ return new Ellipsoid(semiMajorAxis, semiMajorAxis, semiMinorAxis);
51
+ }
52
+
35
53
  readonly radii: Vector3;
36
54
  readonly radiiSquared: Vector3;
37
55
  readonly radiiToTheFourth: Vector3;
@@ -41,6 +59,7 @@ export class Ellipsoid {
41
59
  readonly maximumRadius: number;
42
60
  readonly centerToleranceSquared: number = _MathUtils.EPSILON1;
43
61
  readonly squaredXOverSquaredZ: number;
62
+ private readonly spheroidGeometry?: SpheroidGeometry;
44
63
 
45
64
  /** Creates an Ellipsoid from a Cartesian specifying the radii in x, y, and z directions. */
46
65
  constructor(x: number, y: number, z: number);
@@ -77,9 +96,33 @@ export class Ellipsoid {
77
96
  this.squaredXOverSquaredZ = this.radiiSquared.x / this.radiiSquared.z;
78
97
  }
79
98
 
99
+ // Setup only: retain the three-radius path for unsupported/lossy shapes.
100
+ if (
101
+ x === y &&
102
+ z > 0 &&
103
+ z <= x &&
104
+ Number.isFinite(this.radiiSquared.x) &&
105
+ this.radiiSquared.z > 0
106
+ )
107
+ this.spheroidGeometry = Object.freeze({
108
+ semiMajorAxis: x,
109
+ semiMinorAxis: z,
110
+ eccentricitySquared: 1 - this.radiiSquared.z / this.radiiSquared.x
111
+ });
80
112
  Object.freeze(this);
81
113
  }
82
114
 
115
+ /** Return an owned geometry snapshot for a sphere or oblate spheroid.
116
+ * Triaxial, prolate and degenerate ellipsoids cannot be represented by this contract.
117
+ * Call during setup, outside coordinate loops.
118
+ */
119
+ toSpheroid(): SpheroidParameters {
120
+ const {x, y, z} = this.radii;
121
+ if (x !== y) throw new Error('Spheroid requires equal X/Y radii');
122
+ validateSpheroid(x, z);
123
+ return Object.freeze({semiMajorAxis: x, semiMinorAxis: z});
124
+ }
125
+
83
126
  /** Compares this Ellipsoid against the provided Ellipsoid componentwise */
84
127
  equals(right: Ellipsoid): boolean {
85
128
  return this === right || Boolean(right && this.radii.equals(right.radii));
@@ -98,6 +141,14 @@ export class Ellipsoid {
98
141
  cartographicToCartesian(cartographic: number[], result?: number[]): number[];
99
142
 
100
143
  cartographicToCartesian(cartographic: Readonly<NumericArray>, result = [0, 0, 0]) {
144
+ if (this.spheroidGeometry) {
145
+ const llh = fromCartographicToRadians(cartographic, scratchCartesian);
146
+ spheroidScratch.x = llh[0];
147
+ spheroidScratch.y = llh[1];
148
+ spheroidScratch.z = llh[2];
149
+ if (!spheroidToCartesian(spheroidScratch, this.spheroidGeometry)) return undefined;
150
+ return writeResult(result, spheroidScratch.x, spheroidScratch.y, spheroidScratch.z);
151
+ }
101
152
  const normal = scratchNormal;
102
153
  const k = scratchK;
103
154
 
@@ -123,7 +174,33 @@ export class Ellipsoid {
123
174
  cartesianToCartographic(cartesian: Readonly<NumericArray>, result?: number[]): number[];
124
175
 
125
176
  cartesianToCartographic(cartesian: Readonly<NumericArray>, result = [0, 0, 0]) {
126
- scratchCartesian.from(cartesian);
177
+ const object = cartesian as unknown as {x: number; y: number; z: number};
178
+ const x = 'x' in cartesian ? object.x : cartesian[0];
179
+ const y = 'x' in cartesian ? object.y : cartesian[1];
180
+ const z = 'x' in cartesian ? object.z : cartesian[2];
181
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return undefined;
182
+ // Sphere/oblate conversion chooses the unique nearest normal, including interiors.
183
+ // Preserve legacy geometry magnitude limits; prolate/triaxial keep their own kernel.
184
+ if (
185
+ this.spheroidGeometry &&
186
+ Number.isFinite(x * x + y * y + z * z) &&
187
+ Number.isFinite(this.oneOverRadiiSquared.x) &&
188
+ Number.isFinite(this.oneOverRadiiSquared.z)
189
+ ) {
190
+ spheroidScratch.x = x;
191
+ spheroidScratch.y = y;
192
+ spheroidScratch.z = z;
193
+ if (!cartesianToSpheroid(spheroidScratch, this.spheroidGeometry)) return undefined;
194
+ // Preserve signed-zero atan2 at exact poles rather than projection's canonical zero.
195
+ const longitude = x === 0 && y === 0 ? Math.atan2(y, x) : spheroidScratch.x;
196
+ return toCartographicFromRadiansComponents(
197
+ longitude,
198
+ spheroidScratch.y,
199
+ spheroidScratch.z,
200
+ result
201
+ );
202
+ }
203
+ scratchCartesian.set(x, y, z);
127
204
  const point = this.scaleToGeodeticSurface(scratchCartesian, scratchPosition);
128
205
 
129
206
  if (!point) {
@@ -136,10 +213,11 @@ export class Ellipsoid {
136
213
  h.copy(scratchCartesian).subtract(point);
137
214
 
138
215
  const longitude = Math.atan2(normal.y, normal.x);
139
- const latitude = Math.asin(normal.z);
216
+ // atan2 retains latitude precision when the normal is almost vertical.
217
+ const latitude = Math.atan2(normal.z, Math.hypot(normal.x, normal.y));
140
218
  const height = Math.sign(vec3.dot(h, scratchCartesian)) * vec3.length(h);
141
219
 
142
- return toCartographicFromRadians([longitude, latitude, height], result);
220
+ return toCartographicFromRadiansComponents(longitude, latitude, height, result);
143
221
  }
144
222
 
145
223
  /** Computes a 4x4 transformation matrix from a reference frame with an east-north-up axes
@@ -279,3 +357,9 @@ export class Ellipsoid {
279
357
  return scratchPosition.set(0.0, 0.0, z).to(result);
280
358
  }
281
359
  }
360
+
361
+ // Original parameter adapter; conversion and surface kernels above retain Cesium attribution.
362
+ function validateSpheroid(a: number, b: number): void {
363
+ if (!Number.isFinite(a) || !Number.isFinite(b) || a <= 0 || b <= 0 || b > a)
364
+ throw new Error('Spheroid requires finite positive axes with semiMinorAxis <= semiMajorAxis');
365
+ }
@@ -0,0 +1,87 @@
1
+ // math.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
+ import {splitGlobeBounds, type GlobeBounds} from './globe-horizon-bounds';
5
+
6
+ /** Equal-angle longitude/latitude grid: one world tile at level zero, north-origin Y. */
7
+ export type GeographicTile = {x: number; y: number; level: number};
8
+ /** Inclusive integer tile ranges. Ranges are disjoint and ordered by minX. */
9
+ export type GeographicTileRange = {minX: number; maxX: number; minY: number; maxY: number};
10
+
11
+ /** Geographic degree bounds of an equal-angle tile, not Web Mercator XYZ. */
12
+ export function getGeographicTileBounds(tile: GeographicTile): GlobeBounds {
13
+ const size = gridSize(tile.level);
14
+ if (![tile.x, tile.y].every(v => Number.isInteger(v) && v >= 0 && v < size))
15
+ throw new RangeError('Tile coordinates must be integers inside the level grid');
16
+ return [
17
+ -180 + (360 * tile.x) / size,
18
+ 90 - (180 * (tile.y + 1)) / size,
19
+ -180 + (360 * (tile.x + 1)) / size,
20
+ 90 - (180 * tile.y) / size
21
+ ];
22
+ }
23
+
24
+ /** Address a point in degrees. Interior edges belong to the east/south tile. */
25
+ export function getGeographicTile(
26
+ longitude: number,
27
+ latitude: number,
28
+ level: number
29
+ ): GeographicTile {
30
+ const size = gridSize(level);
31
+ if (
32
+ !Number.isFinite(longitude) ||
33
+ longitude < -180 ||
34
+ longitude > 180 ||
35
+ !Number.isFinite(latitude) ||
36
+ latitude < -90 ||
37
+ latitude > 90
38
+ )
39
+ throw new RangeError('Expected longitude in [-180, 180] and latitude in [-90, 90]');
40
+ return {
41
+ x: Math.min(size - 1, Math.floor(((longitude + 180) / 360) * size)),
42
+ y: Math.min(size - 1, Math.floor(((90 - latitude) / 180) * size)),
43
+ level
44
+ };
45
+ }
46
+
47
+ /** Cover a degree rectangle without enumerating tiles, including wrapped longitude.
48
+ * Positive-area queries exclude edge-only neighbors; zero-width/height queries use
49
+ * point-address edge ownership. Both +/-180 spellings of the seam stay explicit.
50
+ */
51
+ export function getGeographicTileRanges(bounds: GlobeBounds, level: number): GeographicTileRange[] {
52
+ const size = gridSize(level);
53
+ const split = splitGlobeBounds(bounds);
54
+ const positiveWidth = split.filter(([west, , east]) => east > west);
55
+ const rectangles = positiveWidth.length ? positiveWidth : split;
56
+ const intervals = rectangles
57
+ .map(([west, south, east, north]) => {
58
+ const start = getGeographicTile(west, north, level);
59
+ return {
60
+ minX: start.x,
61
+ maxX:
62
+ west === east
63
+ ? start.x
64
+ : Math.max(start.x, Math.min(size - 1, Math.ceil(((east + 180) / 360) * size) - 1)),
65
+ minY: start.y,
66
+ maxY:
67
+ south === north
68
+ ? start.y
69
+ : Math.max(start.y, Math.min(size - 1, Math.ceil(((90 - south) / 180) * size) - 1))
70
+ };
71
+ })
72
+ .sort((a, b) => a.minX - b.minX);
73
+ const result: GeographicTileRange[] = [];
74
+ for (const range of intervals) {
75
+ const previous = result[result.length - 1];
76
+ if (previous && range.minX <= previous.maxX + 1)
77
+ previous.maxX = Math.max(previous.maxX, range.maxX);
78
+ else result.push(range);
79
+ }
80
+ return result;
81
+ }
82
+
83
+ function gridSize(level: number): number {
84
+ if (!Number.isInteger(level) || level < 0 || level > 30)
85
+ throw new RangeError('Level must be an integer from 0 through 30');
86
+ return 2 ** level;
87
+ }
@@ -0,0 +1,68 @@
1
+ // math.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
+ // SPDX-FileComment: Original spherical-cap extrema; requirements from https://github.com/keplergl/kepler.gl/pull/3557. No sampled viewport or upstream implementation copied.
5
+
6
+ /** Degrees [west, south, east, north]; west > east means a dateline crossing. */
7
+ export type GlobeBounds = readonly [number, number, number, number];
8
+
9
+ /** Conservative geographic extent of every visible surface point on a sphere.
10
+ * Cartesian X points to longitude 0, Y to longitude 90, Z north. Covers the
11
+ * entire horizon cap, not just the screen: safe overfetch, not viewport-tight.
12
+ * Returns undefined on/inside the sphere. Radius and camera use matching units.
13
+ */
14
+ export function getGlobeHorizonBounds(
15
+ camera: Readonly<ArrayLike<number>>,
16
+ radius: number
17
+ ): GlobeBounds | undefined {
18
+ if (
19
+ camera.length !== 3 ||
20
+ !Array.from(camera).every(Number.isFinite) ||
21
+ !Number.isFinite(radius) ||
22
+ radius <= 0
23
+ )
24
+ throw new RangeError('Expected a finite camera and positive sphere radius');
25
+ const distance = Math.hypot(camera[0], camera[1], camera[2]);
26
+ if (!Number.isFinite(distance)) throw new RangeError('Camera magnitude must be finite');
27
+ if (distance <= radius) return undefined;
28
+ const latitude = Math.atan2(camera[2], Math.hypot(camera[0], camera[1]));
29
+ const longitude = Math.atan2(camera[1], camera[0]);
30
+ const angle = Math.acos(radius / distance);
31
+ const degrees = 180 / Math.PI;
32
+ // Outward padding protects tangent and near-pole limits from floating rounding.
33
+ const padding = 1e-10;
34
+ const south = Math.max(-90, (latitude - angle) * degrees - padding);
35
+ const north = Math.min(90, (latitude + angle) * degrees + padding);
36
+ if (Math.abs(latitude) + angle >= Math.PI / 2 - 1e-12) return [-180, south, 180, north];
37
+ const extent = Math.asin(Math.min(1, Math.sin(angle) / Math.cos(latitude))) * degrees + padding;
38
+ return [wrap(longitude * degrees - extent), south, wrap(longitude * degrees + extent), north];
39
+ }
40
+
41
+ /** Split wrapped bounds into protocol-safe rectangles (e.g. EPSG:4326 WMS).
42
+ * No screen sampling, image caching or request policy is implied.
43
+ */
44
+ export function splitGlobeBounds(bounds: GlobeBounds): GlobeBounds[] {
45
+ const [west, south, east, north] = bounds;
46
+ if (
47
+ bounds.length !== 4 ||
48
+ !bounds.every(Number.isFinite) ||
49
+ west < -180 ||
50
+ west > 180 ||
51
+ east < -180 ||
52
+ east > 180 ||
53
+ south < -90 ||
54
+ north > 90 ||
55
+ south > north
56
+ )
57
+ throw new RangeError('Invalid degree bounds');
58
+ return west <= east
59
+ ? [[west, south, east, north]]
60
+ : [
61
+ [west, south, 180, north],
62
+ [-180, south, east, north]
63
+ ];
64
+ }
65
+
66
+ function wrap(longitude: number): number {
67
+ return ((((longitude + 180) % 360) + 360) % 360) - 180;
68
+ }
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
  export {Ellipsoid} from './ellipsoid';
6
6
  export {EllipsoidTangentPlane} from './ellipsoid-tangent-plane';
@@ -8,3 +8,17 @@ export {LngLatRectangle} from './lng-lat-rectangle';
8
8
  export {makeOBBFromRegion} from './make-obb-from-region';
9
9
  export type {GeodeticRegion, MakeOBBFromRegionOptions} from './make-obb-from-region';
10
10
  export {isWGS84} from './type-utils';
11
+
12
+ export type {SpheroidParameters} from '@math.gl/types';
13
+
14
+ export {EllipsoidOccluder} from './ellipsoid-occluder';
15
+ export type {EllipsoidHorizon} from './ellipsoid-occluder';
16
+ export {getGlobeHorizonBounds, splitGlobeBounds} from './globe-horizon-bounds';
17
+ export type {GlobeBounds} from './globe-horizon-bounds';
18
+
19
+ export {
20
+ getGeographicTile,
21
+ getGeographicTileBounds,
22
+ getGeographicTileRanges
23
+ } from './geographic-tiles';
24
+ export type {GeographicTile, GeographicTileRange} from './geographic-tiles';
@@ -1,6 +1,8 @@
1
1
  // math.gl
2
- // SPDX-License-Identifier: MIT and Apache-2.0
3
- // Copyright (c) vis.gl contributors
2
+ // SPDX-License-Identifier: MIT AND Apache-2.0
3
+ // SPDX-FileCopyrightText: Copyright 2011-2018 CesiumJS Contributors
4
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
5
+ // SPDX-FileComment: Derived from Cesium. See the repository LICENSE for upstream attribution and Apache-2.0 terms.
4
6
 
5
7
  // This file is derived from the Cesium library under Apache 2 license
6
8
  // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md
@@ -1,6 +1,8 @@
1
1
  // math.gl
2
- // SPDX-License-Identifier: MIT and Apache-2.0
3
- // Copyright (c) vis.gl contributors
2
+ // SPDX-License-Identifier: MIT AND Apache-2.0
3
+ // SPDX-FileCopyrightText: Copyright 2011-2018 CesiumJS Contributors
4
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
5
+ // SPDX-FileComment: Derived from Cesium. See the repository LICENSE for upstream attribution and Apache-2.0 terms.
4
6
 
5
7
  // This file is derived from the Cesium math library under Apache 2 license
6
8
  // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md
package/src/type-utils.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // math.gl
2
- // SPDX-License-Identifier: MIT and Apache-2.0
3
- // Copyright (c) vis.gl contributors
2
+ // SPDX-License-Identifier: MIT AND Apache-2.0
3
+ // SPDX-FileCopyrightText: Copyright 2011-2018 CesiumJS Contributors
4
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
5
+ // SPDX-FileComment: Derived from Cesium. See the repository LICENSE for upstream attribution and Apache-2.0 terms. The numeric-component cartographic adapter is original math.gl code.
4
6
 
5
7
  // This file is derived from the Cesium math library under Apache 2 license
6
8
  // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md
@@ -114,6 +116,32 @@ export function toCartographicFromRadians<T extends Cartographic>(
114
116
  return toCartographic(vector, cartographic, config._cartographicRadians ? identity : toDegrees);
115
117
  }
116
118
 
119
+ /** Original numeric adapter: snapshot values before writing application-owned outputs. */
120
+ export function toCartographicFromRadiansComponents<T extends Cartographic>(
121
+ longitude: number,
122
+ latitude: number,
123
+ height: number,
124
+ cartographic: T
125
+ ): T {
126
+ const map = config._cartographicRadians ? identity : toDegrees;
127
+ const mappedLongitude = map(longitude),
128
+ mappedLatitude = map(latitude);
129
+ if ('longitude' in cartographic) {
130
+ cartographic.longitude = mappedLongitude;
131
+ cartographic.latitude = mappedLatitude;
132
+ cartographic.height = height;
133
+ } else if ('x' in cartographic) {
134
+ cartographic.x = mappedLongitude;
135
+ cartographic.y = mappedLatitude;
136
+ cartographic.z = height;
137
+ } else {
138
+ cartographic[0] = mappedLongitude;
139
+ cartographic[1] = mappedLatitude;
140
+ cartographic[2] = height;
141
+ }
142
+ return cartographic;
143
+ }
144
+
117
145
  export function toCartographicFromDegrees<T extends Cartographic>(
118
146
  vector: Readonly<NumericArray>,
119
147
  cartographic: T