/
sphere.js
263 lines (255 loc) · 8.71 KB
/
sphere.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/**
* @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<import("./coordinate.js").Coordinate>} 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)];
}