From a555a7257ee8a5ee10c7fa01f5b0d7658c8e4f2a Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 11 Oct 2024 23:24:42 +0200 Subject: [PATCH 01/13] refacto: migrate Ellipsoid to typescript --- src/Core/Math/{Ellipsoid.js => Ellipsoid.ts} | 107 ++++++++++++------- test/unit/ellipsoid.js | 29 +++-- 2 files changed, 89 insertions(+), 47 deletions(-) rename src/Core/Math/{Ellipsoid.js => Ellipsoid.ts} (57%) diff --git a/src/Core/Math/Ellipsoid.js b/src/Core/Math/Ellipsoid.ts similarity index 57% rename from src/Core/Math/Ellipsoid.js rename to src/Core/Math/Ellipsoid.ts index 8c0ea17221..1b5d7bf751 100644 --- a/src/Core/Math/Ellipsoid.js +++ b/src/Core/Math/Ellipsoid.ts @@ -1,6 +1,6 @@ import * as THREE from 'three'; import proj4 from 'proj4'; -import Coordinates from 'Core/Geographic/Coordinates'; +import Coordinates from '../Geographic/Coordinates'; export const ellipsoidSizes = new THREE.Vector3( proj4.WGS84.a, @@ -10,7 +10,13 @@ export const ellipsoidSizes = new THREE.Vector3( const normal = new THREE.Vector3(); class Ellipsoid { - constructor(size = ellipsoidSizes) { + size: THREE.Vector3; + eccentricity: number; + + private _radiiSquared: THREE.Vector3; + private _invRadiiSquared: THREE.Vector3; + + constructor(size: THREE.Vector3 = ellipsoidSizes) { this.size = new THREE.Vector3(); this._radiiSquared = new THREE.Vector3(); this._invRadiiSquared = new THREE.Vector3(); @@ -19,11 +25,11 @@ class Ellipsoid { this.setSize(size); } - geodeticSurfaceNormal(cartesian, target = new THREE.Vector3()) { + geodeticSurfaceNormal(cartesian: Coordinates, target = new THREE.Vector3()) { return cartesian.toVector3(target).multiply(this._invRadiiSquared).normalize(); } - geodeticSurfaceNormalCartographic(coordCarto, target = new THREE.Vector3()) { + geodeticSurfaceNormalCartographic(coordCarto: Coordinates, target = new THREE.Vector3()) { const longitude = THREE.MathUtils.degToRad(coordCarto.longitude); const latitude = THREE.MathUtils.degToRad(coordCarto.latitude); const cosLatitude = Math.cos(latitude); @@ -33,19 +39,19 @@ class Ellipsoid { Math.sin(latitude)); } - setSize(size) { + setSize(size: THREE.Vector3Like) { this.size.set(size.x, size.y, size.z); this._radiiSquared.multiplyVectors(size, size); - this._invRadiiSquared.x = (size.x == 0) ? 0 : 1 / this._radiiSquared.x; - this._invRadiiSquared.y = (size.y == 0) ? 0 : 1 / this._radiiSquared.y; - this._invRadiiSquared.z = (size.z == 0) ? 0 : 1 / this._radiiSquared.z; + this._invRadiiSquared.x = (size.x === 0) ? 0 : 1 / this._radiiSquared.x; + this._invRadiiSquared.y = (size.y === 0) ? 0 : 1 / this._radiiSquared.y; + this._invRadiiSquared.z = (size.z === 0) ? 0 : 1 / this._radiiSquared.z; this.eccentricity = Math.sqrt(this._radiiSquared.x - this._radiiSquared.z) / this.size.x; } - cartographicToCartesian(coordCarto, target = new THREE.Vector3()) { + cartographicToCartesian(coordCarto: Coordinates, target = new THREE.Vector3()) { normal.copy(coordCarto.geodesicNormal); target.multiplyVectors(this._radiiSquared, normal); @@ -60,18 +66,23 @@ class Ellipsoid { } /** - * Convert cartesian coordinates to geographic according to the current ellipsoid of revolution. - * @param {Object} position - The coordinate to convert - * @param {number} position.x - * @param {number} position.y - * @param {number} position.z - * @param {Coordinate} [target] coordinate to copy result - * @returns {Coordinate} an object describing the coordinates on the reference ellipsoid, angles are in degree + * Convert cartesian coordinates to geographic according to the current + * ellipsoid of revolution. + * @param position - The coordinate to convert + * @param target - coordinate to copy result + * @returns an object describing the coordinates on the reference ellipsoid, + * angles are in degree */ - cartesianToCartographic(position, target = new Coordinates('EPSG:4326', 0, 0, 0)) { + cartesianToCartographic( + position: THREE.Vector3Like, + target = new Coordinates('EPSG:4326', 0, 0, 0), + ) { // for details, see for example http://www.linz.govt.nz/data/geodetic-system/coordinate-conversion/geodetic-datum-conversions/equations-used-datum - // TODO the following is only valable for oblate ellipsoid of revolution. do we want to support triaxial ellipsoid? - const R = Math.sqrt(position.x * position.x + position.y * position.y + position.z * position.z); + // TODO the following is only valable for oblate ellipsoid of + // revolution. do we want to support triaxial ellipsoid? + const R = Math.sqrt(position.x * position.x + + position.y * position.y + + position.z * position.z); const a = this.size.x; // x const b = this.size.z; // z const e = Math.abs((a * a - b * b) / (a * a)); @@ -84,14 +95,22 @@ class Ellipsoid { const sinu = Math.sin(nu); const cosu = Math.cos(nu); - const phi = Math.atan((position.z * (1 - f) + e * a * sinu * sinu * sinu) / ((1 - f) * (rsqXY - e * a * cosu * cosu * cosu))); + const phi = Math.atan( + (position.z * (1 - f) + e * a * sinu * sinu * sinu) / + ((1 - f) * (rsqXY - e * a * cosu * cosu * cosu))); - const h = (rsqXY * Math.cos(phi)) + position.z * Math.sin(phi) - a * Math.sqrt(1 - e * Math.sin(phi) * Math.sin(phi)); + const h = (rsqXY * Math.cos(phi)) + + position.z * Math.sin(phi) - + a * Math.sqrt(1 - e * Math.sin(phi) * Math.sin(phi)); - return target.setFromValues(THREE.MathUtils.radToDeg(theta), THREE.MathUtils.radToDeg(phi), h); + return target.setFromValues( + THREE.MathUtils.radToDeg(theta), + THREE.MathUtils.radToDeg(phi), + h, + ); } - cartographicToCartesianArray(coordCartoArray) { + cartographicToCartesianArray(coordCartoArray: Coordinates[]) { const cartesianArray = []; for (let i = 0; i < coordCartoArray.length; i++) { cartesianArray.push(this.cartographicToCartesian(coordCartoArray[i])); @@ -100,19 +119,26 @@ class Ellipsoid { return cartesianArray; } - intersection(ray) { + intersection(ray: THREE.Ray) { const EPSILON = 0.0001; const O_C = ray.origin; const dir = ray.direction; // normalizeVector( dir ); const a = - ((dir.x * dir.x) * this._invRadiiSquared.x) + ((dir.y * dir.y) * this._invRadiiSquared.y) + ((dir.z * dir.z) * this._invRadiiSquared.z); + ((dir.x * dir.x) * this._invRadiiSquared.x) + + ((dir.y * dir.y) * this._invRadiiSquared.y) + + ((dir.z * dir.z) * this._invRadiiSquared.z); const b = - ((2 * O_C.x * dir.x) * this._invRadiiSquared.x) + ((2 * O_C.y * dir.y) * this._invRadiiSquared.y) + ((2 * O_C.z * dir.z) * this._invRadiiSquared.z); + ((2 * O_C.x * dir.x) * this._invRadiiSquared.x) + + ((2 * O_C.y * dir.y) * this._invRadiiSquared.y) + + ((2 * O_C.z * dir.z) * this._invRadiiSquared.z); + const c = - ((O_C.x * O_C.x) * this._invRadiiSquared.x) + ((O_C.y * O_C.y) * this._invRadiiSquared.y) + ((O_C.z * O_C.z) * this._invRadiiSquared.z) - 1; + ((O_C.x * O_C.x) * this._invRadiiSquared.x) + + ((O_C.y * O_C.y) * this._invRadiiSquared.y) + + ((O_C.z * O_C.z) * this._invRadiiSquared.z) - 1; let d = ((b * b) - (4 * a * c)); if (d < 0 || a === 0 || b === 0 || c === 0) { return false; } @@ -122,8 +148,11 @@ class Ellipsoid { const t1 = (-b + d) / (2 * a); const t2 = (-b - d) / (2 * a); - if (t1 <= EPSILON && t2 <= EPSILON) { return false; } // both intersections are behind the ray origin - // var back = (t1 <= EPSILON || t2 <= EPSILON); // If only one intersection (t>0) then we are inside the ellipsoid and the intersection is at the back of the ellipsoid + if (t1 <= EPSILON && t2 <= EPSILON) { + // both intersections are behind the ray origin + return false; + } + let t = 0; if (t1 <= EPSILON) { t = t2; } else if (t2 <= EPSILON) { t = t1; } else { t = (t1 < t2) ? t1 : t2; } @@ -137,21 +166,17 @@ class Ellipsoid { return inter; } - computeDistance(coordCarto1, coordCarto2) { - console.warn('computeDistance is renamed to geodesicDistance'); - this.geodesicDistance(coordCarto1, coordCarto2); - } - /** * Calculate the geodesic distance, between coordCarto1 and coordCarto2. - * It's most short distance on ellipsoid surface between coordCarto1 and coordCarto2. + * It's most short distance on ellipsoid surface between coordCarto1 and + * coordCarto2. * It's called orthodromy. * - * @param {Coordinates} coordCarto1 The coordinate carto 1 - * @param {Coordinates} coordCarto2 The coordinate carto 2 - * @return {number} The orthodromic distance between the two given coordinates. + * @param coordCarto1 - The coordinate carto 1 + * @param coordCarto2 - The coordinate carto 2 + * @returns The orthodromic distance between the two given coordinates. */ - geodesicDistance(coordCarto1, coordCarto2) { + geodesicDistance(coordCarto1: Coordinates, coordCarto2: Coordinates) { // The formula uses the distance on approximated sphere, // with the nearest local radius of curvature of the ellipsoid // https://geodesie.ign.fr/contenu/fichiers/Distance_longitude_latitude.pdf @@ -160,7 +185,9 @@ class Ellipsoid { const longitude2 = THREE.MathUtils.degToRad(coordCarto2.longitude); const latitude2 = THREE.MathUtils.degToRad(coordCarto2.latitude); - const distRad = Math.acos(Math.sin(latitude1) * Math.sin(latitude2) + Math.cos(latitude1) * Math.cos(latitude2) * Math.cos(longitude2 - longitude1)); + const distRad = Math.acos( + Math.sin(latitude1) * Math.sin(latitude2) + + Math.cos(latitude1) * Math.cos(latitude2) * Math.cos(longitude2 - longitude1)); const e = this.eccentricity; const latMoy = (latitude1 + latitude2) * 0.5; diff --git a/test/unit/ellipsoid.js b/test/unit/ellipsoid.js index 877dbe3be3..88f5c23fe9 100644 --- a/test/unit/ellipsoid.js +++ b/test/unit/ellipsoid.js @@ -5,7 +5,20 @@ import Ellipsoid from 'Core/Math/Ellipsoid'; describe('Ellipsoid', function () { const c1 = new Coordinates('EPSG:4326', 0, 0, 0); const ellipsoid = new Ellipsoid(); - it('geodeticSurfaceNormalCartographic', () => { + + it('geodeticSurfaceNormal', function () { + c1.setFromValues(6378137, 0, 0); + const v = ellipsoid.geodeticSurfaceNormal(c1); + assert.equal(v.x, 1); + assert.equal(v.y, 0); + assert.equal(v.z, 0); + c1.x = -6378137; + ellipsoid.geodeticSurfaceNormal(c1, v); + assert.equal(v.x, -1); + }); + + it('geodeticSurfaceNormalCartographic', function () { + c1.setFromValues(0, 0, 0); const v = ellipsoid.geodeticSurfaceNormalCartographic(c1); assert.equal(v.x, 1); assert.equal(v.y, 0); @@ -15,13 +28,14 @@ describe('Ellipsoid', function () { assert.equal(v.x, -1); }); - it('cartographicToCartesian', () => { - c1.x = 0; + it('cartographicToCartesian', function () { + c1.setFromValues(0, 0, 0); const v = ellipsoid.cartographicToCartesian(c1); assert.equal(v.x, ellipsoid.size.x); }); - it('cartesianToCartographic', () => { + it('cartesianToCartographic', function () { + c1.setFromValues(0, 0, 0); const altitude = 2000; const v = ellipsoid.cartographicToCartesian(c1); v.x += altitude; @@ -29,14 +43,15 @@ describe('Ellipsoid', function () { assert.equal(c1.z, altitude); }); - it('cartographicToCartesianArray', () => { - c1.z = 0; + it('cartographicToCartesianArray', function () { + c1.setFromValues(0, 0, 0); const a = ellipsoid.cartographicToCartesianArray([c1]); assert.equal(a.length, 1); assert.equal(a[0].x, ellipsoid.size.x); }); - it('geodesic distance', () => { + it('geodesic distance', function () { + c1.setFromValues(0, 0, 0); const a = ellipsoid.geodesicDistance(c1, c1); assert.equal(a, 0); const c2 = new Coordinates('EPSG:4326', 180, 0, 0); From a5e01159ad5909a530c2ef613fb054ee666b662c Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 11 Oct 2024 23:32:11 +0200 Subject: [PATCH 02/13] feat: deprecate Coordinates constructor with array and vector3 --- examples/jsm/OGC3DTilesHelper.js | 2 +- src/Controls/GlobeControls.js | 10 +++++++--- src/Core/Geographic/Coordinates.js | 9 +++++++++ src/Renderer/Camera.js | 4 +++- src/Utils/CameraUtils.js | 5 ++++- utils/debug/Debug.js | 4 +++- 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/examples/jsm/OGC3DTilesHelper.js b/examples/jsm/OGC3DTilesHelper.js index babf74596a..ce0306bad6 100644 --- a/examples/jsm/OGC3DTilesHelper.js +++ b/examples/jsm/OGC3DTilesHelper.js @@ -47,7 +47,7 @@ function zoomToSphere(view, tile, sphere) { const distance = radius * Math.tan(fov * 2); return { - coord: new Coordinates('EPSG:4978', center), + coord: new Coordinates('EPSG:4978').setFromVector3(center), range: distance + radius, }; } diff --git a/src/Controls/GlobeControls.js b/src/Controls/GlobeControls.js index 76b5901a2a..68ae25518d 100644 --- a/src/Controls/GlobeControls.js +++ b/src/Controls/GlobeControls.js @@ -755,7 +755,7 @@ class GlobeControls extends THREE.EventDispatcher { const range = this.getRange(point); if (point && range > this.minDistance) { return this.lookAtCoordinate({ - coord: new Coordinates('EPSG:4978', point), + coord: new Coordinates('EPSG:4978').setFromVector3(point), range: range * (event.direction === 'out' ? 1 / 0.6 : 0.6), time: 1500, }); @@ -1028,7 +1028,9 @@ class GlobeControls extends THREE.EventDispatcher { */ getCameraCoordinate() { - return new Coordinates('EPSG:4978', this.camera.position).as('EPSG:4326'); + return new Coordinates('EPSG:4978') + .setFromVector3(this.camera.position) + .as('EPSG:4326'); } /** @@ -1216,7 +1218,9 @@ class GlobeControls extends THREE.EventDispatcher { return; } - return new Coordinates('EPSG:4978', pickedPosition).as('EPSG:4326'); + return new Coordinates('EPSG:4978') + .setFromVector3(pickedPosition) + .as('EPSG:4326'); } } diff --git a/src/Core/Geographic/Coordinates.js b/src/Core/Geographic/Coordinates.js index d0934066ea..a3a7e38e5a 100644 --- a/src/Core/Geographic/Coordinates.js +++ b/src/Core/Geographic/Coordinates.js @@ -86,8 +86,16 @@ class Coordinates { this._normal = new THREE.Vector3(); if (v0.length > 0) { + console.warn( + 'Deprecated Coordinates#constructor(string, number[]),', + 'use new Coordinates(string).setFromArray(number[]) instead.', + ); this.setFromArray(v0); } else if (v0.isVector3 || v0.isCoordinates) { + console.warn( + 'Deprecated Coordinates#constructor(string, Vector3),', + 'use new Coordinates(string).setFromVector3(Vector3) instead.', + ); this.setFromVector3(v0); } else { this.setFromValues(v0, v1, v2); @@ -103,6 +111,7 @@ class Coordinates { setCrs(crs) { CRS.isValid(crs); this.crs = crs; + return this; } /** diff --git a/src/Renderer/Camera.js b/src/Renderer/Camera.js index 848936cf2b..99a29b9ee4 100644 --- a/src/Renderer/Camera.js +++ b/src/Renderer/Camera.js @@ -188,7 +188,9 @@ class Camera { * @return {Coordinates} Coordinates object holding camera's position. */ position(crs) { - return new Coordinates(this.crs, this.camera3D.position).as(crs || this.crs); + return new Coordinates(this.crs) + .setFromVector3(this.camera3D.position) + .as(crs || this.crs); } /** diff --git a/src/Utils/CameraUtils.js b/src/Utils/CameraUtils.js index 5bccc8ece6..a586a242e0 100644 --- a/src/Utils/CameraUtils.js +++ b/src/Utils/CameraUtils.js @@ -154,7 +154,10 @@ class CameraRig extends THREE.Object3D { // set rig's objects transformation from camera's position and target's position setFromPositions(view, cameraPosition) { - this.setTargetFromCoordinate(view, new Coordinates(view.referenceCrs, targetPosition)); + this.setTargetFromCoordinate( + view, + new Coordinates(view.referenceCrs).setFromVector3(targetPosition), + ); this.target.rotation.set(0, 0, 0); this.updateMatrixWorld(true); this.camera.position.copy(cameraPosition); diff --git a/utils/debug/Debug.js b/utils/debug/Debug.js index 97420a456b..55361cf5b6 100644 --- a/utils/debug/Debug.js +++ b/utils/debug/Debug.js @@ -177,7 +177,9 @@ function Debug(view, datDebugTool, chartDivContainer) { const size = { x: g.width * ratio, y: g.height * ratio }; debugCamera.aspect = size.x / size.y; const camera = view.camera3D; - const coord = new Coordinates(view.referenceCrs, camera.position).as(tileLayer.extent.crs); + const coord = new Coordinates(view.referenceCrs) + .setFromVector3(camera.position) + .as(tileLayer.extent.crs); const extent = view.tileLayer.info.displayed.extent; displayedTilesObb.setFromExtent(extent); displayedTilesObbHelper.visible = true; From 5460fd38aa24b8015758dae7c23876da245208aa Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 11 Oct 2024 23:46:07 +0200 Subject: [PATCH 03/13] refacto: migrate Coordinates to typescript --- .../{Coordinates.js => Coordinates.ts} | 221 ++++++++++-------- 1 file changed, 120 insertions(+), 101 deletions(-) rename src/Core/Geographic/{Coordinates.js => Coordinates.ts} (55%) diff --git a/src/Core/Geographic/Coordinates.js b/src/Core/Geographic/Coordinates.ts similarity index 55% rename from src/Core/Geographic/Coordinates.js rename to src/Core/Geographic/Coordinates.ts index a3a7e38e5a..fea739d4d1 100644 --- a/src/Core/Geographic/Coordinates.js +++ b/src/Core/Geographic/Coordinates.ts @@ -3,18 +3,25 @@ import proj4 from 'proj4'; import * as 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> = {}; 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] = {}; } @@ -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|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); @@ -85,20 +97,24 @@ class Coordinates { // Normal this._normal = new THREE.Vector3(); - if (v0.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((x 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; @@ -106,9 +122,9 @@ class Coordinates { /** * 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; @@ -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; @@ -135,48 +151,50 @@ class Coordinates { /** * Set the values of this Coordinates from an array. * - * @param {Array} 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); } @@ -197,6 +215,9 @@ class Coordinates { this.z = value; } + /** + * The geodesic normal of the coordinate. + */ get geodesicNormal() { if (this._normalNeedsUpdate) { this._normalNeedsUpdate = false; @@ -216,38 +237,36 @@ 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 { 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); @@ -255,16 +274,13 @@ class Coordinates { /** * 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); @@ -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 }; @@ -318,7 +336,7 @@ 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 { @@ -326,7 +344,8 @@ class Coordinates { 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; From 12365ee95e539de4086860d423a7ff19f992eb64 Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 11 Oct 2024 23:47:49 +0200 Subject: [PATCH 04/13] doc(Coordinates): update and refine documentation --- src/Core/Geographic/Coordinates.ts | 178 +++++++++++++++-------------- 1 file changed, 91 insertions(+), 87 deletions(-) diff --git a/src/Core/Geographic/Coordinates.ts b/src/Core/Geographic/Coordinates.ts index fea739d4d1..ca134ee415 100644 --- a/src/Core/Geographic/Coordinates.ts +++ b/src/Core/Geographic/Coordinates.ts @@ -34,50 +34,59 @@ function proj4cache(crsIn: string, crsOut: string): proj4.Converter { } /** - * A Coordinates object, defined by a [crs](http://inspire.ec.europa.eu/theme/rs) - * and three values. These values are accessible through `x`, `y` and `z`, - * although it can also be accessible through `latitude`, `longitude` and - * `altitude`. To change a value, prefer the `set()` method below. + * A class representing a geographic or geocentric coordinate. * - * `EPSG:4978` and `EPSG:4326` are supported by default. To use another CRS, - * you have to declare it with `proj4`. You can find most projections and their - * proj4 code at [epsg.io](https://epsg.io/). + * A coordinate is defined by a [CRS](http://inspire.ec.europa.eu/theme/rs) + * (Coordinate Reference System) and a 3-dimensional vector `(x, y, z)`. + * For geocentric projections, it is recommended to use the `latitude`, + * `longitude` and `altitude` aliases to refer to vector components. * - * @example - * new Coordinates('EPSG:4978', 20885167, 849862, 23385912); //Geocentric coordinates - * new Coordinates('EPSG:4326', 2.33, 48.24, 24999549); //Geographic coordinates + * To change a value, prefer the use of the `set*` methods. * - * @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'); + * By default, the `EPSG:4978` and `EPSG:4326` projections are supported. To use + * a different projection, it must have been declared previously with `proj4`. + * A comprehensive list of projections and their corresponding proj4 string can + * be found at [epsg.io](https://epsg.io/). + * + * @example Geocentric coordinates + * ```js + * new Coordinates('EPSG:4978', 20885167, 849862, 23385912); + * ``` + * + * @example Geographic coordinates + * ```js + * new Coordinates('EPSG:4326', 2.33, 48.24, 24999549); + * ``` + * + * @example Defining the EPSG:2154 projection with proj4 + * ```js + * proj4.defs('EPSG:2154', `+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 + * +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m + * +no_defs +type=crs`); + * ``` */ class Coordinates { /** - * Used to checkout whether this coordinates is a Coordinates. Default is - * true. You should not change this, as it is used internally for - * optimisation. + * Read-only flag to check if a given object is of type `Coordinates`. */ 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. + * A default or user-defined CRS (see {@link ProjectionLike}). */ crs: ProjectionLike; - /** The first value of the coordinate. */ + /** The x value (or longitude) of this coordinate. */ x: number; - /** The second value of the coordinate. */ + /** The y value (or latitude) of this coordinate. */ y: number; - /** The third value of the coordinate. */ + /** The z value (or altitude) of this coordinate. */ z: number; private _normal: THREE.Vector3; private _normalNeedsUpdate: boolean; /** - * @param crs - A supported Coordinate Reference System. + * @param crs - A default or user-defined CRS (see {@link ProjectionLike}). * @param x - x or longitude value. * @param y - y or latitude value. * @param z - z or altitude value. @@ -131,13 +140,11 @@ class Coordinates { } /** - * Set the values of this Coordinates. + * Sets the x, y and z components of this coordinate. * * @param x - x or longitude value. * @param y - y or latitude value. * @param z - z or altitude value. - * - * @returns This Coordinates. */ setFromValues(x: number = 0, y: number = 0, z: number = 0): this { this.x = x; @@ -149,12 +156,13 @@ class Coordinates { } /** - * Set the values of this Coordinates from an array. + * Sets the coordinates's {@link Coordinates#x | x} component to + * `array[offset + 0]`, {@link Coordinates#y | y} component to + * `array[offset + 1]` and {@link Coordinates#z | z} component to + * `array[offset + 2]`. * - * @param array - An array of number to assign to the Coordinates. - * @param offset - Optional offset into the array. - * - * @returns This Coordinates. + * @param array - The source array. + * @param offset - Optional offset into the array. Default is 0. */ setFromArray(array: number[], offset: number = 0): this { return this.setFromValues( @@ -165,34 +173,29 @@ class Coordinates { } /** - * Set the values of this Coordinates from a `THREE.Vector3` or an `Object` - * having `x/y/z` properties, like a `Coordinates`. - * - * @param v - The object to read the values from. + * Sets the `(x, y, z)` vector of this coordinate from a 3-dimensional + * vector-like object. This object shall have both `x`, `y` and `z` + * properties. * - * @returns This Coordinates. + * @param v - The source object. */ 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. - * - * @returns The target with its new coordinates. + * Returns a new coordinate with the same `(x, y, z)` vector and crs as this + * one. */ 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 src - The source to copy from. + * Copies the `(x, y, z)` vector components and crs of the passed coordinate + * to this coordinate. * - * @returns This Coordinates. + * @param src - The source coordinate to copy from. */ copy(src: CoordinatesLike): this { this.crs = src.crs; @@ -235,21 +238,24 @@ class Coordinates { } /** - * Return this Coordinates values into a `THREE.Vector3`. + * Copies the `x`, `y` and `z` components into the provided `THREE.Vector3`. * - * @param target - The target to put the values in. If not specified, a new - * vector will be created. + * @param target - An object to store this vector to. If this is not + * specified, a new vector will be created. + * + * @returns A vector `(x, y, z)`, or copies x, y and z into the provided + * vector. */ toVector3(target: THREE.Vector3 = new THREE.Vector3()): THREE.Vector3 { return target.copy(this); } /** - * Copy values coordinates to array + * Copies the `x`, `y` and `z` components into the provided array. * - * @param array - array to store this vector to. If this is not + * @param array - An array to store this vector to. If this is not * provided a new array will be created. - * @param offset - optional offset into the array. + * @param offset - An optional offset into the array. * * @returns An array [x, y, z], or copies x, y and z into the provided * array. @@ -259,12 +265,9 @@ class Coordinates { } /** - * Calculate planar distance between this coordinates and `coord`. - * Planar distance is the straight-line euclidean distance calculated in a - * 2D cartesian coordinate system. - * - * @param coord - The coordinate - * @returns planar distance + * Computes the planar distance from this coordinates to `coord`. + * **Planar distance** is the straight-line euclidean distance calculated in + * a 2D cartesian coordinate system. */ planarDistanceTo(coord: Coordinates): number { this.toVector3(v0).setZ(0); @@ -273,12 +276,9 @@ class Coordinates { } /** - * 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. - * - * @param coord - The coordinate - * @returns geodetic distance + * Computes the geodetic distance from this coordinates to `coord`. + * **Geodetic distance** is calculated in an ellipsoid space as the shortest + * distance across the curved surface of the ellipsoid. */ geodeticDistanceTo(coord: Coordinates): number { this.as('EPSG:4326', coord0); @@ -287,7 +287,8 @@ class Coordinates { } /** - * Calculate earth euclidean distance between this coordinates and `coord`. + * Computes the euclidean distance from this coordinates to `coord` in a + * WGS84 projection. * * @param coord - The coordinate * @returns earth euclidean distance @@ -299,11 +300,10 @@ class Coordinates { } /** - * Multiplies this `coordinates` (with an implicit 1 in the 4th dimension) - * and `mat`. + * Multiplies this coordinate (with an implicit 1 in the 4th dimension) + * by `mat`, and divides by perspective. * * @param mat - The matrix. - * @returns return this object. */ applyMatrix4(mat: THREE.Matrix4): this { THREE.Vector3.prototype.applyMatrix4.call(this, mat); @@ -311,30 +311,34 @@ class Coordinates { } /** - * Returns coordinates in the wanted + * Projects this coordinate to the specified * [CRS](http://inspire.ec.europa.eu/theme/rs). * - * @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. - * - * @returns The resulting Coordinates after the conversion. - * - * @example - * const position = { longitude: 2.33, latitude: 48.24, altitude: 24999549 }; - * const coords = new Coordinates('EPSG:4326', position.longitude, position.latitude, position.altitude); // Geographic system - * const coordinates = coords.as('EPSG:4978'); // Geocentric system + * @param crs - The target CRS to which the coordinate will be converted. + * @param target - The target to store the projected coordinate. If this not + * provided a new coordinate will be created. * - * @example - * const position = { x: 20885167, y: 849862, z: 23385912 }; - * const coords = new Coordinates('EPSG:4978', position.x, position.y, position.z); // Geocentric system - * const coordinates = coords.as('EPSG:4326'); // Geographic system + * @returns The coordinate projected into the specified CRS. * - * @example - * new Coordinates('EPSG:4326', longitude: 2.33, latitude: 48.24, altitude: 24999549).as('EPSG:4978'); // Geocentric system + * @example Conversion from a geographic to a geocentric reference system + * ```js + * const geographicCoords = new Coordinates('EPSG:4326', + * 2.33, // longitude + * 48.24, // latitude + * 24999549, // altitude + * ); + * const geocentricCoords = geographicCoords.as('EPSG:4978'); + * ``` * - * @example - * new Coordinates('EPSG:4978', x: 20885167, y: 849862, z: 23385912).as('EPSG:4326'); // Geographic system + * @example Conversion from a geocentric to a geographic reference system + * ```js + * const geocentricCoords = new Coordinates('EPSG:4978', + * 20885167, // x + * 849862, // y + * 23385912, // z + * ); + * const geographicCoords = geocentricCoords.as('EPSG:4326'); + * ``` */ as(crs: ProjectionLike, target = new Coordinates(crs)): Coordinates { if (this.crs == crs) { From d717a6cfbeae9d925023955101274f4d9b35b9f0 Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Thu, 21 Nov 2024 13:51:56 +0100 Subject: [PATCH 05/13] doc(Ellipsoid): update and refine documentation --- src/Core/Math/Ellipsoid.ts | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Core/Math/Ellipsoid.ts b/src/Core/Math/Ellipsoid.ts index 1b5d7bf751..2b35488b42 100644 --- a/src/Core/Math/Ellipsoid.ts +++ b/src/Core/Math/Ellipsoid.ts @@ -2,6 +2,10 @@ import * as THREE from 'three'; import proj4 from 'proj4'; import Coordinates from '../Geographic/Coordinates'; +/** + * Length of the semi-axes of the WGS84 ellipsoid. + * @internal + */ export const ellipsoidSizes = new THREE.Vector3( proj4.WGS84.a, proj4.WGS84.a, @@ -10,12 +14,22 @@ export const ellipsoidSizes = new THREE.Vector3( const normal = new THREE.Vector3(); class Ellipsoid { + /** + * Length of the semi-axes of the ellipsoid. + */ size: THREE.Vector3; + /** + * Eccentricity of the ellipsoid. + */ eccentricity: number; private _radiiSquared: THREE.Vector3; private _invRadiiSquared: THREE.Vector3; + /** + * @param size - Length of the semi-axes of the ellipsoid. Defaults to those + * defined by the WGS84 ellipsoid. + */ constructor(size: THREE.Vector3 = ellipsoidSizes) { this.size = new THREE.Vector3(); this._radiiSquared = new THREE.Vector3(); @@ -25,10 +39,26 @@ class Ellipsoid { this.setSize(size); } + /** + * Computes the normal vector to an ellipsoid at the given cartesian + * coordinate `(x, y, z)`. + * + * @param cartesian - The given cartesian coordinate. + * @param target - An object to store this vector to. If this is not + * specified, a new vector will be created. + */ geodeticSurfaceNormal(cartesian: Coordinates, target = new THREE.Vector3()) { return cartesian.toVector3(target).multiply(this._invRadiiSquared).normalize(); } + /** + * Computes the normal vector to an ellipsoid at the given geographic + * coordinate `(longitude, latitude, altitude)`. + * + * @param coordCarto - The given geographic coordinate. + * @param target - An object to store this vector to. If this is not + * specified, a new vector will be created. + */ geodeticSurfaceNormalCartographic(coordCarto: Coordinates, target = new THREE.Vector3()) { const longitude = THREE.MathUtils.degToRad(coordCarto.longitude); const latitude = THREE.MathUtils.degToRad(coordCarto.latitude); @@ -39,7 +69,14 @@ class Ellipsoid { Math.sin(latitude)); } - setSize(size: THREE.Vector3Like) { + /** + * Sets the length of the semi-axes of this ellipsoid from a 3-dimensional + * vector-like object. The object shall have both `x`, `y` and `z` + * properties. + * + * @param size - The source vector. + */ + setSize(size: THREE.Vector3Like): this { this.size.set(size.x, size.y, size.z); this._radiiSquared.multiplyVectors(size, size); @@ -49,6 +86,8 @@ class Ellipsoid { this._invRadiiSquared.z = (size.z === 0) ? 0 : 1 / this._radiiSquared.z; this.eccentricity = Math.sqrt(this._radiiSquared.x - this._radiiSquared.z) / this.size.x; + + return this; } cartographicToCartesian(coordCarto: Coordinates, target = new THREE.Vector3()) { From 333cf724226cd789945c54a0254b6f886c44f28e Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Wed, 18 Dec 2024 15:18:38 +0100 Subject: [PATCH 06/13] chore(Ellipsoid): add method return types --- src/Core/Math/Ellipsoid.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Core/Math/Ellipsoid.ts b/src/Core/Math/Ellipsoid.ts index 2b35488b42..67ef10d405 100644 --- a/src/Core/Math/Ellipsoid.ts +++ b/src/Core/Math/Ellipsoid.ts @@ -47,7 +47,10 @@ class Ellipsoid { * @param target - An object to store this vector to. If this is not * specified, a new vector will be created. */ - geodeticSurfaceNormal(cartesian: Coordinates, target = new THREE.Vector3()) { + geodeticSurfaceNormal( + cartesian: Coordinates, + target = new THREE.Vector3(), + ): THREE.Vector3 { return cartesian.toVector3(target).multiply(this._invRadiiSquared).normalize(); } @@ -59,7 +62,10 @@ class Ellipsoid { * @param target - An object to store this vector to. If this is not * specified, a new vector will be created. */ - geodeticSurfaceNormalCartographic(coordCarto: Coordinates, target = new THREE.Vector3()) { + geodeticSurfaceNormalCartographic( + coordCarto: Coordinates, + target = new THREE.Vector3(), + ): THREE.Vector3 { const longitude = THREE.MathUtils.degToRad(coordCarto.longitude); const latitude = THREE.MathUtils.degToRad(coordCarto.latitude); const cosLatitude = Math.cos(latitude); @@ -90,7 +96,10 @@ class Ellipsoid { return this; } - cartographicToCartesian(coordCarto: Coordinates, target = new THREE.Vector3()) { + cartographicToCartesian( + coordCarto: Coordinates, + target = new THREE.Vector3(), + ): THREE.Vector3 { normal.copy(coordCarto.geodesicNormal); target.multiplyVectors(this._radiiSquared, normal); @@ -115,7 +124,7 @@ class Ellipsoid { cartesianToCartographic( position: THREE.Vector3Like, target = new Coordinates('EPSG:4326', 0, 0, 0), - ) { + ): Coordinates { // for details, see for example http://www.linz.govt.nz/data/geodetic-system/coordinate-conversion/geodetic-datum-conversions/equations-used-datum // TODO the following is only valable for oblate ellipsoid of // revolution. do we want to support triaxial ellipsoid? @@ -149,7 +158,7 @@ class Ellipsoid { ); } - cartographicToCartesianArray(coordCartoArray: Coordinates[]) { + cartographicToCartesianArray(coordCartoArray: Coordinates[]): THREE.Vector3[] { const cartesianArray = []; for (let i = 0; i < coordCartoArray.length; i++) { cartesianArray.push(this.cartographicToCartesian(coordCartoArray[i])); @@ -158,7 +167,7 @@ class Ellipsoid { return cartesianArray; } - intersection(ray: THREE.Ray) { + intersection(ray: THREE.Ray): THREE.Vector3 | false { const EPSILON = 0.0001; const O_C = ray.origin; const dir = ray.direction; @@ -215,7 +224,7 @@ class Ellipsoid { * @param coordCarto2 - The coordinate carto 2 * @returns The orthodromic distance between the two given coordinates. */ - geodesicDistance(coordCarto1: Coordinates, coordCarto2: Coordinates) { + geodesicDistance(coordCarto1: Coordinates, coordCarto2: Coordinates): number { // The formula uses the distance on approximated sphere, // with the nearest local radius of curvature of the ellipsoid // https://geodesie.ign.fr/contenu/fichiers/Distance_longitude_latitude.pdf From 3a45cb78642c7237df5bb4fa10ebc75c83ca6dee Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 18 Oct 2024 09:23:47 +0200 Subject: [PATCH 07/13] chore(Extent): remove deprecated methods BREAKING CHANGE: - Remove deprecated Extent#dimensions method --- src/Core/Geographic/Extent.js | 14 -------------- test/unit/extent.js | 8 -------- 2 files changed, 22 deletions(-) diff --git a/src/Core/Geographic/Extent.js b/src/Core/Geographic/Extent.js index 39c9ddf1ab..07a10394b7 100644 --- a/src/Core/Geographic/Extent.js +++ b/src/Core/Geographic/Extent.js @@ -127,20 +127,6 @@ class Extent { return target; } - /** - * Returns the dimension of the extent, in a `THREE.Vector2`. - * - * @param {THREE.Vector2} [target] - The target to assign the result in. - * - * @return {THREE.Vector2} - */ - dimensions(target = new THREE.Vector2()) { - console.warn('Extent.dimensions is deprecated, use planarDimensions, geodeticDimensions or spatialEuclideanDimensions'); - target.x = Math.abs(this.east - this.west); - target.y = Math.abs(this.north - this.south); - return target; - } - /** * Planar dimensions are two planar distances west/east and south/north. * Planar distance straight-line Euclidean distance calculated in a 2D Cartesian coordinate system. diff --git a/test/unit/extent.js b/test/unit/extent.js index 1557526f0e..d259b6ed2e 100644 --- a/test/unit/extent.js +++ b/test/unit/extent.js @@ -104,14 +104,6 @@ describe('Extent', function () { assert.equal(dimensions.y, 20); }); - it('should return the same planar dimensions with deprecated dimensions method', function () { - const extent = new Extent('EPSG:4326', -15, 10, -10, 10); - const dimensions = extent.planarDimensions(); - const dimensions_2 = extent.dimensions(); - assert.equal(dimensions.x, dimensions_2.x); - assert.equal(dimensions.y, dimensions_2.y); - }); - it('should return the correct earth euclidean dimensions', function () { const extent = new Extent('EPSG:4326', 3, 3.01, 46, 46.01); const dimensions = new Vector2(); From c005d00f0113fa00d4b86cb4514fdf9e3aee1145 Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 18 Oct 2024 09:26:49 +0200 Subject: [PATCH 08/13] feat(Extent): add setFromArray and setFromExtent methods --- src/Core/Geographic/Extent.js | 30 +++++++++++++++++++++++++++++ test/unit/extent.js | 36 ++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Core/Geographic/Extent.js b/src/Core/Geographic/Extent.js index 07a10394b7..dce3334611 100644 --- a/src/Core/Geographic/Extent.js +++ b/src/Core/Geographic/Extent.js @@ -335,6 +335,36 @@ class Extent { return this; } + /** + * Set this extent `west` property to `array[offset + 0]`, `east` property + * to `array[offset + 1]`, `south` property to `array[offset + 2]` and + * `north` property to `array[offset + 3]`. + * @param {number[]} array - the source array + * @param {number} [offset=0] - offset into the array. Default is 0. + * @returns {this} + */ + setFromArray(array, offset = 0) { + this.west = array[offset]; + this.east = array[offset + 1]; + this.south = array[offset + 2]; + this.north = array[offset + 3]; + return this; + } + + /** + * Set this extent `west`, `east`, `south` and `north` properties from an + * `extent` bounds. + * @param {Object} extent - the source extent + * @returns {this} + */ + setFromExtent(extent) { + this.west = extent.west; + this.east = extent.east; + this.south = extent.south; + this.north = extent.north; + return this; + } + /** * Copy to this extent to input extent. * @param {Extent} extent diff --git a/test/unit/extent.js b/test/unit/extent.js index d259b6ed2e..3bc50d1222 100644 --- a/test/unit/extent.js +++ b/test/unit/extent.js @@ -11,6 +11,8 @@ describe('Extent', function () { const maxX = 10; const minY = -1; const maxY = 3; + const minZ = -50; + const maxZ = 42; it('should build the expected extent using Coordinates', function () { const withCoords = new Extent('EPSG:4326', @@ -199,10 +201,42 @@ describe('Extent', function () { assert.equal(maxY, withValues.north); }); + it('should set values from array', function () { + const extent = new Extent('EPSG:4326', 0, 0, 0, 0); + const array = [minX, maxX, minY, maxY, minZ, maxZ]; + + extent.setFromArray(array); + assert.deepEqual( + [minX, maxX, minY, maxY], + [extent.west, extent.east, extent.south, extent.north], + ); + + extent.setFromArray(array, 2); + assert.deepEqual( + [minY, maxY, minZ, maxZ], + [extent.west, extent.east, extent.south, extent.north], + ); + }); + + it('sould set values from an extent-like object', function () { + const extent = new Extent('EPSG:4326', 0, 0, 0, 0); + extent.setFromExtent({ + west: minX, + east: maxX, + south: minY, + north: maxY, + }); + assert.equal(minX, extent.west); + assert.equal(maxX, extent.east); + assert.equal(minY, extent.south); + assert.equal(maxY, extent.north); + }); + it('should copy extent', function () { - const toCopy = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); + const toCopy = new Extent('EPSG:2154', [minX, maxX, minY, maxY]); const withValues = new Extent('EPSG:4326', [0, 0, 0, 0]); withValues.copy(toCopy); + assert.equal('EPSG:2154', withValues.crs); assert.equal(minX, withValues.west); assert.equal(maxX, withValues.east); assert.equal(minY, withValues.south); From 3056f857f710eccd447e2c94f1df4c1237ee36aa Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 18 Oct 2024 10:45:37 +0200 Subject: [PATCH 09/13] chore(Extent): deprecate array and extent constructor parameters BREAKING CHANGE: - Deprecate Extent#constructor with array and extent parameters - Deprecate Extent#set with array and extent parameters --- src/Converter/textureConverter.js | 2 +- src/Core/Geographic/Extent.js | 45 +++++------- src/Core/Prefab/Planar/PlanarTileBuilder.ts | 2 +- src/Core/Tile/Tile.js | 6 +- src/Source/Source.js | 2 +- src/Source/WFSSource.js | 2 +- src/Source/WMSSource.js | 2 +- test/unit/extent.js | 79 +++++++-------------- 8 files changed, 49 insertions(+), 91 deletions(-) diff --git a/src/Converter/textureConverter.js b/src/Converter/textureConverter.js index 5455e9d96f..7dee2c674c 100644 --- a/src/Converter/textureConverter.js +++ b/src/Converter/textureConverter.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; import Feature2Texture from 'Converter/Feature2Texture'; import Extent from 'Core/Geographic/Extent'; -const extentTexture = new Extent('EPSG:4326', [0, 0, 0, 0]); +const extentTexture = new Extent('EPSG:4326'); const textureLayer = (texture, layer) => { texture.generateMipmaps = false; diff --git a/src/Core/Geographic/Extent.js b/src/Core/Geographic/Extent.js index dce3334611..3599cbc71e 100644 --- a/src/Core/Geographic/Extent.js +++ b/src/Core/Geographic/Extent.js @@ -44,7 +44,7 @@ class Extent { * @param {number} [v2] south value * @param {number} [v3] north value */ - constructor(crs, v0, v1, v2, v3) { + constructor(crs, v0 = 0, v1 = 0, v2 = 0, v3 = 0) { if (CRS.isGeocentric(crs)) { throw new Error(`${crs} is a geocentric projection, it doesn't make sense with a geographical extent`); } @@ -76,7 +76,7 @@ class Extent { */ as(crs, target) { CRS.isValid(crs); - target = target || new Extent('EPSG:4326', [0, 0, 0, 0]); + target = target || new Extent('EPSG:4326'); if (this.crs != crs) { // Compute min/max in x/y by projecting 8 cardinal points, // and then taking the min/max of each coordinates. @@ -272,7 +272,7 @@ class Extent { */ intersect(extent) { if (!this.intersectsExtent(extent)) { - return new Extent(this.crs, 0, 0, 0, 0); + return new Extent(this.crs); } if (extent.crs != this.crs) { extent = extent.as(this.crs, _extent); @@ -302,29 +302,18 @@ class Extent { if (v0 == undefined) { throw new Error('No values to set in the extent'); } - if (v0.isExtent) { - v1 = v0.east; - v2 = v0.south; - v3 = v0.north; - v0 = v0.west; - } - - if (v0.isCoordinates) { - // seem never used - this.west = v0.x; - this.east = v1.x; - this.south = v0.y; - this.north = v1.y; - } else if (v0.west !== undefined) { - this.west = v0.west; - this.east = v0.east; - this.south = v0.south; - this.north = v0.north; + if (v0.west !== undefined) { + console.warn( + 'Deprecated Extent#constructor(string, Extent) and Extent#set(Extent),', + 'use new Extent(string).setFromExtent(Extent) instead.', + ); + this.setFromExtent(v0); } else if (v0.length == 4) { - this.west = v0[0]; - this.east = v0[1]; - this.south = v0[2]; - this.north = v0[3]; + console.warn( + 'Deprecated Extent#constructor(string, number[]) and Extent#set(number[]),', + 'use new Extent(string).setFromArray(number[]) instead.', + ); + this.setFromArray(v0); } else if (v3 !== undefined) { this.west = v0; this.east = v1; @@ -372,7 +361,7 @@ class Extent { */ copy(extent) { this.crs = extent.crs; - return this.set(extent); + return this.setFromExtent(extent); } /** @@ -463,7 +452,7 @@ class Extent { cNorthEast.setFromVector3(box.max).as(crs, cNorthEast).toVector3(box.max); } - return new Extent(crs, { + return new Extent(crs).setFromExtent({ west: box.min.x, east: box.max.x, south: box.min.y, @@ -576,6 +565,6 @@ class Extent { } } -_extent = new Extent('EPSG:4326', [0, 0, 0, 0]); +_extent = new Extent('EPSG:4326'); export default Extent; diff --git a/src/Core/Prefab/Planar/PlanarTileBuilder.ts b/src/Core/Prefab/Planar/PlanarTileBuilder.ts index 30a18e7650..73ee75bf1c 100644 --- a/src/Core/Prefab/Planar/PlanarTileBuilder.ts +++ b/src/Core/Prefab/Planar/PlanarTileBuilder.ts @@ -97,7 +97,7 @@ export class PlanarTileBuilder implements TileBuilder { // the geometry in common extent is identical to the existing input // with a translation return { - shareableExtent: new Extent(extent.crs, { + shareableExtent: new Extent(extent.crs).setFromExtent({ west: 0, east: Math.abs(extent.west - extent.east), south: 0, diff --git a/src/Core/Tile/Tile.js b/src/Core/Tile/Tile.js index d0912b7c90..7be73427ca 100644 --- a/src/Core/Tile/Tile.js +++ b/src/Core/Tile/Tile.js @@ -18,8 +18,8 @@ function _rowColfromParent(/** @type {Tile} */ tile, /** @type {number} */ zoom) return r; } -const _extent = new Extent('EPSG:4326', [0, 0, 0, 0]); -const _extent2 = new Extent('EPSG:4326', [0, 0, 0, 0]); +const _extent = new Extent('EPSG:4326'); +const _extent2 = new Extent('EPSG:4326'); const _c = new Coordinates('EPSG:4326', 0, 0); @@ -57,7 +57,7 @@ class Tile { */ toExtent(crs, target) { CRS.isValid(crs); - target = target || new Extent('EPSG:4326', [0, 0, 0, 0]); + target = target || new Extent('EPSG:4326'); const { epsg, globalExtent, globalDimension } = getInfoTms(this.crs); const countTiles = getCountTiles(this.crs, this.zoom); diff --git a/src/Source/Source.js b/src/Source/Source.js index 11c54b5f20..1e8f986c98 100644 --- a/src/Source/Source.js +++ b/src/Source/Source.js @@ -125,7 +125,7 @@ class Source extends InformationsData { this.whenReady = Promise.resolve(); this._featuresCaches = {}; if (source.extent && !(source.extent.isExtent)) { - this.extent = new Extent(this.crs, source.extent); + this.extent = new Extent(this.crs).setFromExtent(source.extent); } else { this.extent = source.extent; } diff --git a/src/Source/WFSSource.js b/src/Source/WFSSource.js index 5d2c771fa6..fa4154f051 100644 --- a/src/Source/WFSSource.js +++ b/src/Source/WFSSource.js @@ -2,7 +2,7 @@ import Source from 'Source/Source'; import URLBuilder from 'Provider/URLBuilder'; import Extent from 'Core/Geographic/Extent'; -const _extent = new Extent('EPSG:4326', [0, 0, 0, 0]); +const _extent = new Extent('EPSG:4326'); /** * An object defining the source of resources to get from a diff --git a/src/Source/WMSSource.js b/src/Source/WMSSource.js index 41dab478b1..26f483e488 100644 --- a/src/Source/WMSSource.js +++ b/src/Source/WMSSource.js @@ -3,7 +3,7 @@ import URLBuilder from 'Provider/URLBuilder'; import Extent from 'Core/Geographic/Extent'; import * as CRS from 'Core/Geographic/Crs'; -const _extent = new Extent('EPSG:4326', [0, 0, 0, 0]); +const _extent = new Extent('EPSG:4326'); /** * Proj provides an optional param to define axis order and orientation for a diff --git a/test/unit/extent.js b/test/unit/extent.js index 3bc50d1222..2f8c24eb87 100644 --- a/test/unit/extent.js +++ b/test/unit/extent.js @@ -14,29 +14,6 @@ describe('Extent', function () { const minZ = -50; const maxZ = 42; - it('should build the expected extent using Coordinates', function () { - const withCoords = new Extent('EPSG:4326', - new Coordinates('EPSG:4326', minX, minY), - new Coordinates('EPSG:4326', maxX, maxY)); - assert.equal(minX, withCoords.west); - assert.equal(maxX, withCoords.east); - assert.equal(minY, withCoords.south); - assert.equal(maxY, withCoords.north); - }); - - it('should build the expected extent using keywords', function () { - const withKeywords = new Extent('EPSG:4326', { - south: minY, - east: maxX, - north: maxY, - west: minX, - }); - assert.equal(minX, withKeywords.west); - assert.equal(maxX, withKeywords.east); - assert.equal(minY, withKeywords.south); - assert.equal(maxY, withKeywords.north); - }); - it('should build the expected extent using values', function () { const withValues = new Extent('EPSG:4326', minX, @@ -49,14 +26,6 @@ describe('Extent', function () { assert.equal(maxY, withValues.north); }); - it('should build the expected extent using Array', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); - assert.equal(minX, withValues.west); - assert.equal(maxX, withValues.east); - assert.equal(minY, withValues.south); - assert.equal(maxY, withValues.north); - }); - it('should build the expected extent from box3', function () { const box = new Box3( new Vector3(Math.random(), Math.random()), @@ -125,7 +94,7 @@ describe('Extent', function () { }); it('should clone extent like expected', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); const clonedExtent = withValues.clone(); assert.equal(clonedExtent.west, withValues.west); assert.equal(clonedExtent.east, withValues.east); @@ -134,7 +103,7 @@ describe('Extent', function () { }); it('should convert extent EPSG:4326 like expected', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]).as('EPSG:3857'); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY).as('EPSG:3857'); assert.equal(0, withValues.west); assert.equal(1113194.9079327357, withValues.east); assert.equal(-111325.14286638597, withValues.south); @@ -142,33 +111,33 @@ describe('Extent', function () { }); it('should return center of extent expected', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); const center = withValues.center(); assert.equal(5, center.longitude); assert.equal(1, center.latitude); }); it('should return dimensions of extent expected', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); const dimensions = withValues.planarDimensions(); assert.equal(10, dimensions.x); assert.equal(4, dimensions.y); }); it('should return true is point is inside extent expected', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); const coord = new Coordinates('EPSG:4326', minX + 1, minY + 2); assert.ok(withValues.isPointInside(coord)); }); it('should return true is extent is inside extent expected', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); - const inside = new Extent('EPSG:4326', [minX + 1, maxX - 1, minY + 1, maxY - 1]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); + const inside = new Extent('EPSG:4326', minX + 1, maxX - 1, minY + 1, maxY - 1); assert.ok(withValues.isInside(inside, 1)); }); it('should return expected offset', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); - const inside = new Extent('EPSG:4326', [minX + 1, maxX - 1, minY + 1, maxY - 1]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); + const inside = new Extent('EPSG:4326', minX + 1, maxX - 1, minY + 1, maxY - 1); const offset = withValues.offsetToParent(inside); assert.equal(offset.x, -0.125); assert.equal(offset.y, -0.5); @@ -177,14 +146,14 @@ describe('Extent', function () { }); it('should return true if intersect other extent', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); - const inter = new Extent('EPSG:4326', [minX + 1, maxX - 1, maxY - 1, maxY + 2]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); + const inter = new Extent('EPSG:4326', minX + 1, maxX - 1, maxY - 1, maxY + 2); assert.ok(withValues.intersectsExtent(inter)); }); it('should intersect like expected', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); - const extent = new Extent('EPSG:4326', [minX + 1, maxX - 1, maxY - 1, maxY + 2]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); + const extent = new Extent('EPSG:4326', minX + 1, maxX - 1, maxY - 1, maxY + 2); const inter = withValues.intersect(extent); assert.equal(1, inter.west); assert.equal(9, inter.east); @@ -193,7 +162,7 @@ describe('Extent', function () { }); it('should set values', function () { - const withValues = new Extent('EPSG:4326', [0, 0, 0, 0]); + const withValues = new Extent('EPSG:4326'); withValues.set(minX, maxX, minY, maxY); assert.equal(minX, withValues.west); assert.equal(maxX, withValues.east); @@ -202,7 +171,7 @@ describe('Extent', function () { }); it('should set values from array', function () { - const extent = new Extent('EPSG:4326', 0, 0, 0, 0); + const extent = new Extent('EPSG:4326'); const array = [minX, maxX, minY, maxY, minZ, maxZ]; extent.setFromArray(array); @@ -219,7 +188,7 @@ describe('Extent', function () { }); it('sould set values from an extent-like object', function () { - const extent = new Extent('EPSG:4326', 0, 0, 0, 0); + const extent = new Extent('EPSG:4326'); extent.setFromExtent({ west: minX, east: maxX, @@ -233,8 +202,8 @@ describe('Extent', function () { }); it('should copy extent', function () { - const toCopy = new Extent('EPSG:2154', [minX, maxX, minY, maxY]); - const withValues = new Extent('EPSG:4326', [0, 0, 0, 0]); + const toCopy = new Extent('EPSG:2154', minX, maxX, minY, maxY); + const withValues = new Extent('EPSG:4326'); withValues.copy(toCopy); assert.equal('EPSG:2154', withValues.crs); assert.equal(minX, withValues.west); @@ -244,8 +213,8 @@ describe('Extent', function () { }); it('should union like expected', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); - const extent = new Extent('EPSG:4326', [minX + 1, maxX - 1, maxY - 1, maxY + 2]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); + const extent = new Extent('EPSG:4326', minX + 1, maxX - 1, maxY - 1, maxY + 2); withValues.union(extent); assert.equal(0, withValues.west); assert.equal(10, withValues.east); @@ -254,7 +223,7 @@ describe('Extent', function () { }); it('should expand by point', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); const coord = new Coordinates('EPSG:4326', maxX + 1, maxY + 2); withValues.expandByCoordinates(coord); assert.equal(0, withValues.west); @@ -264,7 +233,7 @@ describe('Extent', function () { }); it('should convert EPSG extent values to string', function () { - const withValues = new Extent('EPSG:4326', [minX, maxX, minY, maxY]); + const withValues = new Extent('EPSG:4326', minX, maxX, minY, maxY); const tostring = withValues.toString(','); const toValues = tostring.split(',').map(s => Number(s)); assert.equal(toValues[0], withValues.east); @@ -274,8 +243,8 @@ describe('Extent', function () { }); it('should copy and transform extent', function () { - const withValues = new Extent('EPSG:4326', [0, 0, 0, 0]); - const extent = new Extent('EPSG:4326', [minX + 1, maxX - 1, maxY - 1, maxY + 2]); + const withValues = new Extent('EPSG:4326', 0, 0, 0, 0); + const extent = new Extent('EPSG:4326', minX + 1, maxX - 1, maxY - 1, maxY + 2); const position = new Vector3(1, 2, 0); const scale = new Vector3(2, -2, 1); const quaternion = new Quaternion(); From 1f9c4f0d1fc3292fb3d68beb3caba53342b8b177 Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 18 Oct 2024 11:48:01 +0200 Subject: [PATCH 10/13] refacto: migrate Extent to typescript --- src/Core/Geographic/{Extent.js => Extent.ts} | 89 +++++++++++++------- 1 file changed, 57 insertions(+), 32 deletions(-) rename src/Core/Geographic/{Extent.js => Extent.ts} (87%) diff --git a/src/Core/Geographic/Extent.js b/src/Core/Geographic/Extent.ts similarity index 87% rename from src/Core/Geographic/Extent.js rename to src/Core/Geographic/Extent.ts index 3599cbc71e..7a14676996 100644 --- a/src/Core/Geographic/Extent.js +++ b/src/Core/Geographic/Extent.ts @@ -2,6 +2,8 @@ import * as THREE from 'three'; import * as CRS from './Crs'; import Coordinates from './Coordinates'; +import type { ProjectionLike } from './Crs'; + /** * Extent is a SIG-area (so 2D) * It can use explicit coordinates (e.g: lon/lat) or implicit (WMTS coordinates) @@ -19,8 +21,7 @@ const cNorthEast = new Coordinates('EPSG:4326', 0, 0, 0); const southWest = new THREE.Vector3(); const northEast = new THREE.Vector3(); -/** @type {Extent} */ -let _extent; +let _extent: Extent; const cardinals = new Array(8); for (let i = cardinals.length - 1; i >= 0; i--) { @@ -29,7 +30,21 @@ for (let i = cardinals.length - 1; i >= 0; i--) { const _c = new Coordinates('EPSG:4326', 0, 0); +export interface ExtentLike { + readonly west: number; + readonly east: number; + readonly south: number; + readonly north: number; +} + class Extent { + readonly isExtent: true; + crs: ProjectionLike; + west: number; + east: number; + south: number; + north: number; + /** * Extent is geographical bounding rectangle defined by 4 limits: west, east, south and north. * @@ -44,9 +59,11 @@ class Extent { * @param {number} [v2] south value * @param {number} [v3] north value */ - constructor(crs, v0 = 0, v1 = 0, v2 = 0, v3 = 0) { + constructor(crs: ProjectionLike, v0 = 0, v1 = 0, v2 = 0, v3 = 0) { if (CRS.isGeocentric(crs)) { - throw new Error(`${crs} is a geocentric projection, it doesn't make sense with a geographical extent`); + throw new Error( + `Non-compatible geocentric projection ${crs} to build a geographical extent`, + ); } this.isExtent = true; @@ -74,9 +91,8 @@ class Extent { * @param {Extent} [target] copy the destination to target. * @return {Extent} */ - as(crs, target) { + as(crs: string, target: Extent = new Extent('EPSG:4326')) { CRS.isValid(crs); - target = target || new Extent('EPSG:4326'); if (this.crs != crs) { // Compute min/max in x/y by projecting 8 cardinal points, // and then taking the min/max of each coordinates. @@ -158,7 +174,10 @@ class Extent { cNorthEast.setFromValues(this.east, this.north, 0); // calcul geodetic distance northWest/northEast and northWest/southWest - return target.set(cNorthWest.geodeticDistanceTo(cNorthEast), cNorthWest.geodeticDistanceTo(cSouthWest)); + return target.set( + cNorthWest.geodeticDistanceTo(cNorthEast), + cNorthWest.geodeticDistanceTo(cSouthWest), + ); } /** @@ -179,7 +198,10 @@ class Extent { cNorthEast.setFromValues(this.east, this.north, 0); // calcul chord distance northWest/northEast and northWest/southWest - return target.set(cNorthWest.spatialEuclideanDistanceTo(cNorthEast), cNorthWest.spatialEuclideanDistanceTo(cSouthWest)); + return target.set( + cNorthWest.spatialEuclideanDistanceTo(cNorthEast), + cNorthWest.spatialEuclideanDistanceTo(cSouthWest), + ); } /** @@ -191,7 +213,7 @@ class Extent { * * @return {boolean} */ - isPointInside(coord, epsilon = 0) { + isPointInside(coord: Coordinates, epsilon = 0) { if (this.crs == coord.crs) { _c.copy(coord); } else { @@ -214,9 +236,8 @@ class Extent { * * @return {boolean} */ - isInside(extent, epsilon) { + isInside(extent: Extent, epsilon = CRS.reasonableEpsilon(this.crs)) { extent.as(this.crs, _extent); - epsilon = epsilon ?? CRS.reasonableEpsilon(this.crs); return this.east - _extent.east <= epsilon && _extent.west - this.west <= epsilon && this.north - _extent.north <= epsilon && @@ -230,7 +251,7 @@ class Extent { * @param {THREE.Vector4} target copy the result to target. * @return {THREE.Vector4} {x: translation on west-east, y: translation on south-north, z: scale on west-east, w: scale on south-north} */ - offsetToParent(extent, target = new THREE.Vector4()) { + offsetToParent(extent: Extent, target = new THREE.Vector4()) { if (this.crs != extent.crs) { throw new Error('unsupported mix'); } @@ -252,11 +273,11 @@ class Extent { * @param {Extent} extent * @returns {Boolean} */ - intersectsExtent(extent) { + intersectsExtent(extent: Extent) { return Extent.intersectsExtent(this, extent); } - static intersectsExtent(/** @type {Extent} */extentA, /** @type {Extent} */ extentB) { + static intersectsExtent(extentA: Extent, extentB: Extent) { // TODO don't work when is on limit const other = extentB.crs == extentA.crs ? extentB : extentB.as(extentA.crs, _extent); return !(extentA.west >= other.east || @@ -270,7 +291,7 @@ class Extent { * @param {Extent} extent * @returns {Extent} */ - intersect(extent) { + intersect(extent: Extent) { if (!this.intersectsExtent(extent)) { return new Extent(this.crs); } @@ -298,22 +319,26 @@ class Extent { * * @return {Extent} */ - set(v0, v1, v2, v3) { + set(v0: number, v1: number, v2: number, v3: number): this { if (v0 == undefined) { throw new Error('No values to set in the extent'); } - if (v0.west !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((v0 as any).west !== undefined) { console.warn( 'Deprecated Extent#constructor(string, Extent) and Extent#set(Extent),', 'use new Extent(string).setFromExtent(Extent) instead.', ); - this.setFromExtent(v0); - } else if (v0.length == 4) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.setFromExtent(v0 as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } else if ((v0 as any).length == 4) { // deepscan-disable-line console.warn( 'Deprecated Extent#constructor(string, number[]) and Extent#set(number[]),', 'use new Extent(string).setFromArray(number[]) instead.', ); - this.setFromArray(v0); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.setFromArray(v0 as any); } else if (v3 !== undefined) { this.west = v0; this.east = v1; @@ -332,7 +357,7 @@ class Extent { * @param {number} [offset=0] - offset into the array. Default is 0. * @returns {this} */ - setFromArray(array, offset = 0) { + setFromArray(array: ArrayLike, offset: number = 0): this { this.west = array[offset]; this.east = array[offset + 1]; this.south = array[offset + 2]; @@ -346,7 +371,7 @@ class Extent { * @param {Object} extent - the source extent * @returns {this} */ - setFromExtent(extent) { + setFromExtent(extent: ExtentLike): this { this.west = extent.west; this.east = extent.east; this.south = extent.south; @@ -359,7 +384,7 @@ class Extent { * @param {Extent} extent * @return {Extent} copied extent */ - copy(extent) { + copy(extent: Extent): this { this.crs = extent.crs; return this.setFromExtent(extent); } @@ -368,7 +393,7 @@ class Extent { * Union this extent with the input extent. * @param {Extent} extent the extent to union. */ - union(extent) { + union(extent: Extent) { if (extent.crs != this.crs) { throw new Error('unsupported union between 2 diff crs'); } @@ -402,7 +427,7 @@ class Extent { * for the coordinates to belong to this Extent object * @param {Coordinates} coordinates The coordinates to belong */ - expandByCoordinates(coordinates) { + expandByCoordinates(coordinates: Coordinates) { const coords = coordinates.crs == this.crs ? coordinates : coordinates.as(this.crs, _c); this.expandByValuesCoordinates(coords.x, coords.y); } @@ -414,7 +439,7 @@ class Extent { * @param {number} sn The coordinate on south-north * */ - expandByValuesCoordinates(we, sn) { + expandByValuesCoordinates(we: number, sn: number) { if (we < this.west) { this.west = we; } @@ -440,7 +465,7 @@ class Extent { * @param {THREE.Box3} box * @return {Extent} */ - static fromBox3(crs, box) { + static fromBox3(crs: ProjectionLike, box: THREE.Box3) { if (CRS.isGeocentric(crs)) { // if geocentric reproject box on 'EPSG:4326' crs = 'EPSG:4326'; @@ -485,7 +510,7 @@ class Extent { * @param {THREE.Vector2} [scheme=Vector2(2,2)] The scheme to subdivise. * @return {Array} subdivised extents. */ - subdivisionByScheme(scheme = defaultScheme) { + subdivisionByScheme(scheme = defaultScheme): Extent[] { const subdivisedExtents = []; const dimSub = this.planarDimensions(_dim).divide(scheme); for (let x = scheme.x - 1; x >= 0; x--) { @@ -508,7 +533,7 @@ class Extent { * @param {THREE.Matrix4} matrix The matrix * @return {Extent} return this extent instance. */ - applyMatrix4(matrix) { + applyMatrix4(matrix: THREE.Matrix4): this { southWest.set(this.west, this.south, 0).applyMatrix4(matrix); northEast.set(this.east, this.north, 0).applyMatrix4(matrix); this.west = southWest.x; @@ -535,7 +560,7 @@ class Extent { * @param {number} [north=this.north] The max north * @return {Extent} this extent */ - clampSouthNorth(south = this.south, north = this.north) { + clampSouthNorth(south = this.south, north = this.north): this { this.south = Math.max(this.south, south); this.north = Math.min(this.north, north); return this; @@ -548,7 +573,7 @@ class Extent { * @param {number} [east=this.east] The max east * @return {Extent} this extent */ - clampWestEast(west = this.west, east = this.east) { + clampWestEast(west = this.west, east = this.east): this { this.west = Math.max(this.west, west); this.east = Math.min(this.east, east); return this; @@ -559,7 +584,7 @@ class Extent { * @param {Extent} extent The maximum extent. * @return {Extent} this extent. */ - clampByExtent(extent) { + clampByExtent(extent: ExtentLike): this { this.clampSouthNorth(extent.south, extent.north); return this.clampWestEast(extent.west, extent.east); } From 3f367b82b0d0747562c5db1c07533ed61ba7e0ce Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Fri, 18 Oct 2024 15:55:05 +0200 Subject: [PATCH 11/13] doc(Extent): update and refine documentation --- src/Core/Geographic/Extent.ts | 227 ++++++++++++++++++---------------- 1 file changed, 117 insertions(+), 110 deletions(-) diff --git a/src/Core/Geographic/Extent.ts b/src/Core/Geographic/Extent.ts index 7a14676996..01d2709aa2 100644 --- a/src/Core/Geographic/Extent.ts +++ b/src/Core/Geographic/Extent.ts @@ -4,11 +4,6 @@ import Coordinates from './Coordinates'; import type { ProjectionLike } from './Crs'; -/** - * Extent is a SIG-area (so 2D) - * It can use explicit coordinates (e.g: lon/lat) or implicit (WMTS coordinates) - */ - const _dim = new THREE.Vector2(); const _dim2 = new THREE.Vector2(); const _box = new THREE.Box3(); @@ -37,29 +32,49 @@ export interface ExtentLike { readonly north: number; } +/** + * A class representing a geographical extent. + * + * An extent is a geographical bounding rectangle defined by 4 limits: west, + * east, south and north. + * + * **Warning**: Using a geocentric projection is not suitable for representing a + * geographical extent. Please use a geographic projection. + */ class Extent { + /** + * Read-only flag to check if a given object is of type `Extent`. + */ readonly isExtent: true; + /** + * A default or user-defined CRS (see {@link ProjectionLike}). + */ crs: ProjectionLike; + /** + * West longitude bound of this extent. + */ west: number; + /** + * East longitude bound of this extent. + */ east: number; + /** + * South latitude bound of this extent. + */ south: number; + /** + * North latitude bound of this extent. + */ north: number; /** - * Extent is geographical bounding rectangle defined by 4 limits: west, east, south and north. - * - * Warning, using geocentric projection isn't consistent with geographical extent. - * - * @param {String} crs projection of limit values. - * @param {number|Array.|Coordinates|Object} v0 west value, Array - * of values [west, east, south and north], Coordinates of west-south - * corner or object {west, east, south and north} - * @param {number|Coordinates} [v1] east value or Coordinates of - * east-north corner - * @param {number} [v2] south value - * @param {number} [v3] north value - */ - constructor(crs: ProjectionLike, v0 = 0, v1 = 0, v2 = 0, v3 = 0) { + * @param crs - A default or user-defined CRS (see {@link ProjectionLike}). + * @param west - the `west` value of this extent. Default is 0. + * @param east - the `east` value of this extent. Default is 0. + * @param south - the `south` value of this extent. Default is 0. + * @param north - the `north` value of this extent. Default is 0. + */ + constructor(crs: ProjectionLike, west = 0, east = 0, south = 0, north = 0) { if (CRS.isGeocentric(crs)) { throw new Error( `Non-compatible geocentric projection ${crs} to build a geographical extent`, @@ -74,22 +89,22 @@ class Extent { this.south = 0; this.north = 0; - this.set(v0, v1, v2, v3); + this.set(west, east, south, north); } /** - * Clone this extent - * @return {Extent} cloned extent + * Returns a new extent with the same bounds and crs as this one. */ clone() { return new Extent(this.crs, this.west, this.east, this.south, this.north); } /** - * Convert Extent to the specified projection. - * @param {string} crs the projection of destination. - * @param {Extent} [target] copy the destination to target. - * @return {Extent} + * Projects this extent to the specified projection. + * + * @param crs - target's projection. + * @param target - The target to store the projected extent. If this not + * provided a new extent will be created. */ as(crs: string, target: Extent = new Extent('EPSG:4326')) { CRS.isValid(crs); @@ -130,9 +145,10 @@ class Extent { } /** - * Return the center of Extent - * @param {Coordinates} target copy the center to the target. - * @return {Coordinates} + * Returns the center of the extent. + * + * @param target - The target to store the center coordinate. If this not + * provided a new coordinate will be created. */ center(target = new Coordinates(this.crs)) { this.planarDimensions(_dim); @@ -144,11 +160,12 @@ class Extent { } /** - * Planar dimensions are two planar distances west/east and south/north. - * Planar distance straight-line Euclidean distance calculated in a 2D Cartesian coordinate system. + * Returns the planar dimensions as two-vector planar distances west/east + * and south/north. + * The planar distance is a straight-line Euclidean distance calculated in a + * 2D Cartesian coordinate system. * - * @param {THREE.Vector2} [target=new THREE.Vector2()] The target - * @return {THREE.Vector2} Planar dimensions + * @param target - optional target */ planarDimensions(target = new THREE.Vector2()) { // Calculte the dimensions for x and y @@ -156,12 +173,12 @@ class Extent { } /** - * Geodetic dimensions are two planar distances west/east and south/north. - * Geodetic distance is calculated in an ellispoid space as the distance - * across the curved surface of the world. + * Returns the geodetic dimensions as two-vector planar distances west/east + * and south/north. + * Geodetic distance is calculated in an ellispoid space as the distance + * across the curved surface of the ellipsoid. * - * @param {THREE.Vector2} [target=new THREE.Vector2()] The target - * @return {THREE.Vector2} geodetic dimensions + * @param target - optional target */ geodeticDimensions(target = new THREE.Vector2()) { // set 3 corners extent @@ -181,11 +198,11 @@ class Extent { } /** - * Spatial euclidean dimensions are two spatial euclidean distances between west/east corner and south/north corner. - * Spatial euclidean distance chord is calculated in a ellispoid space. + * Returns the spatial euclidean dimensions as a two-vector spatial + * euclidean distances between west/east corner and south/north corner. + * Spatial euclidean distance chord is calculated in an ellispoid space. * - * @param {THREE.Vector2} [target=new THREE.Vector2()] The target - * @return {THREE.Vector2} spatial euclidean dimensions + * @param target - optional target */ spatialEuclideanDimensions(target = new THREE.Vector2()) { // set 3 corners extent @@ -205,13 +222,11 @@ class Extent { } /** - * Return true if `coord` is inside the bounding box. - * - * @param {Coordinates} coord - * @param {number} [epsilon=0] - to take into account when comparing to the - * point. + * Checks whether a coordinates is inside the extent. * - * @return {boolean} + * @param coord - the given coordinates. + * @param epsilon - error margin when comparing to the coordinates. + * Default is 0. */ isPointInside(coord: Coordinates, epsilon = 0) { if (this.crs == coord.crs) { @@ -228,13 +243,10 @@ class Extent { } /** - * Return true if `extent` is inside this extent. - * - * @param {Extent} extent the extent to check - * @param {number} epsilon to take into account when comparing to the - * point. + * Checks whether another extent is inside the extent. * - * @return {boolean} + * @param extent - the extent to check + * @param epsilon - error margin when comparing the extent bounds. */ isInside(extent: Extent, epsilon = CRS.reasonableEpsilon(this.crs)) { extent.as(this.crs, _extent); @@ -245,11 +257,15 @@ class Extent { } /** - * Return the translation and scale to transform this extent to input extent. + * Return the translation and scale to transform this extent to the input + * extent. * - * @param {Extent} extent input extent - * @param {THREE.Vector4} target copy the result to target. - * @return {THREE.Vector4} {x: translation on west-east, y: translation on south-north, z: scale on west-east, w: scale on south-north} + * @param extent - input extent + * @param target - copy the result to target. + * @returns A {@link THREE.Vector4} where the `x` property encodes the + * translation on west-east, the `y` property the translation on + * south-north, the `z` property the scale on west-east, the `w` property + * the scale on south-north. */ offsetToParent(extent: Extent, target = new THREE.Vector4()) { if (this.crs != extent.crs) { @@ -269,9 +285,9 @@ class Extent { } /** - * Return true if this bounding box intersect with the bouding box parameter - * @param {Extent} extent - * @returns {Boolean} + * Checks wheter this bounding box intersects with the given extent + * parameter. + * @param extent - the provided extent */ intersectsExtent(extent: Extent) { return Extent.intersectsExtent(this, extent); @@ -287,9 +303,8 @@ class Extent { } /** - * Return the intersection of this extent with another one - * @param {Extent} extent - * @returns {Extent} + * Returns the intersection of this extent with another one. + * @param extent - extent to intersect */ intersect(extent: Extent) { if (!this.intersectsExtent(extent)) { @@ -308,16 +323,10 @@ class Extent { /** * Set west, east, south and north values. * - * @param {number|Array.|Coordinates|Object|Extent} v0 west value, - * Array of values [west, east, south and north], Extent of same type (tiled - * or not), Coordinates of west-south corner or object {west, east, south - * and north} - * @param {number|Coordinates} [v1] east value, row value or Coordinates of - * east-north corner - * @param {number} [v2] south value or column value - * @param {number} [v3] north value - * - * @return {Extent} + * @param v0 - the `west` value of this extent. Default is 0. + * @param v1 - the `east` value of this extent. Default is 0. + * @param v2 - the `south` value of this extent. Default is 0. + * @param v3 - the `north` value of this extent. Default is 0. */ set(v0: number, v1: number, v2: number, v3: number): this { if (v0 == undefined) { @@ -350,12 +359,11 @@ class Extent { } /** - * Set this extent `west` property to `array[offset + 0]`, `east` property + * Sets this extent `west` property to `array[offset + 0]`, `east` property * to `array[offset + 1]`, `south` property to `array[offset + 2]` and * `north` property to `array[offset + 3]`. - * @param {number[]} array - the source array - * @param {number} [offset=0] - offset into the array. Default is 0. - * @returns {this} + * @param array - the source array + * @param offset - offset into the array. Default is 0. */ setFromArray(array: ArrayLike, offset: number = 0): this { this.west = array[offset]; @@ -366,10 +374,9 @@ class Extent { } /** - * Set this extent `west`, `east`, `south` and `north` properties from an + * Sets this extent `west`, `east`, `south` and `north` properties from an * `extent` bounds. - * @param {Object} extent - the source extent - * @returns {this} + * @param extent - the source extent */ setFromExtent(extent: ExtentLike): this { this.west = extent.west; @@ -380,9 +387,8 @@ class Extent { } /** - * Copy to this extent to input extent. - * @param {Extent} extent - * @return {Extent} copied extent + * Copies the passed extent to this extent. + * @param extent - extent to copy. */ copy(extent: Extent): this { this.crs = extent.crs; @@ -391,7 +397,7 @@ class Extent { /** * Union this extent with the input extent. - * @param {Extent} extent the extent to union. + * @param extent - the extent to union. */ union(extent: Extent) { if (extent.crs != this.crs) { @@ -425,7 +431,7 @@ class Extent { /** * expandByCoordinates perfoms the minimal extension * for the coordinates to belong to this Extent object - * @param {Coordinates} coordinates The coordinates to belong + * @param coordinates - The coordinates to belong */ expandByCoordinates(coordinates: Coordinates) { const coords = coordinates.crs == this.crs ? coordinates : coordinates.as(this.crs, _c); @@ -435,8 +441,8 @@ class Extent { /** * expandByValuesCoordinates perfoms the minimal extension * for the coordinates values to belong to this Extent object - * @param {number} we The coordinate on west-east - * @param {number} sn The coordinate on south-north + * @param we - The coordinate on west-east + * @param sn - The coordinate on south-north * */ expandByValuesCoordinates(we: number, sn: number) { @@ -461,9 +467,8 @@ class Extent { * should be the geocentric coordinates of `min` and `max` of a `box3` * in local tangent plane. * - * @param {string} crs Projection of extent to instancied. - * @param {THREE.Box3} box - * @return {Extent} + * @param crs - Projection of extent to instancied. + * @param box - Bounding-box */ static fromBox3(crs: ProjectionLike, box: THREE.Box3) { if (CRS.isGeocentric(crs)) { @@ -487,28 +492,28 @@ class Extent { /** * Return values of extent in string, separated by the separator input. - * @param {string} separator - * @return {string} + * @param sep - string separator */ - toString(separator = '') { - return `${this.east}${separator}${this.north}${separator}${this.west}${separator}${this.south}`; + toString(sep = '') { + return `${this.east}${sep}${this.north}${sep}${this.west}${sep}${this.south}`; } /** * Subdivide equally an extent from its center to return four extents: * north-west, north-east, south-west and south-east. * - * @returns {Extent[]} An array containing the four sections of the extent. The - * order of the sections is [NW, NE, SW, SE]. + * @returns An array containing the four sections of the extent. The order + * of the sections is [NW, NE, SW, SE]. */ subdivision() { return this.subdivisionByScheme(); } + /** * subdivise extent by scheme.x on west-east and scheme.y on south-north. * - * @param {THREE.Vector2} [scheme=Vector2(2,2)] The scheme to subdivise. - * @return {Array} subdivised extents. + * @param scheme - The scheme to subdivise. + * @returns subdivised extents. */ subdivisionByScheme(scheme = defaultScheme): Extent[] { const subdivisedExtents = []; @@ -528,10 +533,11 @@ class Extent { } /** - * Multiplies all extent `coordinates` (with an implicit 1 in the 4th dimension) and `matrix`. + * Multiplies all extent `coordinates` (with an implicit 1 in the 4th + * dimension) and `matrix`. * - * @param {THREE.Matrix4} matrix The matrix - * @return {Extent} return this extent instance. + * @param matrix - The matrix + * @returns return this extent instance. */ applyMatrix4(matrix: THREE.Matrix4): this { southWest.set(this.west, this.south, 0).applyMatrix4(matrix); @@ -556,9 +562,9 @@ class Extent { /** * clamp south and north values * - * @param {number} [south=this.south] The min south - * @param {number} [north=this.north] The max north - * @return {Extent} this extent + * @param south - The min south + * @param north - The max north + * @returns this extent */ clampSouthNorth(south = this.south, north = this.north): this { this.south = Math.max(this.south, south); @@ -569,20 +575,21 @@ class Extent { /** * clamp west and east values * - * @param {number} [west=this.west] The min west - * @param {number} [east=this.east] The max east - * @return {Extent} this extent + * @param west - The min west + * @param east - The max east + * @returns this extent */ clampWestEast(west = this.west, east = this.east): this { this.west = Math.max(this.west, west); this.east = Math.min(this.east, east); return this; } + /** * clamp this extent by passed extent * - * @param {Extent} extent The maximum extent. - * @return {Extent} this extent. + * @param extent - The maximum extent. + * @returns this extent. */ clampByExtent(extent: ExtentLike): this { this.clampSouthNorth(extent.south, extent.north); From 88ad67df792738eab5841b90f84c0b58db47fcbe Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Thu, 21 Nov 2024 14:27:29 +0100 Subject: [PATCH 12/13] refacto: migrate Tile to typescript --- src/Core/Tile/{Tile.js => Tile.ts} | 52 +++++++++++++--------- src/Core/Tile/{TileGrid.js => TileGrid.ts} | 19 ++++---- 2 files changed, 43 insertions(+), 28 deletions(-) rename src/Core/Tile/{Tile.js => Tile.ts} (81%) rename src/Core/Tile/{TileGrid.js => TileGrid.ts} (72%) diff --git a/src/Core/Tile/Tile.js b/src/Core/Tile/Tile.ts similarity index 81% rename from src/Core/Tile/Tile.js rename to src/Core/Tile/Tile.ts index 7be73427ca..0d1b6199bb 100644 --- a/src/Core/Tile/Tile.js +++ b/src/Core/Tile/Tile.ts @@ -8,7 +8,7 @@ const _tmsCoord = new THREE.Vector2(); const _dimensionTile = new THREE.Vector2(); const r = { row: 0, col: 0, invDiff: 0 }; -function _rowColfromParent(/** @type {Tile} */ tile, /** @type {number} */ zoom) { +function _rowColfromParent(tile: Tile, zoom: number) { const diffLevel = tile.zoom - zoom; const diff = 2 ** diffLevel; r.invDiff = 1 / diff; @@ -24,6 +24,13 @@ const _extent2 = new Extent('EPSG:4326'); const _c = new Coordinates('EPSG:4326', 0, 0); class Tile { + readonly isTile: true; + + crs: string; + zoom: number; + row: number; + col: number; + /** * Tile is a geographical bounding rectangle defined by zoom, row and column. * @@ -32,7 +39,7 @@ class Tile { * @param {number} [row=0] row value * @param {number} [col=0] column value */ - constructor(crs, zoom = 0, row = 0, col = 0) { + constructor(crs: string, zoom = 0, row = 0, col = 0) { this.isTile = true; this.crs = crs; @@ -55,20 +62,20 @@ class Tile { * @param {Extent} target copy the destination to target. * @return {Extent} */ - toExtent(crs, target) { + toExtent(crs: string, target = new Extent('EPSG:4326')) { CRS.isValid(crs); - target = target || new Extent('EPSG:4326'); const { epsg, globalExtent, globalDimension } = getInfoTms(this.crs); const countTiles = getCountTiles(this.crs, this.zoom); _dimensionTile.set(1, 1).divide(countTiles).multiply(globalDimension); - target.west = globalExtent.west + (globalDimension.x - _dimensionTile.x * (countTiles.x - this.col)); + target.west = globalExtent.west + + (globalDimension.x - _dimensionTile.x * (countTiles.x - this.col)); target.east = target.west + _dimensionTile.x; - target.south = globalExtent.south + _dimensionTile.y * (countTiles.y - this.row - 1); + target.south = globalExtent.south + + _dimensionTile.y * (countTiles.y - this.row - 1); target.north = target.south + _dimensionTile.y; target.crs = epsg; - target.zoom = this.zoom; return crs == epsg ? target : target.as(crs, target); } @@ -80,14 +87,14 @@ class Tile { * * @return {boolean} */ - isInside(tile) { + isInside(tile: Tile) { if (this.zoom == tile.zoom) { return this.row == tile.row && this.col == tile.col; } else if (this.zoom < tile.zoom) { return false; } else { - _rowColfromParent(this, tile.zoom); + const r = _rowColfromParent(this, tile.zoom); return r.row == tile.row && r.col == tile.col; } } @@ -99,12 +106,12 @@ class Tile { * @param {THREE.Vector4} target copy the result to target. * @return {THREE.Vector4} {x: translation on west-east, y: translation on south-north, z: scale on west-east, w: scale on south-north} */ - offsetToParent(tile, target = new THREE.Vector4()) { + offsetToParent(tile: Tile, target = new THREE.Vector4()) { if (this.crs != tile.crs) { throw new Error('unsupported mix'); } - _rowColfromParent(this, tile.zoom); + const r = _rowColfromParent(this, tile.zoom); return target.set( this.col * r.invDiff - r.col, this.row * r.invDiff - r.row, @@ -117,9 +124,9 @@ class Tile { * @param {number} levelParent level of parent. * @return {Tile} */ - tiledExtentParent(levelParent) { + tiledExtentParent(levelParent: number) { if (levelParent && levelParent < this.zoom) { - _rowColfromParent(this, levelParent); + const r = _rowColfromParent(this, levelParent); return new Tile(this.crs, levelParent, r.row, r.col); } else { return this; @@ -148,7 +155,7 @@ class Tile { * @param {Tile} tile * @return {Tile} copied extent */ - copy(tile) { + copy(tile: Tile): this { this.crs = tile.crs; return this.set(tile.zoom, tile.row, tile.col); } @@ -168,7 +175,7 @@ class Tile { * @param {string} tms * @returns {Tile[]} */ -export function tiledCovering(e, tms) { +export function tiledCovering(e: Extent, tms: string) { if (e.crs == 'EPSG:4326' && tms == 'EPSG:3857') { const WMTS_PM = []; const extent = _extent.copy(e).as(tms, _extent2); @@ -176,7 +183,8 @@ export function tiledCovering(e, tms) { extent.clampByExtent(globalExtent); extent.planarDimensions(_dimensionTile); - const zoom = (e.zoom + 1) || Math.floor(Math.log2(Math.round(globalDimension.x / (_dimensionTile.x * sTs.x)))); + const zoom = Math.floor(Math.log2( + Math.round(globalDimension.x / (_dimensionTile.x * sTs.x)))); const countTiles = getCountTiles(tms, zoom); const center = extent.center(_c); @@ -185,7 +193,8 @@ export function tiledCovering(e, tms) { _tmsCoord.divide(globalDimension).multiply(countTiles).floor(); // ]N; N+1] => N - const maxRow = Math.ceil((globalExtent.north - extent.south) / globalDimension.x * countTiles.y) - 1; + const maxRow = Math.ceil( + (globalExtent.north - extent.south) / globalDimension.x * countTiles.y) - 1; for (let r = maxRow; r >= _tmsCoord.y; r--) { WMTS_PM.push(new Tile(tms, zoom, r, _tmsCoord.x)); @@ -198,12 +207,15 @@ export function tiledCovering(e, tms) { const center = e.center(_c); e.planarDimensions(_dimensionTile); // Each level has 2^n * 2^n tiles... - // ... so we count how many tiles of the same width as tile we can fit in the layer + // ... so we count how many tiles of the same width as tile we can fit + // in the layer // ... 2^zoom = tilecount => zoom = log2(tilecount) - const zoom = Math.floor(Math.log2(Math.round(globalDimension.x / (_dimensionTile.x * sTs.x)))); + const zoom = Math.floor(Math.log2( + Math.round(globalDimension.x / (_dimensionTile.x * sTs.x)))); const countTiles = getCountTiles(tms, zoom); - // Now that we have computed zoom, we can deduce x and y (or row / column) + // Now that we have computed zoom, we can deduce x and y (or row / + // column) _tmsCoord.x = center.x - globalExtent.west; _tmsCoord.y = isInverted ? globalExtent.north - center.y : center.y - globalExtent.south; _tmsCoord.divide(globalDimension).multiply(countTiles).floor(); diff --git a/src/Core/Tile/TileGrid.js b/src/Core/Tile/TileGrid.ts similarity index 72% rename from src/Core/Tile/TileGrid.js rename to src/Core/Tile/TileGrid.ts index a6eb619b35..f1fad053fa 100644 --- a/src/Core/Tile/TileGrid.js +++ b/src/Core/Tile/TileGrid.ts @@ -4,8 +4,8 @@ import Extent from '../Geographic/Extent'; const _countTiles = new THREE.Vector2(); const _dim = new THREE.Vector2(); -export const globalExtentTMS = new Map(); -export const schemeTiles = new Map(); +export const globalExtentTMS: Map = new Map(); +export const schemeTiles: Map = new Map(); const extent4326 = new Extent('EPSG:4326', -180, 180, -90, 90); globalExtentTMS.set('EPSG:4326', extent4326); @@ -17,14 +17,17 @@ const extent3857 = extent4326.as('EPSG:3857'); extent3857.clampSouthNorth(extent3857.west, extent3857.east); globalExtentTMS.set('EPSG:3857', extent3857); -schemeTiles.set('default', new THREE.Vector2(1, 1)); -schemeTiles.set('EPSG:3857', schemeTiles.get('default')); +const defaultScheme = new THREE.Vector2(1, 1); +schemeTiles.set('EPSG:3857', defaultScheme); schemeTiles.set('EPSG:4326', new THREE.Vector2(2, 1)); -export function getInfoTms(/** @type {string} */ crs) { +export function getInfoTms(crs: string) { const globalExtent = globalExtentTMS.get(crs); + if (!globalExtent) { + throw new Error(`The tile matrix set ${crs} is not defined.`); + } const globalDimension = globalExtent.planarDimensions(_dim); - const sTs = schemeTiles.get(crs) || schemeTiles.get('default'); + const sTs = schemeTiles.get(crs) ?? defaultScheme; // The isInverted parameter is to be set to the correct value, true or false // (default being false) if the computation of the coordinates needs to be // inverted to match the same scheme as OSM, Google Maps or other system. @@ -35,8 +38,8 @@ export function getInfoTms(/** @type {string} */ crs) { return { epsg: crs, globalExtent, globalDimension, sTs, isInverted }; } -export function getCountTiles(/** @type {string} */ crs, /** @type {number} */ zoom) { - const sTs = schemeTiles.get(crs) || schemeTiles.get('default'); +export function getCountTiles(crs: string, zoom: number) { + const sTs = schemeTiles.get(crs) || defaultScheme; const count = 2 ** zoom; _countTiles.set(count, count).multiply(sTs); return _countTiles; From f990033dcdd42d59f3048129733f1eaa1dafd09e Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Thu, 19 Dec 2024 22:39:16 +0100 Subject: [PATCH 13/13] doc(Tile): update and refine documentation --- src/Core/Tile/Tile.ts | 64 ++++++++++++++++----------------------- src/Core/Tile/TileGrid.ts | 2 ++ 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/Core/Tile/Tile.ts b/src/Core/Tile/Tile.ts index 0d1b6199bb..dfa6501966 100644 --- a/src/Core/Tile/Tile.ts +++ b/src/Core/Tile/Tile.ts @@ -32,12 +32,13 @@ class Tile { col: number; /** - * Tile is a geographical bounding rectangle defined by zoom, row and column. + * A tile is a geographical bounding rectangle uniquely defined by its zoom, + * row and column. * - * @param {String} crs projection of limit values. - * @param {number} [zoom=0] zoom value - * @param {number} [row=0] row value - * @param {number} [col=0] column value + * @param crs - projection of limit values. + * @param zoom - `zoom` value. Default is 0. + * @param row - `row` value. Default is 0. + * @param col - `column` value. Default is 0. */ constructor(crs: string, zoom = 0, row = 0, col = 0) { this.isTile = true; @@ -49,18 +50,17 @@ class Tile { } /** - * Clone this tile - * @return {Tile} cloned tile + * Returns a new tile with the same bounds and crs as this one. */ clone() { return new Tile(this.crs, this.zoom, this.row, this.col); } /** - * Convert tile to the specified extent. - * @param {string} crs the projection of destination. - * @param {Extent} target copy the destination to target. - * @return {Extent} + * Converts this tile to the specified extent. + * @param crs - target's projection. + * @param target - The target to store the projected extent. If this not + * provided a new extent will be created. */ toExtent(crs: string, target = new Extent('EPSG:4326')) { CRS.isValid(crs); @@ -81,11 +81,9 @@ class Tile { } /** - * Return true if `tile` is inside this tile. + * Checks whether another tile is inside this tile. * - * @param {Tile} tile the tile to check - * - * @return {boolean} + * @param extent - the tile to check. */ isInside(tile: Tile) { if (this.zoom == tile.zoom) { @@ -100,11 +98,11 @@ class Tile { } /** - * Return the translation and scale to transform this tile to input tile. + * Returns the translation and scale to transform this tile to the input + * tile. * - * @param {Tile} tile input tile - * @param {THREE.Vector4} target copy the result to target. - * @return {THREE.Vector4} {x: translation on west-east, y: translation on south-north, z: scale on west-east, w: scale on south-north} + * @param tile - the input tile. + * @param target - copy the result to target. */ offsetToParent(tile: Tile, target = new THREE.Vector4()) { if (this.crs != tile.crs) { @@ -119,10 +117,9 @@ class Tile { } /** - * Return parent tile with input level + * Returns the parent tile at the given level. * - * @param {number} levelParent level of parent. - * @return {Tile} + * @param levelParent - the level of the parent tile. */ tiledExtentParent(levelParent: number) { if (levelParent && levelParent < this.zoom) { @@ -134,13 +131,11 @@ class Tile { } /** - * Set zoom, row and column values - * - * @param {number} [zoom=0] zoom value - * @param {number} [row=0] row value - * @param {number} [col=0] column value + * Sets zoom, row and column values. * - * @return {Tile} + * @param zoom - zoom value. + * @param row - row value. + * @param col - column value. */ set(zoom = 0, row = 0, col = 0) { this.zoom = zoom; @@ -151,9 +146,8 @@ class Tile { } /** - * Copy to this tile to input tile. - * @param {Tile} tile - * @return {Tile} copied extent + * Copies the passed tile to this tile. + * @param tile - tile to copy. */ copy(tile: Tile): this { this.crs = tile.crs; @@ -162,19 +156,13 @@ class Tile { /** * Return values of tile in string, separated by the separator input. - * @param {string} separator - * @return {string} + * @param separator - string separator */ toString(separator = '') { return `${this.zoom}${separator}${this.row}${separator}${this.col}`; } } -/** - * @param {Extent} e - * @param {string} tms - * @returns {Tile[]} - */ export function tiledCovering(e: Extent, tms: string) { if (e.crs == 'EPSG:4326' && tms == 'EPSG:3857') { const WMTS_PM = []; diff --git a/src/Core/Tile/TileGrid.ts b/src/Core/Tile/TileGrid.ts index f1fad053fa..9a1c5f219a 100644 --- a/src/Core/Tile/TileGrid.ts +++ b/src/Core/Tile/TileGrid.ts @@ -21,6 +21,8 @@ const defaultScheme = new THREE.Vector2(1, 1); schemeTiles.set('EPSG:3857', defaultScheme); schemeTiles.set('EPSG:4326', new THREE.Vector2(2, 1)); +// TODO: For now we can only have a single TMS grid per proj4 identifier. +// This causes TMS identifier to be proj4 identifier. export function getInfoTms(crs: string) { const globalExtent = globalExtentTMS.get(crs); if (!globalExtent) {