UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

554 lines (508 loc) 19.6 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { register } from 'ol/proj/proj4'; import proj4 from 'proj4'; import { MathUtils, Vector2, type Vector2Like, Vector3, type Vector3Like } from 'three'; import CoordinateSystem from './CoordinateSystem'; import { getConverter } from './ProjectionCache'; proj4.defs('EPSG:4978', '+proj=geocent +datum=WGS84 +units=m +no_defs +type=crs'); proj4.defs('EPSG:4979', '+proj=longlat +datum=WGS84 +no_defs +type=crs'); // Note this is exactly the same definition as EPSG:4326. However, for clarity // in the context of panoramic images, we use a name that is not associated with // georeferencing since the image itself is not georeferenced in the world in the // same way as an orthoimage, but simply positioned in the environment of the panorama. proj4.defs('equirectangular', '+proj=longlat +datum=WGS84 +no_defs +type=crs'); register(proj4); /** * A geographic coordinate expressed in degrees, minutes, seconds. */ export interface DMS { degrees: number; minutes?: number; seconds?: number; } export function parseDMS(dms: DMS): number { const { degrees, minutes, seconds } = dms; const result = degrees + (minutes ?? 0) / 60 + (seconds ?? 0) / 3600; return result; } function assertIsGeographic(crs: CoordinateSystem): void { if (!crs.isGeographic()) { throw new Error('This operation is only permitted on geographic coordinates.'); } } function assertIsNotGeographic(crs: CoordinateSystem): void { if (crs.isGeographic()) { throw new Error('This operation is only permitted on non-geographic coordinates.'); } } /** * Represents coordinates associated with a {@link CoordinateSystem | coordinate reference system (CRS)}. * The exact semantics of the values in the coordinates depend on the kind of CRS used: * - for projected systems, X is the easting, Y is the northing and Z is the elevation above/below * the map plane. * - for geocentric systems (such as ECEF), XYZ are cartesian coordinates in the 3D frame. * - for geographic systems (such as EPSG:4326), X is the longitude, Y is the latitude and Z is the * elevation above/below the reference ellipsoid. */ export class Coordinates { public readonly isCoordinates = true as const; private readonly _values: Float64Array; public crs: CoordinateSystem; /** * Create coordinates from a pair of XY coordinates. * @param crs - The coordinate system to use. * @param x - The X coordinate. * @param y - The Y coordinate. * @example * const x = 124225; * const y = 10244.2; * const mercator = new Coordinates(CoordinateSystem.epsg3857, x, y); * * // If using geographic coordinates, X is the longitude and Y is the latitude. * const lon = 4.2123; * const lat = 43.256; * const geo = new Coordinates(CoordinateSystem.epsg4326, lon, lat); */ public constructor(crs: CoordinateSystem, x: number, y: number); /** * Create coordinates from a XYZ triplet. * @param crs - The coordinate system to use. * @param x - The X coordinate. * @param y - The Y coordinate. * @param z - The Z coordinate. * @example * const x = 124225; * const y = 10244.2; * const z = 1000; * const mercator = new Coordinates(CoordinateSystem.epsg3857, x, y, z); * * // If using geographic coordinates, X is the longitude and Y is the latitude. * // Z is still the elevation in meters. * const lon = 4.2123; * const lat = 43.256; * const geo = new Coordinates(CoordinateSystem.epsg4326, lon, lat, z); */ public constructor(crs: CoordinateSystem, x: number, y: number, z: number); /** * Create coordinates from a {@link Vector2Like} * @param crs - The coordinate system to use. * @param xy - The vector to initialize coordinates. * @example * const coord = new Coordinates(CoordinateSystem.epsg3857, new THREE.Vector2(1020, 20924)); * // Alternatively, you don't have to use an actual Vector2 instance. * // Any object that matches the Vector2Like interface will do. * const coord = new Coordinates(CoordinateSystem.epsg3857, \{ x: 1020, y: 20924 \}); */ public constructor(crs: CoordinateSystem, xy: Vector2Like); /** * Create coordinates from a {@link Vector3Like} * @param crs - The coordinate system to use. * @param xyz - The vector to initialize coordinates. * @example * const coord = new Coordinates(CoordinateSystem.epsg3857, new THREE.Vector3(1020, 20924, 1000)); * // Alternatively, you don't have to use an actual Vector3 instance. * // Any object that matches the Vector3Like interface will do. * const coord = new Coordinates(CoordinateSystem.epsg3857, \{ x: 1020, y: 20924, z: 1000 \}); */ public constructor(crs: CoordinateSystem, xyz: Vector3Like); public constructor( crs: CoordinateSystem, x: number | Vector2Like | Vector3Like, y?: number, z?: number, ) { this._values = new Float64Array(3); this.crs = crs; // @ts-expect-error type shenanigans this.set(crs, x, y, z); } public get values(): Float64Array { return this._values; } /** * Sets the values in this coordinate from a XY pair. * @param crs - The coordinate system to use. * @param x - The X coordinate. * @param y - The Y coordinate. */ public set(crs: CoordinateSystem, x: number, y: number): this; /** * Sets the values in this coordinate from a XYZ triplet. * @param crs - The coordinate system to use. * @param x - The X coordinate. * @param y - The Y coordinate. * @param z - The Z coordinate. */ public set(crs: CoordinateSystem, x: number, y: number, z: number): this; /** * Sets the values in this coordinate from a {@link Vector2Like} * @param crs - The coordinate system to use. * @param vector - The vector to initialize coordinates. */ public set(crs: CoordinateSystem, xy: Vector2Like): this; /** * Sets the values in this coordinate from a {@link Vector3Like} * @param crs - The coordinate system to use. * @param vector - The vector to initialize coordinates. */ public set(crs: CoordinateSystem, xyz: Vector3Like): this; public set( crs: CoordinateSystem, x: number | Vector2Like | Vector3Like, y?: number, z?: number, ): this { this.crs = crs; if (typeof x === 'object') { const c = x as Vector2Like | Vector3Like; this._values[0] = c.x; this._values[1] = c.y; this._values[2] = 'z' in c ? c.z : 0; } else { this._values[0] = x; this._values[1] = y ?? 0; this._values[2] = z ?? 0; } return this; } public clone(target?: Coordinates): Coordinates { let r; if (target) { target.set(this.crs, this.x, this.y, this.z); r = target; } else { r = new Coordinates(this.crs, this._values[0], this._values[1], this._values[2]); } return r; } public copy(src: Coordinates): this { const v = src._values; this.set(src.crs, v[0], v[1], v[2]); return this; } /** * Returns the longitude in geographic coordinates. * Coordinates must be in geographic system (can be * converted by using {@link as} ). * * ```js * const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 }; * const coordinates = new Coordinates( * 'EPSG:4326', position.longitude, position.latitude, position.altitude); // Geographic * coordinates.longitude; // Longitude in geographic system * // returns 2.33 * * // or * * const position = { x: 20885167, y: 849862, z: 23385912 }; * // Geocentric system * const coords = new Coordinates(CoordinateSystem.epsg4978, position.x, position.y, position.z); * const coordinates = coords.as(CoordinateSystem.epsg4326); // Geographic system * coordinates.longitude; // Longitude in geographic system * // returns 2.330201911389028 * ``` * @returns The longitude of the position. */ public get longitude(): number { assertIsGeographic(this.crs); return this._values[0]; } /** * Returns the latitude in geographic coordinates. * Coordinates must be in geographic system (can be converted by using {@link as}). * * ```js * const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 }; * const coordinates = new Coordinates( * 'EPSG:4326', position.longitude, position.latitude, position.altitude); // Geographic * coordinates.latitude; // Latitude in geographic system * // returns : 48.24 * * // or * * const position = { x: 20885167, y: 849862, z: 23385912 }; * // Geocentric system * const coords = new Coordinates(CoordinateSystem.epsg4978, position.x, position.y, position.z); * const coordinates = coords.as(CoordinateSystem.epsg4326); // Geographic system * coordinates.latitude; // Latitude in geographic system * // returns : 48.24830764643365 * ``` * @returns The latitude of the position. */ public get latitude(): number { assertIsGeographic(this.crs); return this._values[1]; } /** * Returns the altitude in geographic coordinates. * Coordinates must be in geographic system (can be converted by using {@link as}). * * ```js * const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 }; * // Geographic system * const coordinates = * new Coordinates(CoordinateSystem.epsg4326, position.longitude, position.latitude, position.altitude); * coordinates.altitude; // Altitude in geographic system * // returns : 24999549 * * // or * * const position = { x: 20885167, y: 849862, z: 23385912 }; * // Geocentric system * const coords = new Coordinates(CoordinateSystem.epsg4978, position.x, position.y, position.z); * const coordinates = coords.as(CoordinateSystem.epsg4326); // Geographic system * coordinates.altitude; // Altitude in geographic system * // returns : 24999548.046711832 * ``` * @returns The altitude of the position. */ public get altitude(): number { assertIsGeographic(this.crs); return this._values[2]; } /** * Set the altitude. * * @param altitude - the new altitude. * ```js * coordinates.setAltitude(10000) * ``` */ public setAltitude(altitude: number): void { assertIsGeographic(this.crs); this._values[2] = altitude; } public withLongitude(longitude: number | DMS): this { assertIsGeographic(this.crs); if (typeof longitude === 'number') { this._values[0] = longitude; } else { this._values[0] = parseDMS(longitude); } return this; } public withLatitude(latitude: number | DMS): this { assertIsGeographic(this.crs); if (typeof latitude === 'number') { this._values[1] = latitude; } else { this._values[1] = parseDMS(latitude); } return this; } public withCRS(crs: CoordinateSystem): this { this.crs = crs; return this; } public withAltitude(altitude: number): this { assertIsGeographic(this.crs); this._values[2] = altitude; return this; } /** * Returns the `x` component of this coordinate in geocentric coordinates. * Coordinates must be in geocentric system (can be * converted by using {@link as}). * * ```js * const position = { x: 20885167, y: 849862, z: 23385912 }; * const coordinates = new Coordinates(CoordinateSystem.epsg4978, position.x, position.y, position.z); * coordinates.x; // Geocentric system * // returns : 20885167 * * // or * * const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 }; * // Geographic system * const coords = * new Coordinates(CoordinateSystem.epsg4326, position.longitude, position.latitude, position.altitude); * const coordinates = coords.as(CoordinateSystem.epsg4978); // Geocentric system * coordinates.x; // Geocentric system * // returns : 20888561.0301258 * ``` * @returns The `x` component of the position. */ public get x(): number { assertIsNotGeographic(this.crs); return this._values[0]; } /** * Returns the `y` component of this coordinate in geocentric coordinates. * Coordinates must be in geocentric system (can be * converted by using {@link as}). * * ```js * const position = { x: 20885167, y: 849862, z: 23385912 }; * const coordinates = new Coordinates(CoordinateSystem.epsg4978, position.x, position.y, position.z); * coordinates.y; // Geocentric system * // returns : 849862 * ``` * @returns The `y` component of the position. */ public get y(): number { assertIsNotGeographic(this.crs); return this._values[1]; } /** * Returns the `z` component of this coordinate in geocentric coordinates. * Coordinates must be in geocentric system (can be * converted by using {@link as}). * * ```js * const position = { x: 20885167, y: 849862, z: 23385912 }; * const coordinates = new Coordinates(CoordinateSystem.epsg4978, position.x, position.y, position.z); * coordinates.z; // Geocentric system * // returns : 23385912 * ``` * @returns The `z` component of the position. */ public get z(): number { assertIsNotGeographic(this.crs); return this._values[2]; } /** * Returns the equivalent `Vector3` of this coordinate. * * ```js * const position = { x: 20885167, y: 849862, z: 23385912 }; * // Geocentric system * const coordinates = new Coordinates(CoordinateSystem.epsg4978, position.x, position.y, position.z); * coordinates.toVector3(); * // returns : Vector3 * // x: 20885167 * // y: 849862 * // z: 23385912 * * // or * * const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 }; * // Geographic system * const coordinates = * new Coordinates(CoordinateSystem.epsg4326, position.longitude, position.latitude, position.altitude); * coordinates.toVector3(); * // returns : Vector3 * // x: 2.33 * // y: 48.24 * // z: 24999549 * ``` * @param target - the geocentric coordinate * @returns target position */ public toVector3(target?: Vector3): Vector3 { const v = target || new Vector3(); v.fromArray(this._values); return v; } /** * Returns the equivalent `Vector2` of this coordinate. Note that the Z component (elevation) is * lost. * * ```js * * const position = { x: 20885167, y: 849862, z: 23385912 }; * // Metric system * const coordinates = new Coordinates(CoordinateSystem.epsg3857, position.x, position.y, position.z); * coordinates.toVector2(); * // returns : Vector2 * // x: 20885167 * // y: 849862 * * // or * * const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 }; * // Geographic system * const coordinates = * new Coordinates(CoordinateSystem.epsg4326, position.longitude, position.latitude, position.altitude); * coordinates.toVector2(); * // returns : Vector2 * // x: 2.33 * // y: 48.24 * ``` * @param target - the geocentric coordinate * @returns target position */ public toVector2(target?: Vector2): Vector2 { const v = target || new Vector2(); v.fromArray(this._values); return v; } /** * Converts coordinates in another [CRS](http://inspire.ec.europa.eu/theme/rs). * * If target is not specified, creates a new instance. * The original instance is never modified (except if you passed it as `target`). * * ```js * const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 }; * // Geographic system * const coords = * new Coordinates(CoordinateSystem.epsg4326, position.longitude, position.latitude, position.altitude); * const coordinates = coords.as(CoordinateSystem.epsg4978); // Geocentric system * ``` * @param crs - the [CRS](http://inspire.ec.europa.eu/theme/rs) EPSG string * @param target - the object that is returned * @returns the converted coordinate */ public as(crs: CoordinateSystem, target?: Coordinates): Coordinates { return this.convert(crs, target); } // Only support explicit conversions private convert(newCrs: CoordinateSystem, target?: Coordinates): Coordinates { target = target || new Coordinates(newCrs, 0, 0, 0); if (newCrs.id === this.crs.id) { return target.copy(this); } if (this.crs.id in proj4.defs && newCrs.id in proj4.defs) { const val0 = this._values[0]; let val1 = this._values[1]; const crsIn = this.crs; // there is a bug for converting anything from and to 4978 with proj4 // https://github.com/proj4js/proj4js/issues/195 // the workaround is to use an intermediate projection, like EPSG:4326 if (crsIn.isEpsg(4326) && newCrs.isEpsg(3857)) { val1 = MathUtils.clamp(val1, -89.999999, 89.999999); const p = getConverter(crsIn, newCrs).forward([val0, val1]); return target.set(newCrs, p[0], p[1], this._values[2]); } // here is the normal case with proj4 const p = getConverter(crsIn, newCrs).forward([val0, val1]); return target.set(newCrs, p[0], p[1], this._values[2]); } throw new Error(`Cannot convert from crs ${this.crs.id} to ${newCrs.id}`); } /** * Returns the boolean result of the check if this coordinate is geographic (true) * or geocentric (false). * * ```js * const position = { x: 20885167, y: 849862, z: 23385912 }; * const coordinates = new Coordinates(CoordinateSystem.epsg4978, position.x, position.y, position.z); * coordinates.isGeographic(); // Geocentric system * // returns : false * ``` * @returns `true` if the coordinate is geographic. */ public isGeographic(): boolean { return this.crs.isGeographic(); } /** * Creates a geographic coordinate in EPSG:4326 */ public static WGS84( latitude: number | DMS, longitude: number | DMS, altitude?: number, ): Coordinates { return new Coordinates(CoordinateSystem.epsg4326, 0, 0) .withLatitude(latitude) .withLongitude(longitude) .withAltitude(altitude ?? 0); } } export function isCoordinates(obj: unknown): obj is Coordinates { return (obj as Coordinates).isCoordinates === true; } export default Coordinates;