heli-agri
Version:
HeliAgri is a high-performance, feature-packed library for creating interactive maps on the web. It can display map tiles, vector data and markers loaded from any source on any web page. OpenLayers has been developed to further the use of geographic infor
752 lines (710 loc) • 28.1 kB
JavaScript
/**
* @module ol/proj
*/
/**
* The ol/proj module stores:
* * a list of {@link module:ol/proj/Projection~Projection}
* objects, one for each projection supported by the application
* * a list of transform functions needed to convert coordinates in one projection
* into another.
*
* The static functions are the methods used to maintain these.
* Each transform function can handle not only simple coordinate pairs, but also
* large arrays of coordinates such as vector geometries.
*
* When loaded, the library adds projection objects for EPSG:4326 (WGS84
* geographic coordinates) and EPSG:3857 (Web or Spherical Mercator, as used
* for example by Bing Maps or OpenStreetMap), together with the relevant
* transform functions.
*
* Additional transforms may be added by using the http://proj4js.org/
* library (version 2.2 or later). You can use the full build supplied by
* Proj4js, or create a custom build to support those projections you need; see
* the Proj4js website for how to do this. You also need the Proj4js definitions
* for the required projections. These definitions can be obtained from
* https://epsg.io/, and are a JS function, so can be loaded in a script
* tag (as in the examples) or pasted into your application.
*
* After all required projection definitions are added to proj4's registry (by
* using `proj4.defs()`), simply call `register(proj4)` from the `ol/proj/proj4`
* package. Existing transforms are not changed by this function. See
* examples/wms-image-custom-proj for an example of this.
*
* Additional projection definitions can be registered with `proj4.defs()` any
* time. Just make sure to call `register(proj4)` again; for example, with user-supplied data where you don't
* know in advance what projections are needed, you can initially load minimal
* support and then load whichever are requested.
*
* Note that Proj4js does not support projection extents. If you want to add
* one for creating default tile grids, you can add it after the Projection
* object has been created with `setExtent`, for example,
* `get('EPSG:1234').setExtent(extent)`.
*
* In addition to Proj4js support, any transform functions can be added with
* {@link module:ol/proj.addCoordinateTransforms}. To use this, you must first create
* a {@link module:ol/proj/Projection~Projection} object for the new projection and add it with
* {@link module:ol/proj.addProjection}. You can then add the forward and inverse
* functions with {@link module:ol/proj.addCoordinateTransforms}. See
* examples/wms-custom-proj for an example of this.
*
* Note that if no transforms are needed and you only need to define the
* projection, just add a {@link module:ol/proj/Projection~Projection} with
* {@link module:ol/proj.addProjection}. See examples/wms-no-proj for an example of
* this.
*/
import Projection from './proj/Projection.js';
import {
PROJECTIONS as EPSG3857_PROJECTIONS,
fromEPSG4326,
toEPSG4326,
} from './proj/epsg3857.js';
import {PROJECTIONS as EPSG4326_PROJECTIONS} from './proj/epsg4326.js';
import {METERS_PER_UNIT} from './proj/Units.js';
import {
add as addProj,
clear as clearProj,
get as getProj,
} from './proj/projections.js';
import {
add as addTransformFunc,
clear as clearTransformFuncs,
get as getTransformFunc,
} from './proj/transforms.js';
import {applyTransform, getWidth} from './extent.js';
import {clamp, modulo} from './math.js';
import {equals, getWorldsAway} from './coordinate.js';
import {getDistance} from './sphere.js';
import {warn} from './console.js';
/**
* A projection as {@link module:ol/proj/Projection~Projection}, SRS identifier
* string or undefined.
* @typedef {Projection|string|undefined} ProjectionLike
* @api
*/
/**
* A transform function accepts an array of input coordinate values, an optional
* output array, and an optional dimension (default should be 2). The function
* transforms the input coordinate values, populates the output array, and
* returns the output array.
*
* @typedef {function(Array<number>, Array<number>=, number=): Array<number>} TransformFunction
* @api
*/
export {METERS_PER_UNIT};
export {Projection};
let showCoordinateWarning = true;
/**
* @param {boolean} [disable = true] Disable console info about `useGeographic()`
*/
export function disableCoordinateWarning(disable) {
const hide = disable === undefined ? true : disable;
showCoordinateWarning = !hide;
}
/**
* @param {Array<number>} input Input coordinate array.
* @param {Array<number>} [output] Output array of coordinate values.
* @return {Array<number>} Output coordinate array (new array, same coordinate
* values).
*/
export function cloneTransform(input, output) {
if (output !== undefined) {
for (let i = 0, ii = input.length; i < ii; ++i) {
output[i] = input[i];
}
output = output;
} else {
output = input.slice();
}
return output;
}
/**
* @param {Array<number>} input Input coordinate array.
* @param {Array<number>} [output] Output array of coordinate values.
* @return {Array<number>} Input coordinate array (same array as input).
*/
export function identityTransform(input, output) {
if (output !== undefined && input !== output) {
for (let i = 0, ii = input.length; i < ii; ++i) {
output[i] = input[i];
}
input = output;
}
return input;
}
/**
* Add a Projection object to the list of supported projections that can be
* looked up by their code.
*
* @param {Projection} projection Projection instance.
* @api
*/
export function addProjection(projection) {
addProj(projection.getCode(), projection);
addTransformFunc(projection, projection, cloneTransform);
}
/**
* @param {Array<Projection>} projections Projections.
*/
export function addProjections(projections) {
projections.forEach(addProjection);
}
/**
* Fetches a Projection object for the code specified.
*
* @param {ProjectionLike} projectionLike Either a code string which is
* a combination of authority and identifier such as "EPSG:4326", or an
* existing projection object, or undefined.
* @return {Projection|null} Projection object, or null if not in list.
* @api
*/
export function get(projectionLike) {
return typeof projectionLike === 'string'
? getProj(/** @type {string} */ (projectionLike))
: /** @type {Projection} */ (projectionLike) || null;
}
/**
* Get the resolution of the point in degrees or distance units.
* For projections with degrees as the unit this will simply return the
* provided resolution. For other projections the point resolution is
* by default estimated by transforming the `point` pixel to EPSG:4326,
* measuring its width and height on the normal sphere,
* and taking the average of the width and height.
* A custom function can be provided for a specific projection, either
* by setting the `getPointResolution` option in the
* {@link module:ol/proj/Projection~Projection} constructor or by using
* {@link module:ol/proj/Projection~Projection#setGetPointResolution} to change an existing
* projection object.
* @param {ProjectionLike} projection The projection.
* @param {number} resolution Nominal resolution in projection units.
* @param {import("./coordinate.js").Coordinate} point Point to find adjusted resolution at.
* @param {import("./proj/Units.js").Units} [units] Units to get the point resolution in.
* Default is the projection's units.
* @return {number} Point resolution.
* @api
*/
export function getPointResolution(projection, resolution, point, units) {
projection = get(projection);
let pointResolution;
const getter = projection.getPointResolutionFunc();
if (getter) {
pointResolution = getter(resolution, point);
if (units && units !== projection.getUnits()) {
const metersPerUnit = projection.getMetersPerUnit();
if (metersPerUnit) {
pointResolution =
(pointResolution * metersPerUnit) / METERS_PER_UNIT[units];
}
}
} else {
const projUnits = projection.getUnits();
if ((projUnits == 'degrees' && !units) || units == 'degrees') {
pointResolution = resolution;
} else {
// Estimate point resolution by transforming the center pixel to EPSG:4326,
// measuring its width and height on the normal sphere, and taking the
// average of the width and height.
const toEPSG4326 = getTransformFromProjections(
projection,
get('EPSG:4326')
);
if (toEPSG4326 === identityTransform && projUnits !== 'degrees') {
// no transform is available
pointResolution = resolution * projection.getMetersPerUnit();
} else {
let vertices = [
point[0] - resolution / 2,
point[1],
point[0] + resolution / 2,
point[1],
point[0],
point[1] - resolution / 2,
point[0],
point[1] + resolution / 2,
];
vertices = toEPSG4326(vertices, vertices, 2);
const width = getDistance(vertices.slice(0, 2), vertices.slice(2, 4));
const height = getDistance(vertices.slice(4, 6), vertices.slice(6, 8));
pointResolution = (width + height) / 2;
}
const metersPerUnit = units
? METERS_PER_UNIT[units]
: projection.getMetersPerUnit();
if (metersPerUnit !== undefined) {
pointResolution /= metersPerUnit;
}
}
}
return pointResolution;
}
/**
* Registers transformation functions that don't alter coordinates. Those allow
* to transform between projections with equal meaning.
*
* @param {Array<Projection>} projections Projections.
* @api
*/
export function addEquivalentProjections(projections) {
addProjections(projections);
projections.forEach(function (source) {
projections.forEach(function (destination) {
if (source !== destination) {
addTransformFunc(source, destination, cloneTransform);
}
});
});
}
/**
* Registers transformation functions to convert coordinates in any projection
* in projection1 to any projection in projection2.
*
* @param {Array<Projection>} projections1 Projections with equal
* meaning.
* @param {Array<Projection>} projections2 Projections with equal
* meaning.
* @param {TransformFunction} forwardTransform Transformation from any
* projection in projection1 to any projection in projection2.
* @param {TransformFunction} inverseTransform Transform from any projection
* in projection2 to any projection in projection1..
*/
export function addEquivalentTransforms(
projections1,
projections2,
forwardTransform,
inverseTransform
) {
projections1.forEach(function (projection1) {
projections2.forEach(function (projection2) {
addTransformFunc(projection1, projection2, forwardTransform);
addTransformFunc(projection2, projection1, inverseTransform);
});
});
}
/**
* Clear all cached projections and transforms.
*/
export function clearAllProjections() {
clearProj();
clearTransformFuncs();
}
/**
* @param {Projection|string|undefined} projection Projection.
* @param {string} defaultCode Default code.
* @return {Projection} Projection.
*/
export function createProjection(projection, defaultCode) {
if (!projection) {
return get(defaultCode);
}
if (typeof projection === 'string') {
return get(projection);
}
return /** @type {Projection} */ (projection);
}
/**
* Creates a {@link module:ol/proj~TransformFunction} from a simple 2D coordinate transform
* function.
* @param {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} coordTransform Coordinate
* transform.
* @return {TransformFunction} Transform function.
*/
export function createTransformFromCoordinateTransform(coordTransform) {
return (
/**
* @param {Array<number>} input Input.
* @param {Array<number>} [output] Output.
* @param {number} [dimension] Dimension.
* @return {Array<number>} Output.
*/
function (input, output, dimension) {
const length = input.length;
dimension = dimension !== undefined ? dimension : 2;
output = output !== undefined ? output : new Array(length);
for (let i = 0; i < length; i += dimension) {
const point = coordTransform(input.slice(i, i + dimension));
const pointLength = point.length;
for (let j = 0, jj = dimension; j < jj; ++j) {
output[i + j] = j >= pointLength ? input[i + j] : point[j];
}
}
return output;
}
);
}
/**
* Registers coordinate transform functions to convert coordinates between the
* source projection and the destination projection.
* The forward and inverse functions convert coordinate pairs; this function
* converts these into the functions used internally which also handle
* extents and coordinate arrays.
*
* @param {ProjectionLike} source Source projection.
* @param {ProjectionLike} destination Destination projection.
* @param {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} forward The forward transform
* function (that is, from the source projection to the destination
* projection) that takes a {@link module:ol/coordinate~Coordinate} as argument and returns
* the transformed {@link module:ol/coordinate~Coordinate}.
* @param {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} inverse The inverse transform
* function (that is, from the destination projection to the source
* projection) that takes a {@link module:ol/coordinate~Coordinate} as argument and returns
* the transformed {@link module:ol/coordinate~Coordinate}. If the transform function can only
* transform less dimensions than the input coordinate, it is supposeed to return a coordinate
* with only the length it can transform. The other dimensions will be taken unchanged from the
* source.
* @api
*/
export function addCoordinateTransforms(source, destination, forward, inverse) {
const sourceProj = get(source);
const destProj = get(destination);
addTransformFunc(
sourceProj,
destProj,
createTransformFromCoordinateTransform(forward)
);
addTransformFunc(
destProj,
sourceProj,
createTransformFromCoordinateTransform(inverse)
);
}
/**
* Transforms a coordinate from longitude/latitude to a different projection.
* @param {import("./coordinate.js").Coordinate} coordinate Coordinate as longitude and latitude, i.e.
* an array with longitude as 1st and latitude as 2nd element.
* @param {ProjectionLike} [projection] Target projection. The
* default is Web Mercator, i.e. 'EPSG:3857'.
* @return {import("./coordinate.js").Coordinate} Coordinate projected to the target projection.
* @api
*/
export function fromLonLat(coordinate, projection) {
disableCoordinateWarning();
return transform(
coordinate,
'EPSG:4326',
projection !== undefined ? projection : 'EPSG:3857'
);
}
/**
* Transforms a coordinate to longitude/latitude.
* @param {import("./coordinate.js").Coordinate} coordinate Projected coordinate.
* @param {ProjectionLike} [projection] Projection of the coordinate.
* The default is Web Mercator, i.e. 'EPSG:3857'.
* @return {import("./coordinate.js").Coordinate} Coordinate as longitude and latitude, i.e. an array
* with longitude as 1st and latitude as 2nd element.
* @api
*/
export function toLonLat(coordinate, projection) {
const lonLat = transform(
coordinate,
projection !== undefined ? projection : 'EPSG:3857',
'EPSG:4326'
);
const lon = lonLat[0];
if (lon < -180 || lon > 180) {
lonLat[0] = modulo(lon + 180, 360) - 180;
}
return lonLat;
}
/**
* Checks if two projections are the same, that is every coordinate in one
* projection does represent the same geographic point as the same coordinate in
* the other projection.
*
* @param {Projection} projection1 Projection 1.
* @param {Projection} projection2 Projection 2.
* @return {boolean} Equivalent.
* @api
*/
export function equivalent(projection1, projection2) {
if (projection1 === projection2) {
return true;
}
const equalUnits = projection1.getUnits() === projection2.getUnits();
if (projection1.getCode() === projection2.getCode()) {
return equalUnits;
}
const transformFunc = getTransformFromProjections(projection1, projection2);
return transformFunc === cloneTransform && equalUnits;
}
/**
* Searches in the list of transform functions for the function for converting
* coordinates from the source projection to the destination projection.
*
* @param {Projection} sourceProjection Source Projection object.
* @param {Projection} destinationProjection Destination Projection
* object.
* @return {TransformFunction} Transform function.
*/
export function getTransformFromProjections(
sourceProjection,
destinationProjection
) {
const sourceCode = sourceProjection.getCode();
const destinationCode = destinationProjection.getCode();
let transformFunc = getTransformFunc(sourceCode, destinationCode);
if (!transformFunc) {
transformFunc = identityTransform;
}
return transformFunc;
}
/**
* Given the projection-like objects, searches for a transformation
* function to convert a coordinates array from the source projection to the
* destination projection.
*
* @param {ProjectionLike} source Source.
* @param {ProjectionLike} destination Destination.
* @return {TransformFunction} Transform function.
* @api
*/
export function getTransform(source, destination) {
const sourceProjection = get(source);
const destinationProjection = get(destination);
return getTransformFromProjections(sourceProjection, destinationProjection);
}
/**
* Transforms a coordinate from source projection to destination projection.
* This returns a new coordinate (and does not modify the original).
*
* See {@link module:ol/proj.transformExtent} for extent transformation.
* See the transform method of {@link module:ol/geom/Geometry~Geometry} and its
* subclasses for geometry transforms.
*
* @param {import("./coordinate.js").Coordinate} coordinate Coordinate.
* @param {ProjectionLike} source Source projection-like.
* @param {ProjectionLike} destination Destination projection-like.
* @return {import("./coordinate.js").Coordinate} Coordinate.
* @api
*/
export function transform(coordinate, source, destination) {
const transformFunc = getTransform(source, destination);
return transformFunc(coordinate, undefined, coordinate.length);
}
/**
* Transforms an extent from source projection to destination projection. This
* returns a new extent (and does not modify the original).
*
* @param {import("./extent.js").Extent} extent The extent to transform.
* @param {ProjectionLike} source Source projection-like.
* @param {ProjectionLike} destination Destination projection-like.
* @param {number} [stops] Number of stops per side used for the transform.
* By default only the corners are used.
* @return {import("./extent.js").Extent} The transformed extent.
* @api
*/
export function transformExtent(extent, source, destination, stops) {
const transformFunc = getTransform(source, destination);
return applyTransform(extent, transformFunc, undefined, stops);
}
/**
* Transforms the given point to the destination projection.
*
* @param {import("./coordinate.js").Coordinate} point Point.
* @param {Projection} sourceProjection Source projection.
* @param {Projection} destinationProjection Destination projection.
* @return {import("./coordinate.js").Coordinate} Point.
*/
export function transformWithProjections(
point,
sourceProjection,
destinationProjection
) {
const transformFunc = getTransformFromProjections(
sourceProjection,
destinationProjection
);
return transformFunc(point);
}
/**
* @type {Projection|null}
*/
let userProjection = null;
/**
* Set the projection for coordinates supplied from and returned by API methods.
* This includes all API methods except for those interacting with tile grids,
* plus {@link import("./Map.js").FrameState} and {@link import("./View.js").State}.
* @param {ProjectionLike} projection The user projection.
* @api
*/
export function setUserProjection(projection) {
userProjection = get(projection);
}
/**
* Clear the user projection if set.
* @api
*/
export function clearUserProjection() {
userProjection = null;
}
/**
* Get the projection for coordinates supplied from and returned by API methods.
* @return {Projection|null} The user projection (or null if not set).
* @api
*/
export function getUserProjection() {
return userProjection;
}
/**
* Use geographic coordinates (WGS-84 datum) in API methods.
* This includes all API methods except for those interacting with tile grids,
* plus {@link import("./Map.js").FrameState} and {@link import("./View.js").State}.
* @api
*/
export function useGeographic() {
setUserProjection('EPSG:4326');
}
/**
* Return a coordinate transformed into the user projection. If no user projection
* is set, the original coordinate is returned.
* @param {Array<number>} coordinate Input coordinate.
* @param {ProjectionLike} sourceProjection The input coordinate projection.
* @return {Array<number>} The input coordinate in the user projection.
*/
export function toUserCoordinate(coordinate, sourceProjection) {
if (!userProjection) {
return coordinate;
}
return transform(coordinate, sourceProjection, userProjection);
}
/**
* Return a coordinate transformed from the user projection. If no user projection
* is set, the original coordinate is returned.
* @param {Array<number>} coordinate Input coordinate.
* @param {ProjectionLike} destProjection The destination projection.
* @return {Array<number>} The input coordinate transformed.
*/
export function fromUserCoordinate(coordinate, destProjection) {
if (!userProjection) {
if (
showCoordinateWarning &&
!equals(coordinate, [0, 0]) &&
coordinate[0] >= -180 &&
coordinate[0] <= 180 &&
coordinate[1] >= -90 &&
coordinate[1] <= 90
) {
showCoordinateWarning = false;
warn(
'Call useGeographic() from ol/proj once to work with [longitude, latitude] coordinates.'
);
}
return coordinate;
}
return transform(coordinate, userProjection, destProjection);
}
/**
* Return an extent transformed into the user projection. If no user projection
* is set, the original extent is returned.
* @param {import("./extent.js").Extent} extent Input extent.
* @param {ProjectionLike} sourceProjection The input extent projection.
* @return {import("./extent.js").Extent} The input extent in the user projection.
*/
export function toUserExtent(extent, sourceProjection) {
if (!userProjection) {
return extent;
}
return transformExtent(extent, sourceProjection, userProjection);
}
/**
* Return an extent transformed from the user projection. If no user projection
* is set, the original extent is returned.
* @param {import("./extent.js").Extent} extent Input extent.
* @param {ProjectionLike} destProjection The destination projection.
* @return {import("./extent.js").Extent} The input extent transformed.
*/
export function fromUserExtent(extent, destProjection) {
if (!userProjection) {
return extent;
}
return transformExtent(extent, userProjection, destProjection);
}
/**
* Return the resolution in user projection units per pixel. If no user projection
* is set, or source or user projection are missing units, the original resolution
* is returned.
* @param {number} resolution Resolution in input projection units per pixel.
* @param {ProjectionLike} sourceProjection The input projection.
* @return {number} Resolution in user projection units per pixel.
*/
export function toUserResolution(resolution, sourceProjection) {
if (!userProjection) {
return resolution;
}
const sourceUnits = get(sourceProjection).getUnits();
const userUnits = userProjection.getUnits();
return sourceUnits && userUnits
? (resolution * METERS_PER_UNIT[sourceUnits]) / METERS_PER_UNIT[userUnits]
: resolution;
}
/**
* Return the resolution in user projection units per pixel. If no user projection
* is set, or source or user projection are missing units, the original resolution
* is returned.
* @param {number} resolution Resolution in user projection units per pixel.
* @param {ProjectionLike} destProjection The destination projection.
* @return {number} Resolution in destination projection units per pixel.
*/
export function fromUserResolution(resolution, destProjection) {
if (!userProjection) {
return resolution;
}
const sourceUnits = get(destProjection).getUnits();
const userUnits = userProjection.getUnits();
return sourceUnits && userUnits
? (resolution * METERS_PER_UNIT[userUnits]) / METERS_PER_UNIT[sourceUnits]
: resolution;
}
/**
* Creates a safe coordinate transform function from a coordinate transform function.
* "Safe" means that it can handle wrapping of x-coordinates for global projections,
* and that coordinates exceeding the source projection validity extent's range will be
* clamped to the validity range.
* @param {Projection} sourceProj Source projection.
* @param {Projection} destProj Destination projection.
* @param {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} transform Transform function (source to destination).
* @return {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} Safe transform function (source to destination).
*/
export function createSafeCoordinateTransform(sourceProj, destProj, transform) {
return function (coord) {
let transformed, worldsAway;
if (sourceProj.canWrapX()) {
const sourceExtent = sourceProj.getExtent();
const sourceExtentWidth = getWidth(sourceExtent);
coord = coord.slice(0);
worldsAway = getWorldsAway(coord, sourceProj, sourceExtentWidth);
if (worldsAway) {
// Move x to the real world
coord[0] = coord[0] - worldsAway * sourceExtentWidth;
}
coord[0] = clamp(coord[0], sourceExtent[0], sourceExtent[2]);
coord[1] = clamp(coord[1], sourceExtent[1], sourceExtent[3]);
transformed = transform(coord);
} else {
transformed = transform(coord);
}
if (worldsAway && destProj.canWrapX()) {
// Move transformed coordinate back to the offset world
transformed[0] += worldsAway * getWidth(destProj.getExtent());
}
return transformed;
};
}
/**
* Add transforms to and from EPSG:4326 and EPSG:3857. This function is called
* by when this module is executed and should only need to be called again after
* `clearAllProjections()` is called (e.g. in tests).
*/
export function addCommon() {
// Add transformations that don't alter coordinates to convert within set of
// projections with equal meaning.
addEquivalentProjections(EPSG3857_PROJECTIONS);
addEquivalentProjections(EPSG4326_PROJECTIONS);
// Add transformations to convert EPSG:4326 like coordinates to EPSG:3857 like
// coordinates and back.
addEquivalentTransforms(
EPSG4326_PROJECTIONS,
EPSG3857_PROJECTIONS,
fromEPSG4326,
toEPSG4326
);
}
addCommon();