UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

351 lines (321 loc) 12.7 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { MathUtils, Matrix4, Ray, Sphere, Vector2, Vector3 } from 'three'; import Coordinates from './Coordinates'; import CoordinateSystem from './CoordinateSystem'; const tmpCoord = new Coordinates(CoordinateSystem.epsg4326, 0, 0); const tmpDims = new Vector2(); const tmpVec3 = new Vector3(); const tmpNormal = new Vector3(); const tmpSphere = new Sphere(); const tmpMatrix4 = new Matrix4(); const tmpRay = new Ray(); const tmpEast = new Vector3(); const tmpNorth = new Vector3(); const ZERO = new Vector3(0, 0, 0); const tmpIntersection = new Vector3(); let wgs84; const Z = new Vector3(0, 0, 1); /** * A configurable spheroid that allows conversion from and to geodetic coordinates * and cartesian coordinates, as well as utility function to compute various geodetic values. */ export class Ellipsoid { get semiMajorAxis() { return this._semiMajor; } get semiMinorAxis() { return this._semiMinor; } /** * The [flattening](https://en.wikipedia.org/wiki/Flattening) of this ellipsoid. */ get flattening() { return this._flattening; } /** * The circumference at the equator. */ get equatorialCircumference() { return this._equatorialCircumference; } /** * The [eccentricity](https://en.wikipedia.org/wiki/Eccentricity_(mathematics)) of this ellipsoid. */ get eccentricity() { return this._eccentricity; } /** * The ratio between the semi-minor axis and the semi-major axis. */ get compressionFactor() { return this._semiMinor / this._semiMajor; } constructor(params) { this._semiMajor = params.semiMajorAxis; // Semi-major axis this._semiMinor = params.semiMinorAxis; // Semi-minor axis const flattening = (this._semiMajor - this._semiMinor) / this._semiMajor; // Flattening this._sqEccentricity = Math.sqrt(1 - this._semiMinor ** 2 / this._semiMajor ** 2); this._eccentricity = Math.sqrt(2 * flattening - flattening * flattening); this._flattening = flattening; const a = this._semiMajor; const aa = (1 / a) ** 2; const b = this._semiMinor; this._radii = new Vector3(a, a, b); this._invRadiiSquared = new Vector3(aa, aa, (1 / b) ** 2); this._equatorialCircumference = Math.PI * 2 * this._semiMajor; } /** * The [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84) ellipsoid. */ static get WGS84() { if (wgs84 == null) { wgs84 = new Ellipsoid({ semiMajorAxis: 6_378_137.0, semiMinorAxis: 6_356_752.314245 }); } return wgs84; } /** * A sphere. */ static sphere(radius) { return new Ellipsoid({ semiMinorAxis: radius, semiMajorAxis: radius }); } /** * Returns a new ellipsoid scaled by the specified factor. */ scale(factor) { return new Ellipsoid({ semiMajorAxis: this.semiMajorAxis * factor, semiMinorAxis: this.semiMinorAxis * factor }); } /** * Returns a new ellipsoid growed by the specified offset. The offset is added to the axes. */ grow(offset) { return new Ellipsoid({ semiMajorAxis: this.semiMajorAxis + offset, semiMinorAxis: this.semiMinorAxis + offset }); } /** * Converts the geodetic coordinates to cartesian coordinates in the ECEF coordinate system. * @param lat - The latitude, in degrees. * @param lon - The longitude, in degrees. * @param alt - The altitude, in meters, above or below the ellipsoid. * @param target - The target vector. If none, one will be created. * @returns The cartesian coordinates. */ toCartesian(lat, lon, alt, target) { target = target ?? new Vector3(); const clat = Math.cos(lat * MathUtils.DEG2RAD); const slat = Math.sin(lat * MathUtils.DEG2RAD); const clon = Math.cos(lon * MathUtils.DEG2RAD); const slon = Math.sin(lon * MathUtils.DEG2RAD); const N = this._semiMajor / Math.sqrt(1.0 - this._eccentricity * this._eccentricity * slat * slat); const z = (N * (1.0 - this._eccentricity * this._eccentricity) + alt) * slat; target.set((N + alt) * clat * clon, (N + alt) * clat * slon, z); return target; } /** * Gets the ENU (east/north/up) matrix for the given location in geodetic coordinates. * @param lat - The latitude of the location. * @param lon - The longitude of the location. * @returns The ENU matrix. */ getEastNorthUpMatrix(lat, lon, target) { const position = this.toCartesian(lat, lon, 0, tmpVec3); return this.getEastNorthUpMatrixFromCartesian(position, target); } /** * Gets the ENU (east/north/up) matrix for the given location. * @param point - The cartesian coordinate in the geocentric system of this ellipsoid. * @param target - The optional matrix to set with the ENU matrix. * @returns The ENU matrix. */ getEastNorthUpMatrixFromCartesian(point, target) { const normal = this.getNormalFromCartesian(point, tmpNormal); // Compute the ENU matrix from the normal and the Z axis. const u = normal; const e = tmpEast.crossVectors(Z, u).normalize(); const n = tmpNorth.crossVectors(u, e).normalize(); const result = target ?? new Matrix4(); // prettier-ignore result.set(e.x, e.y, e.z, 0.0, n.x, n.y, n.z, 0.0, u.x, u.y, u.z, 0.0, 0.0, 0.0, 0.0, 1.0).transpose(); return result; } /** * Returns the first intersection of the ray with the ellipsoid, or `null` if the ray does not intersect the ellipsoid. * @param ray - The ray to intersect. * @param target - The optional vector to store the result. * @returns The intersection or null if not intersection was found. */ intersectRay(ray, target) { tmpMatrix4.makeScale(this._radii.x, this._radii.y, this._radii.z).invert(); tmpSphere.center.set(0, 0, 0); tmpSphere.radius = 1; target = target ?? new Vector3(); tmpRay.copy(ray).applyMatrix4(tmpMatrix4); if (tmpRay.intersectSphere(tmpSphere, target)) { tmpMatrix4.makeScale(this._radii.x, this._radii.y, this._radii.z); target.applyMatrix4(tmpMatrix4); return target; } else { return null; } } /** * Returns the normal of the spheroid for the given location. * @param lat - The latitude, in degrees. * @param lon - The longitude, in degrees. * @param target - The target vector to store the result. If none, one will be created. * @returns The normal vector. */ getNormal(lat, lon, target) { const cartesian = this.toCartesian(lat, lon, 0, target); return cartesian.multiply(this._invRadiiSquared).normalize(); } /** * Returns the normal of the spheroid for the given cartesian coordinate. * @param cartesian - The cartesian coordinates. * @param target - The target vector to store the result. If none, one will be created. * @returns The normal vector. */ getNormalFromCartesian(cartesian, target) { target = target ?? new Vector3(); return target.copy(cartesian).multiply(this._invRadiiSquared).normalize(); } /** * Converts the cartesian coordinates to geodetic coordinates. * @param x - The cartesian X coordinate. * @param y - The cartesian Y coordinate. * @param z - The cartesian Z coordinate. * @returns The geodetic coordinates. */ toGeodetic(x, y, z, target) { target = target ?? new Coordinates(CoordinateSystem.epsg4979, 0, 0, 0); const lon = Math.atan2(y, x); const p = Math.sqrt(x ** 2 + y ** 2); const theta = Math.atan2(z * this._semiMajor, p * this._semiMinor); const lat = Math.atan2(z + this._eccentricity ** 2 * this._semiMinor * Math.sin(theta) ** 3, p - this._sqEccentricity ** 2 * this._semiMajor * Math.cos(theta) ** 3); // # Radius of curvature in the prime vertical const N = this._semiMajor / Math.sqrt(1 - this._sqEccentricity ** 2 * Math.sin(lat) ** 2); let height = p / Math.cos(lat) - N; const latitude = MathUtils.radToDeg(lat); const longitude = MathUtils.radToDeg(lon); // Special case : Math.cos(lat) would give zero for // coordinates on the poles, in turn giving wrong height values. if (Math.abs(Math.abs(latitude) - 90) < 0.0000001) { const radius = this._semiMinor; height = Math.abs(z) - radius; } target.set(CoordinateSystem.epsg4979, longitude, latitude, height); return target; } /** * Returns the length of the parallel arc of the given angle, in meters. * @param latitude - The latitude of the parallel. * @param angle - The angle of the arc in degrees. */ getParallelArcLength(latitude, angle) { // Let's compute the radius of the parallel at this latitude const parallelRadius = this._semiMajor * Math.cos(latitude * MathUtils.DEG2RAD); const paralellCircumference = 2 * Math.PI * parallelRadius; return angle / 360 * paralellCircumference; } /** * Returns an approximated length of the meridian arc of the given angle, in meters. * * Note: this function uses a very simplified method, as the actual method involves * intrgrals. For very oblate spheroids, the results will be wrong. * * @param latitude0 - The latitude of the start of the meridian arc * @param latitude1 - The latitude of the end of the meridian arc */ getMeridianArcLength(latitude0, latitude1) { const angle = Math.abs(latitude0 - latitude1); return angle / 360 * this._equatorialCircumference; } /** * Gets the dimensions (width and height) across the center of of the extent, in **meters**. * * Note: this is distinct to {@link Extent.dimensions} which returns the dimensions * in the extent's own CRS (meters or degrees). * @param extent - The extent. * @param target - The object to store the result. If none, one will be created. * @returns The extent dimensions. * @throws if the extent is not in the EPSG:4326 CRS. */ getExtentDimensions(extent, target) { if (!extent.crs.isEpsg(4326)) { throw new Error('not a WGS 84 extent (EPSG:4326)'); } const center = extent.center(tmpCoord); const dims = extent.dimensions(tmpDims); const width = this.getParallelArcLength(center.latitude, dims.width); const height = this.getMeridianArcLength(extent.north, extent.south); target = target ?? new Vector2(); target.set(width, height); return target; } /** * Gets the distance to the horizon given a camera position. * @param cameraPosition - The camera position. * @param center - The center of the ellipsoid (by default (0, 0, 0)). * @returns The distance, in meters, from the camera to the horizon. */ getOpticalHorizon(cameraPosition, center) { center = center ?? ZERO; const ray = tmpRay.set(cameraPosition, center.clone().sub(cameraPosition)); const intersection = this.intersectRay(ray, tmpIntersection); if (intersection == null) { return null; } const height = cameraPosition.distanceTo(intersection); const horizonDistance = Math.sqrt(height * (2 * this.semiMajorAxis + height)); return horizonDistance; } /** * Determine whether the given point is visible from the camera or occluded by the horizon * of this ellipsoid. * @param cameraPosition - The camera position, in world space coordinates. * @param point - The point to test, in world space coordinates. * @param radiusFactor - An optional factor to apply to ellipsoid radii to add a margin of error. * @returns `true` if the given point is above the horizon, `false` otherwise. */ isHorizonVisible(cameraPosition, point, radiusFactor = 1) { // We use a slightly smaller ellipsoid because we want to avoid false negatives // for negative elevations (think very deep seafloors). // https://cesium.com/blog/2013/04/25/horizon-culling/ // Ellipsoid radii - WGS84 shown here const rX = this._semiMajor * radiusFactor; const rY = this._semiMajor * radiusFactor; const rZ = this._semiMinor * radiusFactor; // Vector CV const cvX = cameraPosition.x / rX; const cvY = cameraPosition.y / rY; const cvZ = cameraPosition.z / rZ; const vhMagnitudeSquared = cvX * cvX + cvY * cvY + cvZ * cvZ - 1.0; // Target position, transformed to scaled space const tX = point.x / rX; const tY = point.y / rY; const tZ = point.z / rZ; // Vector VT const vtX = tX - cvX; const vtY = tY - cvY; const vtZ = tZ - cvZ; // VT dot VC is the inverse of VT dot CV const vtDotVc = -(vtX * cvX + vtY * cvY + vtZ * cvZ); return !(vtDotVc > vhMagnitudeSquared && vtDotVc * vtDotVc / (vtX * vtX + vtY * vtY + vtZ * vtZ) > vhMagnitudeSquared); } } export default Ellipsoid;