geographiclib.polygonarea — geographiclib 2.1 documentation

# Source code for geographiclib.polygonarea

    """Define the :class:`~geographiclib.polygonarea.PolygonArea` class

    The constructor initializes a empty polygon.  The available methods are

      * :meth:`~geographiclib.polygonarea.PolygonArea.Clear` reset the
        polygon
      * :meth:`~geographiclib.polygonarea.PolygonArea.AddPoint` add a vertex
        to the polygon
      * :meth:`~geographiclib.polygonarea.PolygonArea.AddEdge` add an edge
        to the polygon
      * :meth:`~geographiclib.polygonarea.PolygonArea.Compute` compute the
        properties of the polygon
      * :meth:`~geographiclib.polygonarea.PolygonArea.TestPoint` compute the
        properties of the polygon with a tentative additional vertex
      * :meth:`~geographiclib.polygonarea.PolygonArea.TestEdge` compute the
        properties of the polygon with a tentative additional edge

    The public attributes for this class are

      * :attr:`~geographiclib.polygonarea.PolygonArea.earth`
        :attr:`~geographiclib.polygonarea.PolygonArea.polyline`
        :attr:`~geographiclib.polygonarea.PolygonArea.area0`
        :attr:`~geographiclib.polygonarea.PolygonArea.num`
        :attr:`~geographiclib.polygonarea.PolygonArea.lat1`
        :attr:`~geographiclib.polygonarea.PolygonArea.lon1`

    """
    # polygonarea.py
    #
    # This is a rather literal translation of the GeographicLib::PolygonArea class
    # to python.  See the documentation for the C++ class for more information at
    #
    #    https://geographiclib.sourceforge.io/C++/doc/annotated.html
    #
    # The algorithms are derived in
    #
    #    Charles F. F. Karney,
    #    Algorithms for geodesics, J. Geodesy 87, 43-55 (2013),
    #    https://doi.org/10.1007/s00190-012-0578-z
    #    Addenda: https://geographiclib.sourceforge.io/geod-addenda.html
    #
    # Copyright (c) Charles Karney (2011-2022) <karney@alum.mit.edu> and licensed
    # under the MIT/X11 License.  For more information, see
    # https://geographiclib.sourceforge.io/
    ######################################################################

    import math
    from geographiclib.geomath import Math
    from geographiclib.accumulator import Accumulator
    from geographiclib.geodesic import Geodesic

    class PolygonArea:
      """Area of a geodesic polygon"""

      @staticmethod
      def _transit(lon1, lon2):
        """Count crossings of prime meridian for AddPoint."""
        # Return 1 or -1 if crossing prime meridian in east or west direction.
        # Otherwise return zero.
        # Compute lon12 the same way as Geodesic::Inverse.
        lon12, _ = Math.AngDiff(lon1, lon2)
        lon1 = Math.AngNormalize(lon1)
        lon2 = Math.AngNormalize(lon2)
        return (1 if lon12 > 0 and ( lon1 < 0 <= lon2 or
                                    (lon1 > 0 and lon2 == 0))
                else (-1 if lon12 < 0 and lon2 < 0 <= lon1 else 0))

      @staticmethod
      def _transitdirect(lon1, lon2):
        """Count crossings of prime meridian for AddEdge."""
        # We want to compute exactly
        #   int(floor(lon2 / 360)) - int(floor(lon1 / 360))
        lon1 = Math.remainder(lon1, 720.0); lon2 = Math.remainder(lon2, 720.0)
        return ( (0 if 0 <= lon2 < 360 else 1) -
                 (0 if 0 <= lon1 < 360 else 1) )

      @staticmethod
      def _areareduceA(area, area0, crossings, reverse, sign):
        """Reduce accumulator area to allowed range."""
        area.Remainder(area0)
        if crossings & 1:
          area.Add( (1 if area.Sum() < 0 else -1) * area0/2 )
        # area is with the clockwise sense.  If !reverse convert to
        # counter-clockwise convention.
        if not reverse: area.Negate()
        # If sign put area in (-area0/2, area0/2], else put area in [0, area0)
        if sign:
          if area.Sum() > area0/2:
            area.Add( -area0 )
          elif area.Sum() <= -area0/2:
            area.Add(  area0 )
        else:
          if area.Sum() >= area0:
            area.Add( -area0 )
          elif area.Sum() < 0:
            area.Add(  area0 )

        return 0.0 + area.Sum()

      @staticmethod
      def _areareduceB(area, area0, crossings, reverse, sign):
        """Reduce double area to allowed range."""
        area = Math.remainder(area, area0)
        if crossings & 1:
          area += (1 if area < 0 else -1) * area0/2
        # area is with the clockwise sense.  If !reverse convert to
        # counter-clockwise convention.
        if not reverse: area *= -1
        # If sign put area in (-area0/2, area0/2], else put area in [0, area0)
        if sign:
          if area > area0/2:
            area -= area0
          elif area <= -area0/2:
            area += area0
        else:
          if area >= area0:
            area -= area0
          elif area < 0:
            area += area0

        return 0.0 + area

      def __init__(self, earth, polyline = False):
        """Construct a PolygonArea object

        :param earth: a :class:`~geographiclib.geodesic.Geodesic` object
        :param polyline: if true, treat object as a polyline instead of a polygon

        Initially the polygon has no vertices.
        """

        self.earth = earth
        """The geodesic object (readonly)"""
        self.polyline = polyline
        """Is this a polyline? (readonly)"""
        self.area0 = 4 * math.pi * earth._c2
        """The total area of the ellipsoid in meter^2 (readonly)"""
        self._mask = (Geodesic.LATITUDE | Geodesic.LONGITUDE |
                      Geodesic.DISTANCE |
                      (Geodesic.EMPTY if self.polyline else
                       Geodesic.AREA | Geodesic.LONG_UNROLL))
        if not self.polyline: self._areasum = Accumulator()
        self._perimetersum = Accumulator()
        self.num = 0
        """The current number of points in the polygon (readonly)"""
        self.lat1 = math.nan
        """The current latitude in degrees (readonly)"""
        self.lon1 = math.nan
        """The current longitude in degrees (readonly)"""
        self._crossings = 0
        self._lat0 = self._lon0 = math.nan

      def Clear(self):
        """Reset to empty polygon."""
        self.num = 0
        self._crossings = 0
        if not self.polyline: self._areasum.Set(0)
        self._perimetersum.Set(0)
        self._lat0 = self._lon0 = self.lat1 = self.lon1 = math.nan

      def AddPoint(self, lat, lon):
        """Add the next vertex to the polygon

        :param lat: the latitude of the point in degrees
        :param lon: the longitude of the point in degrees

        This adds an edge from the current vertex to the new vertex.
        """

        if self.num == 0:
          self._lat0 = self.lat1 = lat
          self._lon0 = self.lon1 = lon
        else:
          _, s12, _, _, _, _, _, _, _, S12 = self.earth._GenInverse(
            self.lat1, self.lon1, lat, lon, self._mask)
          self._perimetersum.Add(s12)
          if not self.polyline:
            self._areasum.Add(S12)
            self._crossings += PolygonArea._transit(self.lon1, lon)
          self.lat1 = lat
          self.lon1 = lon
        self.num += 1

      def AddEdge(self, azi, s):
        """Add the next edge to the polygon

        :param azi: the azimuth at the current the point in degrees
        :param s: the length of the edge in meters

        This specifies the new vertex in terms of the edge from the current
        vertex.

        """

        if self.num != 0:
          _, lat, lon, _, _, _, _, _, S12 = self.earth._GenDirect(
            self.lat1, self.lon1, azi, False, s, self._mask)
          self._perimetersum.Add(s)
          if not self.polyline:
            self._areasum.Add(S12)
            self._crossings += PolygonArea._transitdirect(self.lon1, lon)
          self.lat1 = lat
          self.lon1 = lon
          self.num += 1

      # return number, perimeter, area

      def Compute(self, reverse = False, sign = True):
        """Compute the properties of the polygon

        :param reverse: if true then clockwise (instead of
          counter-clockwise) traversal counts as a positive area
        :param sign: if true then return a signed result for the area if the
          polygon is traversed in the "wrong" direction instead of returning
          the area for the rest of the earth
        :return: a tuple of number, perimeter (meters), area (meters^2)

        Arbitrarily complex polygons are allowed.  In the case of
        self-intersecting polygons the area is accumulated "algebraically",
        e.g., the areas of the 2 loops in a figure-8 polygon will partially
        cancel.

        If the object is a polygon (and not a polyline), the perimeter
        includes the length of a final edge connecting the current point to
        the initial point.  If the object is a polyline, then area is nan.

        More points can be added to the polygon after this call.

        """
        area = math.nan
        if self.num < 2:
          perimeter = 0.0
          if not self.polyline: area = 0.0
          return self.num, perimeter, area

        if self.polyline:
          perimeter = self._perimetersum.Sum()
          return self.num, perimeter, area

        _, s12, _, _, _, _, _, _, _, S12 = self.earth._GenInverse(
          self.lat1, self.lon1, self._lat0, self._lon0, self._mask)
        perimeter = self._perimetersum.Sum(s12)
        tempsum = Accumulator(self._areasum)
        tempsum.Add(S12)
        crossings = self._crossings + PolygonArea._transit(self.lon1, self._lon0)
        area = PolygonArea._areareduceA(tempsum, self.area0, crossings,
                                        reverse, sign)
        return self.num, perimeter, area

      # return number, perimeter, area

      def TestPoint(self, lat, lon, reverse = False, sign = True):
        """Compute the properties for a tentative additional vertex

        :param lat: the latitude of the point in degrees
        :param lon: the longitude of the point in degrees
        :param reverse: if true then clockwise (instead of
          counter-clockwise) traversal counts as a positive area
        :param sign: if true then return a signed result for the area if the
          polygon is traversed in the "wrong" direction instead of returning
          the area for the rest of the earth
        :return: a tuple of number, perimeter (meters), area (meters^2)

        """
        area = math.nan
        if self.num == 0:
          perimeter = 0.0
          if not self.polyline: area = 0.0
          return 1, perimeter, area

        perimeter = self._perimetersum.Sum()
        tempsum = 0.0 if self.polyline else self._areasum.Sum()
        crossings = self._crossings; num = self.num + 1
        for i in ([0] if self.polyline else [0, 1]):
          _, s12, _, _, _, _, _, _, _, S12 = self.earth._GenInverse(
            self.lat1 if i == 0 else lat, self.lon1 if i == 0 else lon,
            self._lat0 if i != 0 else lat, self._lon0 if i != 0 else lon,
            self._mask)
          perimeter += s12
          if not self.polyline:
            tempsum += S12
            crossings += PolygonArea._transit(self.lon1 if i == 0 else lon,
                                             self._lon0 if i != 0 else lon)

        if self.polyline:
          return num, perimeter, area

        area = PolygonArea._areareduceB(tempsum, self.area0, crossings,
                                        reverse, sign)
        return num, perimeter, area

      # return num, perimeter, area

      def TestEdge(self, azi, s, reverse = False, sign = True):
        """Compute the properties for a tentative additional edge

        :param azi: the azimuth at the current the point in degrees
        :param s: the length of the edge in meters
        :param reverse: if true then clockwise (instead of
          counter-clockwise) traversal counts as a positive area
        :param sign: if true then return a signed result for the area if the
          polygon is traversed in the "wrong" direction instead of returning
          the area for the rest of the earth
        :return: a tuple of number, perimeter (meters), area (meters^2)

        """

        if self.num == 0:           # we don't have a starting point!
          return 0, math.nan, math.nan
        num = self.num + 1
        perimeter = self._perimetersum.Sum() + s
        if self.polyline:
          return num, perimeter, math.nan

        tempsum =  self._areasum.Sum()
        crossings = self._crossings
        _, lat, lon, _, _, _, _, _, S12 = self.earth._GenDirect(
          self.lat1, self.lon1, azi, False, s, self._mask)
        tempsum += S12
        crossings += PolygonArea._transitdirect(self.lon1, lon)
        _, s12, _, _, _, _, _, _, _, S12 = self.earth._GenInverse(
          lat, lon, self._lat0, self._lon0, self._mask)
        perimeter += s12
        tempsum += S12
        crossings += PolygonArea._transit(lon, self._lon0)

        area = PolygonArea._areareduceB(tempsum, self.area0, crossings,
                                        reverse, sign)
        return num, perimeter, area