Skip to content

Commit

Permalink
refacto: migrate Coordinates to typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
Desplandis committed Oct 17, 2024
1 parent 24b8da5 commit 7692d70
Showing 1 changed file with 120 additions and 101 deletions.
221 changes: 120 additions & 101 deletions src/Core/Geographic/Coordinates.js → src/Core/Geographic/Coordinates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ import proj4 from 'proj4';
import CRS from 'Core/Geographic/Crs';
import Ellipsoid from 'Core/Math/Ellipsoid';

proj4.defs('EPSG:4978', '+proj=geocent +datum=WGS84 +units=m +no_defs');
import type { ProjectionLike } from './Crs';

const ellipsoid = new Ellipsoid();
const projectionCache = {};
const projectionCache: Record<string, Record<string, proj4.Converter>> = {};

const v0 = new THREE.Vector3();
const v1 = new THREE.Vector3();

let coord0;
let coord1;
let coord0: Coordinates;
let coord1: Coordinates;

function proj4cache(crsIn, crsOut) {
export interface CoordinatesLike {
readonly crs: string;
readonly x: number;
readonly y: number;
readonly z: number;
}

function proj4cache(crsIn: string, crsOut: string): proj4.Converter {
if (!projectionCache[crsIn]) {
projectionCache[crsIn] = {};
}
Expand Down Expand Up @@ -43,34 +50,39 @@ function proj4cache(crsIn, crsOut) {
* @example
* // Declare EPSG:3946 with proj4
* itowns.proj4.defs('EPSG:3946', '+proj=lcc +lat_1=45.25 +lat_2=46.75 +lat_0=46 +lon_0=3 +x_0=1700000 +y_0=5200000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs');
*
* @property {boolean} isCoordinates - Used to checkout whether this coordinates
* is a Coordinates. Default is true. You should not change this, as it is used
* internally for optimisation.
* @property {string} crs - A supported crs by default in
* [`proj4js`](https://github.com/proj4js/proj4js#named-projections), or an
* added crs to `proj4js` (using `proj4.defs`). Note that `EPSG:4978` is also
* supported by default in itowns.
* @property {number} x - The first value of the coordinate.
* @property {number} y - The second value of the coordinate.
* @property {number} z - The third value of the coordinate.
* @property {number} latitude - The first value of the coordinate.
* @property {number} longitude - The second value of the coordinate.
* @property {number} altitude - The third value of the coordinate.
* @property {THREE.Vector3} geodesicNormal - The geodesic normal of the
* coordinate.
*/
class Coordinates {
/**
* @param {string} crs - A supported Coordinate Reference System.
* @param {number|Array<number>|Coordinates|THREE.Vector3} [v0=0] -
* x or longitude value, or a more complex one: it can be an array of three
* numbers, being x/lon, y/lat, z/alt, or it can be `THREE.Vector3`. It can
* also simply be a Coordinates.
* @param {number} [v1=0] - y or latitude value.
* @param {number} [v2=0] - z or altitude value.
* Used to checkout whether this coordinates is a Coordinates. Default is
* true. You should not change this, as it is used internally for
* optimisation.
*/
readonly isCoordinates: boolean;
/**
* A supported crs by default in
* [`proj4js`](https://github.com/proj4js/proj4js#named-projections), or an
* added crs to `proj4js` (using `proj4.defs`). Note that `EPSG:4978` is
* also supported by default in itowns.
*/
crs: ProjectionLike;

/** The first value of the coordinate. */
x: number;
/** The second value of the coordinate. */
y: number;
/** The third value of the coordinate. */
z: number;

private _normal: THREE.Vector3;
private _normalNeedsUpdate: boolean;

/**
* @param crs - A supported Coordinate Reference System.
* @param x - x or longitude value.
* @param y - y or latitude value.
* @param z - z or altitude value.
*/
constructor(crs, v0 = 0, v1 = 0, v2 = 0) {
constructor(crs: ProjectionLike, x: number = 0, y: number = 0, z: number = 0) {
this.isCoordinates = true;

CRS.isValid(crs);
Expand All @@ -85,30 +97,34 @@ class Coordinates {
// Normal
this._normal = new THREE.Vector3();

if (v0.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((v0 as any).length > 0) { // deepscan-disable-line
console.warn(
'Deprecated Coordinates#constructor(string, number[]),',
'use new Coordinates(string).setFromArray(number[]) instead.',
'use `new Coordinates(string).setFromArray(number[])` instead.',
);
this.setFromArray(v0);
} else if (v0.isVector3 || v0.isCoordinates) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.setFromArray(x as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} else if ((x as any).isVector3 || (x as any).isCoordinates) {
console.warn(
'Deprecated Coordinates#constructor(string, Vector3),',
'use new Coordinates(string).setFromVector3(Vector3) instead.',
'use `new Coordinates(string).setFromVector3(Vector3)` instead.',
);
this.setFromVector3(v0);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.setFromVector3(x as any);
} else {
this.setFromValues(v0, v1, v2);
this.setFromValues(x, y, z);
}

this._normalNeedsUpdate = true;
}

/**
* Sets the Coordinate Reference System.
* @param {String} crs Coordinate Reference System (e.g. 'EPSG:4978')
* @param crs - Coordinate Reference System (e.g. 'EPSG:4978')
*/
setCrs(crs) {
setCrs(crs: ProjectionLike): this {
CRS.isValid(crs);
this.crs = crs;
return this;
Expand All @@ -117,16 +133,16 @@ class Coordinates {
/**
* Set the values of this Coordinates.
*
* @param {number} [v0=0] - x or longitude value.
* @param {number} [v1=0] - y or latitude value.
* @param {number} [v2=0] - z or altitude value.
* @param x - x or longitude value.
* @param y - y or latitude value.
* @param z - z or altitude value.
*
* @return {Coordinates} This Coordinates.
* @returns This Coordinates.
*/
setFromValues(v0 = 0, v1 = 0, v2 = 0) {
this.x = v0 == undefined ? 0 : v0;
this.y = v1 == undefined ? 0 : v1;
this.z = v2 == undefined ? 0 : v2;
setFromValues(x: number = 0, y: number = 0, z: number = 0): this {
this.x = x;
this.y = y;
this.z = z;

this._normalNeedsUpdate = true;
return this;
Expand All @@ -135,48 +151,50 @@ class Coordinates {
/**
* Set the values of this Coordinates from an array.
*
* @param {Array<number>} array - An array of number to assign to the
* Coordinates.
* @param {number} [offset] - Optional offset into the array.
* @param array - An array of number to assign to the Coordinates.
* @param offset - Optional offset into the array.
*
* @return {Coordinates} This Coordinates.
* @returns This Coordinates.
*/
setFromArray(array, offset = 0) {
return this.setFromValues(array[offset], array[offset + 1], array[offset + 2]);
setFromArray(array: number[], offset: number = 0): this {
return this.setFromValues(
array[offset],
array[offset + 1],
array[offset + 2],
);
}

/**
* Set the values of this Coordinates from a `THREE.Vector3` or an `Object`
* having `x/y/z` properties, like a `Coordinates`.
*
* @param {THREE.Vector3|Coordinates} v0 - The object to read the values
* from.
* @param v - The object to read the values from.
*
* @return {Coordinates} This Coordinates.
* @returns This Coordinates.
*/
setFromVector3(v0) {
return this.setFromValues(v0.x, v0.y, v0.z);
setFromVector3(v: THREE.Vector3Like): this {
return this.setFromValues(v.x, v.y, v.z);
}

/**
* Returns a new Coordinates with the same values as this one. It will
* instantiate a new Coordinates with the same CRS as this one.
*
* @return {Coordinates} The target with its new coordinates.
* @returns The target with its new coordinates.
*/
clone() {
return new Coordinates(this.crs, this);
clone(): Coordinates {
return new Coordinates(this.crs, this.x, this.y, this.z);
}

/**
* Copies the values of the passed Coordinates to this one. The CRS is
* however not copied.
*
* @param {Coordinates} src - The source to copy from.
* @param src - The source to copy from.
*
* @return {Coordinates} This Coordinates.
* @returns This Coordinates.
*/
copy(src) {
copy(src: CoordinatesLike): this {
this.crs = src.crs;
return this.setFromVector3(src);
}
Expand All @@ -197,6 +215,9 @@ class Coordinates {
this.z = value;
}

/**
* The geodesic normal of the coordinate.
*/
get geodesicNormal() {
if (this._normalNeedsUpdate) {
this._normalNeedsUpdate = false;
Expand All @@ -216,55 +237,50 @@ class Coordinates {
/**
* Return this Coordinates values into a `THREE.Vector3`.
*
* @param {THREE.Vector3} [target] - The target to put the values in. If not
* specified, a new vector will be created.
*
* @return {THREE.Vector3}
* @param target - The target to put the values in. If not specified, a new
* vector will be created.
*/
toVector3(target = new THREE.Vector3()) {
toVector3(target: THREE.Vector3 = new THREE.Vector3()): THREE.Vector3 {
return target.copy(this);
}

/**
* Copy values coordinates to array
*
* @param {number[]} array - array to store this vector to. If this is not
* @param array - array to store this vector to. If this is not
* provided a new array will be created.
* @param {number} [offset=0] - optional offset into the array.
* @param offset - optional offset into the array.
*
* @return {number[]} Returns an array [x, y, z], or copies x, y and z into
* the provided array.
* @returns An array [x, y, z], or copies x, y and z into the provided
* array.
*/
toArray(array = [], offset = 0) {
toArray(array: number[] = [], offset: number = 0): ArrayLike<number> {
return THREE.Vector3.prototype.toArray.call(this, array, offset);
}

/**
* Calculate planar distance between this coordinates and `coord`.
* Planar distance is the straight-line euclidean distance calculated in a 2D cartesian coordinate system.
*
* @param {Coordinates} coord The coordinate
* @return {number} planar distance
* Planar distance is the straight-line euclidean distance calculated in a
* 2D cartesian coordinate system.
*
* @param coord - The coordinate
* @returns planar distance
*/
planarDistanceTo(coord) {
planarDistanceTo(coord: Coordinates): number {
this.toVector3(v0).setZ(0);
coord.toVector3(v1).setZ(0);
return v0.distanceTo(v1);
}

/**
* Calculate geodetic distance between this coordinates and `coord`.
* **Geodetic distance** is calculated in an ellispoid space as the shortest distance
* across the curved surface of the world.
*
* => As the crow flies/ Orthodromy
*
* @param {Coordinates} coord The coordinate
* @return {number} geodetic distance
* **Geodetic distance** is calculated in an ellispoid space as the shortest
* distance across the curved surface of the world.
*
* @param coord - The coordinate
* @returns geodetic distance
*/
geodeticDistanceTo(coord) {
geodeticDistanceTo(coord: Coordinates): number {
this.as('EPSG:4326', coord0);
coord.as('EPSG:4326', coord1);
return ellipsoid.geodesicDistance(coord0, coord1);
Expand All @@ -273,34 +289,36 @@ class Coordinates {
/**
* Calculate earth euclidean distance between this coordinates and `coord`.
*
* @param {Coordinates} coord The coordinate
* @return {number} earth euclidean distance
*
* @param coord - The coordinate
* @returns earth euclidean distance
*/
spatialEuclideanDistanceTo(coord) {
spatialEuclideanDistanceTo(coord: Coordinates): number {
this.as('EPSG:4978', coord0).toVector3(v0);
coord.as('EPSG:4978', coord1).toVector3(v1);
return v0.distanceTo(v1);
}

/**
* Multiplies this `coordinates` (with an implicit 1 in the 4th dimension) and `mat`.
* Multiplies this `coordinates` (with an implicit 1 in the 4th dimension)
* and `mat`.
*
* @param {THREE.Matrix4} mat The matrix.
* @return {Coordinates} return this object.
* @param mat - The matrix.
* @returns return this object.
*/
applyMatrix4(mat) {
return THREE.Vector3.prototype.applyMatrix4.call(this, mat);
applyMatrix4(mat: THREE.Matrix4): this {
THREE.Vector3.prototype.applyMatrix4.call(this, mat);
return this;
}

/**
* Returns coordinates in the wanted [CRS](http://inspire.ec.europa.eu/theme/rs).
* Returns coordinates in the wanted
* [CRS](http://inspire.ec.europa.eu/theme/rs).
*
* @param {string} crs - The CRS to convert the Coordinates into.
* @param {Coordinates} [target] - The target to put the converted
* @param crs - The CRS to convert the Coordinates into.
* @param target - The target to put the converted
* Coordinates into. If not specified a new one will be created.
*
* @return {Coordinates} - The resulting Coordinates after the conversion.
* @returns The resulting Coordinates after the conversion.
*
* @example
* const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 };
Expand All @@ -318,15 +336,16 @@ class Coordinates {
* @example
* new Coordinates('EPSG:4978', x: 20885167, y: 849862, z: 23385912).as('EPSG:4326'); // Geographic system
*/
as(crs, target = new Coordinates(crs)) {
as(crs: ProjectionLike, target = new Coordinates(crs)): Coordinates {
if (this.crs == crs) {
target.copy(this);
} else {
if (CRS.is4326(this.crs) && crs == 'EPSG:3857') {
this.y = THREE.MathUtils.clamp(this.y, -89.999999, 89.999999);
}

target.setFromArray(proj4cache(this.crs, crs).forward([this.x, this.y, this.z]));
target.setFromArray(proj4cache(this.crs, crs)
.forward([this.x, this.y, this.z]));
}

target.crs = crs;
Expand Down

0 comments on commit 7692d70

Please sign in to comment.