1
2# -*- coding: utf-8 -*-
3
4u'''Ellipsoidal, I{Karney}-based geodesy.
5
6Ellipsoidal geodetic (lat-/longitude) L{LatLon} and geocentric
7(ECEF) L{Cartesian} classes and functions L{areaOf}, L{intersections2},
8L{isclockwise}, L{nearestOn} and L{perimeterOf}, all requiring I{Charles
9Karney}'s U{geographiclib <https://PyPI.org/project/geographiclib>}
10Python package to be installed.
11
12Here's an example usage of C{ellipsoidalKarney}:
13
14    >>> from pygeodesy.ellipsoidalKarney import LatLon
15    >>> Newport_RI = LatLon(41.49008, -71.312796)
16    >>> Cleveland_OH = LatLon(41.499498, -81.695391)
17    >>> Newport_RI.distanceTo(Cleveland_OH)
18    866,455.4329098687  # meter
19
20You can change the ellipsoid model used by the I{Karney} formulae
21as follows:
22
23    >>> from pygeodesy import Datums
24    >>> from pygeodesy.ellipsoidalKarney import LatLon
25    >>> p = LatLon(0, 0, datum=Datums.OSGB36)
26
27or by converting to anothor datum:
28
29    >>> p = p.toDatum(Datums.OSGB36)
30'''
31
32from pygeodesy.datums import _WGS84
33from pygeodesy.ellipsoidalBase import CartesianEllipsoidalBase
34from pygeodesy.ellipsoidalBaseDI import LatLonEllipsoidalBaseDI, \
35                                       _intersection3, _intersections2, \
36                                       _nearestOn
37# from pygeodesy.errors import _xkwds  # from .karney
38from pygeodesy.karney import _polygon, _TOL_M, _xkwds
39from pygeodesy.lazily import _ALL_LAZY, _ALL_OTHER
40from pygeodesy.points import _areaError, ispolar  # PYCHOK exported
41from pygeodesy.props import deprecated_method, Property_RO
42# from pygeodesy.units import _1mm as _TOL_M  # from .karney
43
44__all__ = _ALL_LAZY.ellipsoidalKarney
45__version__ = '21.09.12'
46
47
48class Cartesian(CartesianEllipsoidalBase):
49    '''Extended to convert C{Karney}-based L{Cartesian} to
50       C{Karney}-based L{LatLon} points.
51    '''
52
53    def toLatLon(self, **LatLon_and_kwds):  # PYCHOK LatLon=LatLon, datum=None
54        '''Convert this cartesian point to a C{Karney}-based geodetic point.
55
56           @kwarg LatLon_and_kwds: Optional L{LatLon} and L{LatLon} keyword
57                                   arguments as C{datum}.  Use C{B{LatLon}=...,
58                                   B{datum}=...} to override this L{LatLon}
59                                   class or specify C{B{LatLon}=None}.
60
61           @return: The geodetic point (L{LatLon}) or if B{C{LatLon}} is C{None},
62                    an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)}
63                    with C{C} and C{M} if available.
64
65           @raise TypeError: Invalid B{C{LatLon_and_kwds}} argument.
66        '''
67        kwds = _xkwds(LatLon_and_kwds, LatLon=LatLon, datum=self.datum)
68        return CartesianEllipsoidalBase.toLatLon(self, **kwds)
69
70
71class LatLon(LatLonEllipsoidalBaseDI):
72    '''An ellipsoidal L{LatLon} similar to L{ellipsoidalVincenty.LatLon}
73       but using I{Charles F. F. Karney}'s Python U{geographiclib
74       <https://PyPI.org/project/geographiclib>} to compute the geodesic
75       distance, initial and final bearing (azimuths) between two given
76       points or the destination point given a start point and an (initial)
77       bearing.
78
79       @note: This L{LatLon} require the U{geographiclib
80              <https://PyPI.org/project/geographiclib>} package.
81    '''
82
83    @deprecated_method
84    def bearingTo(self, other, wrap=False):  # PYCHOK no cover
85        '''DEPRECATED, use method L{initialBearingTo}.
86        '''
87        return self.initialBearingTo(other, wrap=wrap)
88
89    @Property_RO
90    def Equidistant(self):
91        '''Get the prefered azimuthal equidistant projection I{class} (L{EquidistantKarney}).
92        '''
93        from pygeodesy.azimuthal import EquidistantKarney
94        return EquidistantKarney
95
96    @Property_RO
97    def geodesic(self):
98        '''Get this C{LatLon}'s I{wrapped} U{Karney Geodesic
99           <https://GeographicLib.SourceForge.io/html/python/code.html>},
100           provided package U{geographiclib
101           <https://PyPI.org/project/geographiclib>} is installed.
102        '''
103        return self.datum.ellipsoid.geodesic
104
105    def toCartesian(self, **Cartesian_datum_kwds):  # PYCHOK Cartesian=Cartesian, datum=None
106        '''Convert this point to C{Karney}-based cartesian (ECEF) coordinates.
107
108           @kwarg Cartesian_datum_kwds: Optional L{Cartesian}, B{C{datum}}
109                  and other keyword arguments, ignored if C{B{Cartesian} is None}.
110                  Use C{B{Cartesian}=...} to override this L{Cartesian} class
111                  or set C{B{Cartesian} is None}.
112
113           @return: The cartesian (ECEF) coordinates (L{Cartesian}) or if
114                    B{C{Cartesian}} is C{None}, an L{Ecef9Tuple}C{(x, y, z,
115                    lat, lon, height, C, M, datum)} with C{C} and C{M} if
116                    available.
117
118           @raise TypeError: Invalid B{C{Cartesian}}, B{C{datum}} or other
119                             B{C{Cartesian_datum_kwds}}.
120        '''
121        kwds = _xkwds(Cartesian_datum_kwds, Cartesian=Cartesian, datum=self.datum)
122        return LatLonEllipsoidalBaseDI.toCartesian(self, **kwds)
123
124
125def areaOf(points, datum=_WGS84, wrap=True):
126    '''Compute the area of an (ellipsoidal) polygon.
127
128       @arg points: The polygon points (L{LatLon}[]).
129       @kwarg datum: Optional datum (L{Datum}).
130       @kwarg wrap: Wrap and unroll longitudes (C{bool}).
131
132       @return: Area (C{meter}, same as units of the
133                B{C{datum}}'s ellipsoid axes, I{squared}).
134
135       @raise ImportError: Package U{geographiclib
136                           <https://PyPI.org/project/geographiclib>}
137                           not installed or not found.
138
139       @raise PointsError: Insufficient number of B{C{points}}.
140
141       @raise TypeError: Some B{C{points}} are not L{LatLon}.
142
143       @raise ValueError: Invalid C{B{wrap}=False}, unwrapped,
144                          unrolled longitudes not supported.
145
146       @note: This function requires the U{geographiclib
147              <https://PyPI.org/project/geographiclib>} package.
148
149       @see: L{pygeodesy.areaOf}, L{ellipsoidalExact.areaOf},
150             L{ellipsoidalGeodSolve.areaOf}, L{sphericalNvector.areaOf}
151             and L{sphericalTrigonometry.areaOf}.
152    '''
153    return abs(_polygon(datum.ellipsoid.geodesic, points, True, False, wrap))
154
155
156def intersection3(start1, end1, start2, end2, height=None, wrap=True,
157                  equidistant=None, tol=_TOL_M, LatLon=LatLon, **LatLon_kwds):
158    '''Interatively compute the intersection point of two paths, each defined
159       by two (ellipsoidal) points or by an (ellipsoidal) start point and a
160       bearing from North.
161
162       @arg start1: Start point of the first path (L{LatLon}).
163       @arg end1: End point of the first path (L{LatLon}) or the initial bearing
164                  at the first point (compass C{degrees360}).
165       @arg start2: Start point of the second path (L{LatLon}).
166       @arg end2: End point of the second path (L{LatLon}) or the initial bearing
167                  at the second point (compass C{degrees360}).
168       @kwarg height: Optional height at the intersection (C{meter}, conventionally)
169                      or C{None} for the mean height.
170       @kwarg wrap: Wrap and unroll longitudes (C{bool}).
171       @kwarg equidistant: An azimuthal equidistant projection (I{class} or function
172                           L{equidistant}) or C{None} for the preferred
173                           C{B{start1}.Equidistant}.
174       @kwarg tol: Tolerance for convergence and for skew line distance and length
175                   (C{meter}, conventionally).
176       @kwarg LatLon: Optional class to return the intersection points (L{LatLon})
177                      or C{None}.
178       @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword arguments,
179                           ignored if C{B{LatLon} is None}.
180
181       @return: An L{Intersection3Tuple}C{(point, outside1, outside2)} with C{point}
182                a B{C{LatLon}} or if C{B{LatLon} is None}, a L{LatLon4Tuple}C{(lat,
183                lon, height, datum)}.
184
185       @raise IntersectionError: Skew, colinear, parallel or otherwise
186                                 non-intersecting paths or no convergence
187                                 for the given B{C{tol}}.
188
189       @raise TypeError: Invalid or non-ellipsoidal B{C{start1}}, B{C{end1}},
190                         B{C{start2}} or B{C{end2}} or invalid B{C{equidistant}}.
191
192       @note: For each path specified with an initial bearing, a pseudo-end point
193              is computed as the C{destination} along that bearing at about 1.5
194              times the distance from the start point to an initial gu-/estimate
195              of the intersection point (and between 1/8 and 3/8 of the authalic
196              earth perimeter).
197    '''
198    return _intersection3(start1, end1, start2, end2, height=height, wrap=wrap,
199                          equidistant=equidistant, tol=tol, LatLon=LatLon, **LatLon_kwds)
200
201
202def intersections2(center1, radius1, center2, radius2, height=None, wrap=True,
203                   equidistant=None, tol=_TOL_M, LatLon=LatLon, **LatLon_kwds):
204    '''Iteratively compute the intersection points of two circles, each defined
205       by an (ellipsoidal) center point and a radius.
206
207       @arg center1: Center of the first circle (L{LatLon}).
208       @arg radius1: Radius of the first circle (C{meter}, conventionally).
209       @arg center2: Center of the second circle (L{LatLon}).
210       @arg radius2: Radius of the second circle (C{meter}, same units as
211                     B{C{radius1}}).
212       @kwarg height: Optional height for the intersection points (C{meter},
213                      conventionally) or C{None} for the I{"radical height"}
214                      at the I{radical line} between both centers.
215       @kwarg wrap: Wrap and unroll longitudes (C{bool}).
216       @kwarg equidistant: An azimuthal equidistant projection (I{class} or
217                           function L{equidistant}) or C{None} for the preferred
218                           C{B{center1}.Equidistant}.
219       @kwarg tol: Convergence tolerance (C{meter}, same units as B{C{radius1}}
220                   and B{C{radius2}}).
221       @kwarg LatLon: Optional class to return the intersection points (L{LatLon})
222                      or C{None}.
223       @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword arguments,
224                           ignored if C{B{LatLon} is None}.
225
226       @return: 2-Tuple of the intersection points, each a B{C{LatLon}} instance
227                or L{LatLon4Tuple}C{(lat, lon, height, datum)} if C{B{LatLon} is
228                None}.  For abutting circles, both points are the same instance,
229                aka the I{radical center}.
230
231       @raise IntersectionError: Concentric, antipodal, invalid or non-intersecting
232                                 circles or no convergence for the B{C{tol}}.
233
234       @raise TypeError: Invalid or non-ellipsoidal B{C{center1}} or B{C{center2}}
235                         or invalid B{C{equidistant}}.
236
237       @raise UnitError: Invalid B{C{radius1}}, B{C{radius2}} or B{C{height}}.
238
239       @see: U{The B{ellipsoidal} case<https://GIS.StackExchange.com/questions/48937/
240             calculating-intersection-of-two-circles>}, U{Karney's paper
241             <https://ArXiv.org/pdf/1102.1215.pdf>}, pp 20-21, section B{14. MARITIME BOUNDARIES},
242             U{circle-circle<https://MathWorld.Wolfram.com/Circle-CircleIntersection.html>} and
243             U{sphere-sphere<https://MathWorld.Wolfram.com/Sphere-SphereIntersection.html>}
244             intersections.
245    '''
246    return _intersections2(center1, radius1, center2, radius2, height=height, wrap=wrap,
247                           equidistant=equidistant, tol=tol, LatLon=LatLon, **LatLon_kwds)
248
249
250def isclockwise(points, datum=_WGS84, wrap=True):
251    '''Determine the direction of a path or polygon.
252
253       @arg points: The path or polygon points (C{LatLon}[]).
254       @kwarg datum: Optional datum (L{Datum}).
255       @kwarg wrap: Wrap and unroll longitudes (C{bool}).
256
257       @return: C{True} if B{C{points}} are clockwise, C{False} otherwise.
258
259       @raise ImportError: Package U{geographiclib
260                           <https://PyPI.org/project/geographiclib>}
261                           not installed or not found.
262
263       @raise PointsError: Insufficient number of B{C{points}}.
264
265       @raise TypeError: Some B{C{points}} are not C{LatLon}.
266
267       @raise ValueError: The B{C{points}} enclose a pole or zero
268                          area.
269
270       @note: This function requires the U{geographiclib
271              <https://PyPI.org/project/geographiclib>} package.
272
273       @see: L{pygeodesy.isclockwise}.
274    '''
275    a = _polygon(datum.ellipsoid.geodesic, points, True, False, wrap)
276    if a > 0:
277        return True
278    elif a < 0:
279        return False
280    raise _areaError(points)
281
282
283def nearestOn(point, point1, point2, within=True, height=None, wrap=False,
284              equidistant=None, tol=_TOL_M, LatLon=LatLon, **LatLon_kwds):
285    '''Iteratively locate the closest point on the arc between two
286       other (ellipsoidal) points.
287
288       @arg point: Reference point (C{LatLon}).
289       @arg point1: Start point of the arc (C{LatLon}).
290       @arg point2: End point of the arc (C{LatLon}).
291       @kwarg within: If C{True} return the closest point I{between}
292                      B{C{point1}} and B{C{point2}}, otherwise the
293                      closest point elsewhere on the arc (C{bool}).
294       @kwarg height: Optional height for the closest point (C{meter},
295                      conventionally) or C{None} or C{False} for the
296                      interpolated height.  If C{False}, the distance
297                      between points takes height into account.
298       @kwarg wrap: Wrap and unroll longitudes (C{bool}).
299       @kwarg equidistant: An azimuthal equidistant projection (I{class}
300                           or function L{equidistant}) or C{None} for
301                           the preferred C{B{point}.Equidistant}.
302       @kwarg tol: Convergence tolerance (C{meter}).
303       @kwarg LatLon: Optional class to return the closest point
304                      (L{LatLon}) or C{None}.
305       @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
306                           arguments, ignored if C{B{LatLon} is None}.
307
308       @return: Closest point, a B{C{LatLon}} instance or if C{B{LatLon}
309                is None}, a L{LatLon4Tuple}C{(lat, lon, height, datum)}.
310
311       @raise ImportError: Package U{geographiclib
312                           <https://PyPI.org/project/geographiclib>}
313                           not installed or not found.
314
315       @raise TypeError: Invalid or non-ellipsoidal B{C{point}}, B{C{point1}}
316                         or B{C{point2}} or invalid B{C{equidistant}}.
317
318       @raise ValueError: No convergence for the B{C{tol}}.
319    '''
320    return _nearestOn(point, point1, point2, within=within, height=height, wrap=wrap,
321                      equidistant=equidistant, tol=tol, LatLon=LatLon, **LatLon_kwds)
322
323
324def perimeterOf(points, closed=False, datum=_WGS84, wrap=True):
325    '''Compute the perimeter of an (ellipsoidal) polygon.
326
327       @arg points: The polygon points (L{LatLon}[]).
328       @kwarg closed: Optionally, close the polygon (C{bool}).
329       @kwarg datum: Optional datum (L{Datum}).
330       @kwarg wrap: Wrap and unroll longitudes (C{bool}).
331
332       @return: Perimeter (C{meter}, same as units of the
333                B{C{datum}}'s ellipsoid axes).
334
335       @raise ImportError: Package U{geographiclib
336                           <https://PyPI.org/project/geographiclib>}
337                           not installed or not found.
338
339       @raise PointsError: Insufficient number of B{C{points}}.
340
341       @raise TypeError: Some B{C{points}} are not L{LatLon}.
342
343       @raise ValueError: Invalid C{B{wrap}=False}, unwrapped,
344                          unrolled longitudes not supported.
345
346       @note: This function requires the U{geographiclib
347              <https://PyPI.org/project/geographiclib>} package.
348
349       @see: L{pygeodesy.perimeterOf}, L{ellipsoidalExact.perimeterOf},
350             L{ellipsoidalGeodSolve.perimeterOf}, L{sphericalNvector.perimeterOf}
351             and L{sphericalTrigonometry.perimeterOf}.
352    '''
353    return _polygon(datum.ellipsoid.geodesic, points, closed, True, wrap)
354
355
356__all__ += _ALL_OTHER(Cartesian, LatLon,  # classes
357                      areaOf,  # functions
358                      intersection3, intersections2, isclockwise, ispolar,
359                      nearestOn, perimeterOf)
360
361# **) MIT License
362#
363# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved.
364#
365# Permission is hereby granted, free of charge, to any person obtaining a
366# copy of this software and associated documentation files (the "Software"),
367# to deal in the Software without restriction, including without limitation
368# the rights to use, copy, modify, merge, publish, distribute, sublicense,
369# and/or sell copies of the Software, and to permit persons to whom the
370# Software is furnished to do so, subject to the following conditions:
371#
372# The above copyright notice and this permission notice shall be included
373# in all copies or substantial portions of the Software.
374#
375# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
376# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
377# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
378# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
379# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
380# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
381# OTHER DEALINGS IN THE SOFTWARE.
382