1 /*
2 * Copyright (C) 2018 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.irurueta.navigation.geodesic;
17
18 /**
19 * Polygon areas.
20 * This computes the area of a geodesic polygon using the method given Section 6 of
21 * <ul>
22 * <li>
23 * C. F. F. Karney, <a href="https://doi.org/10.1007/s00190-012-0578-z">Algorithms for
24 * geodesics</a>,
25 * J. Geodesy <b>87</b>, 43–55 (2013)
26 * </li>
27 * </ul>
28 * This class lets you add vertices one at a time to the polygon. The area and perimeter are
29 * accumulated at two times the standard floating point precision to guard against the loss of
30 * accuracy with many-sided polygons.
31 * At any point you can ask for the perimeter and area so far. There's an option to treat the
32 * points as defining a polyline instead of a polygon; in that case, only the perimeter is
33 * computed.
34 * Example of use:
35 * <pre>
36 * {@code
37 * // Compute the area of a geodesic polygon.
38 *
39 * // This program reads lines with lat, lon for each vertex of a polygon.
40 * // At the end of input, the program prints the number of vertices,
41 * // the perimeter of the polygon and its area (for the WGS84 ellipsoid).
42 *
43 * import java.util.*;
44 * import com.irurueta.navigation.geodesic.*;
45 *
46 * public class Planimeter {
47 * public static void main(String[] args) {
48 * PolygonArea p = new PolygonArea(Geodesic.WGS84, false);
49 * try {
50 * Scanner in = new Scanner(System.in);
51 * while (true) {
52 * double lat = in.nextDouble(), lon = in.nextDouble();
53 * p.AddPoint(lat, lon);
54 * }
55 * }
56 * catch (Exception e) {}
57 * PolygonResult r = p.Compute();
58 * System.out.println(r.num + " " + r.perimeter + " " + r.area);
59 * }
60 * }}</pre>
61 */
62 @SuppressWarnings("DuplicatedCode")
63 public class PolygonArea {
64
65 private final Geodesic earth;
66
67 // full ellipsoid area
68 private final double area0;
69
70 // assume polyline (don't close and skip area)
71 private final boolean polyline;
72
73 private final int mask;
74 private int num;
75 private int crossings;
76
77 private Accumulator areasum;
78 private final Accumulator perimetersum;
79
80 private double lat0;
81 private double lon0;
82 private double lat1;
83 private double lon1;
84
85 /**
86 * Constructor for PolygonArea.
87 *
88 * @param earth the Geodesic object to use for geodesic calculations.
89 * @param polyline if true that treat the points as defining a polyline instead of a polygon.
90 */
91 public PolygonArea(final Geodesic earth, final boolean polyline) {
92 this.earth = earth;
93 area0 = this.earth.getEllipsoidArea();
94 this.polyline = polyline;
95 mask = GeodesicMask.LATITUDE | GeodesicMask.LONGITUDE | GeodesicMask.DISTANCE |
96 (this.polyline ? GeodesicMask.NONE : GeodesicMask.AREA | GeodesicMask.LONG_UNROLL);
97 perimetersum = new Accumulator(0);
98 if (!this.polyline) {
99 areasum = new Accumulator(0);
100 }
101 clear();
102 }
103
104 /**
105 * Clear PolygonArea, allowing a new polygon to be started.
106 */
107 public void clear() {
108 num = 0;
109 crossings = 0;
110 perimetersum.set(0);
111 if (!polyline) {
112 areasum.set(0);
113 }
114 lat0 = lon0 = lat1 = lon1 = Double.NaN;
115 }
116
117 /**
118 * Add a point to the polygon or polyline.
119 * <i>lat</i> should be in the range [−90°, 90°].
120 *
121 * @param lat the latitude of the point (degrees).
122 * @param lon the latitude of the point (degrees).
123 */
124 public void addPoint(final double lat, double lon) {
125 lon = GeoMath.angNormalize(lon);
126 if (num == 0) {
127 lat0 = lat1 = lat;
128 lon0 = lon1 = lon;
129 } else {
130 final var g = earth.inverse(lat1, lon1, lat, lon, mask);
131 perimetersum.add(g.getS12());
132 if (!polyline) {
133 areasum.add(g.getAreaS12());
134 crossings += transit(lon1, lon);
135 }
136 lat1 = lat;
137 lon1 = lon;
138 }
139 ++num;
140 }
141
142 /**
143 * Add an edge to the polygon or polyline.
144 * This does nothing if no points have been added yet. Use PolygonArea.getCurrentPoint to
145 * determine the position of the new vertex.
146 *
147 * @param azi azimuth at current point (degrees).
148 * @param s distance from current point to next point (meters).
149 */
150 public void addEdge(final double azi, final double s) {
151 // do nothing if mNum is zero
152 if (num > 0) {
153 final var g = earth.direct(lat1, lon1, azi, s, mask);
154 perimetersum.add(g.getS12());
155 if (!polyline) {
156 areasum.add(g.getAreaS12());
157 crossings += transitDirect(lon1, g.getLon2());
158 }
159 lat1 = g.getLat2();
160 lon1 = g.getLon2();
161 ++num;
162 }
163 }
164
165 /**
166 * Return the results so far.
167 * Counter-clockwise traversal counts as a positive area.
168 *
169 * @return PolygonResult(<i>num</i>, <i>perimeter</i>, <i>area</i>) where
170 * <i>num</i> is the number of vertices, <i>perimeter</i> is the perimeter of
171 * the polygon or the length of the polyline (meters), and <i>area</i> is the
172 * area of the polygon (meters<sup>2</sup>) or Double.NaN of <i>polyline</i>
173 * is true in the constructor.
174 */
175 public PolygonResult compute() {
176 return compute(false, true);
177 }
178
179 /**
180 * Return the results so far.
181 * More points can be added to the polygon after this call.
182 *
183 * @param reverse if true then clockwise (instead of counter-clockwise) traversal counts as
184 * a positive area.
185 * @param sign if true then return a signed result for the area if the polygon is traversed
186 * in the "wrong" direction instead of returning the area for the rest of the
187 * earth.
188 * @return PolygonResult(<i>num</i>, <i>perimeter</i>, <i>area</i>) where
189 * <i>num</i> is the number of vertices, <i>perimeter</i> is the perimeter of the polygon
190 * or the length of the polyline (meters), and <i>area</i> is the area of the polygon
191 * (meters<sup>2</sup>) or Double.NaN of <i>polyline</i> is true in the constructor.
192 */
193 public PolygonResult compute(final boolean reverse, final boolean sign) {
194 if (num < 2) {
195 return new PolygonResult(num, 0, polyline ? Double.NaN : 0);
196 }
197 if (polyline) {
198 return new PolygonResult(num, perimetersum.getSum(), Double.NaN);
199 }
200
201 final var g = earth.inverse(lat1, lon1, lat0, lon0, mask);
202 final var tempsum = new Accumulator(areasum);
203 tempsum.add(g.getAreaS12());
204 final var tcrossings = this.crossings + transit(lon1, lon0);
205 if ((tcrossings & 1) != 0) {
206 tempsum.add((tempsum.getSum() < 0 ? 1 : -1) * area0 / 2);
207 }
208
209 // area is with the clockwise sense. If !reverse convert to counter-clockwise convention
210 if (!reverse) {
211 tempsum.negate();
212 }
213
214 // if sign put area in (-rea0/2, area0/2], else put area in [0, area0)
215 if (sign) {
216 if (tempsum.getSum() > area0 / 2) {
217 tempsum.add(-area0);
218 } else if (tempsum.getSum() <= -area0 / 2) {
219 tempsum.add(+area0);
220 }
221 } else {
222 if (tempsum.getSum() >= area0) {
223 tempsum.add(-area0);
224 } else if (tempsum.getSum() < 0) {
225 tempsum.add(+area0);
226 }
227 }
228 return new PolygonResult(num, perimetersum.sum(g.getS12()), 0 + tempsum.getSum());
229 }
230
231 /**
232 * Return the results assuming a tentative final test point is added;
233 * however, the data for the test point is not saved. This lets you report a running result
234 * for the perimeter and area as the user moves the mouse cursor. Ordinary floating point
235 * arithmetic is used to accumulate the data for the test point; thus the area and perimeter
236 * returned are less accurate than if addPoint and compute are used.
237 * <i>lat</i> should be in the range [−90°, 90°].
238 *
239 * @param lat the latitude of the test point (degrees).
240 * @param lon the longitude of the test point (degrees).
241 * @param reverse if true then clockwise (instead of counter-clockwise) traversal counts as
242 * a positive area.
243 * @param sign if true then return a signed result for the area if the polygon is traversed
244 * in the "wrong" direction instead of returning the area for the rest of the
245 * earth.
246 * @return PolygonResult(<i>num</i>, <i>perimeter</i>, <i>area</i>) where <i>num</i> is
247 * the number of vertices, <i>perimeter</i> is the perimeter of the polygon or the length
248 * of the polyline (meters), and <i>area</i> is the area of the polygon (meters<sup>2</sup>)
249 * or Double.NaN of <i>polyline</i> is true in the constructor.
250 */
251 public PolygonResult testPoint(final double lat, final double lon, final boolean reverse, final boolean sign) {
252 if (num == 0) {
253 return new PolygonResult(1, 0, polyline ? Double.NaN : 0);
254 }
255
256 var perimeter = perimetersum.getSum();
257 var tempsum = polyline ? 0 : areasum.getSum();
258 var tcrossings = this.crossings;
259 final var tnum = this.num + 1;
260 for (var i = 0; i < (polyline ? 1 : 2); ++i) {
261 final var g = earth.inverse(i == 0 ? lat1 : lat, i == 0 ? lon1 : lon, i != 0 ? lat0 : lat,
262 i != 0 ? lon0 : lon, mask);
263 perimeter += g.getS12();
264 if (!polyline) {
265 tempsum += g.getAreaS12();
266 tcrossings += transit(i == 0 ? lon1 : lon, i != 0 ? lon0 : lon);
267 }
268 }
269
270 if (polyline) {
271 return new PolygonResult(tnum, perimeter, Double.NaN);
272 }
273
274 if ((tcrossings & 1) != 0) {
275 tempsum += (tempsum < 0 ? 1 : -1) * area0 / 2;
276 }
277
278 // area is with the clockwise sense. If !reverse convert to counter-clockwise convention
279 if (!reverse) {
280 tempsum *= -1;
281 }
282
283 // if sign put area in (-area0/2, area0/2], else put area in [0, area0)
284 if (sign) {
285 if (tempsum > area0 / 2) {
286 tempsum -= area0;
287 } else if (tempsum <= -area0 / 2) {
288 tempsum += area0;
289 }
290 } else {
291 if (tempsum >= area0) {
292 tempsum -= area0;
293 } else if (tempsum < 0) {
294 tempsum += area0;
295 }
296 }
297 return new PolygonResult(tnum, perimeter, 0 + tempsum);
298 }
299
300 /**
301 * Return the results assuming a tentative final test point is added via an azimuth and distance;
302 * however, the data for the test point is not saved.
303 * This lets you report a running result for the perimeter and area as the user moves the mouse
304 * cursor. Ordinary floating point arithmetic is used to accumulate the data for the test point;
305 * thus the area and perimeter returned are less accurate than if addPoint and compute are used.
306 *
307 * @param azi azimuth at current point (degrees).
308 * @param s distance from current point to final test point (meters).
309 * @param reverse if true then clockwise (instead of counter-clockwise) traversal counts as a
310 * positive area.
311 * @param sign if true then return a signed result for the area if the polygon is traversed in
312 * the "wrong" direction instead of returning the area for the rest of the earth.
313 * @return PolygonResult(<i>num</i>, <i>perimeter</i>, <i>area</i>) where <i>num</i> is the
314 * number of vertices, <i>perimeter</i> is the perimeter of the polygon or the length of the
315 * polyline (meters), and <i>area</i> is the area of the polygon (meters<sup>2</sup>) or
316 * Double.NaN of <i>polyline</i> is true in the constructor.
317 */
318 public PolygonResult testEdge(
319 final double azi, final double s, final boolean reverse, final boolean sign) {
320 // we don't have a starting point!
321 if (num == 0) {
322 return new PolygonResult(0, Double.NaN, Double.NaN);
323 }
324
325 final var tnum = this.num + 1;
326 var perimeter = perimetersum.getSum() + s;
327 if (polyline) {
328 return new PolygonResult(tnum, perimeter, Double.NaN);
329 }
330
331 var tempsum = areasum.getSum();
332 var tcrossings = this.crossings;
333
334 var g = earth.direct(lat1, lon1, azi, false, s, mask);
335 tempsum += g.getAreaS12();
336 tcrossings += transitDirect(lon1, g.getLon2());
337 g = earth.inverse(g.getLat2(), g.getLon2(), lat0, lon0, mask);
338 perimeter += g.getS12();
339 tempsum += g.getAreaS12();
340 tcrossings += transit(g.getLon2(), lon0);
341
342 if ((tcrossings & 1) != 0) {
343 tempsum += (tempsum < 0 ? 1 : -1) * area0 / 2;
344 }
345
346 // area is with the clockwise sense. If !reverse convert to counter-clockwise convention.
347 if (!reverse) {
348 tempsum *= -1;
349 }
350
351 // if sign put area in (-area0/2, area0/2], else put area in [0, area0)
352 if (sign) {
353 if (tempsum > area0 / 2) {
354 tempsum -= area0;
355 } else if (tempsum <= -area0 / 2) {
356 tempsum += area0;
357 }
358 } else {
359 if (tempsum >= area0) {
360 tempsum -= area0;
361 } else if (tempsum < 0) {
362 tempsum += area0;
363 }
364 }
365
366 return new PolygonResult(tnum, perimeter, 0 + tempsum);
367 }
368
369 /**
370 * Gets the equatorial radius of the ellipsoid (meters).
371 *
372 * @return <i>a</i> the equatorial radius of the ellipsoid (meters). This is the value inherited
373 * from the Geodesic object used in the constructor.
374 */
375 public double getMajorRadius() {
376 return earth.getMajorRadius();
377 }
378
379 /**
380 * Gets the flattening of the ellipsoid.
381 *
382 * @return <i>f</i> the flattening of the ellipsoid. This is the value inherited from the Geodesic
383 * object used in the constructor.
384 */
385 public double getFlattening() {
386 return earth.getFlattening();
387 }
388
389 /**
390 * Report the previous vertex added to the polygon or polyline.
391 * If no points have been added, then Double.NaN is returned. Otherwise, <i>lon</i> will be
392 * in the range [−180°, 180°].
393 *
394 * @return Pair(<i>lat</i>, <i>lon</i>), the current latitude and longitude.
395 */
396 public Pair getCurrentPoint() {
397 return new Pair(lat1, lon1);
398 }
399
400 private static int transit(double lon1, double lon2) {
401 // return 1 or -1 if crossing prime meridian in east or west direction.
402 // Otherwise, return zero.
403 // Compute lon12 the same way as Geodesic.inverse.
404 lon1 = GeoMath.angNormalize(lon1);
405 lon2 = GeoMath.angNormalize(lon2);
406
407 final var lon12 = GeoMath.angDiff(lon1, lon2).getFirst();
408 if (lon1 <= 0 && lon2 > 0 && lon12 > 0) {
409 return 1;
410 } else {
411 return lon2 <= 0 && lon1 > 0 && lon12 < 0 ? -1 : 0;
412 }
413 }
414
415 // an alternate version of transit to deal with longitudes in the direct problem.
416 private static int transitDirect(double lon1, double lon2) {
417 // we want to compute exactly
418 // int(floor(lon2 / 360)) - int(floor(lon1 / 360))
419 lon1 = lon1 % 720.0;
420 lon2 = lon2 % 720.0;
421 return (((lon2 >= 0 && lon2 < 360) || lon2 < -360 ? 0 : 1) -
422 ((lon1 >= 0 && lon1 < 360) || lon1 < -360 ? 0 : 1));
423 }
424 }