diff --git a/modules/core/src/classes/matrix3.ts b/modules/core/src/classes/matrix3.ts index 2c622286..1b8f04d5 100644 --- a/modules/core/src/classes/matrix3.ts +++ b/modules/core/src/classes/matrix3.ts @@ -5,6 +5,7 @@ import {NumericArray, NumericArray9} from '@math.gl/types'; import {Matrix} from './base/matrix'; +import {Vector3} from './vector3'; import {checkVector} from '../lib/validators'; import {vec4_transformMat3} from '../lib/gl-matrix-extras'; @@ -202,6 +203,54 @@ export class Matrix3 extends Matrix { return this.check(); } + /** + * Computes the product of this matrix and a column vector. + * + * @param {Vector3} cartesian The column. + * @param {Vector3} result The object onto which to store the result. + * @returns {Vector3} The modified result parameter. + */ + multiplyByVector(cartesian: Vector3, result?: Vector3): Vector3 { + if (!result) result = new Vector3(); + + const vX = cartesian.x; + const vY = cartesian.y; + const vZ = cartesian.z; + + const x = this[0] * vX + this[3] * vY + this[6] * vZ; + const y = this[1] * vX + this[4] * vY + this[7] * vZ; + const z = this[2] * vX + this[5] * vY + this[8] * vZ; + + result.x = x; + result.y = y; + result.z = z; + + return result; + } + + /** + * Computes the product of this matrix times a (non-uniform) scale, as if the scale were a scale matrix. + * + * @param {Vector3} scale The non-uniform scale on the right-hand side. + * @param {Matrix3} result The object onto which to store the result. + * @returns {Matrix3} The modified result parameter. + */ + multiplyByScale(scale: Vector3, result?: Matrix3): Matrix3 { + if (!result) result = new Matrix3(); + + result[0] = this[0] * scale.x; + result[1] = this[1] * scale.x; + result[2] = this[2] * scale.x; + result[3] = this[3] * scale.y; + result[4] = this[4] * scale.y; + result[5] = this[5] * scale.y; + result[6] = this[6] * scale.z; + result[7] = this[7] * scale.z; + result[8] = this[8] * scale.z; + + return result; + } + rotate(radians: number): this { mat3_rotate(this, this, radians); return this.check(); diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts index 373c167c..ec5932ab 100644 --- a/modules/core/src/index.ts +++ b/modules/core/src/index.ts @@ -48,6 +48,8 @@ export { // math.gl global utility methods config, configure, + safeMod, + normalizeAngle, formatValue, isArray, clone, diff --git a/modules/core/src/lib/common.ts b/modules/core/src/lib/common.ts index c571f93f..c32c91f8 100644 --- a/modules/core/src/lib/common.ts +++ b/modules/core/src/lib/common.ts @@ -7,6 +7,7 @@ import type {NumericArray} from '@math.gl/types'; import type {MathArray} from '../classes/base/math-array'; +import {EPSILON14, TWO_PI, PI} from './math-utils'; const RADIANS_TO_DEGREES = (1 / Math.PI) * 180; const DEGREES_TO_RADIANS = (1 / 180) * Math.PI; @@ -122,6 +123,72 @@ export function degrees( return map(radians, (radians) => radians * RADIANS_TO_DEGREES, result); } +/** + * The modulo operation that also works for negative dividends. + * + * @param m The dividend. + * @param n The divisor. + * @returns The remainder. + */ +export function safeMod(m: number, n: number): number { + if (Math.sign(m) === Math.sign(n) && Math.abs(m) < Math.abs(n)) { + return m; + } + + return ((m % n) + n) % n; +} + +/** + * Produces an angle restricted to its equivalent in a normalized range + * + * @param angle in radians + * @param range 'zero-to-two-pi' - in the range 0 <= angle <= 2PI, | 'negative-pi-to-pi' - -Pi <= angle <= Pi + * @returns The angle in the range [0, TWO_PI] or [-PI, PI].. + */ +export function normalizeAngle( + angle: number, + range: 'zero-to-two-pi' | 'negative-pi-to-pi' +): number { + switch (range) { + case 'negative-pi-to-pi': + return negativePiToPi(angle); + case 'zero-to-two-pi': + return zeroToTwoPi(angle); + default: + return angle; + } +} + +/** + * Produces an angle in the range 0 <= angle <= 2Pi which is equivalent to the provided angle. + * + * @param angle in radians + * @returns The angle in the range [0, TWO_PI]. + */ +function zeroToTwoPi(angle: number): number { + if (angle >= 0 && angle <= TWO_PI) { + return angle; + } + const remainder = safeMod(angle, TWO_PI); + if (Math.abs(remainder) < EPSILON14 && Math.abs(angle) > EPSILON14) { + return TWO_PI; + } + return remainder; +} + +/** + * Produces an angle in the range -Pi <= angle <= Pi which is equivalent to the provided angle. + * + * @param angle in radians + * @returns The angle in the range [-PI, PI]. + */ +function negativePiToPi(angle: number): number { + if (angle >= -PI && angle <= PI) { + return angle; + } + return zeroToTwoPi(angle + PI) - PI; +} + /** * "GLSL equivalent" of `Math.sin`: Works on single values and vectors * @deprecated diff --git a/modules/core/src/lib/math-utils.ts b/modules/core/src/lib/math-utils.ts index f28ea7c5..a523b10d 100644 --- a/modules/core/src/lib/math-utils.ts +++ b/modules/core/src/lib/math-utils.ts @@ -29,4 +29,5 @@ export const PI_OVER_TWO = Math.PI / 2; export const PI_OVER_FOUR = Math.PI / 4; export const PI_OVER_SIX = Math.PI / 6; +export const PI = Math.PI; export const TWO_PI = Math.PI * 2; diff --git a/modules/culling/src/index.ts b/modules/culling/src/index.ts index 5cbc162b..53d6282c 100644 --- a/modules/culling/src/index.ts +++ b/modules/culling/src/index.ts @@ -9,6 +9,7 @@ export {BoundingSphere} from './lib/bounding-volumes/bounding-sphere'; export {OrientedBoundingBox} from './lib/bounding-volumes/oriented-bounding-box'; export {CullingVolume} from './lib/culling-volume'; export {Plane} from './lib/plane'; +export {Ray} from './lib/ray'; export {PerspectiveOffCenterFrustum as _PerspectiveOffCenterFrustum} from './lib/perspective-off-center-frustum'; export {PerspectiveFrustum as _PerspectiveFrustum} from './lib/perspective-frustum'; diff --git a/modules/culling/src/lib/bounding-volumes/oriented-bounding-box.ts b/modules/culling/src/lib/bounding-volumes/oriented-bounding-box.ts index d5fa7c91..2e512952 100644 --- a/modules/culling/src/lib/bounding-volumes/oriented-bounding-box.ts +++ b/modules/culling/src/lib/bounding-volumes/oriented-bounding-box.ts @@ -7,9 +7,10 @@ import {NumericArray} from '@math.gl/types'; import {Vector3, Matrix3, Matrix4, Quaternion} from '@math.gl/core'; + import type {BoundingVolume} from './bounding-volume'; import {BoundingSphere} from './bounding-sphere'; -import type {Plane} from '../plane'; +import {Plane} from '../plane'; import {INTERSECTION} from '../../constants'; const scratchVector3 = new Vector3(); diff --git a/modules/culling/src/lib/plane.ts b/modules/culling/src/lib/plane.ts index 3547d72d..2d3edc67 100644 --- a/modules/culling/src/lib/plane.ts +++ b/modules/culling/src/lib/plane.ts @@ -6,7 +6,8 @@ // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md /* eslint-disable */ -import {Vector3, equals, assert, NumericArray} from '@math.gl/core'; +import {Vector3, equals, assert, NumericArray, _MathUtils} from '@math.gl/core'; +import {Ray} from './ray'; const scratchPosition = new Vector3(); const scratchNormal = new Vector3(); @@ -86,4 +87,33 @@ export class Plane { return scratchPoint.subtract(scaledNormal).to(result); } + + /** + * Computes the intersection of a ray and this plane. + * + * @param {Ray} ray The ray. + * @param {Vector3} [result] The object onto which to store the result. + * @returns {Vector3} The intersection point or undefined if there is no intersections. + */ + intersectWithRay(ray: Ray, result?: Vector3): Vector3 { + if (!result) result = new Vector3(); + + const origin = ray.origin; + const direction = ray.direction; + const normal = this.normal; + const denominator = normal.dot(direction); + + if (Math.abs(denominator) < _MathUtils.EPSILON15) { + return undefined; + } + + const t = (-this.distance - normal.dot(origin)) / denominator; + + if (t < 0) { + return undefined; + } + + result = result.copy(direction).multiplyByScalar(t); + return origin.add(result); + } } diff --git a/modules/culling/src/lib/ray.ts b/modules/culling/src/lib/ray.ts new file mode 100644 index 00000000..a2c19969 --- /dev/null +++ b/modules/culling/src/lib/ray.ts @@ -0,0 +1,31 @@ +// math.gl +// SPDX-License-Identifier: MIT and Apache-2.0 +// Copyright (c) vis.gl contributors + +// This file is derived from the Cesium library under Apache 2 license +// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md + +import {Vector3} from '@math.gl/core'; + +/* Represents a ray that extends infinitely from the provided origin in the provided direction. */ +export class Ray { + origin: Vector3; + direction: Vector3; + + /** + * Creates a new ray that extends infinitely from the provided origin in the provided direction. + * + * @param [origin=Vector3] The origin of the ray. + * @param [direction=Vector3] The direction of the ray. + */ + constructor(origin?: Vector3, direction?: Vector3) { + if (origin) origin = origin.clone(); + else origin = new Vector3(); + + if (direction) direction = direction.clone().normalize(); + else direction = new Vector3(); + + this.origin = origin; + this.direction = direction; + } +} diff --git a/modules/culling/test/lib/ray.spec.ts b/modules/culling/test/lib/ray.spec.ts new file mode 100644 index 00000000..daf6a394 --- /dev/null +++ b/modules/culling/test/lib/ray.spec.ts @@ -0,0 +1,15 @@ +// This file is derived from the Cesium math library under Apache 2 license +// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md + +/* eslint-disable */ +import test from 'tape-promise/tape'; +// import {tapeEquals} from 'test/utils/tape-assertions'; + +import {Vector3, _MathUtils} from '@math.gl/core'; +import {Ray} from '@math.gl/culling'; + +test('Ray#constructs', (t) => { + const ray = new Ray(new Vector3(1, 0, 0)); + t.ok(ray); + t.end(); +}); diff --git a/modules/geospatial/package.json b/modules/geospatial/package.json index d421f1a0..7dcd409f 100644 --- a/modules/geospatial/package.json +++ b/modules/geospatial/package.json @@ -40,6 +40,7 @@ ], "dependencies": { "@math.gl/core": "4.1.0-alpha.9", + "@math.gl/culling": "4.1.0-alpha.9", "@math.gl/types": "4.1.0-alpha.9" }, "gitHead": "e1a95300cb225a90da6e90333d4adf290f7ba501" diff --git a/modules/geospatial/src/ellipsoid/helpers/ellipsoid-transform.ts b/modules/geospatial/src/ellipsoid-helpers/ellipsoid-transform.ts similarity index 100% rename from modules/geospatial/src/ellipsoid/helpers/ellipsoid-transform.ts rename to modules/geospatial/src/ellipsoid-helpers/ellipsoid-transform.ts diff --git a/modules/geospatial/src/ellipsoid/helpers/scale-to-geodetic-surface.ts b/modules/geospatial/src/ellipsoid-helpers/scale-to-geodetic-surface.ts similarity index 100% rename from modules/geospatial/src/ellipsoid/helpers/scale-to-geodetic-surface.ts rename to modules/geospatial/src/ellipsoid-helpers/scale-to-geodetic-surface.ts diff --git a/modules/geospatial/src/ellipsoid-tangent-plane.ts b/modules/geospatial/src/ellipsoid-tangent-plane.ts new file mode 100644 index 00000000..de7c3d75 --- /dev/null +++ b/modules/geospatial/src/ellipsoid-tangent-plane.ts @@ -0,0 +1,98 @@ +// math.gl +// SPDX-License-Identifier: MIT and Apache-2.0 +// Copyright (c) vis.gl contributors + +// This file is derived from the Cesium library under Apache 2 license +// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md + +import {Vector2, Vector3, Matrix4} from '@math.gl/core'; +import {Plane, Ray} from '@math.gl/culling'; +import {Ellipsoid} from './ellipsoid'; + +const scratchOrigin = new Vector3(); +const scratchCart3 = new Vector3(); +const scratchEastNorthUp = new Matrix4(); +const scratchPlane = new Plane(); + +const scratchProjectPointOntoPlaneRay = new Ray(); +const scratchProjectPointOntoPlaneCartesian3 = new Vector3(); +const scratchDirection = new Vector3(); + +/** A plane tangent to the WGS84 ellipsoid at the provided origin */ +export class EllipsoidTangentPlane { + private _origin: Vector3; + private _xAxis: Vector3; + private _yAxis: Vector3; + private _plane: Plane; + + /** + * Creates a new plane tangent to the WGS84 ellipsoid at the provided origin. + * If origin is not on the surface of the ellipsoid, it's surface projection will be used. + * + * @param {Cartesian3} origin The point on the surface of the ellipsoid where the tangent plane touches. + */ + constructor(origin: number[]) { + origin = Ellipsoid.WGS84.scaleToGeodeticSurface(origin, scratchOrigin); + + const eastNorthUp = Ellipsoid.WGS84.eastNorthUpToFixedFrame(origin, scratchEastNorthUp); + + this._origin = origin as Vector3; + this._xAxis = new Vector3(scratchCart3.from(eastNorthUp.getColumn(0))); + this._yAxis = new Vector3(scratchCart3.from(eastNorthUp.getColumn(1))); + const normal = new Vector3(scratchCart3.from(eastNorthUp.getColumn(2))); + + this._plane = scratchPlane.fromPointNormal(origin, normal); + } + + /** + * Computes the projection of the provided 3D position onto the 2D plane, along the plane normal. + * + * @param {Vector3} cartesian The point to project. + * @param {Vector2} [result] The object onto which to store the result. + * @returns {Vector2} The modified result parameter or a new Cartesian2 instance if none was provided. + */ + projectPointToNearestOnPlane(cartesian: Vector3, result?: Vector2): Vector2 { + if (!result) result = new Vector2(); + + const plane = this._plane; + + const ray = scratchProjectPointOntoPlaneRay; + scratchProjectPointOntoPlaneRay.origin = cartesian; + scratchProjectPointOntoPlaneRay.direction = scratchDirection.copy(plane.normal); + + let intersectionPoint = plane.intersectWithRay(ray, scratchProjectPointOntoPlaneCartesian3); + + if (!intersectionPoint) { + ray.direction = ray.direction.negate(); + intersectionPoint = plane.intersectWithRay(ray, scratchProjectPointOntoPlaneCartesian3); + } + + const v = intersectionPoint.subtract(this._origin); + const x = this._xAxis.dot(v); + const y = this._yAxis.dot(v); + + result.x = x; + result.y = y; + return result; + } + + get plane() { + return this._plane; + } + + get origin() { + return this._origin; + } + + get xAxis() { + return this._xAxis; + } + + get yAxis() { + return this._yAxis; + } + + get zAxis() { + return this._plane.normal; + } +} diff --git a/modules/geospatial/src/ellipsoid/ellipsoid.ts b/modules/geospatial/src/ellipsoid.ts similarity index 96% rename from modules/geospatial/src/ellipsoid/ellipsoid.ts rename to modules/geospatial/src/ellipsoid.ts index fee125e3..73b535ff 100644 --- a/modules/geospatial/src/ellipsoid/ellipsoid.ts +++ b/modules/geospatial/src/ellipsoid.ts @@ -8,12 +8,12 @@ /* eslint-disable */ import {Vector3, Matrix4, assert, equals, _MathUtils, NumericArray, vec3} from '@math.gl/core'; -import {WGS84_RADIUS_X, WGS84_RADIUS_Y, WGS84_RADIUS_Z} from '../constants'; -import {fromCartographicToRadians, toCartographicFromRadians} from '../type-utils'; +import {WGS84_RADIUS_X, WGS84_RADIUS_Y, WGS84_RADIUS_Z} from './constants'; +import {fromCartographicToRadians, toCartographicFromRadians} from './type-utils'; -import type {AxisDirection} from './helpers/ellipsoid-transform'; -import {localFrameToFixedFrame} from './helpers/ellipsoid-transform'; -import {scaleToGeodeticSurface} from './helpers/scale-to-geodetic-surface'; +import type {AxisDirection} from './ellipsoid-helpers/ellipsoid-transform'; +import {localFrameToFixedFrame} from './ellipsoid-helpers/ellipsoid-transform'; +import {scaleToGeodeticSurface} from './ellipsoid-helpers/scale-to-geodetic-surface'; const scratchVector = new Vector3(); const scratchNormal = new Vector3(); diff --git a/modules/geospatial/src/index.ts b/modules/geospatial/src/index.ts index 93a582ac..09fabb3f 100644 --- a/modules/geospatial/src/index.ts +++ b/modules/geospatial/src/index.ts @@ -2,5 +2,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -export {Ellipsoid} from './ellipsoid/ellipsoid'; +export {Ellipsoid} from './ellipsoid'; +export {EllipsoidTangentPlane} from './ellipsoid-tangent-plane'; +export {LngLatRectangle} from './lng-lat-rectangle'; export {isWGS84} from './type-utils'; diff --git a/modules/geospatial/src/lng-lat-rectangle.ts b/modules/geospatial/src/lng-lat-rectangle.ts new file mode 100644 index 00000000..8e4d4a06 --- /dev/null +++ b/modules/geospatial/src/lng-lat-rectangle.ts @@ -0,0 +1,67 @@ +// math.gl +// SPDX-License-Identifier: MIT and Apache-2.0 +// Copyright (c) vis.gl contributors + +// This file is derived from the Cesium library under Apache 2 license +// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md + +import {Vector3, normalizeAngle, _MathUtils} from '@math.gl/core'; + +/** A two dimensional region specified as longitude and latitude coordinates. */ +export class LngLatRectangle { + west: number; + south: number; + east: number; + north: number; + + /** + * Creates a new two dimensional region specified as longitude and latitude coordinates. + * + * @param west The westernmost longitude, in radians, in the range [-Pi, Pi]. + * @param south The southernmost latitude, in radians, in the range [-Pi/2, Pi/2]. + * @param east The easternmost longitude, in radians, in the range [-Pi, Pi]. + * @param north The northernmost latitude, in radians, in the range [-Pi/2, Pi/2]. + */ + constructor(west: number, south: number, east: number, north: number) { + this.west = west; + this.south = south; + this.east = east; + this.north = north; + } + + /** + * Computes the center of a rectangle. + * + * @param rectangle The rectangle for which to find the center + * @param [result] The object onto which to store the result. + * @returns The modified result parameter or a new Cartographic instance if none was provided. + */ + static center(rectangle: LngLatRectangle, result?: Vector3): Vector3 { + if (!result) result = new Vector3(); + + let east = rectangle.east; + const west = rectangle.west; + + if (east < west) { + east += _MathUtils.TWO_PI; + } + + const longitude = normalizeAngle((west + east) * 0.5, 'negative-pi-to-pi'); + const latitude = (rectangle.south + rectangle.north) * 0.5; + + result.x = longitude; + result.y = latitude; + result.z = 0.0; + return result; + } + + get width() { + let east = this.east; + const west = this.west; + + if (east < west) { + east += _MathUtils.TWO_PI; + } + return east - west; + } +} diff --git a/modules/geospatial/src/make-obb-from-region.ts b/modules/geospatial/src/make-obb-from-region.ts new file mode 100644 index 00000000..ec52a800 --- /dev/null +++ b/modules/geospatial/src/make-obb-from-region.ts @@ -0,0 +1,303 @@ +// math.gl +// SPDX-License-Identifier: MIT and Apache-2.0 +// Copyright (c) vis.gl contributors + +// This file is derived from the Cesium math library under Apache 2 license +// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md + +import {Vector2, Vector3, degrees, _MathUtils} from '@math.gl/core'; +import {OrientedBoundingBox, Plane} from '@math.gl/culling'; +// @ts-ignore +// eslint-disable-next-line import/no-unresolved +import {LngLatRectangle} from './lng-lat-rectangle'; +import {Ellipsoid} from './ellipsoid'; +import {EllipsoidTangentPlane} from './ellipsoid-tangent-plane'; + +const scratchOffset = new Vector3(); + +const scratchTangentPoint = new Vector3(); +const scratchPerimeterCartographicNC = new Vector3(); +const scratchPerimeterCartographicNW = new Vector3(); +const scratchPerimeterCartographicCW = new Vector3(); +const scratchPerimeterCartographicSW = new Vector3(); +const scratchPerimeterCartographicSC = new Vector3(); +const scratchPerimeterCartesianNC = new Vector3(); +const scratchPerimeterCartesianNW = new Vector3(); +const scratchPerimeterCartesianCW = new Vector3(); +const scratchPerimeterCartesianSW = new Vector3(); +const scratchPerimeterCartesianSC = new Vector3(); +const scratchPerimeterProjectedNC = new Vector2(); +const scratchPerimeterProjectedNW = new Vector2(); +const scratchPerimeterProjectedCW = new Vector2(); +const scratchPerimeterProjectedSW = new Vector2(); +const scratchPerimeterProjectedSC = new Vector2(); + +const scratchPlane = new Plane(); +const scratchPlaneOrigin = new Vector3(); +const scratchPlaneNormal = new Vector3(); +const scratchPlaneXAxis = new Vector3(); +const scratchHorizonCartesian = new Vector3(); +const scratchHorizonProjected = new Vector2(); +const scratchMaxY = new Vector3(); +const scratchMinY = new Vector3(); +const scratchZ = new Vector3(); + +const VECTOR3_UNIT_X = new Vector3(1, 0, 0); +const VECTOR3_UNIT_Z = new Vector3(0, 0, 1); + +/** + * Computes an OrientedBoundingBox that bounds a region on the surface of the WGS84 ellipsoid. + * There are no guarantees about the orientation of the bounding box. + * + * @param {number[]} region The cartographic region ([west, south, east, north, minimum height, maximum height]) + * on the surface of the ellipsoid. + * @returns {OrientedBoundingBox} The modified result parameter or a new OrientedBoundingBox instance if none was provided. + */ +// eslint-disable-next-line max-statements +export function makeOBBfromRegion(region: number[]): OrientedBoundingBox { + const obb = new OrientedBoundingBox(); + + const [west, south, east, north, minimumHeight, maximumHeight] = region; + + const northDeg = degrees(north); + const southDeg = degrees(south); + + let maxX: number; + let maxY: number; + let maxZ: number; + let minX: number; + let minY: number; + let minZ: number; + let plane: Plane; + + const rectangle = new LngLatRectangle(west, south, east, north); + const tangentPoint = LngLatRectangle.center(rectangle, scratchTangentPoint); + const tangentPointCartographic = new Vector3([ + degrees(tangentPoint.x), + degrees(tangentPoint.y), + 0.0 + ]); + + const lonCenter = tangentPoint.x; + const lonCenterDeg = tangentPointCartographic.x; + + if (rectangle.width <= _MathUtils.PI) { + const westDeg = degrees(west); + + const tangentPoint = Ellipsoid.WGS84.cartographicToCartesian(tangentPointCartographic); + const ellipsoidTangentPlane = new EllipsoidTangentPlane(tangentPoint); + + const latCenter = southDeg < 0.0 && northDeg > 0.0 ? 0.0 : tangentPointCartographic.y; + + const perimeterCartographicNC = scratchPerimeterCartographicNC.copy([ + lonCenterDeg, + northDeg, + maximumHeight + ]); + const perimeterCartographicNW = scratchPerimeterCartographicNW.copy([ + westDeg, + northDeg, + maximumHeight + ]); + const perimeterCartographicCW = scratchPerimeterCartographicCW.copy([ + westDeg, + latCenter, + maximumHeight + ]); + const perimeterCartographicSW = scratchPerimeterCartographicSW.copy([ + westDeg, + southDeg, + maximumHeight + ]); + const perimeterCartographicSC = scratchPerimeterCartographicSC.copy([ + lonCenterDeg, + southDeg, + maximumHeight + ]); + + const perimeterCartesianNC = Ellipsoid.WGS84.cartographicToCartesian( + perimeterCartographicNC, + scratchPerimeterCartesianNC + ); + let perimeterCartesianNW = Ellipsoid.WGS84.cartographicToCartesian( + perimeterCartographicNW, + scratchPerimeterCartesianNW + ); + const perimeterCartesianCW = Ellipsoid.WGS84.cartographicToCartesian( + perimeterCartographicCW, + scratchPerimeterCartesianCW + ); + let perimeterCartesianSW = Ellipsoid.WGS84.cartographicToCartesian( + perimeterCartographicSW, + scratchPerimeterCartesianSW + ); + const perimeterCartesianSC = Ellipsoid.WGS84.cartographicToCartesian( + perimeterCartographicSC, + scratchPerimeterCartesianSC + ); + + const perimeterProjectedNC = ellipsoidTangentPlane.projectPointToNearestOnPlane( + perimeterCartesianNC, + scratchPerimeterProjectedNC + ); + const perimeterProjectedNW = ellipsoidTangentPlane.projectPointToNearestOnPlane( + perimeterCartesianNW, + scratchPerimeterProjectedNW + ); + const perimeterProjectedCW = ellipsoidTangentPlane.projectPointToNearestOnPlane( + perimeterCartesianCW, + scratchPerimeterProjectedCW + ); + const perimeterProjectedSW = ellipsoidTangentPlane.projectPointToNearestOnPlane( + perimeterCartesianSW, + scratchPerimeterProjectedSW + ); + const perimeterProjectedSC = ellipsoidTangentPlane.projectPointToNearestOnPlane( + perimeterCartesianSC, + scratchPerimeterProjectedSC + ); + + minX = Math.min(perimeterProjectedNW.x, perimeterProjectedCW.x, perimeterProjectedSW.x); + maxX = -minX; + + maxY = Math.max(perimeterProjectedNW.y, perimeterProjectedNC.y); + minY = Math.min(perimeterProjectedSW.y, perimeterProjectedSC.y); + + perimeterCartographicNW.z = perimeterCartographicSW.z = minimumHeight; + perimeterCartesianNW = Ellipsoid.WGS84.cartographicToCartesian( + perimeterCartographicNW, + scratchPerimeterCartesianNW + ); + perimeterCartesianSW = Ellipsoid.WGS84.cartographicToCartesian( + perimeterCartographicSW, + scratchPerimeterCartesianSW + ); + + plane = ellipsoidTangentPlane.plane; + minZ = Math.min( + plane.getPointDistance(perimeterCartesianNW), + plane.getPointDistance(perimeterCartesianSW) + ); + maxZ = maximumHeight; + + return fromPlaneExtents( + ellipsoidTangentPlane.origin, + ellipsoidTangentPlane.xAxis, + ellipsoidTangentPlane.yAxis, + ellipsoidTangentPlane.zAxis, + minX, + maxX, + minY, + maxY, + minZ, + maxZ, + obb + ); + } + + const eastDeg = degrees(east); + + const fullyAboveEquator = south > 0.0; + const fullyBelowEquator = north < 0.0; + const latitudeNearestToEquator = fullyAboveEquator + ? southDeg + : fullyBelowEquator + ? northDeg + : 0.0; + + const planeOrigin = Ellipsoid.WGS84.cartographicToCartesian( + [lonCenterDeg, latitudeNearestToEquator, maximumHeight], + scratchPlaneOrigin + ); + planeOrigin.z = 0.0; + const isPole = + Math.abs(planeOrigin.x) < _MathUtils.EPSILON10 && + Math.abs(planeOrigin.y) < _MathUtils.EPSILON10; + const planeNormal = !isPole ? scratchPlaneNormal.copy(planeOrigin).normalize() : VECTOR3_UNIT_X; + const planeYAxis = VECTOR3_UNIT_Z; + const planeXAxis = scratchPlaneXAxis.copy(planeNormal).cross(planeYAxis); + plane = scratchPlane.fromPointNormal(planeOrigin, planeNormal); + + const horizonCartesian = Ellipsoid.WGS84.cartographicToCartesian( + [degrees(lonCenter + _MathUtils.PI_OVER_TWO), latitudeNearestToEquator, maximumHeight], + scratchHorizonCartesian + ); + const projectedPoint = plane.projectPointOntoPlane( + horizonCartesian, + scratchHorizonProjected + ) as Vector3; + maxX = projectedPoint.dot(planeXAxis); + minX = -maxX; + + maxY = Ellipsoid.WGS84.cartographicToCartesian( + [0.0, northDeg, fullyBelowEquator ? minimumHeight : maximumHeight], + scratchMaxY + ).z; + minY = Ellipsoid.WGS84.cartographicToCartesian( + [0.0, southDeg, fullyAboveEquator ? minimumHeight : maximumHeight], + scratchMinY + ).z; + + const farZ = Ellipsoid.WGS84.cartographicToCartesian( + [eastDeg, latitudeNearestToEquator, maximumHeight], + scratchZ + ); + + minZ = plane.getPointDistance(farZ); + maxZ = 0.0; + + return fromPlaneExtents( + planeOrigin, + planeXAxis, + planeYAxis, + planeNormal, + minX, + maxX, + minY, + maxY, + minZ, + maxZ, + obb + ); +} + +const scratchScale = new Vector3(); + +/** helper function for OrientedBoundingBox.fromRegion() */ +// eslint-disable-next-line max-params +function fromPlaneExtents( + planeOrigin: Vector3, + planeXAxis: Vector3, + planeYAxis: Vector3, + planeZAxis: Vector3, + minimumX: number, + maximumX: number, + minimumY: number, + maximumY: number, + minimumZ: number, + maximumZ: number, + result: OrientedBoundingBox +) { + const center = result.center; + const halfAxes = result.halfAxes; + halfAxes.setColumn(0, planeXAxis); + halfAxes.setColumn(1, planeYAxis); + halfAxes.setColumn(2, planeZAxis); + + let centerOffset = scratchOffset; + centerOffset.x = (minimumX + maximumX) / 2.0; + centerOffset.y = (minimumY + maximumY) / 2.0; + centerOffset.z = (minimumZ + maximumZ) / 2.0; + centerOffset = halfAxes.multiplyByVector(centerOffset, scratchOffset); + + const scale = scratchScale; + scale.x = (maximumX - minimumX) / 2.0; + scale.y = (maximumY - minimumY) / 2.0; + scale.z = (maximumZ - minimumZ) / 2.0; + + planeOrigin = center.copy(planeOrigin); + result.center = planeOrigin.add(centerOffset); + result.halfAxes = halfAxes.multiplyByScale(scale, halfAxes); + + return result; +} diff --git a/modules/geospatial/test/lng-lat-rectangle.spec.ts b/modules/geospatial/test/lng-lat-rectangle.spec.ts new file mode 100644 index 00000000..fda35cbc --- /dev/null +++ b/modules/geospatial/test/lng-lat-rectangle.spec.ts @@ -0,0 +1,7 @@ +import test from 'tape-promise/tape'; +import {LngLatRectangle} from '@math.gl/geospatial'; + +test('LngLatRectangle', (t) => { + t.ok(new LngLatRectangle(0, 0, 0, 0)); + t.end(); +}); diff --git a/modules/geospatial/tsconfig.json b/modules/geospatial/tsconfig.json index b6d00108..d57a67bf 100644 --- a/modules/geospatial/tsconfig.json +++ b/modules/geospatial/tsconfig.json @@ -9,6 +9,7 @@ }, "references": [ {"path": "../types"}, - {"path": "../core"} + {"path": "../core"}, + {"path": "../culling"} ] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 84b13758..a7e3a340 100644 --- a/yarn.lock +++ b/yarn.lock @@ -812,6 +812,7 @@ __metadata: resolution: "@math.gl/culling@workspace:modules/culling" dependencies: "@math.gl/core": "npm:4.1.0-alpha.9" + "@math.gl/geospatial": "npm:4.1.0-alpha.9" "@math.gl/types": "npm:4.1.0-alpha.9" languageName: unknown linkType: soft @@ -844,7 +845,7 @@ __metadata: languageName: unknown linkType: soft -"@math.gl/geospatial@workspace:modules/geospatial": +"@math.gl/geospatial@npm:4.1.0-alpha.9, @math.gl/geospatial@workspace:modules/geospatial": version: 0.0.0-use.local resolution: "@math.gl/geospatial@workspace:modules/geospatial" dependencies: