View Javadoc
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&ndash;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 [&minus;90&deg;, 90&deg;].
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 [&minus;90&deg;, 90&deg;].
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 [&minus;180&deg;, 180&deg;].
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 }