/** * @module ol/sphere */ import {toDegrees, toRadians} from './math.js'; /** * Object literal with options for the {@link getLength} or {@link getArea} * functions. * @typedef {Object} SphereMetricOptions * @property {import("./proj.js").ProjectionLike} [projection='EPSG:3857'] * Projection of the geometry. By default, the geometry is assumed to be in * Web Mercator. * @property {number} [radius=6371008.8] Sphere radius. By default, the * [mean Earth radius](https://en.wikipedia.org/wiki/Earth_radius#Mean_radius) * for the WGS84 ellipsoid is used. */ /** * The mean Earth radius (1/3 * (2a + b)) for the WGS84 ellipsoid. * https://en.wikipedia.org/wiki/Earth_radius#Mean_radius * @type {number} */ export const DEFAULT_RADIUS = 6371008.8; /** * Get the great circle distance (in meters) between two geographic coordinates. * @param {Array} c1 Starting coordinate. * @param {Array} c2 Ending coordinate. * @param {number} [radius] The sphere radius to use. Defaults to the Earth's * mean radius using the WGS84 ellipsoid. * @return {number} The great circle distance between the points (in meters). * @api */ export function getDistance(c1, c2, radius) { radius = radius || DEFAULT_RADIUS; const lat1 = toRadians(c1[1]); const lat2 = toRadians(c2[1]); const deltaLatBy2 = (lat2 - lat1) / 2; const deltaLonBy2 = toRadians(c2[0] - c1[0]) / 2; const a = Math.sin(deltaLatBy2) * Math.sin(deltaLatBy2) + Math.sin(deltaLonBy2) * Math.sin(deltaLonBy2) * Math.cos(lat1) * Math.cos(lat2); return 2 * radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } /** * Get the cumulative great circle length of linestring coordinates (geographic). * @param {Array} coordinates Linestring coordinates. * @param {number} radius The sphere radius to use. * @return {number} The length (in meters). */ function getLengthInternal(coordinates, radius) { let length = 0; for (let i = 0, ii = coordinates.length; i < ii - 1; ++i) { length += getDistance(coordinates[i], coordinates[i + 1], radius); } return length; } /** * Get the spherical length of a geometry. This length is the sum of the * great circle distances between coordinates. For polygons, the length is * the sum of all rings. For points, the length is zero. For multi-part * geometries, the length is the sum of the length of each part. * @param {import("./geom/Geometry.js").default} geometry A geometry. * @param {SphereMetricOptions} [options] Options for the * length calculation. By default, geometries are assumed to be in 'EPSG:3857'. * You can change this by providing a `projection` option. * @return {number} The spherical length (in meters). * @api */ export function getLength(geometry, options) { options = options || {}; const radius = options.radius || DEFAULT_RADIUS; const projection = options.projection || 'EPSG:3857'; const type = geometry.getType(); if (type !== 'GeometryCollection') { geometry = geometry.clone().transform(projection, 'EPSG:4326'); } let length = 0; let coordinates, coords, i, ii, j, jj; switch (type) { case 'Point': case 'MultiPoint': { break; } case 'LineString': case 'LinearRing': { coordinates = /** @type {import("./geom/SimpleGeometry.js").default} */ ( geometry ).getCoordinates(); length = getLengthInternal(coordinates, radius); break; } case 'MultiLineString': case 'Polygon': { coordinates = /** @type {import("./geom/SimpleGeometry.js").default} */ ( geometry ).getCoordinates(); for (i = 0, ii = coordinates.length; i < ii; ++i) { length += getLengthInternal(coordinates[i], radius); } break; } case 'MultiPolygon': { coordinates = /** @type {import("./geom/SimpleGeometry.js").default} */ ( geometry ).getCoordinates(); for (i = 0, ii = coordinates.length; i < ii; ++i) { coords = coordinates[i]; for (j = 0, jj = coords.length; j < jj; ++j) { length += getLengthInternal(coords[j], radius); } } break; } case 'GeometryCollection': { const geometries = /** @type {import("./geom/GeometryCollection.js").default} */ ( geometry ).getGeometries(); for (i = 0, ii = geometries.length; i < ii; ++i) { length += getLength(geometries[i], options); } break; } default: { throw new Error('Unsupported geometry type: ' + type); } } return length; } /** * Returns the spherical area for a list of coordinates. * * [Reference](https://trs.jpl.nasa.gov/handle/2014/40409) * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for * Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion * Laboratory, Pasadena, CA, June 2007 * * @param {Array} coordinates List of coordinates of a linear * ring. If the ring is oriented clockwise, the area will be positive, * otherwise it will be negative. * @param {number} radius The sphere radius. * @return {number} Area (in square meters). */ function getAreaInternal(coordinates, radius) { let area = 0; const len = coordinates.length; let x1 = coordinates[len - 1][0]; let y1 = coordinates[len - 1][1]; for (let i = 0; i < len; i++) { const x2 = coordinates[i][0]; const y2 = coordinates[i][1]; area += toRadians(x2 - x1) * (2 + Math.sin(toRadians(y1)) + Math.sin(toRadians(y2))); x1 = x2; y1 = y2; } return (area * radius * radius) / 2.0; } /** * Get the spherical area of a geometry. This is the area (in meters) assuming * that polygon edges are segments of great circles on a sphere. * @param {import("./geom/Geometry.js").default} geometry A geometry. * @param {SphereMetricOptions} [options] Options for the area * calculation. By default, geometries are assumed to be in 'EPSG:3857'. * You can change this by providing a `projection` option. * @return {number} The spherical area (in square meters). * @api */ export function getArea(geometry, options) { options = options || {}; const radius = options.radius || DEFAULT_RADIUS; const projection = options.projection || 'EPSG:3857'; const type = geometry.getType(); if (type !== 'GeometryCollection') { geometry = geometry.clone().transform(projection, 'EPSG:4326'); } let area = 0; let coordinates, coords, i, ii, j, jj; switch (type) { case 'Point': case 'MultiPoint': case 'LineString': case 'MultiLineString': case 'LinearRing': { break; } case 'Polygon': { coordinates = /** @type {import("./geom/Polygon.js").default} */ ( geometry ).getCoordinates(); area = Math.abs(getAreaInternal(coordinates[0], radius)); for (i = 1, ii = coordinates.length; i < ii; ++i) { area -= Math.abs(getAreaInternal(coordinates[i], radius)); } break; } case 'MultiPolygon': { coordinates = /** @type {import("./geom/SimpleGeometry.js").default} */ ( geometry ).getCoordinates(); for (i = 0, ii = coordinates.length; i < ii; ++i) { coords = coordinates[i]; area += Math.abs(getAreaInternal(coords[0], radius)); for (j = 1, jj = coords.length; j < jj; ++j) { area -= Math.abs(getAreaInternal(coords[j], radius)); } } break; } case 'GeometryCollection': { const geometries = /** @type {import("./geom/GeometryCollection.js").default} */ ( geometry ).getGeometries(); for (i = 0, ii = geometries.length; i < ii; ++i) { area += getArea(geometries[i], options); } break; } default: { throw new Error('Unsupported geometry type: ' + type); } } return area; } /** * Returns the coordinate at the given distance and bearing from `c1`. * * @param {import("./coordinate.js").Coordinate} c1 The origin point (`[lon, lat]` in degrees). * @param {number} distance The great-circle distance between the origin * point and the target point. * @param {number} bearing The bearing (in radians). * @param {number} [radius] The sphere radius to use. Defaults to the Earth's * mean radius using the WGS84 ellipsoid. * @return {import("./coordinate.js").Coordinate} The target point. */ export function offset(c1, distance, bearing, radius) { radius = radius || DEFAULT_RADIUS; const lat1 = toRadians(c1[1]); const lon1 = toRadians(c1[0]); const dByR = distance / radius; const lat = Math.asin( Math.sin(lat1) * Math.cos(dByR) + Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing) ); const lon = lon1 + Math.atan2( Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1), Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat) ); return [toDegrees(lon), toDegrees(lat)]; }