@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
956 lines (897 loc) • 31 kB
JavaScript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { Box3, Vector2, Vector3 } from 'three';
import ProjUtils from '../../utils/ProjUtils';
import { nonNull } from '../../utils/tsutils';
import OffsetScale from '../OffsetScale';
import Coordinates from './Coordinates';
import CoordinateSystem from './CoordinateSystem';
const tmpXY = new Vector2();
const SIDE = {
LEFT: 0,
RIGHT: 1,
BOTTOM: 2,
TOP: 3
};
export function reasonnableEpsilonForCRS(crs, width, height) {
if (crs.isEpsg(4326)) {
return 0.01;
}
return 0.01 * Math.min(width, height);
}
const cardinals = [new Vector2(), new Vector2(), new Vector2(), new Vector2(), new Vector2(), new Vector2(), new Vector2(), new Vector2()];
/**
* A rectangular extent in a specific {@link CoordinateSystem | coordinate system}.
*/
export class Extent {
/**
* Creates an extent from a coordinate system and a pair of coordinates.
* @param crs - The coordinate system to use.
* @param bottomLeft - The bottom-left corner of the extent.
* @param topRight - The top-right corner of the extent.
* @remarks Both coordinates must be in the same coordinate system as this extent.
*/
/**
* Creates an extent from an object containing the min/max XY values.
* @param crs - The coordinate system to use.
* @param values - The extent limits.
*/
/**
* Creates an extent from the min/max XY values.
* @param crs - The coordinate system to use.
* @param values - The extent limits.
* @deprecated Due to ambiguity (not all coordinate systems are north-up), this constructor
* should not be used.
*/
/**
* Creates an extent from the min/max XY values
* @param crs - The coordinate system to use.
* @param minX - The X coordinate of the left side of this extent.
* @param maxX - The X coordinate of the righ side of this extent.
* @param minY - The Y coordinate of the bottom side of this extent.
* @param maxY - The Y coordinate of the top side of this extent.
*/
constructor(crs, arg0, arg1, arg2, arg3) {
this._values = new Float64Array(4);
this._crs = crs;
// @ts-expect-error type overload shenanigans
this.set(crs, arg0, arg1, arg2, arg3);
}
/**
* Returns an extent centered at the specified coordinate, and with the specified size.
*
* @param crs - The CRS identifier.
* @param center - The center.
* @param width - The width, in CRS units.
* @param height - The height, in CRS units.
* @returns The produced extent.
*/
static fromCenterAndSize(crs, center, width, height) {
const minX = center.x - width / 2;
const maxX = center.x + width / 2;
const minY = center.y - height / 2;
const maxY = center.y + height / 2;
return new Extent(crs, minX, maxX, minY, maxY);
}
/**
* Returns the internal value array in this order: [minX, maxX, minY, maxY]
*/
get values() {
return this._values;
}
/**
* Returns the coordinate of the location on the extent that matches U and V, where U and V
* are normalized (in the range [0, 1]), and U = 0 and V = 0 are the bottom/left corner of
* the extent, and U = 1 and V = 1 are to top right corner.
* @param u - The normalized coordinate over the X-axis.
* @param v - The normalized coordinate over the Y-axis.
* @param target - The target to store the result. If unspecified, one will be created.
* @returns The sampled coordinate.
* @example
* const extent = new Extent(CoordinateSystem.epsg4326, 0, 10, 0, 5);
* // Get the bottom left corner
* extent.sampleUV(0, 0)
* // [0, 0]
*
* // Get the center
* extent.sampleUV(0.5, 0.5)
* // [5, 2.5]
*
* // Get the top right corner
* extent.sampleUV(1, 1)
* // [10, 5]
*/
sampleUV(u, v, target) {
const {
width,
height
} = this.dimensions(tmpXY);
const bottom = this.minY;
const left = this.minX;
const x = left + width * u;
const y = bottom + height * v;
if (target != null) {
return target.set(this._crs, x, y);
} else {
return new Coordinates(this._crs, x, y);
}
}
/**
* Returns `true` if the two extents are equal.
*
* @param other - The extent to compare.
* @param epsilon - The optional comparison epsilon.
* @returns `true` if the extents are equal, otherwise `false`.
*/
equals(other, epsilon = 0.00001) {
return other._crs.equals(this._crs) && Math.abs(other._values[0] - this._values[0]) <= epsilon && Math.abs(other._values[1] - this._values[1]) <= epsilon && Math.abs(other._values[2] - this._values[2]) <= epsilon && Math.abs(other._values[3] - this._values[3]) <= epsilon;
}
/**
* Checks the validity of the extent. Valid extents must not have infinite or NaN values.
*
* @returns `true` if the extent is valid, `false` otherwise.
*/
isValid() {
if (!(Number.isFinite(this.minX) && Number.isFinite(this.maxX) && Number.isFinite(this.minY) && Number.isFinite(this.maxY))) {
return false;
}
// Geographic coordinate systems may allow a greater "west" than "east"
// to account for the wrap around the 180° longitude line.
if (!this.crs.isGeographic()) {
if (this.minX > this.maxX) {
return false;
}
}
if (this.minY > this.maxY) {
return false;
}
return true;
}
/**
* Clones this object.
*
* @returns a copy of this object.
*/
clone() {
const minx = this._values[SIDE.LEFT];
const maxx = this._values[SIDE.RIGHT];
const miny = this._values[SIDE.BOTTOM];
const maxy = this._values[SIDE.TOP];
const result = new Extent(this._crs, minx, maxx, miny, maxy);
return result;
}
/**
* Returns an extent grown the specified relative margin.
* The margin is relative to the width or height of the extent.
*
* @param marginRatio - The margin, in normalized value ([0, 1]).
* A margin of 1 means 100% of the width or height of the extent.
* @example
* // Create an extent with a 10% margin applied:
* const extent = new Extent(CoordinateSystem.epsg3857, 0, 100, 0, 100);
* const margin = extent.withRelativeMargin(0.1);
* // new Extent(CoordinateSystem.epsg3857, -10, 110, -10, 110);
* @returns a new extent with a specified margin applied.
*/
withRelativeMargin(marginRatio) {
const w = Math.abs(this.minX - this.maxX);
const h = Math.abs(this.maxY - this.minY);
return this.withMargin(marginRatio * w, marginRatio * h);
}
/**
* Returns an extent grown or shrinked with the specified margin.
* If the margin is positive, the new extent is bigger, and if the margin is negative the new extent is smaller.
*
* @param x - The horizontal margin, in CRS units.
* @param y - The vertical margin, in CRS units.
* @example
* const extent = new Extent(CoordinateSystem.epsg3857, 0, 100, 0, 100);
* const margin = extent.withMargin(10, 15);
* // new Extent(CoordinateSystem.epsg3857, -10, 110, -15, 115);
* @returns a new extent with a specified margin applied.
*/
withMargin(x, y) {
const w = this.minX - x;
const e = this.maxX + x;
const n = this.maxY + y;
const s = this.minY - y;
return new Extent(this.crs, w, e, s, n);
}
/**
* Converts this extent into another CRS.
* If `crs` is the same as the current CRS, the original object is returned.
*
* @param crs - the new CRS
* @returns the converted extent.
* @example
* const original = new Extent(CoordinateSystem.epsg4326, -5, 5, -5, 5);
* const transformed = original.as(CoordinateSystem.epsg3857);
* // [-556597.4539663679, 556597.4539663679, -557305.2572745769, 557305.2572745753]
*/
as(crs) {
if (!this._crs.equals(crs) && !(this._crs.isEpsg(4326) && crs.isEpsg(4326))) {
// Compute min/max in x/y by projecting 8 cardinal points,
// and then taking the min/max of each coordinates.
const c = this.centerAsVector2(tmpXY);
const cx = c.x;
const cy = c.y;
const e = this.maxX;
const w = this.minX;
const n = this.maxY;
const s = this.minY;
cardinals[0].set(w, n);
cardinals[1].set(cx, n);
cardinals[2].set(e, n);
cardinals[3].set(e, cy);
cardinals[4].set(e, s);
cardinals[5].set(cx, s);
cardinals[6].set(w, s);
cardinals[7].set(w, cy);
let north = -Infinity;
let south = Infinity;
let east = -Infinity;
let west = Infinity;
// convert the coordinates
ProjUtils.transformVectors(this._crs, crs, cardinals);
// loop over the coordinates
for (let i = 0; i < cardinals.length; i++) {
north = Math.max(north, cardinals[i].y);
south = Math.min(south, cardinals[i].y);
east = Math.max(east, cardinals[i].x);
west = Math.min(west, cardinals[i].x);
}
return new Extent(crs, {
north,
south,
east,
west
});
}
return this;
}
offsetToParent(other, target = new OffsetScale()) {
if (!this.crs.equals(other.crs)) {
throw new Error('unsupported mix');
}
const oDim = other.dimensions();
const dim = this.dimensions();
const originX = Math.round(1000 * (this.minX - other.minX) / oDim.x) * 0.001;
const originY = Math.round(1000 * (this.minY - other.minY) / oDim.y) * 0.001;
const scaleX = Math.round(1000 * dim.x / oDim.x) * 0.001;
const scaleY = Math.round(1000 * dim.y / oDim.y) * 0.001;
return target.set(originX, originY, scaleX, scaleY);
}
/**
* @returns the horizontal coordinate of the westernmost side
* @deprecated Use {@link minX} instead.
*/
get west() {
return this.minX;
}
/**
* The minimum X value of this extent (the X coordinate of the left side).
*/
get minX() {
return this._values[SIDE.LEFT];
}
/**
* @returns the horizontal coordinate of the easternmost side
* @deprecated Use {@link maxX} instead.
*/
get east() {
return this.maxX;
}
/**
* The maximum X value of this extent (the X coordinate of the right side).
*/
get maxX() {
return this._values[SIDE.RIGHT];
}
/**
* @returns the vertical coordinate of the northernmost side
* @deprecated Use {@link maxY} instead.
*/
get north() {
return this.maxY;
}
/**
* The maximum Y value of this extent (the Y coordinate of the top side).
*/
get maxY() {
return this._values[SIDE.TOP];
}
/**
* @returns the horizontal coordinate of the southermost side
* @deprecated Use {@link minY} instead.
*/
get south() {
return this.minY;
}
/**
* The minimum Y value of this extent (the Y coordinate of the bottom side).
*/
get minY() {
return this._values[SIDE.BOTTOM];
}
/**
* @returns the coordinates of the top left corner
*/
topLeft() {
return new Coordinates(this.crs, this.minX, this.maxY, 0);
}
/**
* @returns the coordinates of the top right corner
*/
topRight() {
return new Coordinates(this.crs, this.maxX, this.maxY, 0);
}
/**
* @returns the coordinates of the bottom right corner
*/
bottomRight() {
return new Coordinates(this.crs, this.maxX, this.minY, 0);
}
/**
* @returns the coordinates of the bottom right corner
*/
bottomLeft() {
return new Coordinates(this.crs, this.minX, this.minY, 0);
}
/**
* Gets the coordinate reference system of this extent.
*/
get crs() {
return this._crs;
}
/**
* Sets `target` with the center of this extent.
*
* @param target - the coordinate to set with the center's coordinates.
* If none provided, a new one is created.
* @returns the modified object passed in argument.
*/
center(target) {
const center = this.centerAsVector2(tmpXY);
let result;
if (target) {
result = target;
result.set(this._crs, center.x, center.y, 0);
} else {
result = new Coordinates(this._crs, center.x, center.y, 0);
}
return result;
}
/**
* Sets `target` with the center of this extent.
*
* @param target - the vector to set with the center's coordinates.
* If none provided, a new one is created.
* @returns the modified object passed in argument.
*/
centerAsVector2(target) {
const dim = this.dimensions(tmpXY);
const x = this._values[0] + dim.x * 0.5;
const y = this._values[2] + dim.y * 0.5;
let result;
if (target) {
result = target;
result.set(x, y);
} else {
result = new Vector2(x, y);
}
return result;
}
/**
* Sets `target` with the center of this extent.
* Note: The z coordinate of the resulting vector will be set to zero.
*
* @param target - the vector to set with the center's coordinates.
* If none provided, a new one is created.
* @returns the modified object passed in argument.
*/
centerAsVector3(target) {
const center = this.centerAsVector2(tmpXY);
let result;
if (target) {
result = target;
result.set(center.x, center.y, 0);
} else {
result = new Vector3(center.x, center.y, 0);
}
return result;
}
getQuadrant(x, y) {
const dims = this.dimensions(tmpXY);
const midX = this.west + dims.width / 2;
const midY = this.south + dims.height / 2;
if (x < midX) {
if (y < midY) {
return 0;
}
return 1;
} else {
if (y < midY) {
return 3;
}
return 2;
}
}
/**
* Sets the target with the width and height of this extent.
* The `x` property will be set with the width,
* and the `y` property will be set with the height.
*
* @param target - the optional target to set with the result.
* @returns the modified object passed in argument,
* or a new object if none was provided.
*/
dimensions(target = new Vector2()) {
target.x = Math.abs(this.maxX - this.minX);
target.y = Math.abs(this.maxY - this.minY);
return target;
}
/**
* Checks whether the specified coordinate is inside this extent.
*
* @param coord - the coordinate to test
* @param epsilon - the precision delta (+/- epsilon)
* @returns `true` if the coordinate is inside the bounding box
*/
isPointInside(coord, epsilon = 0) {
const c = this.crs.equals(coord.crs) ? coord : coord.as(this.crs);
// TODO this ignores altitude
if (this.crs.isGeographic()) {
return c.longitude <= this.maxX + epsilon && c.longitude >= this.minX - epsilon && c.latitude <= this.maxY + epsilon && c.latitude >= this.minY - epsilon;
}
return this.isXYInside(c.x, c.y, epsilon);
}
isXYInside(x, y, epsilon = 0) {
return x <= this.maxX + epsilon && x >= this.minX - epsilon && y <= this.maxY + epsilon && y >= this.minY - epsilon;
}
/**
* Tests whether this extent is contained in another extent.
*
* @param other - the other extent to test
* @param epsilon - the precision delta (+/- epsilon).
* If this value is not provided, a reasonable epsilon will be computed.
* @returns `true` if this extent is contained in the other extent.
*/
isInside(other, epsilon = null) {
const o = other.as(this._crs);
// 0 is an acceptable value for epsilon:
const dims = this.dimensions(tmpXY);
epsilon = epsilon == null ? reasonnableEpsilonForCRS(this._crs, dims.x, dims.y) : epsilon;
return this.maxX - o.maxX <= epsilon && o.minX - this.minX <= epsilon && this.maxY - o.maxY <= epsilon && o.minY - this.minY <= epsilon;
}
/**
* Tests whether this extent contains entirely another extent.
*
* @param other - the other extent to test
* @param epsilon - the precision delta (+/- epsilon).
* If this value is not provided, a reasonable epsilon will be computed.
* @returns `true` if this extent contains the other extent.
*/
contains(other, epsilon = null) {
return other.isInside(this, epsilon);
}
/**
* Returns `true` if this extent intersects with the specified extent.
*
* @param bbox - the extent to test
* @returns `true` if this extent intersects with the provided extent, `false` otherwise.
*/
intersectsExtent(bbox) {
const other = bbox.as(this.crs);
return !(this.minX >= other.maxX || this.maxX <= other.minX || this.minY >= other.maxY || this.maxY <= other.minY);
}
/**
* Set this extent to the intersection of itself and other
*
* @param other - the bounding box to intersect
* @returns the modified extent
*/
intersect(other) {
if (!this.intersectsExtent(other)) {
this.set(this.crs, 0, 0, 0, 0);
return this;
}
if (!other.crs.equals(this.crs)) {
other = other.as(this.crs);
}
this.set(this.crs, Math.max(this.minX, other.minX), Math.min(this.maxX, other.maxX), Math.max(this.minY, other.minY), Math.min(this.maxY, other.maxY));
return this;
}
/**
* Returns an extent that is adjusted so that its edges land exactly at the border
* of the grid pixels. Optionally, you can specify the minimum pixel size of the
* resulting extent.
*
* @param gridExtent - The grid extent.
* @param gridWidth - The grid width, in pixels.
* @param gridHeight - The grid height, in pixels.
* @param minPixWidth - The minimum width, in pixels, of the resulting extent.
* @param minPixHeight - The minimum height, in pixels, of the resulting extent.
* @returns The adjusted extent and pixel
* size of the adjusted extent.
*/
fitToGrid(gridExtent, gridWidth, gridHeight, minPixWidth, minPixHeight) {
const gridDims = gridExtent.dimensions(tmpXY);
const pixelWidth = gridDims.x / gridWidth;
const pixelHeight = gridDims.y / gridHeight;
let leftPixels = Math.floor((this.minX - gridExtent.minX) / pixelWidth);
let rightPixels = Math.ceil((this.maxX - gridExtent.minX) / pixelWidth);
let bottomPixels = Math.floor((this.minY - gridExtent.minY) / pixelHeight);
let topPixels = Math.ceil((this.maxY - gridExtent.minY) / pixelHeight);
if (minPixWidth !== undefined && minPixHeight !== undefined) {
const pixelCountX = rightPixels - leftPixels;
const pixelCountY = topPixels - bottomPixels;
if (pixelCountX < minPixWidth) {
const margin = (minPixWidth - pixelCountX) / 2;
leftPixels -= margin;
rightPixels += margin;
}
if (pixelCountY < minPixHeight) {
const margin = (minPixHeight - pixelCountY) / 2;
bottomPixels -= margin;
topPixels += margin;
}
}
leftPixels = Math.max(0, Math.floor(leftPixels));
rightPixels = Math.min(gridWidth, Math.ceil(rightPixels));
bottomPixels = Math.max(0, Math.floor(bottomPixels));
topPixels = Math.min(gridHeight, Math.ceil(topPixels));
const west = gridExtent.minX + leftPixels * pixelWidth;
const east = gridExtent.minX + rightPixels * pixelWidth;
const south = gridExtent.minY + bottomPixels * pixelHeight;
const north = gridExtent.minY + topPixels * pixelHeight;
return {
extent: new Extent(this.crs, west, east, south, north),
width: rightPixels - leftPixels,
height: topPixels - bottomPixels
};
}
/**
* Sets the values of this extent from a coordinate system and a pair of coordinates.
* @param crs - The coordinate system to use.
* @param bottomLeft - The bottom-left corner of the extent.
* @param topRight - The top-right corner of the extent.
* @remarks Both coordinates must be in the same coordinate system as this extent.
*/
/**
* Sets the values of this extent from an object containing the min/max XY values.
* @param crs - The coordinate system to use.
* @param values - The extent limits.
*/
/**
* Sets the values of this extent from the min/max XY values.
* @param crs - The coordinate system to use.
* @param values - The extent limits.
* @deprecated Due to ambiguity (not all coordinate systems are north-up), this constructor
* should not be used.
*/
/**
* Sets the values of this extent from the min/max XY values
* @param crs - The coordinate system to use.
* @param minX - The X coordinate of the left side of this extent.
* @param maxX - The X coordinate of the righ side of this extent.
* @param minY - The Y coordinate of the bottom side of this extent.
* @param maxY - The Y coordinate of the top side of this extent.
*/
set(crs, arg0, arg1, arg2, arg3) {
this._crs = crs;
if (isCoordinates(arg0) && isCoordinates(arg1)) {
[this._values[SIDE.LEFT], this._values[SIDE.BOTTOM]] = arg0.values;
[this._values[SIDE.RIGHT], this._values[SIDE.TOP]] = arg1.values;
} else if (typeof arg0 === 'object') {
const obj = arg0;
if ('west' in obj && 'east' in obj && 'south' in obj && 'north' in obj) {
// deprecated code path
this._values[SIDE.LEFT] = obj.west;
this._values[SIDE.RIGHT] = obj.east;
this._values[SIDE.BOTTOM] = obj.south;
this._values[SIDE.TOP] = obj.north;
} else if ('minX' in obj && 'maxX' in obj && 'minY' in obj && 'maxY' in obj) {
this._values[SIDE.LEFT] = obj.minX;
this._values[SIDE.RIGHT] = obj.maxX;
this._values[SIDE.BOTTOM] = obj.minY;
this._values[SIDE.TOP] = obj.maxY;
}
} else if (typeof arg0 === 'number' && typeof arg1 === 'number' && typeof arg2 === 'number' && typeof arg3 === 'number') {
this._values[SIDE.LEFT] = arg0;
this._values[SIDE.RIGHT] = arg1;
this._values[SIDE.BOTTOM] = arg2;
this._values[SIDE.TOP] = arg3;
} else {
throw new Error(`Unsupported constructor args`);
}
return this;
}
copy(other) {
this._crs = other.crs;
this._values[SIDE.LEFT] = other._values[SIDE.LEFT];
this._values[SIDE.RIGHT] = other._values[SIDE.RIGHT];
this._values[SIDE.BOTTOM] = other._values[SIDE.BOTTOM];
this._values[SIDE.TOP] = other._values[SIDE.TOP];
return this;
}
/** @internal */
static unionMany(...extents) {
if (extents == null || extents.length === 0) {
return null;
}
if (extents.length === 1) {
return extents[0].clone();
}
let south = +Infinity;
let north = -Infinity;
let east = -Infinity;
let west = +Infinity;
let valid = false;
let crs = null;
for (let i = 0; i < extents.length; i++) {
const e = nonNull(extents[i]);
valid = true;
if (crs != null) {
if (!crs.equals(e.crs)) {
throw new Error(`Unsupported union between different CRSes (${e.crs.id} and ${crs.id} differ)`);
}
} else {
crs = e.crs;
}
south = Math.min(e.minY, south);
north = Math.max(e.maxY, north);
east = Math.max(e.maxX, east);
west = Math.min(e.minX, west);
}
if (valid) {
return new Extent(extents[0].crs, west, east, south, north);
} else {
return null;
}
}
union(extent) {
if (extent == null) {
return;
}
if (!extent.crs.equals(this.crs)) {
throw new Error(`unsupported union between different CRSes (${extent.crs.id} and ${this.crs.id} differ)`);
}
const west = extent.minX;
if (west < this.minX) {
this._values[SIDE.LEFT] = west;
}
const east = extent.maxX;
if (east > this.maxX) {
this._values[SIDE.RIGHT] = east;
}
const south = extent.minY;
if (south < this.minY) {
this._values[SIDE.BOTTOM] = south;
}
const north = extent.maxY;
if (north > this.maxY) {
this._values[SIDE.TOP] = north;
}
}
/**
* Expands the extent to contain the specified coordinates.
*
* @param coordinates - The coordinates to include
*/
expandByPoint(coordinates) {
const coords = coordinates.as(this.crs);
const we = coords.values[0];
if (we < this.minX) {
this._values[SIDE.LEFT] = we;
}
if (we > this.maxX) {
this._values[SIDE.RIGHT] = we;
}
const sn = coords.values[1];
if (sn < this.minY) {
this._values[SIDE.BOTTOM] = sn;
}
if (sn > this.maxY) {
this._values[SIDE.TOP] = sn;
}
return this;
}
/**
* Moves the extent by the provided `x` and `y` values.
*
* @param x - the horizontal shift
* @param y - the vertical shift
* @returns the modified extent.
*/
shift(x, y) {
this._values[SIDE.LEFT] += x;
this._values[SIDE.RIGHT] += x;
this._values[SIDE.BOTTOM] += y;
this._values[SIDE.TOP] += y;
return this;
}
/**
* Constructs an extent from the specified box.
*
* @param crs - the coordinate reference system of the new extent.
* @param box - the box to read values from
* @returns the constructed extent.
*/
static fromBox3(crs, box) {
return new Extent(crs, {
west: box.min.x,
east: box.max.x,
south: box.min.y,
north: box.max.y
});
}
/**
* Returns a [Box3](https://threejs.org/docs/?q=box3#api/en/math/Box3) that matches this extent.
*
* @param minHeight - The min height of the box.
* @param maxHeight - The max height of the box.
* @returns The box.
*/
toBox3(minHeight, maxHeight) {
const min = new Vector3(this.minX, this.minY, minHeight);
const max = new Vector3(this.maxX, this.maxY, maxHeight);
const box = new Box3(min, max);
return box;
}
/**
* Returns the normalized offset from bottom-left in extent of this Coordinates
*
* @param coordinate - the coordinate
* @param target - optional `Vector2` target.
* If not present a new one will be created.
* @returns normalized offset in extent
* @example
* extent.offsetInExtent(extent.center())
* // returns `(0.5, 0.5)`.
*/
offsetInExtent(coordinate, target = new Vector2()) {
if (!coordinate.crs.equals(this.crs)) {
throw new Error('unsupported mix');
}
const dimX = Math.abs(this.maxX - this.minX);
const dimY = Math.abs(this.maxY - this.minY);
const isGeographic = coordinate.crs.isGeographic();
const x = isGeographic ? coordinate.longitude : coordinate.x;
const y = isGeographic ? coordinate.latitude : coordinate.y;
const originX = (x - this.minX) / dimX;
const originY = (y - this.minY) / dimY;
target.set(originX, originY);
return target;
}
/**
* Divides this extent into a regular grid.
* The number of points in each direction is equal to the number of subdivisions + 1.
* The points are laid out row-wise, from west to east, and north to south:
*
* ```
* 1 -- 2
* | |
* 3 -- 4
* ```
*
* @param xSubdivs - The number of grid subdivisions in the x-axis.
* @param ySubdivs - The number of grid subdivisions in the y-axis.
* @param target - The array to fill.
* @param stride - The number of elements per item (2 for XY, 3 for XYZ).
* @returns the target.
*/
toGrid(xSubdivs, ySubdivs, target, stride) {
const dims = this.dimensions(tmpXY);
const west = this.minX;
const north = this.maxY;
// The size of an horizontal/vertical step
const xStep = dims.x / xSubdivs;
const yStep = dims.y / ySubdivs;
// The number of vertices in each direction
const xCount = xSubdivs + 1;
for (let j = 0; j < ySubdivs + 1; j++) {
for (let i = 0; i < xCount; i++) {
const x = west + xStep * i;
const y = north - yStep * j;
const index = stride * (xCount * j + i);
target[index + 0] = x;
target[index + 1] = y;
}
}
return target;
}
/**
* Subdivides this extents into x and y subdivisions.
*
* Notes:
* - Subdivisions must be strictly positive.
* - If both subvisions are `1`, an array of one element is returned,
* containing a copy of this extent.
*
* @param xSubdivs - The number of subdivisions on the X/longitude axis.
* @param ySubdivs - The number of subdivisions on the Y/latitude axis.
* @returns the resulting extents.
* @example
* const extent = new Extent(CoordinateSystem.epsg3857, 0, 100, 0, 100);
* extent.split(2, 1);
* // [0, 50, 0, 50], [50, 100, 50, 100]
*/
split(xSubdivs, ySubdivs) {
if (xSubdivs < 1 || ySubdivs < 1) {
throw new Error('Invalid subdivisions. Must be strictly positive.');
}
if (xSubdivs === 1 && ySubdivs === 1) {
return [this.clone()];
}
const dims = this.dimensions();
const minX = this.minX;
const minY = this.minY;
const w = dims.x / xSubdivs;
const h = dims.y / ySubdivs;
const crs = this.crs;
const result = [];
for (let x = 0; x < xSubdivs; x++) {
for (let y = 0; y < ySubdivs; y++) {
const west = minX + x * w;
const south = minY + y * h;
const extent = new Extent(crs, west, west + w, south, south + h);
result.push(extent);
}
}
return result;
}
/**
* The bounds of the Web Mercator (EPSG:3857) projection.
*/
static get webMercator() {
return new Extent(CoordinateSystem.epsg3857, -20037508.34, 20037508.34, -20048966.1, 20048966.1);
}
/**
* The bounds of the whole world in the EPSG:4326 projection.
*
* @example
* const bounds = Extent.WGS84;
* // [-180, 180, -90, 90]
*/
static get WGS84() {
return new Extent(CoordinateSystem.epsg4326, -180, 180, -90, 90);
}
/**
* The bounds of the whole sphere in the `'equirectangular'` projection.
*/
static get fullEquirectangularProjection() {
// Note that those are the same values as WGS84.
// However, since panoramic images are not georeferenced,
// speaking about WGS84 makes no sense.
return new Extent(CoordinateSystem.equirectangular, -180, 180, -90, 90);
}
/**
* Creates an extent from parameters of a photosphere in the `'equirectangular'` projection for the given image parameters.
* See [the Google Street View documentation](https://developers.google.com/streetview/spherical-metadata) for additional information.
* @param params - The parameters of the image. If undefined, then it returns the extent
* for the full sphere equivalent to {@link fullEquirectangularProjection}
* @returns The extent of the image in the `'equirectangular'` projection.
*/
static fromPhotosphere(params) {
if (params == null) {
return Extent.fullEquirectangularProjection;
}
const west = 360 * (params.croppedAreaLeftPixels / params.fullPanoImageWidthPixels) - 180;
const north = 90 - 180 * (params.croppedAreaTopPixels / params.fullPanoImageHeightPixels);
const south = north - 180 * (params.croppedAreaImageHeightPixels / params.fullPanoImageHeightPixels);
const east = west + 360 * (params.croppedAreaImageWidthPixels / params.fullPanoImageWidthPixels);
return new Extent(CoordinateSystem.equirectangular, {
west,
east,
south,
north
});
}
}
export default Extent;
export function isCoordinates(obj) {
return obj != null && obj.isCoordinates === true;
}