././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1650718633.7976391 geographiclib-2.0/0000775000771000077100000000000014230773652014103 5ustar00ckarneyckarney././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650704832.0 geographiclib-2.0/LICENSE0000664000771000077100000000210114230740700015066 0ustar00ckarneyckarneyThe MIT License (MIT). Copyright (c) 2012-2022, Charles Karney Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626272511.0 geographiclib-2.0/MANIFEST.in0000664000771000077100000000004214073571377015642 0ustar00ckarneyckarneyinclude README.md include LICENSE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1650718633.7976391 geographiclib-2.0/PKG-INFO0000664000771000077100000000255114230773652015203 0ustar00ckarneyckarneyMetadata-Version: 2.1 Name: geographiclib Version: 2.0 Summary: The geodesic routines from GeographicLib Home-page: https://geographiclib.sourceforge.io/Python/2.0 Author: Charles Karney Author-email: charles@karney.com License: MIT Keywords: gis geographical earth distance geodesic Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering :: GIS Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: LICENSE # Python implementation of the geodesic routines in GeographicLib This is a library to solve geodesic problems on an ellipsoid model of the earth. Licensed under the MIT/X11 License; see [LICENSE.txt](https://geographiclib.sourceforge.io/LICENSE.txt). The algorithms are documented in * C. F. F. Karney, [Algorithms for geodesics](https://doi.org/10.1007/s00190-012-0578-z), J. Geodesy **87**(1), 43–55 (2013); [Addenda](https://geographiclib.sourceforge.io/geod-addenda.html). The documentation for this package is in https://geographiclib.sourceforge.io/Python/doc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650629777.0 geographiclib-2.0/README.md0000664000771000077100000000110414230516221015341 0ustar00ckarneyckarney# Python implementation of the geodesic routines in GeographicLib This is a library to solve geodesic problems on an ellipsoid model of the earth. Licensed under the MIT/X11 License; see [LICENSE.txt](https://geographiclib.sourceforge.io/LICENSE.txt). The algorithms are documented in * C. F. F. Karney, [Algorithms for geodesics](https://doi.org/10.1007/s00190-012-0578-z), J. Geodesy **87**(1), 43–55 (2013); [Addenda](https://geographiclib.sourceforge.io/geod-addenda.html). The documentation for this package is in https://geographiclib.sourceforge.io/Python/doc ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1650718633.7966392 geographiclib-2.0/geographiclib/0000775000771000077100000000000014230773652016702 5ustar00ckarneyckarney././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650718628.0 geographiclib-2.0/geographiclib/__init__.py0000664000771000077100000000044614230773644021020 0ustar00ckarneyckarney"""geographiclib: geodesic routines from GeographicLib""" __version_info__ = (2, 0, 0) """GeographicLib version as a tuple""" __version__ = "2.0" """GeographicLib version as a string""" __release_date__ = "2022-04-23" """GeographicLib release date""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626272511.0 geographiclib-2.0/geographiclib/accumulator.py0000664000771000077100000000612714073571377021606 0ustar00ckarneyckarney"""accumulator.py: transcription of GeographicLib::Accumulator class.""" # accumulator.py # # This is a rather literal translation of the GeographicLib::Accumulator class # from to python. See the documentation for the C++ class for more information # at # # https://geographiclib.sourceforge.io/html/annotated.html # # Copyright (c) Charles Karney (2011-2019) and # licensed under the MIT/X11 License. For more information, see # https://geographiclib.sourceforge.io/ ###################################################################### from geographiclib.geomath import Math class Accumulator: """Like math.fsum, but allows a running sum""" def Set(self, y): """Set value from argument""" if isinstance(y, Accumulator): self._s, self._t = y._s, y._t else: self._s, self._t = float(y), 0.0 def __init__(self, y = 0.0): """Constructor""" self._s = self._t = 0.0 self.Set(y) def Add(self, y): """Add a value""" # Here's Shewchuk's solution... # hold exact sum as [s, t, u] y, u = Math.sum(y, self._t) # Accumulate starting at self._s, self._t = Math.sum(y, self._s) # least significant end # Start is _s, _t decreasing and non-adjacent. Sum is now (s + t + u) # exactly with s, t, u non-adjacent and in decreasing order (except # for possible zeros). The following code tries to normalize the # result. Ideally, we want _s = round(s+t+u) and _u = round(s+t+u - # _s). The follow does an approximate job (and maintains the # decreasing non-adjacent property). Here are two "failures" using # 3-bit floats: # # Case 1: _s is not equal to round(s+t+u) -- off by 1 ulp # [12, -1] - 8 -> [4, 0, -1] -> [4, -1] = 3 should be [3, 0] = 3 # # Case 2: _s+_t is not as close to s+t+u as it shold be # [64, 5] + 4 -> [64, 8, 1] -> [64, 8] = 72 (off by 1) # should be [80, -7] = 73 (exact) # # "Fixing" these problems is probably not worth the expense. The # representation inevitably leads to small errors in the accumulated # values. The additional errors illustrated here amount to 1 ulp of # the less significant word during each addition to the Accumulator # and an additional possible error of 1 ulp in the reported sum. # # Incidentally, the "ideal" representation described above is not # canonical, because _s = round(_s + _t) may not be true. For # example, with 3-bit floats: # # [128, 16] + 1 -> [160, -16] -- 160 = round(145). # But [160, 0] - 16 -> [128, 16] -- 128 = round(144). # if self._s == 0: # This implies t == 0, self._s = u # so result is u else: self._t += u # otherwise just accumulate u to t. def Sum(self, y = 0.0): """Return sum + y""" if y == 0.0: return self._s b = Accumulator(self) b.Add(y) return b._s def Negate(self): """Negate sum""" self._s *= -1 self._t *= -1 def Remainder(self, y): """Remainder on division by y""" self._s = Math.remainder(self._s, y) self.Add(0.0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626272511.0 geographiclib-2.0/geographiclib/constants.py0000664000771000077100000000141314073571377021274 0ustar00ckarneyckarney"""Define the WGS84 ellipsoid""" # constants.py # # This is a translation of the GeographicLib::Constants class to python. See # the documentation for the C++ class for more information at # # https://geographiclib.sourceforge.io/html/annotated.html # # Copyright (c) Charles Karney (2011-2016) and # licensed under the MIT/X11 License. For more information, see # https://geographiclib.sourceforge.io/ ###################################################################### class Constants: """ Constants describing the WGS84 ellipsoid """ WGS84_a = 6378137.0 # meters """the equatorial radius in meters of the WGS84 ellipsoid in meters""" WGS84_f = 1/298.257223563 """the flattening of the WGS84 ellipsoid, 1/298.257223563""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649456505.0 geographiclib-2.0/geographiclib/geodesic.py0000664000771000077100000014230114224132571021027 0ustar00ckarneyckarney"""Define the :class:`~geographiclib.geodesic.Geodesic` class The ellipsoid parameters are defined by the constructor. The direct and inverse geodesic problems are solved by * :meth:`~geographiclib.geodesic.Geodesic.Inverse` Solve the inverse geodesic problem * :meth:`~geographiclib.geodesic.Geodesic.Direct` Solve the direct geodesic problem * :meth:`~geographiclib.geodesic.Geodesic.ArcDirect` Solve the direct geodesic problem in terms of spherical arc length :class:`~geographiclib.geodesicline.GeodesicLine` objects can be created with * :meth:`~geographiclib.geodesic.Geodesic.Line` * :meth:`~geographiclib.geodesic.Geodesic.DirectLine` * :meth:`~geographiclib.geodesic.Geodesic.ArcDirectLine` * :meth:`~geographiclib.geodesic.Geodesic.InverseLine` :class:`~geographiclib.polygonarea.PolygonArea` objects can be created with * :meth:`~geographiclib.geodesic.Geodesic.Polygon` The public attributes for this class are * :attr:`~geographiclib.geodesic.Geodesic.a` :attr:`~geographiclib.geodesic.Geodesic.f` *outmask* and *caps* bit masks are * :const:`~geographiclib.geodesic.Geodesic.EMPTY` * :const:`~geographiclib.geodesic.Geodesic.LATITUDE` * :const:`~geographiclib.geodesic.Geodesic.LONGITUDE` * :const:`~geographiclib.geodesic.Geodesic.AZIMUTH` * :const:`~geographiclib.geodesic.Geodesic.DISTANCE` * :const:`~geographiclib.geodesic.Geodesic.STANDARD` * :const:`~geographiclib.geodesic.Geodesic.DISTANCE_IN` * :const:`~geographiclib.geodesic.Geodesic.REDUCEDLENGTH` * :const:`~geographiclib.geodesic.Geodesic.GEODESICSCALE` * :const:`~geographiclib.geodesic.Geodesic.AREA` * :const:`~geographiclib.geodesic.Geodesic.ALL` * :const:`~geographiclib.geodesic.Geodesic.LONG_UNROLL` :Example: >>> from geographiclib.geodesic import Geodesic >>> # The geodesic inverse problem ... Geodesic.WGS84.Inverse(-41.32, 174.81, 40.96, -5.50) {'lat1': -41.32, 'a12': 179.6197069334283, 's12': 19959679.26735382, 'lat2': 40.96, 'azi2': 18.825195123248392, 'azi1': 161.06766998615882, 'lon1': 174.81, 'lon2': -5.5} """ # geodesic.py # # This is a rather literal translation of the GeographicLib::Geodesic class to # python. See the documentation for the C++ class for more information at # # https://geographiclib.sourceforge.io/html/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) and licensed # under the MIT/X11 License. For more information, see # https://geographiclib.sourceforge.io/ ###################################################################### import math import sys from geographiclib.geomath import Math from geographiclib.constants import Constants from geographiclib.geodesiccapability import GeodesicCapability class Geodesic: """Solve geodesic problems""" GEOGRAPHICLIB_GEODESIC_ORDER = 6 nA1_ = GEOGRAPHICLIB_GEODESIC_ORDER nC1_ = GEOGRAPHICLIB_GEODESIC_ORDER nC1p_ = GEOGRAPHICLIB_GEODESIC_ORDER nA2_ = GEOGRAPHICLIB_GEODESIC_ORDER nC2_ = GEOGRAPHICLIB_GEODESIC_ORDER nA3_ = GEOGRAPHICLIB_GEODESIC_ORDER nA3x_ = nA3_ nC3_ = GEOGRAPHICLIB_GEODESIC_ORDER nC3x_ = (nC3_ * (nC3_ - 1)) // 2 nC4_ = GEOGRAPHICLIB_GEODESIC_ORDER nC4x_ = (nC4_ * (nC4_ + 1)) // 2 maxit1_ = 20 maxit2_ = maxit1_ + sys.float_info.mant_dig + 10 tiny_ = math.sqrt(sys.float_info.min) tol0_ = sys.float_info.epsilon tol1_ = 200 * tol0_ tol2_ = math.sqrt(tol0_) tolb_ = tol0_ * tol2_ xthresh_ = 1000 * tol2_ CAP_NONE = GeodesicCapability.CAP_NONE CAP_C1 = GeodesicCapability.CAP_C1 CAP_C1p = GeodesicCapability.CAP_C1p CAP_C2 = GeodesicCapability.CAP_C2 CAP_C3 = GeodesicCapability.CAP_C3 CAP_C4 = GeodesicCapability.CAP_C4 CAP_ALL = GeodesicCapability.CAP_ALL CAP_MASK = GeodesicCapability.CAP_MASK OUT_ALL = GeodesicCapability.OUT_ALL OUT_MASK = GeodesicCapability.OUT_MASK @staticmethod def _SinCosSeries(sinp, sinx, cosx, c): """Private: Evaluate a trig series using Clenshaw summation.""" # Evaluate # y = sinp ? sum(c[i] * sin( 2*i * x), i, 1, n) : # sum(c[i] * cos((2*i+1) * x), i, 0, n-1) # using Clenshaw summation. N.B. c[0] is unused for sin series # Approx operation count = (n + 5) mult and (2 * n + 2) add k = len(c) # Point to one beyond last element n = k - sinp ar = 2 * (cosx - sinx) * (cosx + sinx) # 2 * cos(2 * x) y1 = 0 # accumulators for sum if n & 1: k -= 1; y0 = c[k] else: y0 = 0 # Now n is even n = n // 2 while n: # while n--: n -= 1 # Unroll loop x 2, so accumulators return to their original role k -= 1; y1 = ar * y0 - y1 + c[k] k -= 1; y0 = ar * y1 - y0 + c[k] return ( 2 * sinx * cosx * y0 if sinp # sin(2 * x) * y0 else cosx * (y0 - y1) ) # cos(x) * (y0 - y1) @staticmethod def _Astroid(x, y): """Private: solve astroid equation.""" # Solve k^4+2*k^3-(x^2+y^2-1)*k^2-2*y^2*k-y^2 = 0 for positive root k. # This solution is adapted from Geocentric::Reverse. p = Math.sq(x) q = Math.sq(y) r = (p + q - 1) / 6 if not(q == 0 and r <= 0): # Avoid possible division by zero when r = 0 by multiplying equations # for s and t by r^3 and r, resp. S = p * q / 4 # S = r^3 * s r2 = Math.sq(r) r3 = r * r2 # The discriminant of the quadratic equation for T3. This is zero on # the evolute curve p^(1/3)+q^(1/3) = 1 disc = S * (S + 2 * r3) u = r if disc >= 0: T3 = S + r3 # Pick the sign on the sqrt to maximize abs(T3). This minimizes loss # of precision due to cancellation. The result is unchanged because # of the way the T is used in definition of u. T3 += -math.sqrt(disc) if T3 < 0 else math.sqrt(disc) # T3 = (r * t)^3 # N.B. cbrt always returns the real root. cbrt(-8) = -2. T = Math.cbrt(T3) # T = r * t # T can be zero; but then r2 / T -> 0. u += T + (r2 / T if T != 0 else 0) else: # T is complex, but the way u is defined the result is real. ang = math.atan2(math.sqrt(-disc), -(S + r3)) # There are three possible cube roots. We choose the root which # avoids cancellation. Note that disc < 0 implies that r < 0. u += 2 * r * math.cos(ang / 3) v = math.sqrt(Math.sq(u) + q) # guaranteed positive # Avoid loss of accuracy when u < 0. uv = q / (v - u) if u < 0 else u + v # u+v, guaranteed positive w = (uv - q) / (2 * v) # positive? # Rearrange expression for k to avoid loss of accuracy due to # subtraction. Division by 0 not possible because uv > 0, w >= 0. k = uv / (math.sqrt(uv + Math.sq(w)) + w) # guaranteed positive else: # q == 0 && r <= 0 # y = 0 with |x| <= 1. Handle this case directly. # for y small, positive root is k = abs(y)/sqrt(1-x^2) k = 0 return k @staticmethod def _A1m1f(eps): """Private: return A1-1.""" coeff = [ 1, 4, 64, 0, 256, ] m = Geodesic.nA1_//2 t = Math.polyval(m, coeff, 0, Math.sq(eps)) / coeff[m + 1] return (t + eps) / (1 - eps) @staticmethod def _C1f(eps, c): """Private: return C1.""" coeff = [ -1, 6, -16, 32, -9, 64, -128, 2048, 9, -16, 768, 3, -5, 512, -7, 1280, -7, 2048, ] eps2 = Math.sq(eps) d = eps o = 0 for l in range(1, Geodesic.nC1_ + 1): # l is index of C1p[l] m = (Geodesic.nC1_ - l) // 2 # order of polynomial in eps^2 c[l] = d * Math.polyval(m, coeff, o, eps2) / coeff[o + m + 1] o += m + 2 d *= eps @staticmethod def _C1pf(eps, c): """Private: return C1'""" coeff = [ 205, -432, 768, 1536, 4005, -4736, 3840, 12288, -225, 116, 384, -7173, 2695, 7680, 3467, 7680, 38081, 61440, ] eps2 = Math.sq(eps) d = eps o = 0 for l in range(1, Geodesic.nC1p_ + 1): # l is index of C1p[l] m = (Geodesic.nC1p_ - l) // 2 # order of polynomial in eps^2 c[l] = d * Math.polyval(m, coeff, o, eps2) / coeff[o + m + 1] o += m + 2 d *= eps @staticmethod def _A2m1f(eps): """Private: return A2-1""" coeff = [ -11, -28, -192, 0, 256, ] m = Geodesic.nA2_//2 t = Math.polyval(m, coeff, 0, Math.sq(eps)) / coeff[m + 1] return (t - eps) / (1 + eps) @staticmethod def _C2f(eps, c): """Private: return C2""" coeff = [ 1, 2, 16, 32, 35, 64, 384, 2048, 15, 80, 768, 7, 35, 512, 63, 1280, 77, 2048, ] eps2 = Math.sq(eps) d = eps o = 0 for l in range(1, Geodesic.nC2_ + 1): # l is index of C2[l] m = (Geodesic.nC2_ - l) // 2 # order of polynomial in eps^2 c[l] = d * Math.polyval(m, coeff, o, eps2) / coeff[o + m + 1] o += m + 2 d *= eps def __init__(self, a, f): """Construct a Geodesic object :param a: the equatorial radius of the ellipsoid in meters :param f: the flattening of the ellipsoid An exception is thrown if *a* or the polar semi-axis *b* = *a* (1 - *f*) is not a finite positive quantity. """ self.a = float(a) """The equatorial radius in meters (readonly)""" self.f = float(f) """The flattening (readonly)""" self._f1 = 1 - self.f self._e2 = self.f * (2 - self.f) self._ep2 = self._e2 / Math.sq(self._f1) # e2 / (1 - e2) self._n = self.f / ( 2 - self.f) self._b = self.a * self._f1 # authalic radius squared self._c2 = (Math.sq(self.a) + Math.sq(self._b) * (1 if self._e2 == 0 else (math.atanh(math.sqrt(self._e2)) if self._e2 > 0 else math.atan(math.sqrt(-self._e2))) / math.sqrt(abs(self._e2))))/2 # The sig12 threshold for "really short". Using the auxiliary sphere # solution with dnm computed at (bet1 + bet2) / 2, the relative error in # the azimuth consistency check is sig12^2 * abs(f) * min(1, 1-f/2) / 2. # (Error measured for 1/100 < b/a < 100 and abs(f) >= 1/1000. For a given # f and sig12, the max error occurs for lines near the pole. If the old # rule for computing dnm = (dn1 + dn2)/2 is used, then the error increases # by a factor of 2.) Setting this equal to epsilon gives sig12 = etol2. # Here 0.1 is a safety factor (error decreased by 100) and max(0.001, # abs(f)) stops etol2 getting too large in the nearly spherical case. self._etol2 = 0.1 * Geodesic.tol2_ / math.sqrt( max(0.001, abs(self.f)) * min(1.0, 1-self.f/2) / 2 ) if not(math.isfinite(self.a) and self.a > 0): raise ValueError("Equatorial radius is not positive") if not(math.isfinite(self._b) and self._b > 0): raise ValueError("Polar semi-axis is not positive") self._A3x = list(range(Geodesic.nA3x_)) self._C3x = list(range(Geodesic.nC3x_)) self._C4x = list(range(Geodesic.nC4x_)) self._A3coeff() self._C3coeff() self._C4coeff() def _A3coeff(self): """Private: return coefficients for A3""" coeff = [ -3, 128, -2, -3, 64, -1, -3, -1, 16, 3, -1, -2, 8, 1, -1, 2, 1, 1, ] o = 0; k = 0 for j in range(Geodesic.nA3_ - 1, -1, -1): # coeff of eps^j m = min(Geodesic.nA3_ - j - 1, j) # order of polynomial in n self._A3x[k] = Math.polyval(m, coeff, o, self._n) / coeff[o + m + 1] k += 1 o += m + 2 def _C3coeff(self): """Private: return coefficients for C3""" coeff = [ 3, 128, 2, 5, 128, -1, 3, 3, 64, -1, 0, 1, 8, -1, 1, 4, 5, 256, 1, 3, 128, -3, -2, 3, 64, 1, -3, 2, 32, 7, 512, -10, 9, 384, 5, -9, 5, 192, 7, 512, -14, 7, 512, 21, 2560, ] o = 0; k = 0 for l in range(1, Geodesic.nC3_): # l is index of C3[l] for j in range(Geodesic.nC3_ - 1, l - 1, -1): # coeff of eps^j m = min(Geodesic.nC3_ - j - 1, j) # order of polynomial in n self._C3x[k] = Math.polyval(m, coeff, o, self._n) / coeff[o + m + 1] k += 1 o += m + 2 def _C4coeff(self): """Private: return coefficients for C4""" coeff = [ 97, 15015, 1088, 156, 45045, -224, -4784, 1573, 45045, -10656, 14144, -4576, -858, 45045, 64, 624, -4576, 6864, -3003, 15015, 100, 208, 572, 3432, -12012, 30030, 45045, 1, 9009, -2944, 468, 135135, 5792, 1040, -1287, 135135, 5952, -11648, 9152, -2574, 135135, -64, -624, 4576, -6864, 3003, 135135, 8, 10725, 1856, -936, 225225, -8448, 4992, -1144, 225225, -1440, 4160, -4576, 1716, 225225, -136, 63063, 1024, -208, 105105, 3584, -3328, 1144, 315315, -128, 135135, -2560, 832, 405405, 128, 99099, ] o = 0; k = 0 for l in range(Geodesic.nC4_): # l is index of C4[l] for j in range(Geodesic.nC4_ - 1, l - 1, -1): # coeff of eps^j m = Geodesic.nC4_ - j - 1 # order of polynomial in n self._C4x[k] = Math.polyval(m, coeff, o, self._n) / coeff[o + m + 1] k += 1 o += m + 2 def _A3f(self, eps): """Private: return A3""" # Evaluate A3 return Math.polyval(Geodesic.nA3_ - 1, self._A3x, 0, eps) def _C3f(self, eps, c): """Private: return C3""" # Evaluate C3 # Elements c[1] thru c[nC3_ - 1] are set mult = 1 o = 0 for l in range(1, Geodesic.nC3_): # l is index of C3[l] m = Geodesic.nC3_ - l - 1 # order of polynomial in eps mult *= eps c[l] = mult * Math.polyval(m, self._C3x, o, eps) o += m + 1 def _C4f(self, eps, c): """Private: return C4""" # Evaluate C4 coeffs by Horner's method # Elements c[0] thru c[nC4_ - 1] are set mult = 1 o = 0 for l in range(Geodesic.nC4_): # l is index of C4[l] m = Geodesic.nC4_ - l - 1 # order of polynomial in eps c[l] = mult * Math.polyval(m, self._C4x, o, eps) o += m + 1 mult *= eps # return s12b, m12b, m0, M12, M21 def _Lengths(self, eps, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, cbet1, cbet2, outmask, # Scratch areas of the right size C1a, C2a): """Private: return a bunch of lengths""" # Return s12b, m12b, m0, M12, M21, where # m12b = (reduced length)/_b; s12b = distance/_b, # m0 = coefficient of secular term in expression for reduced length. outmask &= Geodesic.OUT_MASK # outmask & DISTANCE: set s12b # outmask & REDUCEDLENGTH: set m12b & m0 # outmask & GEODESICSCALE: set M12 & M21 s12b = m12b = m0 = M12 = M21 = math.nan if outmask & (Geodesic.DISTANCE | Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): A1 = Geodesic._A1m1f(eps) Geodesic._C1f(eps, C1a) if outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): A2 = Geodesic._A2m1f(eps) Geodesic._C2f(eps, C2a) m0x = A1 - A2 A2 = 1 + A2 A1 = 1 + A1 if outmask & Geodesic.DISTANCE: B1 = (Geodesic._SinCosSeries(True, ssig2, csig2, C1a) - Geodesic._SinCosSeries(True, ssig1, csig1, C1a)) # Missing a factor of _b s12b = A1 * (sig12 + B1) if outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): B2 = (Geodesic._SinCosSeries(True, ssig2, csig2, C2a) - Geodesic._SinCosSeries(True, ssig1, csig1, C2a)) J12 = m0x * sig12 + (A1 * B1 - A2 * B2) elif outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): # Assume here that nC1_ >= nC2_ for l in range(1, Geodesic.nC2_): C2a[l] = A1 * C1a[l] - A2 * C2a[l] J12 = m0x * sig12 + (Geodesic._SinCosSeries(True, ssig2, csig2, C2a) - Geodesic._SinCosSeries(True, ssig1, csig1, C2a)) if outmask & Geodesic.REDUCEDLENGTH: m0 = m0x # Missing a factor of _b. # Add parens around (csig1 * ssig2) and (ssig1 * csig2) to ensure # accurate cancellation in the case of coincident points. m12b = (dn2 * (csig1 * ssig2) - dn1 * (ssig1 * csig2) - csig1 * csig2 * J12) if outmask & Geodesic.GEODESICSCALE: csig12 = csig1 * csig2 + ssig1 * ssig2 t = self._ep2 * (cbet1 - cbet2) * (cbet1 + cbet2) / (dn1 + dn2) M12 = csig12 + (t * ssig2 - csig2 * J12) * ssig1 / dn1 M21 = csig12 - (t * ssig1 - csig1 * J12) * ssig2 / dn2 return s12b, m12b, m0, M12, M21 # return sig12, salp1, calp1, salp2, calp2, dnm def _InverseStart(self, sbet1, cbet1, dn1, sbet2, cbet2, dn2, lam12, slam12, clam12, # Scratch areas of the right size C1a, C2a): """Private: Find a starting value for Newton's method.""" # Return a starting point for Newton's method in salp1 and calp1 (function # value is -1). If Newton's method doesn't need to be used, return also # salp2 and calp2 and function value is sig12. sig12 = -1; salp2 = calp2 = dnm = math.nan # Return values # bet12 = bet2 - bet1 in [0, pi); bet12a = bet2 + bet1 in (-pi, 0] sbet12 = sbet2 * cbet1 - cbet2 * sbet1 cbet12 = cbet2 * cbet1 + sbet2 * sbet1 # Volatile declaration needed to fix inverse cases # 88.202499451857 0 -88.202499451857 179.981022032992859592 # 89.262080389218 0 -89.262080389218 179.992207982775375662 # 89.333123580033 0 -89.333123580032997687 179.99295812360148422 # which otherwise fail with g++ 4.4.4 x86 -O3 sbet12a = sbet2 * cbet1 sbet12a += cbet2 * sbet1 shortline = cbet12 >= 0 and sbet12 < 0.5 and cbet2 * lam12 < 0.5 if shortline: sbetm2 = Math.sq(sbet1 + sbet2) # sin((bet1+bet2)/2)^2 # = (sbet1 + sbet2)^2 / ((sbet1 + sbet2)^2 + (cbet1 + cbet2)^2) sbetm2 /= sbetm2 + Math.sq(cbet1 + cbet2) dnm = math.sqrt(1 + self._ep2 * sbetm2) omg12 = lam12 / (self._f1 * dnm) somg12 = math.sin(omg12); comg12 = math.cos(omg12) else: somg12 = slam12; comg12 = clam12 salp1 = cbet2 * somg12 calp1 = ( sbet12 + cbet2 * sbet1 * Math.sq(somg12) / (1 + comg12) if comg12 >= 0 else sbet12a - cbet2 * sbet1 * Math.sq(somg12) / (1 - comg12)) ssig12 = math.hypot(salp1, calp1) csig12 = sbet1 * sbet2 + cbet1 * cbet2 * comg12 if shortline and ssig12 < self._etol2: # really short lines salp2 = cbet1 * somg12 calp2 = sbet12 - cbet1 * sbet2 * (Math.sq(somg12) / (1 + comg12) if comg12 >= 0 else 1 - comg12) salp2, calp2 = Math.norm(salp2, calp2) # Set return value sig12 = math.atan2(ssig12, csig12) elif (abs(self._n) >= 0.1 or # Skip astroid calc if too eccentric csig12 >= 0 or ssig12 >= 6 * abs(self._n) * math.pi * Math.sq(cbet1)): # Nothing to do, zeroth order spherical approximation is OK pass else: # Scale lam12 and bet2 to x, y coordinate system where antipodal point # is at origin and singular point is at y = 0, x = -1. # real y, lamscale, betscale lam12x = math.atan2(-slam12, -clam12) if self.f >= 0: # In fact f == 0 does not get here # x = dlong, y = dlat k2 = Math.sq(sbet1) * self._ep2 eps = k2 / (2 * (1 + math.sqrt(1 + k2)) + k2) lamscale = self.f * cbet1 * self._A3f(eps) * math.pi betscale = lamscale * cbet1 x = lam12x / lamscale y = sbet12a / betscale else: # _f < 0 # x = dlat, y = dlong cbet12a = cbet2 * cbet1 - sbet2 * sbet1 bet12a = math.atan2(sbet12a, cbet12a) # real m12b, m0, dummy # In the case of lon12 = 180, this repeats a calculation made in # Inverse. dummy, m12b, m0, dummy, dummy = self._Lengths( self._n, math.pi + bet12a, sbet1, -cbet1, dn1, sbet2, cbet2, dn2, cbet1, cbet2, Geodesic.REDUCEDLENGTH, C1a, C2a) x = -1 + m12b / (cbet1 * cbet2 * m0 * math.pi) betscale = (sbet12a / x if x < -0.01 else -self.f * Math.sq(cbet1) * math.pi) lamscale = betscale / cbet1 y = lam12x / lamscale if y > -Geodesic.tol1_ and x > -1 - Geodesic.xthresh_: # strip near cut if self.f >= 0: salp1 = min(1.0, -x); calp1 = - math.sqrt(1 - Math.sq(salp1)) else: calp1 = max((0.0 if x > -Geodesic.tol1_ else -1.0), x) salp1 = math.sqrt(1 - Math.sq(calp1)) else: # Estimate alp1, by solving the astroid problem. # # Could estimate alpha1 = theta + pi/2, directly, i.e., # calp1 = y/k; salp1 = -x/(1+k); for _f >= 0 # calp1 = x/(1+k); salp1 = -y/k; for _f < 0 (need to check) # # However, it's better to estimate omg12 from astroid and use # spherical formula to compute alp1. This reduces the mean number of # Newton iterations for astroid cases from 2.24 (min 0, max 6) to 2.12 # (min 0 max 5). The changes in the number of iterations are as # follows: # # change percent # 1 5 # 0 78 # -1 16 # -2 0.6 # -3 0.04 # -4 0.002 # # The histogram of iterations is (m = number of iterations estimating # alp1 directly, n = number of iterations estimating via omg12, total # number of trials = 148605): # # iter m n # 0 148 186 # 1 13046 13845 # 2 93315 102225 # 3 36189 32341 # 4 5396 7 # 5 455 1 # 6 56 0 # # Because omg12 is near pi, estimate work with omg12a = pi - omg12 k = Geodesic._Astroid(x, y) omg12a = lamscale * ( -x * k/(1 + k) if self.f >= 0 else -y * (1 + k)/k ) somg12 = math.sin(omg12a); comg12 = -math.cos(omg12a) # Update spherical estimate of alp1 using omg12 instead of lam12 salp1 = cbet2 * somg12 calp1 = sbet12a - cbet2 * sbet1 * Math.sq(somg12) / (1 - comg12) # Sanity check on starting guess. Backwards check allows NaN through. if not (salp1 <= 0): salp1, calp1 = Math.norm(salp1, calp1) else: salp1 = 1; calp1 = 0 return sig12, salp1, calp1, salp2, calp2, dnm # return lam12, salp2, calp2, sig12, ssig1, csig1, ssig2, csig2, eps, # domg12, dlam12 def _Lambda12(self, sbet1, cbet1, dn1, sbet2, cbet2, dn2, salp1, calp1, slam120, clam120, diffp, # Scratch areas of the right size C1a, C2a, C3a): """Private: Solve hybrid problem""" if sbet1 == 0 and calp1 == 0: # Break degeneracy of equatorial line. This case has already been # handled. calp1 = -Geodesic.tiny_ # sin(alp1) * cos(bet1) = sin(alp0) salp0 = salp1 * cbet1 calp0 = math.hypot(calp1, salp1 * sbet1) # calp0 > 0 # real somg1, comg1, somg2, comg2, lam12 # tan(bet1) = tan(sig1) * cos(alp1) # tan(omg1) = sin(alp0) * tan(sig1) = tan(omg1)=tan(alp1)*sin(bet1) ssig1 = sbet1; somg1 = salp0 * sbet1 csig1 = comg1 = calp1 * cbet1 ssig1, csig1 = Math.norm(ssig1, csig1) # Math.norm(somg1, comg1); -- don't need to normalize! # Enforce symmetries in the case abs(bet2) = -bet1. Need to be careful # about this case, since this can yield singularities in the Newton # iteration. # sin(alp2) * cos(bet2) = sin(alp0) salp2 = salp0 / cbet2 if cbet2 != cbet1 else salp1 # calp2 = sqrt(1 - sq(salp2)) # = sqrt(sq(calp0) - sq(sbet2)) / cbet2 # and subst for calp0 and rearrange to give (choose positive sqrt # to give alp2 in [0, pi/2]). calp2 = (math.sqrt(Math.sq(calp1 * cbet1) + ((cbet2 - cbet1) * (cbet1 + cbet2) if cbet1 < -sbet1 else (sbet1 - sbet2) * (sbet1 + sbet2))) / cbet2 if cbet2 != cbet1 or abs(sbet2) != -sbet1 else abs(calp1)) # tan(bet2) = tan(sig2) * cos(alp2) # tan(omg2) = sin(alp0) * tan(sig2). ssig2 = sbet2; somg2 = salp0 * sbet2 csig2 = comg2 = calp2 * cbet2 ssig2, csig2 = Math.norm(ssig2, csig2) # Math.norm(somg2, comg2); -- don't need to normalize! # sig12 = sig2 - sig1, limit to [0, pi] sig12 = math.atan2(max(0.0, csig1 * ssig2 - ssig1 * csig2) + 0.0, csig1 * csig2 + ssig1 * ssig2) # omg12 = omg2 - omg1, limit to [0, pi] somg12 = max(0.0, comg1 * somg2 - somg1 * comg2) + 0.0 comg12 = comg1 * comg2 + somg1 * somg2 # eta = omg12 - lam120 eta = math.atan2(somg12 * clam120 - comg12 * slam120, comg12 * clam120 + somg12 * slam120) # real B312 k2 = Math.sq(calp0) * self._ep2 eps = k2 / (2 * (1 + math.sqrt(1 + k2)) + k2) self._C3f(eps, C3a) B312 = (Geodesic._SinCosSeries(True, ssig2, csig2, C3a) - Geodesic._SinCosSeries(True, ssig1, csig1, C3a)) domg12 = -self.f * self._A3f(eps) * salp0 * (sig12 + B312) lam12 = eta + domg12 if diffp: if calp2 == 0: dlam12 = - 2 * self._f1 * dn1 / sbet1 else: dummy, dlam12, dummy, dummy, dummy = self._Lengths( eps, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, cbet1, cbet2, Geodesic.REDUCEDLENGTH, C1a, C2a) dlam12 *= self._f1 / (calp2 * cbet2) else: dlam12 = math.nan return (lam12, salp2, calp2, sig12, ssig1, csig1, ssig2, csig2, eps, domg12, dlam12) # return a12, s12, salp1, calp1, salp2, calp2, m12, M12, M21, S12 def _GenInverse(self, lat1, lon1, lat2, lon2, outmask): """Private: General version of the inverse problem""" a12 = s12 = m12 = M12 = M21 = S12 = math.nan # return vals outmask &= Geodesic.OUT_MASK # Compute longitude difference (AngDiff does this carefully). Result is # in [-180, 180] but -180 is only for west-going geodesics. 180 is for # east-going and meridional geodesics. lon12, lon12s = Math.AngDiff(lon1, lon2) # Make longitude difference positive. lonsign = math.copysign(1, lon12) lon12 = lonsign * lon12; lon12s = lonsign * lon12s lam12 = math.radians(lon12) # Calculate sincos of lon12 + error (this applies AngRound internally). slam12, clam12 = Math.sincosde(lon12, lon12s) lon12s = (180 - lon12) - lon12s # the supplementary longitude difference # If really close to the equator, treat as on equator. lat1 = Math.AngRound(Math.LatFix(lat1)) lat2 = Math.AngRound(Math.LatFix(lat2)) # Swap points so that point with higher (abs) latitude is point 1 # If one latitude is a nan, then it becomes lat1. swapp = -1 if abs(lat1) < abs(lat2) or math.isnan(lat2) else 1 if swapp < 0: lonsign *= -1 lat2, lat1 = lat1, lat2 # Make lat1 <= 0 latsign = math.copysign(1, -lat1) lat1 *= latsign lat2 *= latsign # Now we have # # 0 <= lon12 <= 180 # -90 <= lat1 <= 0 # lat1 <= lat2 <= -lat1 # # longsign, swapp, latsign register the transformation to bring the # coordinates to this canonical form. In all cases, 1 means no change was # made. We make these transformations so that there are few cases to # check, e.g., on verifying quadrants in atan2. In addition, this # enforces some symmetries in the results returned. # real phi, sbet1, cbet1, sbet2, cbet2, s12x, m12x sbet1, cbet1 = Math.sincosd(lat1); sbet1 *= self._f1 # Ensure cbet1 = +epsilon at poles sbet1, cbet1 = Math.norm(sbet1, cbet1); cbet1 = max(Geodesic.tiny_, cbet1) sbet2, cbet2 = Math.sincosd(lat2); sbet2 *= self._f1 # Ensure cbet2 = +epsilon at poles sbet2, cbet2 = Math.norm(sbet2, cbet2); cbet2 = max(Geodesic.tiny_, cbet2) # If cbet1 < -sbet1, then cbet2 - cbet1 is a sensitive measure of the # |bet1| - |bet2|. Alternatively (cbet1 >= -sbet1), abs(sbet2) + sbet1 is # a better measure. This logic is used in assigning calp2 in Lambda12. # Sometimes these quantities vanish and in that case we force bet2 = +/- # bet1 exactly. An example where is is necessary is the inverse problem # 48.522876735459 0 -48.52287673545898293 179.599720456223079643 # which failed with Visual Studio 10 (Release and Debug) if cbet1 < -sbet1: if cbet2 == cbet1: sbet2 = math.copysign(sbet1, sbet2) else: if abs(sbet2) == -sbet1: cbet2 = cbet1 dn1 = math.sqrt(1 + self._ep2 * Math.sq(sbet1)) dn2 = math.sqrt(1 + self._ep2 * Math.sq(sbet2)) # real a12, sig12, calp1, salp1, calp2, salp2 # index zero elements of these arrays are unused C1a = list(range(Geodesic.nC1_ + 1)) C2a = list(range(Geodesic.nC2_ + 1)) C3a = list(range(Geodesic.nC3_)) meridian = lat1 == -90 or slam12 == 0 if meridian: # Endpoints are on a single full meridian, so the geodesic might lie on # a meridian. calp1 = clam12; salp1 = slam12 # Head to the target longitude calp2 = 1.0; salp2 = 0.0 # At the target we're heading north # tan(bet) = tan(sig) * cos(alp) ssig1 = sbet1; csig1 = calp1 * cbet1 ssig2 = sbet2; csig2 = calp2 * cbet2 # sig12 = sig2 - sig1 sig12 = math.atan2(max(0.0, csig1 * ssig2 - ssig1 * csig2) + 0.0, csig1 * csig2 + ssig1 * ssig2) s12x, m12x, dummy, M12, M21 = self._Lengths( self._n, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, cbet1, cbet2, outmask | Geodesic.DISTANCE | Geodesic.REDUCEDLENGTH, C1a, C2a) # Add the check for sig12 since zero length geodesics might yield m12 < # 0. Test case was # # echo 20.001 0 20.001 0 | GeodSolve -i # # In fact, we will have sig12 > pi/2 for meridional geodesic which is # not a shortest path. if sig12 < 1 or m12x >= 0: if (sig12 < 3 * Geodesic.tiny_ or # Prevent negative s12 or m12 for short lines (sig12 < Geodesic.tol0_ and (s12x < 0 or m12x < 0))): sig12 = m12x = s12x = 0.0 m12x *= self._b s12x *= self._b a12 = math.degrees(sig12) else: # m12 < 0, i.e., prolate and too close to anti-podal meridian = False # end if meridian: # somg12 == 2 marks that it needs to be calculated somg12 = 2.0; comg12 = 0.0; omg12 = 0.0 if (not meridian and sbet1 == 0 and # and sbet2 == 0 # Mimic the way Lambda12 works with calp1 = 0 (self.f <= 0 or lon12s >= self.f * 180)): # Geodesic runs along equator calp1 = calp2 = 0.0; salp1 = salp2 = 1.0 s12x = self.a * lam12 sig12 = omg12 = lam12 / self._f1 m12x = self._b * math.sin(sig12) if outmask & Geodesic.GEODESICSCALE: M12 = M21 = math.cos(sig12) a12 = lon12 / self._f1 elif not meridian: # Now point1 and point2 belong within a hemisphere bounded by a # meridian and geodesic is neither meridional or equatorial. # Figure a starting point for Newton's method sig12, salp1, calp1, salp2, calp2, dnm = self._InverseStart( sbet1, cbet1, dn1, sbet2, cbet2, dn2, lam12, slam12, clam12, C1a, C2a) if sig12 >= 0: # Short lines (InverseStart sets salp2, calp2, dnm) s12x = sig12 * self._b * dnm m12x = (Math.sq(dnm) * self._b * math.sin(sig12 / dnm)) if outmask & Geodesic.GEODESICSCALE: M12 = M21 = math.cos(sig12 / dnm) a12 = math.degrees(sig12) omg12 = lam12 / (self._f1 * dnm) else: # Newton's method. This is a straightforward solution of f(alp1) = # lambda12(alp1) - lam12 = 0 with one wrinkle. f(alp) has exactly one # root in the interval (0, pi) and its derivative is positive at the # root. Thus f(alp) is positive for alp > alp1 and negative for alp < # alp1. During the course of the iteration, a range (alp1a, alp1b) is # maintained which brackets the root and with each evaluation of f(alp) # the range is shrunk if possible. Newton's method is restarted # whenever the derivative of f is negative (because the new value of # alp1 is then further from the solution) or if the new estimate of # alp1 lies outside (0,pi); in this case, the new starting guess is # taken to be (alp1a + alp1b) / 2. # real ssig1, csig1, ssig2, csig2, eps numit = 0 tripn = tripb = False # Bracketing range salp1a = Geodesic.tiny_; calp1a = 1.0 salp1b = Geodesic.tiny_; calp1b = -1.0 while numit < Geodesic.maxit2_: # the WGS84 test set: mean = 1.47, sd = 1.25, max = 16 # WGS84 and random input: mean = 2.85, sd = 0.60 (v, salp2, calp2, sig12, ssig1, csig1, ssig2, csig2, eps, domg12, dv) = self._Lambda12( sbet1, cbet1, dn1, sbet2, cbet2, dn2, salp1, calp1, slam12, clam12, numit < Geodesic.maxit1_, C1a, C2a, C3a) # Reversed test to allow escape with NaNs if tripb or not (abs(v) >= (8 if tripn else 1) * Geodesic.tol0_): break # Update bracketing values if v > 0 and (numit > Geodesic.maxit1_ or calp1/salp1 > calp1b/salp1b): salp1b = salp1; calp1b = calp1 elif v < 0 and (numit > Geodesic.maxit1_ or calp1/salp1 < calp1a/salp1a): salp1a = salp1; calp1a = calp1 numit += 1 if numit < Geodesic.maxit1_ and dv > 0: dalp1 = -v/dv sdalp1 = math.sin(dalp1); cdalp1 = math.cos(dalp1) nsalp1 = salp1 * cdalp1 + calp1 * sdalp1 if nsalp1 > 0 and abs(dalp1) < math.pi: calp1 = calp1 * cdalp1 - salp1 * sdalp1 salp1 = nsalp1 salp1, calp1 = Math.norm(salp1, calp1) # In some regimes we don't get quadratic convergence because # slope -> 0. So use convergence conditions based on epsilon # instead of sqrt(epsilon). tripn = abs(v) <= 16 * Geodesic.tol0_ continue # Either dv was not positive or updated value was outside # legal range. Use the midpoint of the bracket as the next # estimate. This mechanism is not needed for the WGS84 # ellipsoid, but it does catch problems with more eccentric # ellipsoids. Its efficacy is such for # the WGS84 test set with the starting guess set to alp1 = 90deg: # the WGS84 test set: mean = 5.21, sd = 3.93, max = 24 # WGS84 and random input: mean = 4.74, sd = 0.99 salp1 = (salp1a + salp1b)/2 calp1 = (calp1a + calp1b)/2 salp1, calp1 = Math.norm(salp1, calp1) tripn = False tripb = (abs(salp1a - salp1) + (calp1a - calp1) < Geodesic.tolb_ or abs(salp1 - salp1b) + (calp1 - calp1b) < Geodesic.tolb_) lengthmask = (outmask | (Geodesic.DISTANCE if (outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE)) else Geodesic.EMPTY)) s12x, m12x, dummy, M12, M21 = self._Lengths( eps, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, cbet1, cbet2, lengthmask, C1a, C2a) m12x *= self._b s12x *= self._b a12 = math.degrees(sig12) if outmask & Geodesic.AREA: # omg12 = lam12 - domg12 sdomg12 = math.sin(domg12); cdomg12 = math.cos(domg12) somg12 = slam12 * cdomg12 - clam12 * sdomg12 comg12 = clam12 * cdomg12 + slam12 * sdomg12 # end elif not meridian if outmask & Geodesic.DISTANCE: s12 = 0.0 + s12x # Convert -0 to 0 if outmask & Geodesic.REDUCEDLENGTH: m12 = 0.0 + m12x # Convert -0 to 0 if outmask & Geodesic.AREA: # From Lambda12: sin(alp1) * cos(bet1) = sin(alp0) salp0 = salp1 * cbet1 calp0 = math.hypot(calp1, salp1 * sbet1) # calp0 > 0 # real alp12 if calp0 != 0 and salp0 != 0: # From Lambda12: tan(bet) = tan(sig) * cos(alp) ssig1 = sbet1; csig1 = calp1 * cbet1 ssig2 = sbet2; csig2 = calp2 * cbet2 k2 = Math.sq(calp0) * self._ep2 eps = k2 / (2 * (1 + math.sqrt(1 + k2)) + k2) # Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0). A4 = Math.sq(self.a) * calp0 * salp0 * self._e2 ssig1, csig1 = Math.norm(ssig1, csig1) ssig2, csig2 = Math.norm(ssig2, csig2) C4a = list(range(Geodesic.nC4_)) self._C4f(eps, C4a) B41 = Geodesic._SinCosSeries(False, ssig1, csig1, C4a) B42 = Geodesic._SinCosSeries(False, ssig2, csig2, C4a) S12 = A4 * (B42 - B41) else: # Avoid problems with indeterminate sig1, sig2 on equator S12 = 0.0 if not meridian and somg12 == 2.0: somg12 = math.sin(omg12); comg12 = math.cos(omg12) if (not meridian and # omg12 < 3/4 * pi comg12 > -0.7071 and # Long difference not too big sbet2 - sbet1 < 1.75): # Lat difference not too big # Use tan(Gamma/2) = tan(omg12/2) # * (tan(bet1/2)+tan(bet2/2))/(1+tan(bet1/2)*tan(bet2/2)) # with tan(x/2) = sin(x)/(1+cos(x)) domg12 = 1 + comg12; dbet1 = 1 + cbet1; dbet2 = 1 + cbet2 alp12 = 2 * math.atan2( somg12 * ( sbet1 * dbet2 + sbet2 * dbet1 ), domg12 * ( sbet1 * sbet2 + dbet1 * dbet2 ) ) else: # alp12 = alp2 - alp1, used in atan2 so no need to normalize salp12 = salp2 * calp1 - calp2 * salp1 calp12 = calp2 * calp1 + salp2 * salp1 # The right thing appears to happen if alp1 = +/-180 and alp2 = 0, viz # salp12 = -0 and alp12 = -180. However this depends on the sign # being attached to 0 correctly. The following ensures the correct # behavior. if salp12 == 0 and calp12 < 0: salp12 = Geodesic.tiny_ * calp1 calp12 = -1.0 alp12 = math.atan2(salp12, calp12) S12 += self._c2 * alp12 S12 *= swapp * lonsign * latsign # Convert -0 to 0 S12 += 0.0 # Convert calp, salp to azimuth accounting for lonsign, swapp, latsign. if swapp < 0: salp2, salp1 = salp1, salp2 calp2, calp1 = calp1, calp2 if outmask & Geodesic.GEODESICSCALE: M21, M12 = M12, M21 salp1 *= swapp * lonsign; calp1 *= swapp * latsign salp2 *= swapp * lonsign; calp2 *= swapp * latsign return a12, s12, salp1, calp1, salp2, calp2, m12, M12, M21, S12 def Inverse(self, lat1, lon1, lat2, lon2, outmask = GeodesicCapability.STANDARD): """Solve the inverse geodesic problem :param lat1: latitude of the first point in degrees :param lon1: longitude of the first point in degrees :param lat2: latitude of the second point in degrees :param lon2: longitude of the second point in degrees :param outmask: the :ref:`output mask ` :return: a :ref:`dict` Compute geodesic between (*lat1*, *lon1*) and (*lat2*, *lon2*). The default value of *outmask* is STANDARD, i.e., the *lat1*, *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are returned. """ a12, s12, salp1,calp1, salp2,calp2, m12, M12, M21, S12 = self._GenInverse( lat1, lon1, lat2, lon2, outmask) outmask &= Geodesic.OUT_MASK if outmask & Geodesic.LONG_UNROLL: lon12, e = Math.AngDiff(lon1, lon2) lon2 = (lon1 + lon12) + e else: lon2 = Math.AngNormalize(lon2) result = {'lat1': Math.LatFix(lat1), 'lon1': lon1 if outmask & Geodesic.LONG_UNROLL else Math.AngNormalize(lon1), 'lat2': Math.LatFix(lat2), 'lon2': lon2} result['a12'] = a12 if outmask & Geodesic.DISTANCE: result['s12'] = s12 if outmask & Geodesic.AZIMUTH: result['azi1'] = Math.atan2d(salp1, calp1) result['azi2'] = Math.atan2d(salp2, calp2) if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 if outmask & Geodesic.GEODESICSCALE: result['M12'] = M12; result['M21'] = M21 if outmask & Geodesic.AREA: result['S12'] = S12 return result # return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 def _GenDirect(self, lat1, lon1, azi1, arcmode, s12_a12, outmask): """Private: General version of direct problem""" from geographiclib.geodesicline import GeodesicLine # Automatically supply DISTANCE_IN if necessary if not arcmode: outmask |= Geodesic.DISTANCE_IN line = GeodesicLine(self, lat1, lon1, azi1, outmask) return line._GenPosition(arcmode, s12_a12, outmask) def Direct(self, lat1, lon1, azi1, s12, outmask = GeodesicCapability.STANDARD): """Solve the direct geodesic problem :param lat1: latitude of the first point in degrees :param lon1: longitude of the first point in degrees :param azi1: azimuth at the first point in degrees :param s12: the distance from the first point to the second in meters :param outmask: the :ref:`output mask ` :return: a :ref:`dict` Compute geodesic starting at (*lat1*, *lon1*) with azimuth *azi1* and length *s12*. The default value of *outmask* is STANDARD, i.e., the *lat1*, *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are returned. """ a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenDirect( lat1, lon1, azi1, False, s12, outmask) outmask &= Geodesic.OUT_MASK result = {'lat1': Math.LatFix(lat1), 'lon1': lon1 if outmask & Geodesic.LONG_UNROLL else Math.AngNormalize(lon1), 'azi1': Math.AngNormalize(azi1), 's12': s12} result['a12'] = a12 if outmask & Geodesic.LATITUDE: result['lat2'] = lat2 if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2 if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2 if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 if outmask & Geodesic.GEODESICSCALE: result['M12'] = M12; result['M21'] = M21 if outmask & Geodesic.AREA: result['S12'] = S12 return result def ArcDirect(self, lat1, lon1, azi1, a12, outmask = GeodesicCapability.STANDARD): """Solve the direct geodesic problem in terms of spherical arc length :param lat1: latitude of the first point in degrees :param lon1: longitude of the first point in degrees :param azi1: azimuth at the first point in degrees :param a12: spherical arc length from the first point to the second in degrees :param outmask: the :ref:`output mask ` :return: a :ref:`dict` Compute geodesic starting at (*lat1*, *lon1*) with azimuth *azi1* and arc length *a12*. The default value of *outmask* is STANDARD, i.e., the *lat1*, *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are returned. """ a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenDirect( lat1, lon1, azi1, True, a12, outmask) outmask &= Geodesic.OUT_MASK result = {'lat1': Math.LatFix(lat1), 'lon1': lon1 if outmask & Geodesic.LONG_UNROLL else Math.AngNormalize(lon1), 'azi1': Math.AngNormalize(azi1), 'a12': a12} if outmask & Geodesic.DISTANCE: result['s12'] = s12 if outmask & Geodesic.LATITUDE: result['lat2'] = lat2 if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2 if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2 if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 if outmask & Geodesic.GEODESICSCALE: result['M12'] = M12; result['M21'] = M21 if outmask & Geodesic.AREA: result['S12'] = S12 return result def Line(self, lat1, lon1, azi1, caps = GeodesicCapability.STANDARD | GeodesicCapability.DISTANCE_IN): """Return a GeodesicLine object :param lat1: latitude of the first point in degrees :param lon1: longitude of the first point in degrees :param azi1: azimuth at the first point in degrees :param caps: the :ref:`capabilities ` :return: a :class:`~geographiclib.geodesicline.GeodesicLine` This allows points along a geodesic starting at (*lat1*, *lon1*), with azimuth *azi1* to be found. The default value of *caps* is STANDARD | DISTANCE_IN, allowing direct geodesic problem to be solved. """ from geographiclib.geodesicline import GeodesicLine return GeodesicLine(self, lat1, lon1, azi1, caps) def _GenDirectLine(self, lat1, lon1, azi1, arcmode, s12_a12, caps = GeodesicCapability.STANDARD | GeodesicCapability.DISTANCE_IN): """Private: general form of DirectLine""" from geographiclib.geodesicline import GeodesicLine # Automatically supply DISTANCE_IN if necessary if not arcmode: caps |= Geodesic.DISTANCE_IN line = GeodesicLine(self, lat1, lon1, azi1, caps) if arcmode: line.SetArc(s12_a12) else: line.SetDistance(s12_a12) return line def DirectLine(self, lat1, lon1, azi1, s12, caps = GeodesicCapability.STANDARD | GeodesicCapability.DISTANCE_IN): """Define a GeodesicLine object in terms of the direct geodesic problem specified in terms of spherical arc length :param lat1: latitude of the first point in degrees :param lon1: longitude of the first point in degrees :param azi1: azimuth at the first point in degrees :param s12: the distance from the first point to the second in meters :param caps: the :ref:`capabilities ` :return: a :class:`~geographiclib.geodesicline.GeodesicLine` This function sets point 3 of the GeodesicLine to correspond to point 2 of the direct geodesic problem. The default value of *caps* is STANDARD | DISTANCE_IN, allowing direct geodesic problem to be solved. """ return self._GenDirectLine(lat1, lon1, azi1, False, s12, caps) def ArcDirectLine(self, lat1, lon1, azi1, a12, caps = GeodesicCapability.STANDARD | GeodesicCapability.DISTANCE_IN): """Define a GeodesicLine object in terms of the direct geodesic problem specified in terms of spherical arc length :param lat1: latitude of the first point in degrees :param lon1: longitude of the first point in degrees :param azi1: azimuth at the first point in degrees :param a12: spherical arc length from the first point to the second in degrees :param caps: the :ref:`capabilities ` :return: a :class:`~geographiclib.geodesicline.GeodesicLine` This function sets point 3 of the GeodesicLine to correspond to point 2 of the direct geodesic problem. The default value of *caps* is STANDARD | DISTANCE_IN, allowing direct geodesic problem to be solved. """ return self._GenDirectLine(lat1, lon1, azi1, True, a12, caps) def InverseLine(self, lat1, lon1, lat2, lon2, caps = GeodesicCapability.STANDARD | GeodesicCapability.DISTANCE_IN): """Define a GeodesicLine object in terms of the invese geodesic problem :param lat1: latitude of the first point in degrees :param lon1: longitude of the first point in degrees :param lat2: latitude of the second point in degrees :param lon2: longitude of the second point in degrees :param caps: the :ref:`capabilities ` :return: a :class:`~geographiclib.geodesicline.GeodesicLine` This function sets point 3 of the GeodesicLine to correspond to point 2 of the inverse geodesic problem. The default value of *caps* is STANDARD | DISTANCE_IN, allowing direct geodesic problem to be solved. """ from geographiclib.geodesicline import GeodesicLine a12, _, salp1, calp1, _, _, _, _, _, _ = self._GenInverse( lat1, lon1, lat2, lon2, 0) azi1 = Math.atan2d(salp1, calp1) if caps & (Geodesic.OUT_MASK & Geodesic.DISTANCE_IN): caps |= Geodesic.DISTANCE line = GeodesicLine(self, lat1, lon1, azi1, caps, salp1, calp1) line.SetArc(a12) return line def Polygon(self, polyline = False): """Return a PolygonArea object :param polyline: if True then the object describes a polyline instead of a polygon :return: a :class:`~geographiclib.polygonarea.PolygonArea` """ from geographiclib.polygonarea import PolygonArea return PolygonArea(self, polyline) EMPTY = GeodesicCapability.EMPTY """No capabilities, no output.""" LATITUDE = GeodesicCapability.LATITUDE """Calculate latitude *lat2*.""" LONGITUDE = GeodesicCapability.LONGITUDE """Calculate longitude *lon2*.""" AZIMUTH = GeodesicCapability.AZIMUTH """Calculate azimuths *azi1* and *azi2*.""" DISTANCE = GeodesicCapability.DISTANCE """Calculate distance *s12*.""" STANDARD = GeodesicCapability.STANDARD """All of the above.""" DISTANCE_IN = GeodesicCapability.DISTANCE_IN """Allow distance *s12* to be used as input in the direct geodesic problem.""" REDUCEDLENGTH = GeodesicCapability.REDUCEDLENGTH """Calculate reduced length *m12*.""" GEODESICSCALE = GeodesicCapability.GEODESICSCALE """Calculate geodesic scales *M12* and *M21*.""" AREA = GeodesicCapability.AREA """Calculate area *S12*.""" ALL = GeodesicCapability.ALL """All of the above.""" LONG_UNROLL = GeodesicCapability.LONG_UNROLL """Unroll longitudes, rather than reducing them to the range [-180d,180d]. """ Geodesic.WGS84 = Geodesic(Constants.WGS84_a, Constants.WGS84_f) """Instantiation for the WGS84 ellipsoid""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626272511.0 geographiclib-2.0/geographiclib/geodesiccapability.py0000664000771000077100000000256614073571377023116 0ustar00ckarneyckarney"""geodesiccapability.py: capability constants for geodesic{,line}.py""" # geodesiccapability.py # # This gathers the capability constants need by geodesic.py and # geodesicline.py. See the documentation for the GeographicLib::Geodesic class # for more information at # # https://geographiclib.sourceforge.io/html/annotated.html # # Copyright (c) Charles Karney (2011-2014) and licensed # under the MIT/X11 License. For more information, see # https://geographiclib.sourceforge.io/ ###################################################################### class GeodesicCapability: """ Capability constants shared between Geodesic and GeodesicLine. """ CAP_NONE = 0 CAP_C1 = 1 << 0 CAP_C1p = 1 << 1 CAP_C2 = 1 << 2 CAP_C3 = 1 << 3 CAP_C4 = 1 << 4 CAP_ALL = 0x1F CAP_MASK = CAP_ALL OUT_ALL = 0x7F80 OUT_MASK = 0xFF80 # Includes LONG_UNROLL EMPTY = 0 LATITUDE = 1 << 7 | CAP_NONE LONGITUDE = 1 << 8 | CAP_C3 AZIMUTH = 1 << 9 | CAP_NONE DISTANCE = 1 << 10 | CAP_C1 STANDARD = LATITUDE | LONGITUDE | AZIMUTH | DISTANCE DISTANCE_IN = 1 << 11 | CAP_C1 | CAP_C1p REDUCEDLENGTH = 1 << 12 | CAP_C1 | CAP_C2 GEODESICSCALE = 1 << 13 | CAP_C1 | CAP_C2 AREA = 1 << 14 | CAP_C4 LONG_UNROLL = 1 << 15 ALL = OUT_ALL | CAP_ALL # Does not include LONG_UNROLL ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649456505.0 geographiclib-2.0/geographiclib/geodesicline.py0000664000771000077100000004375014224132571021707 0ustar00ckarneyckarney"""Define the :class:`~geographiclib.geodesicline.GeodesicLine` class The constructor defines the starting point of the line. Points on the line are given by * :meth:`~geographiclib.geodesicline.GeodesicLine.Position` position given in terms of distance * :meth:`~geographiclib.geodesicline.GeodesicLine.ArcPosition` position given in terms of spherical arc length A reference point 3 can be defined with * :meth:`~geographiclib.geodesicline.GeodesicLine.SetDistance` set position of 3 in terms of the distance from the starting point * :meth:`~geographiclib.geodesicline.GeodesicLine.SetArc` set position of 3 in terms of the spherical arc length from the starting point The object can also be constructed by * :meth:`Geodesic.Line ` * :meth:`Geodesic.DirectLine ` * :meth:`Geodesic.ArcDirectLine ` * :meth:`Geodesic.InverseLine ` The public attributes for this class are * :attr:`~geographiclib.geodesicline.GeodesicLine.a` :attr:`~geographiclib.geodesicline.GeodesicLine.f` :attr:`~geographiclib.geodesicline.GeodesicLine.caps` :attr:`~geographiclib.geodesicline.GeodesicLine.lat1` :attr:`~geographiclib.geodesicline.GeodesicLine.lon1` :attr:`~geographiclib.geodesicline.GeodesicLine.azi1` :attr:`~geographiclib.geodesicline.GeodesicLine.salp1` :attr:`~geographiclib.geodesicline.GeodesicLine.calp1` :attr:`~geographiclib.geodesicline.GeodesicLine.s13` :attr:`~geographiclib.geodesicline.GeodesicLine.a13` """ # geodesicline.py # # This is a rather literal translation of the GeographicLib::GeodesicLine class # to python. See the documentation for the C++ class for more information at # # https://geographiclib.sourceforge.io/html/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) and licensed # under the MIT/X11 License. For more information, see # https://geographiclib.sourceforge.io/ ###################################################################### import math from geographiclib.geomath import Math from geographiclib.geodesiccapability import GeodesicCapability class GeodesicLine: """Points on a geodesic path""" def __init__(self, geod, lat1, lon1, azi1, caps = GeodesicCapability.STANDARD | GeodesicCapability.DISTANCE_IN, salp1 = math.nan, calp1 = math.nan): """Construct a GeodesicLine object :param geod: a :class:`~geographiclib.geodesic.Geodesic` object :param lat1: latitude of the first point in degrees :param lon1: longitude of the first point in degrees :param azi1: azimuth at the first point in degrees :param caps: the :ref:`capabilities ` This creates an object allowing points along a geodesic starting at (*lat1*, *lon1*), with azimuth *azi1* to be found. The default value of *caps* is STANDARD | DISTANCE_IN. The optional parameters *salp1* and *calp1* should not be supplied; they are part of the private interface. """ from geographiclib.geodesic import Geodesic self.a = geod.a """The equatorial radius in meters (readonly)""" self.f = geod.f """The flattening (readonly)""" self._b = geod._b self._c2 = geod._c2 self._f1 = geod._f1 self.caps = (caps | Geodesic.LATITUDE | Geodesic.AZIMUTH | Geodesic.LONG_UNROLL) """the capabilities (readonly)""" # Guard against underflow in salp0 self.lat1 = Math.LatFix(lat1) """the latitude of the first point in degrees (readonly)""" self.lon1 = lon1 """the longitude of the first point in degrees (readonly)""" if math.isnan(salp1) or math.isnan(calp1): self.azi1 = Math.AngNormalize(azi1) self.salp1, self.calp1 = Math.sincosd(Math.AngRound(azi1)) else: self.azi1 = azi1 """the azimuth at the first point in degrees (readonly)""" self.salp1 = salp1 """the sine of the azimuth at the first point (readonly)""" self.calp1 = calp1 """the cosine of the azimuth at the first point (readonly)""" # real cbet1, sbet1 sbet1, cbet1 = Math.sincosd(Math.AngRound(self.lat1)); sbet1 *= self._f1 # Ensure cbet1 = +epsilon at poles sbet1, cbet1 = Math.norm(sbet1, cbet1); cbet1 = max(Geodesic.tiny_, cbet1) self._dn1 = math.sqrt(1 + geod._ep2 * Math.sq(sbet1)) # Evaluate alp0 from sin(alp1) * cos(bet1) = sin(alp0), self._salp0 = self.salp1 * cbet1 # alp0 in [0, pi/2 - |bet1|] # Alt: calp0 = hypot(sbet1, calp1 * cbet1). The following # is slightly better (consider the case salp1 = 0). self._calp0 = math.hypot(self.calp1, self.salp1 * sbet1) # Evaluate sig with tan(bet1) = tan(sig1) * cos(alp1). # sig = 0 is nearest northward crossing of equator. # With bet1 = 0, alp1 = pi/2, we have sig1 = 0 (equatorial line). # With bet1 = pi/2, alp1 = -pi, sig1 = pi/2 # With bet1 = -pi/2, alp1 = 0 , sig1 = -pi/2 # Evaluate omg1 with tan(omg1) = sin(alp0) * tan(sig1). # With alp0 in (0, pi/2], quadrants for sig and omg coincide. # No atan2(0,0) ambiguity at poles since cbet1 = +epsilon. # With alp0 = 0, omg1 = 0 for alp1 = 0, omg1 = pi for alp1 = pi. self._ssig1 = sbet1; self._somg1 = self._salp0 * sbet1 self._csig1 = self._comg1 = (cbet1 * self.calp1 if sbet1 != 0 or self.calp1 != 0 else 1) # sig1 in (-pi, pi] self._ssig1, self._csig1 = Math.norm(self._ssig1, self._csig1) # No need to normalize # self._somg1, self._comg1 = Math.norm(self._somg1, self._comg1) self._k2 = Math.sq(self._calp0) * geod._ep2 eps = self._k2 / (2 * (1 + math.sqrt(1 + self._k2)) + self._k2) if self.caps & Geodesic.CAP_C1: self._A1m1 = Geodesic._A1m1f(eps) self._C1a = list(range(Geodesic.nC1_ + 1)) Geodesic._C1f(eps, self._C1a) self._B11 = Geodesic._SinCosSeries( True, self._ssig1, self._csig1, self._C1a) s = math.sin(self._B11); c = math.cos(self._B11) # tau1 = sig1 + B11 self._stau1 = self._ssig1 * c + self._csig1 * s self._ctau1 = self._csig1 * c - self._ssig1 * s # Not necessary because C1pa reverts C1a # _B11 = -_SinCosSeries(true, _stau1, _ctau1, _C1pa) if self.caps & Geodesic.CAP_C1p: self._C1pa = list(range(Geodesic.nC1p_ + 1)) Geodesic._C1pf(eps, self._C1pa) if self.caps & Geodesic.CAP_C2: self._A2m1 = Geodesic._A2m1f(eps) self._C2a = list(range(Geodesic.nC2_ + 1)) Geodesic._C2f(eps, self._C2a) self._B21 = Geodesic._SinCosSeries( True, self._ssig1, self._csig1, self._C2a) if self.caps & Geodesic.CAP_C3: self._C3a = list(range(Geodesic.nC3_)) geod._C3f(eps, self._C3a) self._A3c = -self.f * self._salp0 * geod._A3f(eps) self._B31 = Geodesic._SinCosSeries( True, self._ssig1, self._csig1, self._C3a) if self.caps & Geodesic.CAP_C4: self._C4a = list(range(Geodesic.nC4_)) geod._C4f(eps, self._C4a) # Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0) self._A4 = Math.sq(self.a) * self._calp0 * self._salp0 * geod._e2 self._B41 = Geodesic._SinCosSeries( False, self._ssig1, self._csig1, self._C4a) self.s13 = math.nan """the distance between point 1 and point 3 in meters (readonly)""" self.a13 = math.nan """the arc length between point 1 and point 3 in degrees (readonly)""" # return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 def _GenPosition(self, arcmode, s12_a12, outmask): """Private: General solution of position along geodesic""" from geographiclib.geodesic import Geodesic a12 = lat2 = lon2 = azi2 = s12 = m12 = M12 = M21 = S12 = math.nan outmask &= self.caps & Geodesic.OUT_MASK if not (arcmode or (self.caps & (Geodesic.OUT_MASK & Geodesic.DISTANCE_IN))): # Uninitialized or impossible distance calculation requested return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 # Avoid warning about uninitialized B12. B12 = 0.0; AB1 = 0.0 if arcmode: # Interpret s12_a12 as spherical arc length sig12 = math.radians(s12_a12) ssig12, csig12 = Math.sincosd(s12_a12) else: # Interpret s12_a12 as distance tau12 = s12_a12 / (self._b * (1 + self._A1m1)) tau12 = tau12 if math.isfinite(tau12) else math.nan s = math.sin(tau12); c = math.cos(tau12) # tau2 = tau1 + tau12 B12 = - Geodesic._SinCosSeries(True, self._stau1 * c + self._ctau1 * s, self._ctau1 * c - self._stau1 * s, self._C1pa) sig12 = tau12 - (B12 - self._B11) ssig12 = math.sin(sig12); csig12 = math.cos(sig12) if abs(self.f) > 0.01: # Reverted distance series is inaccurate for |f| > 1/100, so correct # sig12 with 1 Newton iteration. The following table shows the # approximate maximum error for a = WGS_a() and various f relative to # GeodesicExact. # erri = the error in the inverse solution (nm) # errd = the error in the direct solution (series only) (nm) # errda = the error in the direct solution (series + 1 Newton) (nm) # # f erri errd errda # -1/5 12e6 1.2e9 69e6 # -1/10 123e3 12e6 765e3 # -1/20 1110 108e3 7155 # -1/50 18.63 200.9 27.12 # -1/100 18.63 23.78 23.37 # -1/150 18.63 21.05 20.26 # 1/150 22.35 24.73 25.83 # 1/100 22.35 25.03 25.31 # 1/50 29.80 231.9 30.44 # 1/20 5376 146e3 10e3 # 1/10 829e3 22e6 1.5e6 # 1/5 157e6 3.8e9 280e6 ssig2 = self._ssig1 * csig12 + self._csig1 * ssig12 csig2 = self._csig1 * csig12 - self._ssig1 * ssig12 B12 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C1a) serr = ((1 + self._A1m1) * (sig12 + (B12 - self._B11)) - s12_a12 / self._b) sig12 = sig12 - serr / math.sqrt(1 + self._k2 * Math.sq(ssig2)) ssig12 = math.sin(sig12); csig12 = math.cos(sig12) # Update B12 below # real omg12, lam12, lon12 # real ssig2, csig2, sbet2, cbet2, somg2, comg2, salp2, calp2 # sig2 = sig1 + sig12 ssig2 = self._ssig1 * csig12 + self._csig1 * ssig12 csig2 = self._csig1 * csig12 - self._ssig1 * ssig12 dn2 = math.sqrt(1 + self._k2 * Math.sq(ssig2)) if outmask & ( Geodesic.DISTANCE | Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): if arcmode or abs(self.f) > 0.01: B12 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C1a) AB1 = (1 + self._A1m1) * (B12 - self._B11) # sin(bet2) = cos(alp0) * sin(sig2) sbet2 = self._calp0 * ssig2 # Alt: cbet2 = hypot(csig2, salp0 * ssig2) cbet2 = math.hypot(self._salp0, self._calp0 * csig2) if cbet2 == 0: # I.e., salp0 = 0, csig2 = 0. Break the degeneracy in this case cbet2 = csig2 = Geodesic.tiny_ # tan(alp0) = cos(sig2)*tan(alp2) salp2 = self._salp0; calp2 = self._calp0 * csig2 # No need to normalize if outmask & Geodesic.DISTANCE: s12 = self._b * ((1 + self._A1m1) * sig12 + AB1) if arcmode else s12_a12 if outmask & Geodesic.LONGITUDE: # tan(omg2) = sin(alp0) * tan(sig2) somg2 = self._salp0 * ssig2; comg2 = csig2 # No need to normalize E = math.copysign(1, self._salp0) # East or west going? # omg12 = omg2 - omg1 omg12 = (E * (sig12 - (math.atan2( ssig2, csig2) - math.atan2( self._ssig1, self._csig1)) + (math.atan2(E * somg2, comg2) - math.atan2(E * self._somg1, self._comg1))) if outmask & Geodesic.LONG_UNROLL else math.atan2(somg2 * self._comg1 - comg2 * self._somg1, comg2 * self._comg1 + somg2 * self._somg1)) lam12 = omg12 + self._A3c * ( sig12 + (Geodesic._SinCosSeries(True, ssig2, csig2, self._C3a) - self._B31)) lon12 = math.degrees(lam12) lon2 = (self.lon1 + lon12 if outmask & Geodesic.LONG_UNROLL else Math.AngNormalize(Math.AngNormalize(self.lon1) + Math.AngNormalize(lon12))) if outmask & Geodesic.LATITUDE: lat2 = Math.atan2d(sbet2, self._f1 * cbet2) if outmask & Geodesic.AZIMUTH: azi2 = Math.atan2d(salp2, calp2) if outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): B22 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C2a) AB2 = (1 + self._A2m1) * (B22 - self._B21) J12 = (self._A1m1 - self._A2m1) * sig12 + (AB1 - AB2) if outmask & Geodesic.REDUCEDLENGTH: # Add parens around (_csig1 * ssig2) and (_ssig1 * csig2) to ensure # accurate cancellation in the case of coincident points. m12 = self._b * (( dn2 * (self._csig1 * ssig2) - self._dn1 * (self._ssig1 * csig2)) - self._csig1 * csig2 * J12) if outmask & Geodesic.GEODESICSCALE: t = (self._k2 * (ssig2 - self._ssig1) * (ssig2 + self._ssig1) / (self._dn1 + dn2)) M12 = csig12 + (t * ssig2 - csig2 * J12) * self._ssig1 / self._dn1 M21 = csig12 - (t * self._ssig1 - self._csig1 * J12) * ssig2 / dn2 if outmask & Geodesic.AREA: B42 = Geodesic._SinCosSeries(False, ssig2, csig2, self._C4a) # real salp12, calp12 if self._calp0 == 0 or self._salp0 == 0: # alp12 = alp2 - alp1, used in atan2 so no need to normalize salp12 = salp2 * self.calp1 - calp2 * self.salp1 calp12 = calp2 * self.calp1 + salp2 * self.salp1 else: # tan(alp) = tan(alp0) * sec(sig) # tan(alp2-alp1) = (tan(alp2) -tan(alp1)) / (tan(alp2)*tan(alp1)+1) # = calp0 * salp0 * (csig1-csig2) / (salp0^2 + calp0^2 * csig1*csig2) # If csig12 > 0, write # csig1 - csig2 = ssig12 * (csig1 * ssig12 / (1 + csig12) + ssig1) # else # csig1 - csig2 = csig1 * (1 - csig12) + ssig12 * ssig1 # No need to normalize salp12 = self._calp0 * self._salp0 * ( self._csig1 * (1 - csig12) + ssig12 * self._ssig1 if csig12 <= 0 else ssig12 * (self._csig1 * ssig12 / (1 + csig12) + self._ssig1)) calp12 = (Math.sq(self._salp0) + Math.sq(self._calp0) * self._csig1 * csig2) S12 = (self._c2 * math.atan2(salp12, calp12) + self._A4 * (B42 - self._B41)) a12 = s12_a12 if arcmode else math.degrees(sig12) return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 def Position(self, s12, outmask = GeodesicCapability.STANDARD): """Find the position on the line given *s12* :param s12: the distance from the first point to the second in meters :param outmask: the :ref:`output mask ` :return: a :ref:`dict` The default value of *outmask* is STANDARD, i.e., the *lat1*, *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are returned. The :class:`~geographiclib.geodesicline.GeodesicLine` object must have been constructed with the DISTANCE_IN capability. """ from geographiclib.geodesic import Geodesic result = {'lat1': self.lat1, 'lon1': self.lon1 if outmask & Geodesic.LONG_UNROLL else Math.AngNormalize(self.lon1), 'azi1': self.azi1, 's12': s12} a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenPosition( False, s12, outmask) outmask &= Geodesic.OUT_MASK result['a12'] = a12 if outmask & Geodesic.LATITUDE: result['lat2'] = lat2 if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2 if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2 if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 if outmask & Geodesic.GEODESICSCALE: result['M12'] = M12; result['M21'] = M21 if outmask & Geodesic.AREA: result['S12'] = S12 return result def ArcPosition(self, a12, outmask = GeodesicCapability.STANDARD): """Find the position on the line given *a12* :param a12: spherical arc length from the first point to the second in degrees :param outmask: the :ref:`output mask ` :return: a :ref:`dict` The default value of *outmask* is STANDARD, i.e., the *lat1*, *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are returned. """ from geographiclib.geodesic import Geodesic result = {'lat1': self.lat1, 'lon1': self.lon1 if outmask & Geodesic.LONG_UNROLL else Math.AngNormalize(self.lon1), 'azi1': self.azi1, 'a12': a12} a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenPosition( True, a12, outmask) outmask &= Geodesic.OUT_MASK if outmask & Geodesic.DISTANCE: result['s12'] = s12 if outmask & Geodesic.LATITUDE: result['lat2'] = lat2 if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2 if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2 if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 if outmask & Geodesic.GEODESICSCALE: result['M12'] = M12; result['M21'] = M21 if outmask & Geodesic.AREA: result['S12'] = S12 return result def SetDistance(self, s13): """Specify the position of point 3 in terms of distance :param s13: distance from point 1 to point 3 in meters """ self.s13 = s13 self.a13, _, _, _, _, _, _, _, _ = self._GenPosition(False, self.s13, 0) def SetArc(self, a13): """Specify the position of point 3 in terms of arc length :param a13: spherical arc length from point 1 to point 3 in degrees """ from geographiclib.geodesic import Geodesic self.a13 = a13 _, _, _, _, self.s13, _, _, _, _ = self._GenPosition(True, self.a13, Geodesic.DISTANCE) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650714978.0 geographiclib-2.0/geographiclib/geomath.py0000664000771000077100000001076014230764542020702 0ustar00ckarneyckarney"""geomath.py: transcription of GeographicLib::Math class.""" # geomath.py # # This is a rather literal translation of the GeographicLib::Math class to # python. See the documentation for the C++ class for more information at # # https://geographiclib.sourceforge.io/html/annotated.html # # Copyright (c) Charles Karney (2011-2021) and # licensed under the MIT/X11 License. For more information, see # https://geographiclib.sourceforge.io/ ###################################################################### import sys import math class Math: """ Additional math routines for GeographicLib. """ @staticmethod def sq(x): """Square a number""" return x * x @staticmethod def cbrt(x): """Real cube root of a number""" return math.copysign(math.pow(abs(x), 1/3.0), x) @staticmethod def norm(x, y): """Private: Normalize a two-vector.""" r = (math.sqrt(Math.sq(x) + Math.sq(y)) # hypot is inaccurate for 3.[89]. Problem reported by agdhruv # https://github.com/geopy/geopy/issues/466 ; see # https://bugs.python.org/issue43088 # Visual Studio 2015 32-bit has a similar problem. if (3, 8) <= sys.version_info < (3, 10) else math.hypot(x, y)) return x/r, y/r @staticmethod def sum(u, v): """Error free transformation of a sum.""" # Error free transformation of a sum. Note that t can be the same as one # of the first two arguments. s = u + v up = s - v vpp = s - up up -= u vpp -= v t = s if s == 0 else 0.0 - (up + vpp) # u + v = s + t # = round(u + v) + t return s, t @staticmethod def polyval(N, p, s, x): """Evaluate a polynomial.""" y = float(0 if N < 0 else p[s]) # make sure the returned value is a float while N > 0: N -= 1; s += 1 y = y * x + p[s] return y @staticmethod def AngRound(x): """Private: Round an angle so that small values underflow to zero.""" # The makes the smallest gap in x = 1/16 - nextafter(1/16, 0) = 1/2^57 # for reals = 0.7 pm on the earth if x is an angle in degrees. (This # is about 1000 times more resolution than we get with angles around 90 # degrees.) We use this to avoid having to deal with near singular # cases when x is non-zero but tiny (e.g., 1.0e-200). z = 1/16.0 y = abs(x) # The compiler mustn't "simplify" z - (z - y) to y if y < z: y = z - (z - y) return math.copysign(y, x) @staticmethod def remainder(x, y): """remainder of x/y in the range [-y/2, y/2].""" return math.remainder(x, y) if math.isfinite(x) else math.nan @staticmethod def AngNormalize(x): """reduce angle to [-180,180]""" y = Math.remainder(x, 360) return math.copysign(180.0, x) if abs(y) == 180 else y @staticmethod def LatFix(x): """replace angles outside [-90,90] by NaN""" return math.nan if abs(x) > 90 else x @staticmethod def AngDiff(x, y): """compute y - x and reduce to [-180,180] accurately""" d, t = Math.sum(Math.remainder(-x, 360), Math.remainder(y, 360)) d, t = Math.sum(Math.remainder(d, 360), t) if d == 0 or abs(d) == 180: d = math.copysign(d, y - x if t == 0 else -t) return d, t @staticmethod def sincosd(x): """Compute sine and cosine of x in degrees.""" r = math.fmod(x, 360) if math.isfinite(x) else math.nan q = 0 if math.isnan(r) else int(round(r / 90)) r -= 90 * q; r = math.radians(r) s = math.sin(r); c = math.cos(r) q = q % 4 if q == 1: s, c = c, -s elif q == 2: s, c = -s, -c elif q == 3: s, c = -c, s c = c + 0.0 if s == 0: s = math.copysign(s, x) return s, c @staticmethod def sincosde(x, t): """Compute sine and cosine of (x + t) in degrees with x in [-180, 180]""" q = int(round(x / 90)) if math.isfinite(x) else 0 r = x - 90 * q; r = math.radians(Math.AngRound(r + t)) s = math.sin(r); c = math.cos(r) q = q % 4 if q == 1: s, c = c, -s elif q == 2: s, c = -s, -c elif q == 3: s, c = -c, s c = c + 0.0 if s == 0: s = math.copysign(s, x) return s, c @staticmethod def atan2d(y, x): """compute atan2(y, x) with the result in degrees""" if abs(y) > abs(x): q = 2; x, y = y, x else: q = 0 if x < 0: q += 1; x = -x ang = math.degrees(math.atan2(y, x)) if q == 1: ang = math.copysign(180, y) - ang elif q == 2: ang = 90 - ang elif q == 3: ang = -90 + ang return ang ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649456505.0 geographiclib-2.0/geographiclib/polygonarea.py0000664000771000077100000002734314224132571021575 0ustar00ckarneyckarney"""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/html/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) 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. """ if self.polyline: 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) """ if self.polyline: 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1650718633.7976391 geographiclib-2.0/geographiclib/test/0000775000771000077100000000000014230773652017661 5ustar00ckarneyckarney././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626272511.0 geographiclib-2.0/geographiclib/test/__init__.py0000664000771000077100000000041514073571377021777 0ustar00ckarneyckarney""" test_geodesic: test the geodesic routines from GeographicLib Run these tests with one of python2 -m unittest -v geographiclib.test.test_geodesic python3 -m unittest -v geographiclib.test.test_geodesic executed in this directory's parent directory. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1648644295.0 geographiclib-2.0/geographiclib/test/test_geodesic.py0000664000771000077100000011742314221050307023045 0ustar00ckarneyckarney"""Geodesic tests""" import unittest import math from geographiclib.geodesic import Geodesic class GeodesicTest(unittest.TestCase): """Geodesic test suite""" testcases = [ [35.60777, -139.44815, 111.098748429560326, -11.17491, -69.95921, 129.289270889708762, 8935244.5604818305, 80.50729714281974, 6273170.2055303837, 0.16606318447386067, 0.16479116945612937, 12841384694976.432], [55.52454, 106.05087, 22.020059880982801, 77.03196, 197.18234, 109.112041110671519, 4105086.1713924406, 36.892740690445894, 3828869.3344387607, 0.80076349608092607, 0.80101006984201008, 61674961290615.615], [-21.97856, 142.59065, -32.44456876433189, 41.84138, 98.56635, -41.84359951440466, 8394328.894657671, 75.62930491011522, 6161154.5773110616, 0.24816339233950381, 0.24930251203627892, -6637997720646.717], [-66.99028, 112.2363, 173.73491240878403, -12.70631, 285.90344, 2.512956620913668, 11150344.2312080241, 100.278634181155759, 6289939.5670446687, -0.17199490274700385, -0.17722569526345708, -121287239862139.744], [-17.42761, 173.34268, -159.033557661192928, -15.84784, 5.93557, -20.787484651536988, 16076603.1631180673, 144.640108810286253, 3732902.1583877189, -0.81273638700070476, -0.81299800519154474, 97825992354058.708], [32.84994, 48.28919, 150.492927788121982, -56.28556, 202.29132, 48.113449399816759, 16727068.9438164461, 150.565799985466607, 3147838.1910180939, -0.87334918086923126, -0.86505036767110637, -72445258525585.010], [6.96833, 52.74123, 92.581585386317712, -7.39675, 206.17291, 90.721692165923907, 17102477.2496958388, 154.147366239113561, 2772035.6169917581, -0.89991282520302447, -0.89986892177110739, -1311796973197.995], [-50.56724, -16.30485, -105.439679907590164, -33.56571, -94.97412, -47.348547835650331, 6455670.5118668696, 58.083719495371259, 5409150.7979815838, 0.53053508035997263, 0.52988722644436602, 41071447902810.047], [-58.93002, -8.90775, 140.965397902500679, -8.91104, 133.13503, 19.255429433416599, 11756066.0219864627, 105.755691241406877, 6151101.2270708536, -0.26548622269867183, -0.27068483874510741, -86143460552774.735], [-68.82867, -74.28391, 93.774347763114881, -50.63005, -8.36685, 34.65564085411343, 3956936.926063544, 35.572254987389284, 3708890.9544062657, 0.81443963736383502, 0.81420859815358342, -41845309450093.787], [-10.62672, -32.0898, -86.426713286747751, 5.883, -134.31681, -80.473780971034875, 11470869.3864563009, 103.387395634504061, 6184411.6622659713, -0.23138683500430237, -0.23155097622286792, 4198803992123.548], [-21.76221, 166.90563, 29.319421206936428, 48.72884, 213.97627, 43.508671946410168, 9098627.3986554915, 81.963476716121964, 6299240.9166992283, 0.13965943368590333, 0.14152969707656796, 10024709850277.476], [-19.79938, -174.47484, 71.167275780171533, -11.99349, -154.35109, 65.589099775199228, 2319004.8601169389, 20.896611684802389, 2267960.8703918325, 0.93427001867125849, 0.93424887135032789, -3935477535005.785], [-11.95887, -116.94513, 92.712619830452549, 4.57352, 7.16501, 78.64960934409585, 13834722.5801401374, 124.688684161089762, 5228093.177931598, -0.56879356755666463, -0.56918731952397221, -9919582785894.853], [-87.85331, 85.66836, -65.120313040242748, 66.48646, 16.09921, -4.888658719272296, 17286615.3147144645, 155.58592449699137, 2635887.4729110181, -0.90697975771398578, -0.91095608883042767, 42667211366919.534], [1.74708, 128.32011, -101.584843631173858, -11.16617, 11.87109, -86.325793296437476, 12942901.1241347408, 116.650512484301857, 5682744.8413270572, -0.44857868222697644, -0.44824490340007729, 10763055294345.653], [-25.72959, -144.90758, -153.647468693117198, -57.70581, -269.17879, -48.343983158876487, 9413446.7452453107, 84.664533838404295, 6356176.6898881281, 0.09492245755254703, 0.09737058264766572, 74515122850712.444], [-41.22777, 122.32875, 14.285113402275739, -7.57291, 130.37946, 10.805303085187369, 3812686.035106021, 34.34330804743883, 3588703.8812128856, 0.82605222593217889, 0.82572158200920196, -2456961531057.857], [11.01307, 138.25278, 79.43682622782374, 6.62726, 247.05981, 103.708090215522657, 11911190.819018408, 107.341669954114577, 6070904.722786735, -0.29767608923657404, -0.29785143390252321, 17121631423099.696], [-29.47124, 95.14681, -163.779130441688382, -27.46601, -69.15955, -15.909335945554969, 13487015.8381145492, 121.294026715742277, 5481428.9945736388, -0.51527225545373252, -0.51556587964721788, 104679964020340.318]] def test_inverse(self): """Helper function for testing inverse calculation""" for l in GeodesicTest.testcases: (lat1, lon1, azi1, lat2, lon2, azi2, s12, a12, m12, M12, M21, S12) = l inv = Geodesic.WGS84.Inverse(lat1, lon1, lat2, lon2, Geodesic.ALL | Geodesic.LONG_UNROLL) self.assertAlmostEqual(lon2, inv["lon2"], delta = 1e-13) self.assertAlmostEqual(azi1, inv["azi1"], delta = 1e-13) self.assertAlmostEqual(azi2, inv["azi2"], delta = 1e-13) self.assertAlmostEqual(s12, inv["s12"], delta = 1e-8) self.assertAlmostEqual(a12, inv["a12"], delta = 1e-13) self.assertAlmostEqual(m12, inv["m12"], delta = 1e-8) self.assertAlmostEqual(M12, inv["M12"], delta = 1e-15) self.assertAlmostEqual(M21, inv["M21"], delta = 1e-15) self.assertAlmostEqual(S12, inv["S12"], delta = 0.1) def test_direct(self): """Helper function for testing direct calculation""" for l in GeodesicTest.testcases: (lat1, lon1, azi1, lat2, lon2, azi2, s12, a12, m12, M12, M21, S12) = l direct = Geodesic.WGS84.Direct(lat1, lon1, azi1, s12, Geodesic.ALL | Geodesic.LONG_UNROLL) self.assertAlmostEqual(lat2, direct["lat2"], delta = 1e-13) self.assertAlmostEqual(lon2, direct["lon2"], delta = 1e-13) self.assertAlmostEqual(azi2, direct["azi2"], delta = 1e-13) self.assertAlmostEqual(a12, direct["a12"], delta = 1e-13) self.assertAlmostEqual(m12, direct["m12"], delta = 1e-8) self.assertAlmostEqual(M12, direct["M12"], delta = 1e-15) self.assertAlmostEqual(M21, direct["M21"], delta = 1e-15) self.assertAlmostEqual(S12, direct["S12"], delta = 0.1) def test_arcdirect(self): """Helper function for testing direct calculation with arc length""" for l in GeodesicTest.testcases: (lat1, lon1, azi1, lat2, lon2, azi2, s12, a12, m12, M12, M21, S12) = l direct = Geodesic.WGS84.ArcDirect(lat1, lon1, azi1, a12, Geodesic.ALL | Geodesic.LONG_UNROLL) self.assertAlmostEqual(lat2, direct["lat2"], delta = 1e-13) self.assertAlmostEqual(lon2, direct["lon2"], delta = 1e-13) self.assertAlmostEqual(azi2, direct["azi2"], delta = 1e-13) self.assertAlmostEqual(s12, direct["s12"], delta = 1e-8) self.assertAlmostEqual(m12, direct["m12"], delta = 1e-8) self.assertAlmostEqual(M12, direct["M12"], delta = 1e-15) self.assertAlmostEqual(M21, direct["M21"], delta = 1e-15) self.assertAlmostEqual(S12, direct["S12"], delta = 0.1) class GeodSolveTest(unittest.TestCase): """GeodSolve tests""" def test_GeodSolve0(self): """GeodSolve0""" inv = Geodesic.WGS84.Inverse(40.6, -73.8, 49.01666667, 2.55) self.assertAlmostEqual(inv["azi1"], 53.47022, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 111.59367, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 5853226, delta = 0.5) def test_GeodSolve1(self): """GeodSolve1""" direct = Geodesic.WGS84.Direct(40.63972222, -73.77888889, 53.5, 5850e3) self.assertAlmostEqual(direct["lat2"], 49.01467, delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], 2.56106, delta = 0.5e-5) self.assertAlmostEqual(direct["azi2"], 111.62947, delta = 0.5e-5) def test_GeodSolve2(self): """Check fix for antipodal prolate bug found 2010-09-04""" geod = Geodesic(6.4e6, -1/150.0) inv = geod.Inverse(0.07476, 0, -0.07476, 180) self.assertAlmostEqual(inv["azi1"], 90.00078, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 90.00078, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 20106193, delta = 0.5) inv = geod.Inverse(0.1, 0, -0.1, 180) self.assertAlmostEqual(inv["azi1"], 90.00105, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 90.00105, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 20106193, delta = 0.5) def test_GeodSolve4(self): """Check fix for short line bug found 2010-05-21""" inv = Geodesic.WGS84.Inverse(36.493349428792, 0, 36.49334942879201, 0.0000008) self.assertAlmostEqual(inv["s12"], 0.072, delta = 0.5e-3) def test_GeodSolve5(self): """Check fix for point2=pole bug found 2010-05-03""" direct = Geodesic.WGS84.Direct(0.01777745589997, 30, 0, 10e6) self.assertAlmostEqual(direct["lat2"], 90, delta = 0.5e-5) if direct["lon2"] < 0: self.assertAlmostEqual(direct["lon2"], -150, delta = 0.5e-5) self.assertAlmostEqual(abs(direct["azi2"]), 180, delta = 0.5e-5) else: self.assertAlmostEqual(direct["lon2"], 30, delta = 0.5e-5) self.assertAlmostEqual(direct["azi2"], 0, delta = 0.5e-5) def test_GeodSolve6(self): """Check fix for volatile sbet12a bug found 2011-06-25 (gcc 4.4.4 x86 -O3). Found again on 2012-03-27 with tdm-mingw32 (g++ 4.6.1).""" inv = Geodesic.WGS84.Inverse(88.202499451857, 0, -88.202499451857, 179.981022032992859592) self.assertAlmostEqual(inv["s12"], 20003898.214, delta = 0.5e-3) inv = Geodesic.WGS84.Inverse(89.262080389218, 0, -89.262080389218, 179.992207982775375662) self.assertAlmostEqual(inv["s12"], 20003925.854, delta = 0.5e-3) inv = Geodesic.WGS84.Inverse(89.333123580033, 0, -89.333123580032997687, 179.99295812360148422) self.assertAlmostEqual(inv["s12"], 20003926.881, delta = 0.5e-3) def test_GeodSolve9(self): """Check fix for volatile x bug found 2011-06-25 (gcc 4.4.4 x86 -O3)""" inv = Geodesic.WGS84.Inverse(56.320923501171, 0, -56.320923501171, 179.664747671772880215) self.assertAlmostEqual(inv["s12"], 19993558.287, delta = 0.5e-3) def test_GeodSolve10(self): """Check fix for adjust tol1_ bug found 2011-06-25 (Visual Studio 10 rel + debug)""" inv = Geodesic.WGS84.Inverse(52.784459512564, 0, -52.784459512563990912, 179.634407464943777557) self.assertAlmostEqual(inv["s12"], 19991596.095, delta = 0.5e-3) def test_GeodSolve11(self): """Check fix for bet2 = -bet1 bug found 2011-06-25 (Visual Studio 10 rel + debug)""" inv = Geodesic.WGS84.Inverse(48.522876735459, 0, -48.52287673545898293, 179.599720456223079643) self.assertAlmostEqual(inv["s12"], 19989144.774, delta = 0.5e-3) def test_GeodSolve12(self): """Check fix for inverse geodesics on extreme prolate/oblate ellipsoids Reported 2012-08-29 Stefan Guenther ; fixed 2012-10-07""" geod = Geodesic(89.8, -1.83) inv = geod.Inverse(0, 0, -10, 160) self.assertAlmostEqual(inv["azi1"], 120.27, delta = 1e-2) self.assertAlmostEqual(inv["azi2"], 105.15, delta = 1e-2) self.assertAlmostEqual(inv["s12"], 266.7, delta = 1e-1) def test_GeodSolve14(self): """Check fix for inverse ignoring lon12 = nan""" inv = Geodesic.WGS84.Inverse(0, 0, 1, math.nan) self.assertTrue(math.isnan(inv["azi1"])) self.assertTrue(math.isnan(inv["azi2"])) self.assertTrue(math.isnan(inv["s12"])) def test_GeodSolve15(self): """Initial implementation of Math::eatanhe was wrong for e^2 < 0. This checks that this is fixed.""" geod = Geodesic(6.4e6, -1/150.0) direct = geod.Direct(1, 2, 3, 4, Geodesic.AREA) self.assertAlmostEqual(direct["S12"], 23700, delta = 0.5) def test_GeodSolve17(self): """Check fix for LONG_UNROLL bug found on 2015-05-07""" direct = Geodesic.WGS84.Direct(40, -75, -10, 2e7, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat2"], -39, delta = 1) self.assertAlmostEqual(direct["lon2"], -254, delta = 1) self.assertAlmostEqual(direct["azi2"], -170, delta = 1) line = Geodesic.WGS84.Line(40, -75, -10) direct = line.Position(2e7, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat2"], -39, delta = 1) self.assertAlmostEqual(direct["lon2"], -254, delta = 1) self.assertAlmostEqual(direct["azi2"], -170, delta = 1) direct = Geodesic.WGS84.Direct(40, -75, -10, 2e7) self.assertAlmostEqual(direct["lat2"], -39, delta = 1) self.assertAlmostEqual(direct["lon2"], 105, delta = 1) self.assertAlmostEqual(direct["azi2"], -170, delta = 1) direct = line.Position(2e7) self.assertAlmostEqual(direct["lat2"], -39, delta = 1) self.assertAlmostEqual(direct["lon2"], 105, delta = 1) self.assertAlmostEqual(direct["azi2"], -170, delta = 1) def test_GeodSolve26(self): """Check 0/0 problem with area calculation on sphere 2015-09-08""" geod = Geodesic(6.4e6, 0) inv = geod.Inverse(1, 2, 3, 4, Geodesic.AREA) self.assertAlmostEqual(inv["S12"], 49911046115.0, delta = 0.5) def test_GeodSolve28(self): """Check for bad placement of assignment of r.a12 with |f| > 0.01 (bug in Java implementation fixed on 2015-05-19).""" geod = Geodesic(6.4e6, 0.1) direct = geod.Direct(1, 2, 10, 5e6) self.assertAlmostEqual(direct["a12"], 48.55570690, delta = 0.5e-8) def test_GeodSolve29(self): """Check longitude unrolling with inverse calculation 2015-09-16""" direct = Geodesic.WGS84.Inverse(0, 539, 0, 181) self.assertAlmostEqual(direct["lon1"], 179, delta = 1e-10) self.assertAlmostEqual(direct["lon2"], -179, delta = 1e-10) self.assertAlmostEqual(direct["s12"], 222639, delta = 0.5) direct = Geodesic.WGS84.Inverse(0, 539, 0, 181, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lon1"], 539, delta = 1e-10) self.assertAlmostEqual(direct["lon2"], 541, delta = 1e-10) self.assertAlmostEqual(direct["s12"], 222639, delta = 0.5) def test_GeodSolve33(self): """Check max(-0.0,+0.0) issues 2015-08-22 (triggered by bugs in Octave -- sind(-0.0) = +0.0 -- and in some version of Visual Studio -- fmod(-0.0, 360.0) = +0.0.""" inv = Geodesic.WGS84.Inverse(0, 0, 0, 179) self.assertAlmostEqual(inv["azi1"], 90.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 90.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 19926189, delta = 0.5) inv = Geodesic.WGS84.Inverse(0, 0, 0, 179.5) self.assertAlmostEqual(inv["azi1"], 55.96650, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 124.03350, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 19980862, delta = 0.5) inv = Geodesic.WGS84.Inverse(0, 0, 0, 180) self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 20003931, delta = 0.5) inv = Geodesic.WGS84.Inverse(0, 0, 1, 180) self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 19893357, delta = 0.5) geod = Geodesic(6.4e6, 0) inv = geod.Inverse(0, 0, 0, 179) self.assertAlmostEqual(inv["azi1"], 90.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 90.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 19994492, delta = 0.5) inv = geod.Inverse(0, 0, 0, 180) self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 20106193, delta = 0.5) inv = geod.Inverse(0, 0, 1, 180) self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 19994492, delta = 0.5) geod = Geodesic(6.4e6, -1/300.0) inv = geod.Inverse(0, 0, 0, 179) self.assertAlmostEqual(inv["azi1"], 90.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 90.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 19994492, delta = 0.5) inv = geod.Inverse(0, 0, 0, 180) self.assertAlmostEqual(inv["azi1"], 90.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 90.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 20106193, delta = 0.5) inv = geod.Inverse(0, 0, 0.5, 180) self.assertAlmostEqual(inv["azi1"], 33.02493, delta = 0.5e-5) self.assertAlmostEqual(inv["azi2"], 146.97364, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 20082617, delta = 0.5) inv = geod.Inverse(0, 0, 1, 180) self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) self.assertAlmostEqual(inv["s12"], 20027270, delta = 0.5) def test_GeodSolve55(self): """Check fix for nan + point on equator or pole not returning all nans in Geodesic::Inverse, found 2015-09-23.""" inv = Geodesic.WGS84.Inverse(math.nan, 0, 0, 90) self.assertTrue(math.isnan(inv["azi1"])) self.assertTrue(math.isnan(inv["azi2"])) self.assertTrue(math.isnan(inv["s12"])) inv = Geodesic.WGS84.Inverse(math.nan, 0, 90, 9) self.assertTrue(math.isnan(inv["azi1"])) self.assertTrue(math.isnan(inv["azi2"])) self.assertTrue(math.isnan(inv["s12"])) def test_GeodSolve59(self): """Check for points close with longitudes close to 180 deg apart.""" inv = Geodesic.WGS84.Inverse(5, 0.00000000000001, 10, 180) self.assertAlmostEqual(inv["azi1"], 0.000000000000035, delta = 1.5e-14) self.assertAlmostEqual(inv["azi2"], 179.99999999999996, delta = 1.5e-14) self.assertAlmostEqual(inv["s12"], 18345191.174332713, delta = 5e-9) def test_GeodSolve61(self): """Make sure small negative azimuths are west-going""" direct = Geodesic.WGS84.Direct(45, 0, -0.000000000000000003, 1e7, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat2"], 45.30632, delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], -180, delta = 0.5e-5) self.assertAlmostEqual(abs(direct["azi2"]), 180, delta = 0.5e-5) line = Geodesic.WGS84.InverseLine(45, 0, 80, -0.000000000000000003) direct = line.Position(1e7, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat2"], 45.30632, delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], -180, delta = 0.5e-5) self.assertAlmostEqual(abs(direct["azi2"]), 180, delta = 0.5e-5) def test_GeodSolve65(self): """Check for bug in east-going check in GeodesicLine (needed to check for sign of 0) and sign error in area calculation due to a bogus override of the code for alp12. Found/fixed on 2015-12-19.""" line = Geodesic.WGS84.InverseLine(30, -0.000000000000000001, -31, 180, Geodesic.ALL) direct = line.Position(1e7, Geodesic.ALL | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat1"], 30.00000 , delta = 0.5e-5) self.assertAlmostEqual(direct["lon1"], -0.00000 , delta = 0.5e-5) self.assertAlmostEqual(abs(direct["azi1"]), 180.00000, delta = 0.5e-5) self.assertAlmostEqual(direct["lat2"], -60.23169 , delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], -0.00000 , delta = 0.5e-5) self.assertAlmostEqual(abs(direct["azi2"]), 180.00000, delta = 0.5e-5) self.assertAlmostEqual(direct["s12"] , 10000000 , delta = 0.5) self.assertAlmostEqual(direct["a12"] , 90.06544 , delta = 0.5e-5) self.assertAlmostEqual(direct["m12"] , 6363636 , delta = 0.5) self.assertAlmostEqual(direct["M12"] , -0.0012834, delta = 0.5e7) self.assertAlmostEqual(direct["M21"] , 0.0013749 , delta = 0.5e-7) self.assertAlmostEqual(direct["S12"] , 0 , delta = 0.5) direct = line.Position(2e7, Geodesic.ALL | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat1"], 30.00000 , delta = 0.5e-5) self.assertAlmostEqual(direct["lon1"], -0.00000 , delta = 0.5e-5) self.assertAlmostEqual(abs(direct["azi1"]), 180.00000, delta = 0.5e-5) self.assertAlmostEqual(direct["lat2"], -30.03547 , delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], -180.00000, delta = 0.5e-5) self.assertAlmostEqual(direct["azi2"], -0.00000 , delta = 0.5e-5) self.assertAlmostEqual(direct["s12"] , 20000000 , delta = 0.5) self.assertAlmostEqual(direct["a12"] , 179.96459 , delta = 0.5e-5) self.assertAlmostEqual(direct["m12"] , 54342 , delta = 0.5) self.assertAlmostEqual(direct["M12"] , -1.0045592, delta = 0.5e7) self.assertAlmostEqual(direct["M21"] , -0.9954339, delta = 0.5e-7) self.assertAlmostEqual(direct["S12"] , 127516405431022.0, delta = 0.5) def test_GeodSolve66(self): """Check for InverseLine if line is slightly west of S and that s13 is correctly set.""" line = Geodesic.WGS84.InverseLine(-5, -0.000000000000002, -10, 180) direct = line.Position(2e7, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat2"], 4.96445 , delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], -180.00000, delta = 0.5e-5) self.assertAlmostEqual(direct["azi2"], -0.00000 , delta = 0.5e-5) direct = line.Position(0.5 * line.s13, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat2"], -87.52461 , delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], -0.00000 , delta = 0.5e-5) self.assertAlmostEqual(direct["azi2"], -180.00000, delta = 0.5e-5) def test_GeodSolve71(self): """Check that DirectLine sets s13.""" line = Geodesic.WGS84.DirectLine(1, 2, 45, 1e7) direct = line.Position(0.5 * line.s13, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertAlmostEqual(direct["lat2"], 30.92625, delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], 37.54640, delta = 0.5e-5) self.assertAlmostEqual(direct["azi2"], 55.43104, delta = 0.5e-5) def test_GeodSolve73(self): """Check for backwards from the pole bug reported by Anon on 2016-02-13. This only affected the Java implementation. It was introduced in Java version 1.44 and fixed in 1.46-SNAPSHOT on 2016-01-17. Also the + sign on azi2 is a check on the normalizing of azimuths (converting -0.0 to +0.0).""" direct = Geodesic.WGS84.Direct(90, 10, 180, -1e6) self.assertAlmostEqual(direct["lat2"], 81.04623, delta = 0.5e-5) self.assertAlmostEqual(direct["lon2"], -170, delta = 0.5e-5) self.assertAlmostEqual(direct["azi2"], 0, delta = 0.5e-5) self.assertTrue(math.copysign(1, direct["azi2"]) > 0) def test_GeodSolve74(self): """Check fix for inaccurate areas, bug introduced in v1.46, fixed 2015-10-16.""" inv = Geodesic.WGS84.Inverse(54.1589, 15.3872, 54.1591, 15.3877, Geodesic.ALL) self.assertAlmostEqual(inv["azi1"], 55.723110355, delta = 5e-9) self.assertAlmostEqual(inv["azi2"], 55.723515675, delta = 5e-9) self.assertAlmostEqual(inv["s12"], 39.527686385, delta = 5e-9) self.assertAlmostEqual(inv["a12"], 0.000355495, delta = 5e-9) self.assertAlmostEqual(inv["m12"], 39.527686385, delta = 5e-9) self.assertAlmostEqual(inv["M12"], 0.999999995, delta = 5e-9) self.assertAlmostEqual(inv["M21"], 0.999999995, delta = 5e-9) self.assertAlmostEqual(inv["S12"], 286698586.30197, delta = 5e-4) def test_GeodSolve76(self): """The distance from Wellington and Salamanca (a classic failure of Vincenty)""" inv = Geodesic.WGS84.Inverse(-(41+19/60.0), 174+49/60.0, 40+58/60.0, -(5+30/60.0)) self.assertAlmostEqual(inv["azi1"], 160.39137649664, delta = 0.5e-11) self.assertAlmostEqual(inv["azi2"], 19.50042925176, delta = 0.5e-11) self.assertAlmostEqual(inv["s12"], 19960543.857179, delta = 0.5e-6) def test_GeodSolve78(self): """An example where the NGS calculator fails to converge""" inv = Geodesic.WGS84.Inverse(27.2, 0.0, -27.1, 179.5) self.assertAlmostEqual(inv["azi1"], 45.82468716758, delta = 0.5e-11) self.assertAlmostEqual(inv["azi2"], 134.22776532670, delta = 0.5e-11) self.assertAlmostEqual(inv["s12"], 19974354.765767, delta = 0.5e-6) def test_GeodSolve80(self): """Some tests to add code coverage: computing scale in special cases + zero length geodesic (includes GeodSolve80 - GeodSolve83) + using an incapable line.""" inv = Geodesic.WGS84.Inverse(0, 0, 0, 90, Geodesic.GEODESICSCALE) self.assertAlmostEqual(inv["M12"], -0.00528427534, delta = 0.5e-10) self.assertAlmostEqual(inv["M21"], -0.00528427534, delta = 0.5e-10) inv = Geodesic.WGS84.Inverse(0, 0, 1e-6, 1e-6, Geodesic.GEODESICSCALE) self.assertAlmostEqual(inv["M12"], 1, delta = 0.5e-10) self.assertAlmostEqual(inv["M21"], 1, delta = 0.5e-10) inv = Geodesic.WGS84.Inverse(20.001, 0, 20.001, 0, Geodesic.ALL) self.assertAlmostEqual(inv["a12"], 0, delta = 1e-13) self.assertAlmostEqual(inv["s12"], 0, delta = 1e-8) self.assertAlmostEqual(inv["azi1"], 180, delta = 1e-13) self.assertAlmostEqual(inv["azi2"], 180, delta = 1e-13) self.assertAlmostEqual(inv["m12"], 0, delta = 1e-8) self.assertAlmostEqual(inv["M12"], 1, delta = 1e-15) self.assertAlmostEqual(inv["M21"], 1, delta = 1e-15) self.assertAlmostEqual(inv["S12"], 0, delta = 1e-10) self.assertTrue(math.copysign(1, inv["a12"]) > 0) self.assertTrue(math.copysign(1, inv["s12"]) > 0) self.assertTrue(math.copysign(1, inv["m12"]) > 0) inv = Geodesic.WGS84.Inverse(90, 0, 90, 180, Geodesic.ALL) self.assertAlmostEqual(inv["a12"], 0, delta = 1e-13) self.assertAlmostEqual(inv["s12"], 0, delta = 1e-8) self.assertAlmostEqual(inv["azi1"], 0, delta = 1e-13) self.assertAlmostEqual(inv["azi2"], 180, delta = 1e-13) self.assertAlmostEqual(inv["m12"], 0, delta = 1e-8) self.assertAlmostEqual(inv["M12"], 1, delta = 1e-15) self.assertAlmostEqual(inv["M21"], 1, delta = 1e-15) self.assertAlmostEqual(inv["S12"], 127516405431022.0, delta = 0.5) # An incapable line which can't take distance as input line = Geodesic.WGS84.Line(1, 2, 90, Geodesic.LATITUDE) direct = line.Position(1000, Geodesic.EMPTY) self.assertTrue(math.isnan(direct["a12"])) def test_GeodSolve84(self): """Tests for python implementation to check fix for range errors with {fmod,sin,cos}(inf) (includes GeodSolve84 - GeodSolve91).""" direct = Geodesic.WGS84.Direct(0, 0, 90, math.inf) self.assertTrue(math.isnan(direct["lat2"])) self.assertTrue(math.isnan(direct["lon2"])) self.assertTrue(math.isnan(direct["azi2"])) direct = Geodesic.WGS84.Direct(0, 0, 90, math.nan) self.assertTrue(math.isnan(direct["lat2"])) self.assertTrue(math.isnan(direct["lon2"])) self.assertTrue(math.isnan(direct["azi2"])) direct = Geodesic.WGS84.Direct(0, 0, math.inf, 1000) self.assertTrue(math.isnan(direct["lat2"])) self.assertTrue(math.isnan(direct["lon2"])) self.assertTrue(math.isnan(direct["azi2"])) direct = Geodesic.WGS84.Direct(0, 0, math.nan, 1000) self.assertTrue(math.isnan(direct["lat2"])) self.assertTrue(math.isnan(direct["lon2"])) self.assertTrue(math.isnan(direct["azi2"])) direct = Geodesic.WGS84.Direct(0, math.inf, 90, 1000) self.assertTrue(direct["lat1"] == 0) self.assertTrue(math.isnan(direct["lon2"])) self.assertTrue(direct["azi2"] == 90) direct = Geodesic.WGS84.Direct(0, math.nan, 90, 1000) self.assertTrue(direct["lat1"] == 0) self.assertTrue(math.isnan(direct["lon2"])) self.assertTrue(direct["azi2"] == 90) direct = Geodesic.WGS84.Direct(math.inf, 0, 90, 1000) self.assertTrue(math.isnan(direct["lat2"])) self.assertTrue(math.isnan(direct["lon2"])) self.assertTrue(math.isnan(direct["azi2"])) direct = Geodesic.WGS84.Direct(math.nan, 0, 90, 1000) self.assertTrue(math.isnan(direct["lat2"])) self.assertTrue(math.isnan(direct["lon2"])) self.assertTrue(math.isnan(direct["azi2"])) def test_GeodSolve92(self): """Check fix for inaccurate hypot with python 3.[89]. Problem reported by agdhruv https://github.com/geopy/geopy/issues/466 ; see https://bugs.python.org/issue43088""" inv = Geodesic.WGS84.Inverse(37.757540000000006, -122.47018, 37.75754, -122.470177) self.assertAlmostEqual(inv["azi1"], 89.99999923, delta = 1e-7 ) self.assertAlmostEqual(inv["azi2"], 90.00000106, delta = 1e-7 ) self.assertAlmostEqual(inv["s12"], 0.264, delta = 0.5e-3) def test_GeodSolve94(self): """Check fix for lat2 = nan being treated as lat2 = 0 (bug found 2021-07-26)""" inv = Geodesic.WGS84.Inverse(0, 0, math.nan, 90) self.assertTrue(math.isnan(inv["azi1"])) self.assertTrue(math.isnan(inv["azi2"])) self.assertTrue(math.isnan(inv["s12"])) def test_GeodSolve96(self): """Failure with long doubles found with test case from Nowak + Nowak Da Costa (2022). Problem was using somg12 > 1 as a test that it needed to be set when roundoff could result in somg12 slightly bigger that 1. Found + fixed 2022-03-30.""" geod = Geodesic(6378137, 1/298.257222101) inv = geod.Inverse(0, 0, 60.0832522871723, 89.8492185074635, Geodesic.AREA) self.assertAlmostEqual(inv["S12"], 42426932221845, delta = 0.5) class PlanimeterTest(unittest.TestCase): """Planimeter tests""" polygon = Geodesic.WGS84.Polygon(False) polyline = Geodesic.WGS84.Polygon(True) @staticmethod def Planimeter(points): """Helper function for polygons""" PlanimeterTest.polygon.Clear() for p in points: PlanimeterTest.polygon.AddPoint(p[0], p[1]) return PlanimeterTest.polygon.Compute(False, True) @staticmethod def PolyLength(points): """Helper function for polylines""" PlanimeterTest.polyline.Clear() for p in points: PlanimeterTest.polyline.AddPoint(p[0], p[1]) return PlanimeterTest.polyline.Compute(False, True) def test_Planimeter0(self): """Check fix for pole-encircling bug found 2011-03-16""" points = [[89, 0], [89, 90], [89, 180], [89, 270]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 631819.8745, delta = 1e-4) self.assertAlmostEqual(area, 24952305678.0, delta = 1) points = [[-89, 0], [-89, 90], [-89, 180], [-89, 270]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 631819.8745, delta = 1e-4) self.assertAlmostEqual(area, -24952305678.0, delta = 1) points = [[0, -1], [-1, 0], [0, 1], [1, 0]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 627598.2731, delta = 1e-4) self.assertAlmostEqual(area, 24619419146.0, delta = 1) points = [[90, 0], [0, 0], [0, 90]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 30022685, delta = 1) self.assertAlmostEqual(area, 63758202715511.0, delta = 1) _, perimeter, area = PlanimeterTest.PolyLength(points) self.assertAlmostEqual(perimeter, 20020719, delta = 1) self.assertTrue(math.isnan(area)) def test_Planimeter5(self): """Check fix for Planimeter pole crossing bug found 2011-06-24""" points = [[89, 0.1], [89, 90.1], [89, -179.9]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 539297, delta = 1) self.assertAlmostEqual(area, 12476152838.5, delta = 1) def test_Planimeter6(self): """Check fix for Planimeter lon12 rounding bug found 2012-12-03""" points = [[9, -0.00000000000001], [9, 180], [9, 0]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 36026861, delta = 1) self.assertAlmostEqual(area, 0, delta = 1) points = [[9, 0.00000000000001], [9, 0], [9, 180]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 36026861, delta = 1) self.assertAlmostEqual(area, 0, delta = 1) points = [[9, 0.00000000000001], [9, 180], [9, 0]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 36026861, delta = 1) self.assertAlmostEqual(area, 0, delta = 1) points = [[9, -0.00000000000001], [9, 0], [9, 180]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 36026861, delta = 1) self.assertAlmostEqual(area, 0, delta = 1) def test_Planimeter12(self): """Area of arctic circle (not really -- adjunct to rhumb-area test)""" points = [[66.562222222, 0], [66.562222222, 180], [66.562222222, 360]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 10465729, delta = 1) self.assertAlmostEqual(area, 0, delta = 1) def test_Planimeter12r(self): """Reverse area of arctic circle""" points = [[66.562222222, -0], [66.562222222, -180], [66.562222222, -360]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 10465729, delta = 1) self.assertAlmostEqual(area, 0, delta = 1) def test_Planimeter13(self): """Check encircling pole twice""" points = [[89,-360], [89,-240], [89,-120], [89,0], [89,120], [89,240]] _, perimeter, area = PlanimeterTest.Planimeter(points) self.assertAlmostEqual(perimeter, 1160741, delta = 1) self.assertAlmostEqual(area, 32415230256.0, delta = 1) def test_Planimeter15(self): """Coverage tests, includes Planimeter15 - Planimeter18 (combinations of reverse and sign) + calls to testpoint, testedge.""" lat = [2, 1, 3] lon = [1, 2, 3] r = 18454562325.45119 a0 = 510065621724088.5093 # ellipsoid area PlanimeterTest.polygon.Clear() PlanimeterTest.polygon.AddPoint(lat[0], lon[0]) PlanimeterTest.polygon.AddPoint(lat[1], lon[1]) _, _, area = PlanimeterTest.polygon.TestPoint(lat[2], lon[2], False, True) self.assertAlmostEqual(area, r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestPoint(lat[2], lon[2], False, False) self.assertAlmostEqual(area, r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestPoint(lat[2], lon[2], True, True) self.assertAlmostEqual(area, -r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestPoint(lat[2], lon[2], True, False) self.assertAlmostEqual(area, a0-r, delta = 0.5) inv = Geodesic.WGS84.Inverse(lat[1], lon[1], lat[2], lon[2]) azi1 = inv["azi1"] s12 = inv["s12"] _, _, area = PlanimeterTest.polygon.TestEdge(azi1, s12, False, True) self.assertAlmostEqual(area, r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestEdge(azi1, s12, False, False) self.assertAlmostEqual(area, r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestEdge(azi1, s12, True, True) self.assertAlmostEqual(area, -r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestEdge(azi1, s12, True, False) self.assertAlmostEqual(area, a0-r, delta = 0.5) PlanimeterTest.polygon.AddPoint(lat[2], lon[2]) _, _, area = PlanimeterTest.polygon.Compute(False, True) self.assertAlmostEqual(area, r, delta = 0.5) _, _, area = PlanimeterTest.polygon.Compute(False, False) self.assertAlmostEqual(area, r, delta = 0.5) _, _, area = PlanimeterTest.polygon.Compute(True, True) self.assertAlmostEqual(area, -r, delta = 0.5) _, _, area = PlanimeterTest.polygon.Compute(True, False) self.assertAlmostEqual(area, a0-r, delta = 0.5) def test_Planimeter19(self): """Coverage tests, includes Planimeter19 - Planimeter20 (degenerate polygons) + extra cases.""" PlanimeterTest.polygon.Clear() _, perimeter, area = PlanimeterTest.polygon.Compute(False, True) self.assertTrue(area == 0) self.assertTrue(perimeter == 0) _, perimeter, area = PlanimeterTest.polygon.TestPoint(1, 1, False, True) self.assertTrue(area == 0) self.assertTrue(perimeter == 0) _, perimeter, area = PlanimeterTest.polygon.TestEdge(90, 1000, False, True) self.assertTrue(math.isnan(area)) self.assertTrue(math.isnan(perimeter)) PlanimeterTest.polygon.AddPoint(1, 1) _, perimeter, area = PlanimeterTest.polygon.Compute(False, True) self.assertTrue(area == 0) self.assertTrue(perimeter == 0) PlanimeterTest.polyline.Clear() _, perimeter, area = PlanimeterTest.polyline.Compute(False, True) self.assertTrue(perimeter == 0) _, perimeter, area = PlanimeterTest.polyline.TestPoint(1, 1, False, True) self.assertTrue(perimeter == 0) _, perimeter, area = PlanimeterTest.polyline.TestEdge(90, 1000, False, True) self.assertTrue(math.isnan(perimeter)) PlanimeterTest.polyline.AddPoint(1, 1) _, perimeter, area = PlanimeterTest.polyline.Compute(False, True) self.assertTrue(perimeter == 0) PlanimeterTest.polygon.AddPoint(1, 1) _, perimeter, area = PlanimeterTest.polyline.TestEdge(90, 1000, False, True) self.assertAlmostEqual(perimeter, 1000, delta = 1e-10) _, perimeter, area = PlanimeterTest.polyline.TestPoint(2, 2, False, True) self.assertAlmostEqual(perimeter, 156876.149, delta = 0.5e-3) def test_Planimeter21(self): """Some tests to add code coverage: multiple circlings of pole (includes Planimeter21 - Planimeter28) + invocations via testpoint and testedge.""" lat = 45 azi = 39.2144607176828184218 s = 8420705.40957178156285 r = 39433884866571.4277 # Area for one circuit a0 = 510065621724088.5093 # Ellipsoid area PlanimeterTest.polygon.Clear() PlanimeterTest.polygon.AddPoint(lat, 60) PlanimeterTest.polygon.AddPoint(lat, 180) PlanimeterTest.polygon.AddPoint(lat, -60) PlanimeterTest.polygon.AddPoint(lat, 60) PlanimeterTest.polygon.AddPoint(lat, 180) PlanimeterTest.polygon.AddPoint(lat, -60) for i in [3, 4]: PlanimeterTest.polygon.AddPoint(lat, 60) PlanimeterTest.polygon.AddPoint(lat, 180) _, _, area = PlanimeterTest.polygon.TestPoint(lat, -60, False, True) self.assertAlmostEqual(area, i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestPoint(lat, -60, False, False) self.assertAlmostEqual(area, i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestPoint(lat, -60, True, True) self.assertAlmostEqual(area, -i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestPoint(lat, -60, True, False) self.assertAlmostEqual(area, -i*r + a0, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestEdge(azi, s, False, True) self.assertAlmostEqual(area, i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestEdge(azi, s, False, False) self.assertAlmostEqual(area, i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestEdge(azi, s, True, True) self.assertAlmostEqual(area, -i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.TestEdge(azi, s, True, False) self.assertAlmostEqual(area, -i*r + a0, delta = 0.5) PlanimeterTest.polygon.AddPoint(lat, -60) _, _, area = PlanimeterTest.polygon.Compute(False, True) self.assertAlmostEqual(area, i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.Compute(False, False) self.assertAlmostEqual(area, i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.Compute(True, True) self.assertAlmostEqual(area, -i*r, delta = 0.5) _, _, area = PlanimeterTest.polygon.Compute(True, False) self.assertAlmostEqual(area, -i*r + a0, delta = 0.5) def test_Planimeter29(self): """Check fix to transitdirect vs transit zero handling inconsistency""" PlanimeterTest.polygon.Clear() PlanimeterTest.polygon.AddPoint(0, 0) PlanimeterTest.polygon.AddEdge( 90, 1000) PlanimeterTest.polygon.AddEdge( 0, 1000) PlanimeterTest.polygon.AddEdge(-90, 1000) _, _, area = PlanimeterTest.polygon.Compute(False, True) # The area should be 1e6. Prior to the fix it was 1e6 - A/2, where # A = ellipsoid area. self.assertAlmostEqual(area, 1000000.0, delta = 0.01) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1647709966.0 geographiclib-2.0/geographiclib/test/test_sign.py0000664000771000077100000003256314215407416022236 0ustar00ckarneyckarney"""Geodesic tests""" import unittest import math import sys from geographiclib.geomath import Math from geographiclib.geodesic import Geodesic class SignTest(unittest.TestCase): """Sign test suite""" @staticmethod def equiv(x, y): """Test for equivalence""" return ( (math.isnan(x) and math.isnan(y)) or (x == y and math.copysign(1.0, x) == math.copysign(1.0, y)) ) def test_AngRound(self): """Test special cases for AngRound""" eps = sys.float_info.epsilon self.assertTrue(SignTest.equiv(Math.AngRound(-eps/32), -eps/32)) self.assertTrue(SignTest.equiv(Math.AngRound(-eps/64), -0.0 )) self.assertTrue(SignTest.equiv(Math.AngRound(- 0.0 ), -0.0 )) self.assertTrue(SignTest.equiv(Math.AngRound( 0.0 ), +0.0 )) self.assertTrue(SignTest.equiv(Math.AngRound( eps/64), +0.0 )) self.assertTrue(SignTest.equiv(Math.AngRound( eps/32), +eps/32)) self.assertTrue(SignTest.equiv(Math.AngRound((1-2*eps)/64), (1-2*eps)/64)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps )/64), 1.0 /64)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/2)/64), 1.0 /64)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/4)/64), 1.0 /64)) self.assertTrue(SignTest.equiv(Math.AngRound( 1.0 /64), 1.0 /64)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/2)/64), 1.0 /64)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps )/64), 1.0 /64)) self.assertTrue(SignTest.equiv(Math.AngRound((1+2*eps)/64), (1+2*eps)/64)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps )/32), (1-eps )/32)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/2)/32), 1.0 /32)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/4)/32), 1.0 /32)) self.assertTrue(SignTest.equiv(Math.AngRound( 1.0 /32), 1.0 /32)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/2)/32), 1.0 /32)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps )/32), (1+eps )/32)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps )/16), (1-eps )/16)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/2)/16), (1-eps/2)/16)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/4)/16), 1.0 /16)) self.assertTrue(SignTest.equiv(Math.AngRound( 1.0 /16), 1.0 /16)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/4)/16), 1.0 /16)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/2)/16), 1.0 /16)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps )/16), (1+eps )/16)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps )/ 8), (1-eps )/ 8)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/2)/ 8), (1-eps/2)/ 8)) self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/4)/ 8), 1.0 / 8)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/2)/ 8), 1.0 / 8)) self.assertTrue(SignTest.equiv(Math.AngRound((1+eps )/ 8), (1+eps )/ 8)) self.assertTrue(SignTest.equiv(Math.AngRound( 1-eps ), 1-eps )) self.assertTrue(SignTest.equiv(Math.AngRound( 1-eps/2 ), 1-eps/2 )) self.assertTrue(SignTest.equiv(Math.AngRound( 1-eps/4 ), 1 )) self.assertTrue(SignTest.equiv(Math.AngRound( 1.0 ), 1 )) self.assertTrue(SignTest.equiv(Math.AngRound( 1+eps/4 ), 1 )) self.assertTrue(SignTest.equiv(Math.AngRound( 1+eps/2 ), 1 )) self.assertTrue(SignTest.equiv(Math.AngRound( 1+eps ), 1+ eps )) self.assertTrue(SignTest.equiv(Math.AngRound( 90.0-64*eps), 90-64*eps )) self.assertTrue(SignTest.equiv(Math.AngRound( 90.0-32*eps), 90 )) self.assertTrue(SignTest.equiv(Math.AngRound( 90.0 ), 90 )) def test_sincosd(self): """Test special cases for sincosd""" inf = math.inf nan = math.nan s, c = Math.sincosd(- inf) self.assertTrue(SignTest.equiv(s, nan) and SignTest.equiv(c, nan)) s, c = Math.sincosd(-810.0) self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(-720.0) self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, +1.0)) s, c = Math.sincosd(-630.0) self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(-540.0) self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, -1.0)) s, c = Math.sincosd(-450.0) self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(-360.0) self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, +1.0)) s, c = Math.sincosd(-270.0) self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(-180.0) self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, -1.0)) s, c = Math.sincosd(- 90.0) self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(- 0.0) self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, +1.0)) s, c = Math.sincosd(+ 0.0) self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, +1.0)) s, c = Math.sincosd(+ 90.0) self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(+180.0) self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, -1.0)) s, c = Math.sincosd(+270.0) self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(+360.0) self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, +1.0)) s, c = Math.sincosd(+450.0) self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(+540.0) self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, -1.0)) s, c = Math.sincosd(+630.0) self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(+720.0) self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, +1.0)) s, c = Math.sincosd(+810.0) self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) s, c = Math.sincosd(+ inf) self.assertTrue(SignTest.equiv(s, nan) and SignTest.equiv(c, nan)) s, c = Math.sincosd( nan) self.assertTrue(SignTest.equiv(s, nan) and SignTest.equiv(c, nan)) def test_sincosd2(self): """Test accuracy of sincosd""" s1, c1 = Math.sincosd( 9.0) s2, c2 = Math.sincosd( 81.0) s3, c3 = Math.sincosd(-123456789.0) self.assertTrue(SignTest.equiv(s1, c2)) self.assertTrue(SignTest.equiv(s1, s3)) self.assertTrue(SignTest.equiv(c1, s2)) self.assertTrue(SignTest.equiv(c1,-c3)) def test_atan2d(self): """Test special cases for atan2d""" inf = math.inf nan = math.nan self.assertTrue(SignTest.equiv(Math.atan2d(+0.0 , -0.0 ), +180)) self.assertTrue(SignTest.equiv(Math.atan2d(-0.0 , -0.0 ), -180)) self.assertTrue(SignTest.equiv(Math.atan2d(+0.0 , +0.0 ), +0.0)) self.assertTrue(SignTest.equiv(Math.atan2d(-0.0 , +0.0 ), -0.0)) self.assertTrue(SignTest.equiv(Math.atan2d(+0.0 , -1.0 ), +180)) self.assertTrue(SignTest.equiv(Math.atan2d(-0.0 , -1.0 ), -180)) self.assertTrue(SignTest.equiv(Math.atan2d(+0.0 , +1.0 ), +0.0)) self.assertTrue(SignTest.equiv(Math.atan2d(-0.0 , +1.0 ), -0.0)) self.assertTrue(SignTest.equiv(Math.atan2d(-1.0 , +0.0 ), -90)) self.assertTrue(SignTest.equiv(Math.atan2d(-1.0 , -0.0 ), -90)) self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , +0.0 ), +90)) self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , -0.0 ), +90)) self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , -inf), +180)) self.assertTrue(SignTest.equiv(Math.atan2d(-1.0 , -inf), -180)) self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , +inf), +0.0)) self.assertTrue(SignTest.equiv(Math.atan2d(-1.0 , +inf), -0.0)) self.assertTrue(SignTest.equiv(Math.atan2d( +inf, +1.0 ), +90)) self.assertTrue(SignTest.equiv(Math.atan2d( +inf, -1.0 ), +90)) self.assertTrue(SignTest.equiv(Math.atan2d( -inf, +1.0 ), -90)) self.assertTrue(SignTest.equiv(Math.atan2d( -inf, -1.0 ), -90)) self.assertTrue(SignTest.equiv(Math.atan2d( +inf, -inf), +135)) self.assertTrue(SignTest.equiv(Math.atan2d( -inf, -inf), -135)) self.assertTrue(SignTest.equiv(Math.atan2d( +inf, +inf), +45)) self.assertTrue(SignTest.equiv(Math.atan2d( -inf, +inf), -45)) self.assertTrue(SignTest.equiv(Math.atan2d( nan, +1.0 ), nan)) self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , nan), nan)) def test_atan2d2(self): """Test accuracy of atan2d""" s = 7e-16 self.assertEqual(Math.atan2d(s, -1.0), 180 - Math.atan2d(s, 1.0)) def test_sum(self): """Test special cases of sum""" s,_ = Math.sum(+9.0, -9.0); self.assertTrue(SignTest.equiv(s, +0.0)) s,_ = Math.sum(-9.0, +9.0); self.assertTrue(SignTest.equiv(s, +0.0)) s,_ = Math.sum(-0.0, +0.0); self.assertTrue(SignTest.equiv(s, +0.0)) s,_ = Math.sum(+0.0, -0.0); self.assertTrue(SignTest.equiv(s, +0.0)) s,_ = Math.sum(-0.0, -0.0); self.assertTrue(SignTest.equiv(s, -0.0)) s,_ = Math.sum(+0.0, +0.0); self.assertTrue(SignTest.equiv(s, +0.0)) def test_AngNormalize(self): """Test special cases of AngNormalize""" self.assertTrue(SignTest.equiv(Math.AngNormalize(-900.0), -180)) self.assertTrue(SignTest.equiv(Math.AngNormalize(-720.0), -0.0)) self.assertTrue(SignTest.equiv(Math.AngNormalize(-540.0), -180)) self.assertTrue(SignTest.equiv(Math.AngNormalize(-360.0), -0.0)) self.assertTrue(SignTest.equiv(Math.AngNormalize(-180.0), -180)) self.assertTrue(SignTest.equiv(Math.AngNormalize( -0.0), -0.0)) self.assertTrue(SignTest.equiv(Math.AngNormalize( +0.0), +0.0)) self.assertTrue(SignTest.equiv(Math.AngNormalize( 180.0), +180)) self.assertTrue(SignTest.equiv(Math.AngNormalize( 360.0), +0.0)) self.assertTrue(SignTest.equiv(Math.AngNormalize( 540.0), +180)) self.assertTrue(SignTest.equiv(Math.AngNormalize( 720.0), +0.0)) self.assertTrue(SignTest.equiv(Math.AngNormalize( 900.0), +180)) def test_AngDiff(self): """Test special cases of AngDiff""" eps = sys.float_info.epsilon s,_ = Math.AngDiff(+ 0.0,+ 0.0); self.assertTrue(SignTest.equiv(s,+0.0 )) s,_ = Math.AngDiff(+ 0.0,- 0.0); self.assertTrue(SignTest.equiv(s,-0.0 )) s,_ = Math.AngDiff(- 0.0,+ 0.0); self.assertTrue(SignTest.equiv(s,+0.0 )) s,_ = Math.AngDiff(- 0.0,- 0.0); self.assertTrue(SignTest.equiv(s,+0.0 )) s,_ = Math.AngDiff(+ 5.0,+365.0); self.assertTrue(SignTest.equiv(s,+0.0 )) s,_ = Math.AngDiff(+365.0,+ 5.0); self.assertTrue(SignTest.equiv(s,-0.0 )) s,_ = Math.AngDiff(+ 5.0,+185.0); self.assertTrue(SignTest.equiv(s,+180.0)) s,_ = Math.AngDiff(+185.0,+ 5.0); self.assertTrue(SignTest.equiv(s,-180.0)) s,_ = Math.AngDiff( +eps ,+180.0); self.assertTrue(SignTest.equiv(s,+180.0)) s,_ = Math.AngDiff( -eps ,+180.0); self.assertTrue(SignTest.equiv(s,-180.0)) s,_ = Math.AngDiff( +eps ,-180.0); self.assertTrue(SignTest.equiv(s,+180.0)) s,_ = Math.AngDiff( -eps ,-180.0); self.assertTrue(SignTest.equiv(s,-180.0)) def test_AngDiff2(self): """Test accuracy of AngDiff""" eps = sys.float_info.epsilon x = 138 + 128 * eps; y = -164; s,_ = Math.AngDiff(x, y) self.assertEqual(s, 58 - 128 * eps) def test_equatorial_coincident(self): """ azimuth with coincident point on equator """ # lat1 lat2 azi1/2 C = [ [ +0.0, -0.0, 180 ], [ -0.0, +0.0, 0 ] ] for l in C: (lat1, lat2, azi) = l inv = Geodesic.WGS84.Inverse(lat1, 0.0, lat2, 0.0) self.assertTrue(SignTest.equiv(inv["azi1"], azi)) self.assertTrue(SignTest.equiv(inv["azi2"], azi)) def test_equatorial_NS(self): """Does the nearly antipodal equatorial solution go north or south?""" # lat1 lat2 azi1 azi2 C = [ [ +0.0, +0.0, 56, 124], [ -0.0, -0.0, 124, 56] ] for l in C: (lat1, lat2, azi1, azi2) = l inv = Geodesic.WGS84.Inverse(lat1, 0.0, lat2, 179.5) self.assertAlmostEqual(inv["azi1"], azi1, delta = 1) self.assertAlmostEqual(inv["azi2"], azi2, delta = 1) def test_antipodal(self): """How does the exact antipodal equatorial path go N/S + E/W""" # lat1 lat2 lon2 azi1 azi2 C = [ [ +0.0, +0.0, +180, +0.0, +180], [ -0.0, -0.0, +180, +180, +0.0], [ +0.0, +0.0, -180, -0.0, -180], [ -0.0, -0.0, -180, -180, -0.0] ] for l in C: (lat1, lat2, lon2, azi1, azi2) = l inv = Geodesic.WGS84.Inverse(lat1, 0.0, lat2, lon2) self.assertTrue(SignTest.equiv(inv["azi1"], azi1)) self.assertTrue(SignTest.equiv(inv["azi2"], azi2)) def test_antipodal_prolate(self): """Antipodal points on the equator with prolate ellipsoid""" # lon2 azi1/2 C = [ [ +180, +90 ], [ -180, -90 ] ] geod = Geodesic(6.4e6, -1/300.0) for l in C: (lon2, azi) = l inv = geod.Inverse(0.0, 0.0, 0.0, lon2) self.assertTrue(SignTest.equiv(inv["azi1"], azi)) self.assertTrue(SignTest.equiv(inv["azi2"], azi)) def test_azimuth_0_180(self): """azimuths = +/-0 and +/-180 for the direct problem""" # azi1, lon2, azi2 C = [ [ +0.0, +180, +180 ], [ -0.0, -180, -180 ], [ +180, +180, +0.0 ], [ -180, -180, -0.0 ] ] for l in C: (azi1, lon2, azi2) = l direct = Geodesic.WGS84.Direct(0.0, 0.0, azi1, 15e6, Geodesic.STANDARD | Geodesic.LONG_UNROLL) self.assertTrue(SignTest.equiv(direct["lon2"], lon2)) self.assertTrue(SignTest.equiv(direct["azi2"], azi2)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1650718633.7976391 geographiclib-2.0/geographiclib.egg-info/0000775000771000077100000000000014230773652020374 5ustar00ckarneyckarney././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650718633.0 geographiclib-2.0/geographiclib.egg-info/PKG-INFO0000664000771000077100000000255114230773651021473 0ustar00ckarneyckarneyMetadata-Version: 2.1 Name: geographiclib Version: 2.0 Summary: The geodesic routines from GeographicLib Home-page: https://geographiclib.sourceforge.io/Python/2.0 Author: Charles Karney Author-email: charles@karney.com License: MIT Keywords: gis geographical earth distance geodesic Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering :: GIS Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: LICENSE # Python implementation of the geodesic routines in GeographicLib This is a library to solve geodesic problems on an ellipsoid model of the earth. Licensed under the MIT/X11 License; see [LICENSE.txt](https://geographiclib.sourceforge.io/LICENSE.txt). The algorithms are documented in * C. F. F. Karney, [Algorithms for geodesics](https://doi.org/10.1007/s00190-012-0578-z), J. Geodesy **87**(1), 43–55 (2013); [Addenda](https://geographiclib.sourceforge.io/geod-addenda.html). The documentation for this package is in https://geographiclib.sourceforge.io/Python/doc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650718633.0 geographiclib-2.0/geographiclib.egg-info/SOURCES.txt0000664000771000077100000000100114230773651022247 0ustar00ckarneyckarneyLICENSE MANIFEST.in README.md setup.py geographiclib/__init__.py geographiclib/accumulator.py geographiclib/constants.py geographiclib/geodesic.py geographiclib/geodesiccapability.py geographiclib/geodesicline.py geographiclib/geomath.py geographiclib/polygonarea.py geographiclib.egg-info/PKG-INFO geographiclib.egg-info/SOURCES.txt geographiclib.egg-info/dependency_links.txt geographiclib.egg-info/top_level.txt geographiclib/test/__init__.py geographiclib/test/test_geodesic.py geographiclib/test/test_sign.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650718633.0 geographiclib-2.0/geographiclib.egg-info/dependency_links.txt0000664000771000077100000000000114230773651024441 0ustar00ckarneyckarney ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650718633.0 geographiclib-2.0/geographiclib.egg-info/top_level.txt0000664000771000077100000000001614230773651023122 0ustar00ckarneyckarneygeographiclib ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1650718633.7976391 geographiclib-2.0/setup.cfg0000664000771000077100000000004614230773652015724 0ustar00ckarneyckarney[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650718628.0 geographiclib-2.0/setup.py0000664000771000077100000000305314230773644015617 0ustar00ckarneyckarney# setup.py, config file for distutils # # To install this package, execute # # python setup.py install # # in this directory. To run the unit tests, execute # # python setup.py test # # To update the HTML page for this version, run # # python setup.py register # # To upload the latest version to the python repository, run # # python setup.py sdist --formats gztar,zip upload # # The initial version of this file was provided by # Andrew MacIntyre . import setuptools name = "geographiclib" version = "2.0" with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name = name, version = version, author = "Charles Karney", author_email = "charles@karney.com", description = "The geodesic routines from GeographicLib", long_description = long_description, long_description_content_type = "text/markdown", url = "https://geographiclib.sourceforge.io/Python/2.0" , include_package_data = True, packages = setuptools.find_packages(), license = "MIT", keywords = "gis geographical earth distance geodesic", classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Scientific/Engineering :: GIS", "Topic :: Software Development :: Libraries :: Python Modules", ], test_suite = "geographiclib.test.test_geodesic", python_requires = ">=3.7", )