pygeodesy 24.3.24__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyGeodesy-24.3.24.dist-info/METADATA +272 -0
- PyGeodesy-24.3.24.dist-info/RECORD +115 -0
- PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
- PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
- pygeodesy/LICENSE +21 -0
- pygeodesy/__init__.py +615 -0
- pygeodesy/__main__.py +103 -0
- pygeodesy/albers.py +867 -0
- pygeodesy/auxilats/_CX_4.py +218 -0
- pygeodesy/auxilats/_CX_6.py +314 -0
- pygeodesy/auxilats/_CX_8.py +475 -0
- pygeodesy/auxilats/__init__.py +54 -0
- pygeodesy/auxilats/__main__.py +86 -0
- pygeodesy/auxilats/auxAngle.py +548 -0
- pygeodesy/auxilats/auxDLat.py +302 -0
- pygeodesy/auxilats/auxDST.py +296 -0
- pygeodesy/auxilats/auxLat.py +848 -0
- pygeodesy/auxilats/auxily.py +272 -0
- pygeodesy/azimuthal.py +1150 -0
- pygeodesy/basics.py +892 -0
- pygeodesy/booleans.py +2031 -0
- pygeodesy/cartesianBase.py +1062 -0
- pygeodesy/clipy.py +704 -0
- pygeodesy/constants.py +516 -0
- pygeodesy/css.py +660 -0
- pygeodesy/datums.py +752 -0
- pygeodesy/deprecated/__init__.py +61 -0
- pygeodesy/deprecated/bases.py +40 -0
- pygeodesy/deprecated/classes.py +262 -0
- pygeodesy/deprecated/consterns.py +54 -0
- pygeodesy/deprecated/datum.py +40 -0
- pygeodesy/deprecated/functions.py +375 -0
- pygeodesy/deprecated/nvector.py +48 -0
- pygeodesy/deprecated/rhumbBase.py +32 -0
- pygeodesy/deprecated/rhumbaux.py +33 -0
- pygeodesy/deprecated/rhumbsolve.py +33 -0
- pygeodesy/deprecated/rhumbx.py +33 -0
- pygeodesy/dms.py +986 -0
- pygeodesy/ecef.py +1348 -0
- pygeodesy/elevations.py +279 -0
- pygeodesy/ellipsoidalBase.py +1224 -0
- pygeodesy/ellipsoidalBaseDI.py +913 -0
- pygeodesy/ellipsoidalExact.py +343 -0
- pygeodesy/ellipsoidalGeodSolve.py +343 -0
- pygeodesy/ellipsoidalKarney.py +403 -0
- pygeodesy/ellipsoidalNvector.py +685 -0
- pygeodesy/ellipsoidalVincenty.py +590 -0
- pygeodesy/ellipsoids.py +2476 -0
- pygeodesy/elliptic.py +1198 -0
- pygeodesy/epsg.py +243 -0
- pygeodesy/errors.py +804 -0
- pygeodesy/etm.py +1190 -0
- pygeodesy/fmath.py +1013 -0
- pygeodesy/formy.py +1818 -0
- pygeodesy/frechet.py +865 -0
- pygeodesy/fstats.py +760 -0
- pygeodesy/fsums.py +1898 -0
- pygeodesy/gars.py +358 -0
- pygeodesy/geodesicw.py +581 -0
- pygeodesy/geodesicx/_C4_24.py +1699 -0
- pygeodesy/geodesicx/_C4_27.py +2395 -0
- pygeodesy/geodesicx/_C4_30.py +3301 -0
- pygeodesy/geodesicx/__init__.py +48 -0
- pygeodesy/geodesicx/__main__.py +91 -0
- pygeodesy/geodesicx/gx.py +1382 -0
- pygeodesy/geodesicx/gxarea.py +535 -0
- pygeodesy/geodesicx/gxbases.py +154 -0
- pygeodesy/geodesicx/gxline.py +669 -0
- pygeodesy/geodsolve.py +426 -0
- pygeodesy/geohash.py +914 -0
- pygeodesy/geoids.py +1884 -0
- pygeodesy/hausdorff.py +892 -0
- pygeodesy/heights.py +1155 -0
- pygeodesy/interns.py +687 -0
- pygeodesy/iters.py +545 -0
- pygeodesy/karney.py +919 -0
- pygeodesy/ktm.py +633 -0
- pygeodesy/latlonBase.py +1766 -0
- pygeodesy/lazily.py +960 -0
- pygeodesy/lcc.py +684 -0
- pygeodesy/ltp.py +1107 -0
- pygeodesy/ltpTuples.py +1563 -0
- pygeodesy/mgrs.py +721 -0
- pygeodesy/named.py +1324 -0
- pygeodesy/namedTuples.py +683 -0
- pygeodesy/nvectorBase.py +695 -0
- pygeodesy/osgr.py +781 -0
- pygeodesy/points.py +1686 -0
- pygeodesy/props.py +628 -0
- pygeodesy/resections.py +1048 -0
- pygeodesy/rhumb/__init__.py +46 -0
- pygeodesy/rhumb/aux_.py +397 -0
- pygeodesy/rhumb/bases.py +1148 -0
- pygeodesy/rhumb/ekx.py +563 -0
- pygeodesy/rhumb/solve.py +572 -0
- pygeodesy/simplify.py +647 -0
- pygeodesy/solveBase.py +472 -0
- pygeodesy/sphericalBase.py +724 -0
- pygeodesy/sphericalNvector.py +1264 -0
- pygeodesy/sphericalTrigonometry.py +1447 -0
- pygeodesy/streprs.py +627 -0
- pygeodesy/trf.py +2079 -0
- pygeodesy/triaxials.py +1484 -0
- pygeodesy/units.py +969 -0
- pygeodesy/unitsBase.py +349 -0
- pygeodesy/ups.py +538 -0
- pygeodesy/utily.py +1231 -0
- pygeodesy/utm.py +762 -0
- pygeodesy/utmups.py +318 -0
- pygeodesy/utmupsBase.py +517 -0
- pygeodesy/vector2d.py +785 -0
- pygeodesy/vector3d.py +968 -0
- pygeodesy/vector3dBase.py +1049 -0
- pygeodesy/webmercator.py +383 -0
- pygeodesy/wgrs.py +439 -0
pygeodesy/points.py
ADDED
|
@@ -0,0 +1,1686 @@
|
|
|
1
|
+
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
u'''Utilities for point lists, tuples, etc.
|
|
5
|
+
|
|
6
|
+
Functions to handle collections and sequences of C{LatLon} points
|
|
7
|
+
specified as 2-d U{NumPy<https://www.NumPy.org>}, C{arrays} or tuples
|
|
8
|
+
as C{LatLon} or as C{pseudo-x/-y} pairs.
|
|
9
|
+
|
|
10
|
+
C{NumPy} arrays are assumed to contain rows of points with a lat-, a
|
|
11
|
+
longitude -and possibly other- values in different columns. While
|
|
12
|
+
iterating over the array rows, create an instance of a given C{LatLon}
|
|
13
|
+
class "on-the-fly" for each row with the row's lat- and longitude.
|
|
14
|
+
|
|
15
|
+
The original C{NumPy} array is read-accessed only and never duplicated,
|
|
16
|
+
except to return a I{subset} of the original array.
|
|
17
|
+
|
|
18
|
+
For example, to process a C{NumPy} array, wrap the array by instantiating
|
|
19
|
+
class L{Numpy2LatLon} and specifying the column index for the lat- and
|
|
20
|
+
longitude in each row. Then, pass the L{Numpy2LatLon} instance to any
|
|
21
|
+
L{pygeodesy} function or method accepting a I{points} argument.
|
|
22
|
+
|
|
23
|
+
Similarly, class L{Tuple2LatLon} is used to instantiate a C{LatLon} from
|
|
24
|
+
each 2+tuple in a sequence of such 2+tuples using the C{ilat} lat- and
|
|
25
|
+
C{ilon} longitude index in each 2+tuple.
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
from pygeodesy.basics import isclass, isint, isscalar, issequence, \
|
|
29
|
+
issubclassof, _Sequence, _xcopy, _xdup, \
|
|
30
|
+
_xinstanceof
|
|
31
|
+
from pygeodesy.constants import EPS, EPS1, PI_2, R_M, isnear0, isnear1, \
|
|
32
|
+
_umod_360, _0_0, _0_5, _1_0, _2_0, _6_0, \
|
|
33
|
+
_90_0, _N_90_0, _180_0, _360_0
|
|
34
|
+
# from pygeodesy.datums import _spherical_datum # from .formy
|
|
35
|
+
from pygeodesy.dms import F_D, parseDMS
|
|
36
|
+
from pygeodesy.errors import CrossError, crosserrors, _IndexError, \
|
|
37
|
+
_IsnotError, _TypeError, _ValueError, \
|
|
38
|
+
_xattr, _xkwds, _xkwds_item2, _xkwds_pop2
|
|
39
|
+
from pygeodesy.fmath import favg, fdot, hypot, Fsum, fsum
|
|
40
|
+
# from pygeodesy.fsums import Fsum, fsum # from .fmath
|
|
41
|
+
from pygeodesy.formy import _bearingTo2, equirectangular_, _spherical_datum
|
|
42
|
+
from pygeodesy.interns import NN, _colinear_, _COMMASPACE_, _composite_, \
|
|
43
|
+
_DEQUALSPACED_, _ELLIPSIS_, _EW_, _immutable_, \
|
|
44
|
+
_near_, _no_, _not_, _NS_, _point_, _SPACE_, \
|
|
45
|
+
_UNDER_, _valid_ # _lat_, _lon_
|
|
46
|
+
from pygeodesy.iters import LatLon2PsxyIter, PointsIter, points2
|
|
47
|
+
from pygeodesy.latlonBase import LatLonBase, _latlonheight3, \
|
|
48
|
+
_ALL_DOCS, _ALL_LAZY, _MODS
|
|
49
|
+
# from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
|
|
50
|
+
from pygeodesy.named import classname, nameof, notImplemented, notOverloaded, \
|
|
51
|
+
_NamedTuple
|
|
52
|
+
from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, LatLon2Tuple, \
|
|
53
|
+
NearestOn3Tuple, NearestOn5Tuple, \
|
|
54
|
+
Point3Tuple, Vector3Tuple, \
|
|
55
|
+
PhiLam2Tuple # PYCHOK shared
|
|
56
|
+
from pygeodesy.props import Property_RO, property_doc_, property_RO
|
|
57
|
+
from pygeodesy.streprs import Fmt, instr
|
|
58
|
+
from pygeodesy.units import Number_, Radius, Scalar, Scalar_
|
|
59
|
+
from pygeodesy.utily import atan2b, degrees90, degrees180, degrees2m, \
|
|
60
|
+
unroll180, _unrollon, unrollPI, _Wrap, wrap180
|
|
61
|
+
|
|
62
|
+
from math import cos, fabs, fmod, radians, sin
|
|
63
|
+
|
|
64
|
+
__all__ = _ALL_LAZY.points
|
|
65
|
+
__version__ = '24.03.12'
|
|
66
|
+
|
|
67
|
+
_ilat_ = 'ilat'
|
|
68
|
+
_ilon_ = 'ilon'
|
|
69
|
+
_ncols_ = 'ncols'
|
|
70
|
+
_nrows_ = 'nrows'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LatLon_(LatLonBase): # XXX in heights._HeightBase.height
|
|
74
|
+
'''Low-overhead C{LatLon} class, mainly for L{Numpy2LatLon} and L{Tuple2LatLon}.
|
|
75
|
+
'''
|
|
76
|
+
# __slots__ efficiency is voided if the __slots__ class attribute is
|
|
77
|
+
# used in a subclass of a class with the traditional __dict__, @see
|
|
78
|
+
# <https://docs.Python.org/2/reference/datamodel.html#slots> plus ...
|
|
79
|
+
#
|
|
80
|
+
# __slots__ must be repeated in sub-classes, @see Luciano Ramalho,
|
|
81
|
+
# "Fluent Python", O'Reilly, 2016 p. 276+ "Problems with __slots__",
|
|
82
|
+
# 2nd Ed, 2022 p. 390 "Summarizing the Issues with __slots__".
|
|
83
|
+
#
|
|
84
|
+
# __slots__ = (_lat_, _lon_, _height_, _datum_, _name_)
|
|
85
|
+
# Property_RO = property_RO # no __dict__ with __slots__!
|
|
86
|
+
#
|
|
87
|
+
# In addition, both size and overhead have shrunk in recent Python:
|
|
88
|
+
#
|
|
89
|
+
# sys.getsizeof(LatLon_(1, 2)) is 72-88 I{with} __slots__, but
|
|
90
|
+
# only 48-56 bytes I{without in Python 2.7.18+ and Python 3+}.
|
|
91
|
+
#
|
|
92
|
+
# python3 -m timeit -s "from pygeodesy... import LatLonBase as LL" "LL(0, 0)" 2.14 usec
|
|
93
|
+
# python3 -m timeit -s "from pygeodesy import LatLon_" "LatLon_(0, 0)" 216 nsec
|
|
94
|
+
|
|
95
|
+
def __init__(self, latlonh, lon=None, height=0, wrap=False, name=NN, datum=None):
|
|
96
|
+
'''New L{LatLon_}.
|
|
97
|
+
|
|
98
|
+
@note: The lat- and longitude values are taken I{as-given,
|
|
99
|
+
un-clipped and un-validated}.
|
|
100
|
+
|
|
101
|
+
@see: L{latlonBase.LatLonBase} for further details.
|
|
102
|
+
'''
|
|
103
|
+
if name:
|
|
104
|
+
self.name = name
|
|
105
|
+
|
|
106
|
+
if lon is None: # PYCHOK no cover
|
|
107
|
+
lat, lon, height = _latlonheight3(latlonh, height, wrap)
|
|
108
|
+
elif wrap: # PYCHOK no cover
|
|
109
|
+
lat, lon = _Wrap.latlonDMS2(latlonh, lon)
|
|
110
|
+
else: # must be latNS, lonEW
|
|
111
|
+
try:
|
|
112
|
+
lat, lon = float(latlonh), float(lon)
|
|
113
|
+
except (TypeError, ValueError):
|
|
114
|
+
lat = parseDMS(latlonh, suffix=_NS_)
|
|
115
|
+
lon = parseDMS(lon, suffix=_EW_)
|
|
116
|
+
|
|
117
|
+
# get the minimal __dict__, see _isLatLon_ below
|
|
118
|
+
self._lat = lat # un-clipped and ...
|
|
119
|
+
self._lon = lon # ... un-validated
|
|
120
|
+
self._datum = None if datum is None else \
|
|
121
|
+
_spherical_datum(datum, name=self.name)
|
|
122
|
+
self._height = height
|
|
123
|
+
|
|
124
|
+
def __eq__(self, other):
|
|
125
|
+
return isinstance(other, LatLon_) and \
|
|
126
|
+
other.lat == self.lat and \
|
|
127
|
+
other.lon == self.lon
|
|
128
|
+
|
|
129
|
+
def __ne__(self, other):
|
|
130
|
+
return not self.__eq__(other)
|
|
131
|
+
|
|
132
|
+
@Property_RO
|
|
133
|
+
def datum(self):
|
|
134
|
+
'''Get the C{datum} (L{Datum}) or C{None}.
|
|
135
|
+
'''
|
|
136
|
+
return self._datum
|
|
137
|
+
|
|
138
|
+
def intermediateTo(self, other, fraction, height=None, wrap=False):
|
|
139
|
+
'''Locate the point at a given fraction, I{linearly} between
|
|
140
|
+
(or along) this and an other point.
|
|
141
|
+
|
|
142
|
+
@arg other: The other point (C{LatLon}).
|
|
143
|
+
@arg fraction: Fraction between both points (C{float},
|
|
144
|
+
0.0 for this and 1.0 for the other point).
|
|
145
|
+
@kwarg height: Optional height (C{meter}), overriding the
|
|
146
|
+
intermediate height.
|
|
147
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll
|
|
148
|
+
the B{C{other}} point (C{bool}).
|
|
149
|
+
|
|
150
|
+
@return: Intermediate point (this C{LatLon}).
|
|
151
|
+
|
|
152
|
+
@raise TypeError: Incompatible B{C{other}} C{type}.
|
|
153
|
+
'''
|
|
154
|
+
f = Scalar(fraction=fraction)
|
|
155
|
+
if isnear0(f):
|
|
156
|
+
r = self
|
|
157
|
+
else:
|
|
158
|
+
r = self.others(other)
|
|
159
|
+
if wrap or not isnear1(f):
|
|
160
|
+
_, lat, lon = _Wrap.latlon3(self.lon, r.lat, r.lon, wrap)
|
|
161
|
+
lat = favg(self.lat, lat, f=f)
|
|
162
|
+
lon = favg(self.lon, lon, f=f)
|
|
163
|
+
h = height if height is not None else \
|
|
164
|
+
favg(self.height, r.height, f=f)
|
|
165
|
+
# = self._havg(r, f=f, h=height)
|
|
166
|
+
r = self.classof(lat, lon, height=h, datum=r.datum,
|
|
167
|
+
name=r.intermediateTo.__name__)
|
|
168
|
+
return r
|
|
169
|
+
|
|
170
|
+
def toRepr(self, **kwds):
|
|
171
|
+
'''This L{LatLon_} as a string "class(<degrees>, ...)",
|
|
172
|
+
ignoring keyword argument C{B{std}=N/A}.
|
|
173
|
+
|
|
174
|
+
@see: L{latlonBase.LatLonBase.toRepr} for further details.
|
|
175
|
+
'''
|
|
176
|
+
_, kwds = _xkwds_pop2(kwds, std=NotImplemented)
|
|
177
|
+
return LatLonBase.toRepr(self, **kwds)
|
|
178
|
+
|
|
179
|
+
def toStr(self, form=F_D, joined=_COMMASPACE_, **m_prec_sep_s_D_M_S): # PYCHOK expected
|
|
180
|
+
'''Convert this point to a "lat, lon[, height][, name][, ...]"
|
|
181
|
+
string, formatted in the given C{B{form}at}.
|
|
182
|
+
|
|
183
|
+
@see: L{latlonBase.LatLonBase.toStr} for further details.
|
|
184
|
+
'''
|
|
185
|
+
t = LatLonBase.toStr(self, form=form, joined=NN,
|
|
186
|
+
**_xkwds(m_prec_sep_s_D_M_S, m=NN))
|
|
187
|
+
if self.name:
|
|
188
|
+
t += (repr(self.name),)
|
|
189
|
+
return joined.join(t) if joined else t
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _isLatLon(inst):
|
|
193
|
+
'''(INTERNAL) Check a C{LatLon} or C{LatLon_} instance.
|
|
194
|
+
'''
|
|
195
|
+
return isinstance(inst, (LatLon_, _MODS.latlonBase.LatLonBase))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _isLatLon_(LL):
|
|
199
|
+
'''(INTERNAL) Check a (sub-)class of C{LatLon_}.
|
|
200
|
+
'''
|
|
201
|
+
return issubclassof(LL, LatLon_) or (isclass(LL) and
|
|
202
|
+
all(hasattr(LL, _) for _ in LatLon_(0, 0).__dict__.keys()))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class _Basequence(_Sequence): # immutable, on purpose
|
|
206
|
+
'''(INTERNAL) Base class.
|
|
207
|
+
'''
|
|
208
|
+
_array = []
|
|
209
|
+
_epsilon = EPS
|
|
210
|
+
_itemname = _point_
|
|
211
|
+
|
|
212
|
+
def _contains(self, point):
|
|
213
|
+
'''(INTERNAL) Check for a matching point.
|
|
214
|
+
'''
|
|
215
|
+
return any(self._findall(point, ()))
|
|
216
|
+
|
|
217
|
+
def copy(self, deep=False): # PYCHOK no cover
|
|
218
|
+
'''Make a shallow or deep copy of this instance.
|
|
219
|
+
|
|
220
|
+
@kwarg deep: If C{True} make a deep, otherwise a
|
|
221
|
+
shallow copy (C{bool}).
|
|
222
|
+
|
|
223
|
+
@return: The copy (C{This class}).
|
|
224
|
+
'''
|
|
225
|
+
return _xcopy(self, deep=deep)
|
|
226
|
+
|
|
227
|
+
def _count(self, point):
|
|
228
|
+
'''(INTERNAL) Count the number of matching points.
|
|
229
|
+
'''
|
|
230
|
+
return sum(1 for _ in self._findall(point, ())) # NOT len()!
|
|
231
|
+
|
|
232
|
+
def dup(self, **items): # PYCHOK no cover
|
|
233
|
+
'''Duplicate this instance, I{without replacing items}.
|
|
234
|
+
|
|
235
|
+
@kwarg items: No attributes (I{not allowed}).
|
|
236
|
+
|
|
237
|
+
@return: The duplicate (C{This class}).
|
|
238
|
+
|
|
239
|
+
@raise TypeError: Any B{C{items}} invalid.
|
|
240
|
+
'''
|
|
241
|
+
if items:
|
|
242
|
+
t = _SPACE_(classname(self), _immutable_)
|
|
243
|
+
raise _TypeError(txt=t, this=self, **items)
|
|
244
|
+
return _xdup(self)
|
|
245
|
+
|
|
246
|
+
@property_doc_(''' the equality tolerance (C{float}).''')
|
|
247
|
+
def epsilon(self):
|
|
248
|
+
'''Get the tolerance for equality tests (C{float}).
|
|
249
|
+
'''
|
|
250
|
+
return self._epsilon
|
|
251
|
+
|
|
252
|
+
@epsilon.setter # PYCHOK setter!
|
|
253
|
+
def epsilon(self, tol):
|
|
254
|
+
'''Set the tolerance for equality tests (C{scalar}).
|
|
255
|
+
|
|
256
|
+
@raise UnitError: Non-scalar or invalid B{C{tol}}.
|
|
257
|
+
'''
|
|
258
|
+
self._epsilon = Scalar_(tolerance=tol)
|
|
259
|
+
|
|
260
|
+
def _find(self, point, start_end):
|
|
261
|
+
'''(INTERNAL) Find the first matching point index.
|
|
262
|
+
'''
|
|
263
|
+
for i in self._findall(point, start_end):
|
|
264
|
+
return i
|
|
265
|
+
return -1
|
|
266
|
+
|
|
267
|
+
def _findall(self, point, start_end): # PYCHOK no cover
|
|
268
|
+
'''(INTERNAL) I{Must be implemented/overloaded}.'''
|
|
269
|
+
notImplemented(self, point, start_end)
|
|
270
|
+
|
|
271
|
+
def _getitem(self, index):
|
|
272
|
+
'''(INTERNAL) Return point [index] or return a slice.
|
|
273
|
+
'''
|
|
274
|
+
# Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 290+, 2022 p. 405+
|
|
275
|
+
if isinstance(index, slice):
|
|
276
|
+
# XXX an numpy.[nd]array slice is a view, not a copy
|
|
277
|
+
return self.__class__(self._array[index], **self._slicekwds())
|
|
278
|
+
else:
|
|
279
|
+
return self.point(self._array[index])
|
|
280
|
+
|
|
281
|
+
def _index(self, point, start_end):
|
|
282
|
+
'''(INTERNAL) Find the first matching point index.
|
|
283
|
+
'''
|
|
284
|
+
for i in self._findall(point, start_end):
|
|
285
|
+
return i
|
|
286
|
+
raise _IndexError(self._itemname, point, txt=_not_('found'))
|
|
287
|
+
|
|
288
|
+
@property_RO
|
|
289
|
+
def isNumpy2(self): # PYCHOK no cover
|
|
290
|
+
'''Is this a Numpy2 wrapper?
|
|
291
|
+
'''
|
|
292
|
+
return False # isinstance(self, (Numpy2LatLon, ...))
|
|
293
|
+
|
|
294
|
+
@property_RO
|
|
295
|
+
def isPoints2(self): # PYCHOK no cover
|
|
296
|
+
'''Is this a LatLon2 wrapper/converter?
|
|
297
|
+
'''
|
|
298
|
+
return False # isinstance(self, (LatLon2psxy, ...))
|
|
299
|
+
|
|
300
|
+
@property_RO
|
|
301
|
+
def isTuple2(self): # PYCHOK no cover
|
|
302
|
+
'''Is this a Tuple2 wrapper?
|
|
303
|
+
'''
|
|
304
|
+
return False # isinstance(self, (Tuple2LatLon, ...))
|
|
305
|
+
|
|
306
|
+
def _iter(self):
|
|
307
|
+
'''(INTERNAL) Yield all points.
|
|
308
|
+
'''
|
|
309
|
+
_array, _point = self._array, self.point
|
|
310
|
+
for i in range(len(self)):
|
|
311
|
+
yield _point(_array[i])
|
|
312
|
+
|
|
313
|
+
def point(self, *attrs): # PYCHOK no cover
|
|
314
|
+
'''I{Must be overloaded}.'''
|
|
315
|
+
notOverloaded(self, *attrs)
|
|
316
|
+
|
|
317
|
+
def _range(self, start=None, end=None, step=1):
|
|
318
|
+
'''(INTERNAL) Return the range.
|
|
319
|
+
'''
|
|
320
|
+
if step > 0:
|
|
321
|
+
if start is None:
|
|
322
|
+
start = 0
|
|
323
|
+
if end is None:
|
|
324
|
+
end = len(self)
|
|
325
|
+
elif step < 0:
|
|
326
|
+
if start is None:
|
|
327
|
+
start = len(self) - 1
|
|
328
|
+
if end is None:
|
|
329
|
+
end = -1
|
|
330
|
+
else:
|
|
331
|
+
raise _ValueError(step=step)
|
|
332
|
+
return range(start, end, step)
|
|
333
|
+
|
|
334
|
+
def _repr(self):
|
|
335
|
+
'''(INTERNAL) Return a string representation.
|
|
336
|
+
'''
|
|
337
|
+
# XXX use Python 3+ reprlib.repr
|
|
338
|
+
t = repr(self._array[:1]) # only first row
|
|
339
|
+
t = _SPACE_(t[:-1], _ELLIPSIS_, Fmt.SQUARE(t[-1:], len(self)))
|
|
340
|
+
t = _SPACE_.join(t.split()) # coalesce spaces
|
|
341
|
+
return instr(self, t, **self._slicekwds())
|
|
342
|
+
|
|
343
|
+
def _reversed(self): # PYCHOK false
|
|
344
|
+
'''(INTERNAL) Yield all points in reverse order.
|
|
345
|
+
'''
|
|
346
|
+
_array, point = self._array, self.point
|
|
347
|
+
for i in range(len(self) - 1, -1, -1):
|
|
348
|
+
yield point(_array[i])
|
|
349
|
+
|
|
350
|
+
def _rfind(self, point, start_end):
|
|
351
|
+
'''(INTERNAL) Find the last matching point index.
|
|
352
|
+
'''
|
|
353
|
+
def _r3(start=None, end=None, step=-1):
|
|
354
|
+
return (start, end, step) # PYCHOK returns
|
|
355
|
+
|
|
356
|
+
for i in self._findall(point, _r3(*start_end)):
|
|
357
|
+
return i
|
|
358
|
+
return -1
|
|
359
|
+
|
|
360
|
+
def _slicekwds(self): # PYCHOK no cover
|
|
361
|
+
'''(INTERNAL) I{Should be overloaded}.
|
|
362
|
+
'''
|
|
363
|
+
return {}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class _Array2LatLon(_Basequence): # immutable, on purpose
|
|
367
|
+
'''(INTERNAL) Base class for Numpy2LatLon or Tuple2LatLon.
|
|
368
|
+
'''
|
|
369
|
+
_array = ()
|
|
370
|
+
_ilat = 0 # row column index
|
|
371
|
+
_ilon = 0 # row column index
|
|
372
|
+
_LatLon = LatLon_ # default
|
|
373
|
+
_shape = ()
|
|
374
|
+
|
|
375
|
+
def __init__(self, array, ilat=0, ilon=1, LatLon=None, shape=()):
|
|
376
|
+
'''Handle a C{NumPy} or C{Tuple} array as a sequence of C{LatLon} points.
|
|
377
|
+
'''
|
|
378
|
+
ais = (_ilat_, ilat), (_ilon_, ilon)
|
|
379
|
+
|
|
380
|
+
if len(shape) != 2 or shape[0] < 1 or shape[1] < len(ais):
|
|
381
|
+
raise _IndexError('array.shape', shape)
|
|
382
|
+
|
|
383
|
+
self._array = array
|
|
384
|
+
self._shape = Shape2Tuple(shape) # *shape
|
|
385
|
+
|
|
386
|
+
if LatLon: # check the point class
|
|
387
|
+
if not _isLatLon_(LatLon):
|
|
388
|
+
raise _IsnotError(_valid_, LatLon=LatLon)
|
|
389
|
+
self._LatLon = LatLon
|
|
390
|
+
|
|
391
|
+
# check the attr indices
|
|
392
|
+
for n, (ai, i) in enumerate(ais):
|
|
393
|
+
if not isint(i):
|
|
394
|
+
raise _IsnotError(int.__name__, **{ai: i})
|
|
395
|
+
i = int(i)
|
|
396
|
+
if not 0 <= i < shape[1]:
|
|
397
|
+
raise _ValueError(ai, i)
|
|
398
|
+
for aj, j in ais[:n]:
|
|
399
|
+
if int(j) == i:
|
|
400
|
+
raise _ValueError(_DEQUALSPACED_(ai, aj, i))
|
|
401
|
+
setattr(self, NN(_UNDER_, ai), i)
|
|
402
|
+
|
|
403
|
+
def __contains__(self, latlon):
|
|
404
|
+
'''Check for a specific lat-/longitude.
|
|
405
|
+
|
|
406
|
+
@arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
407
|
+
C{(lat, lon)}).
|
|
408
|
+
|
|
409
|
+
@return: C{True} if B{C{latlon}} is present, C{False} otherwise.
|
|
410
|
+
|
|
411
|
+
@raise TypeError: Invalid B{C{latlon}}.
|
|
412
|
+
'''
|
|
413
|
+
return self._contains(latlon)
|
|
414
|
+
|
|
415
|
+
def __getitem__(self, index):
|
|
416
|
+
'''Return row[index] as C{LatLon} or return a L{Numpy2LatLon} slice.
|
|
417
|
+
'''
|
|
418
|
+
return self._getitem(index)
|
|
419
|
+
|
|
420
|
+
def __iter__(self):
|
|
421
|
+
'''Yield rows as C{LatLon}.
|
|
422
|
+
'''
|
|
423
|
+
return self._iter()
|
|
424
|
+
|
|
425
|
+
def __len__(self):
|
|
426
|
+
'''Return the number of rows.
|
|
427
|
+
'''
|
|
428
|
+
return self._shape[0]
|
|
429
|
+
|
|
430
|
+
def __repr__(self):
|
|
431
|
+
'''Return a string representation.
|
|
432
|
+
'''
|
|
433
|
+
return self._repr()
|
|
434
|
+
|
|
435
|
+
def __reversed__(self): # PYCHOK false
|
|
436
|
+
'''Yield rows as C{LatLon} in reverse order.
|
|
437
|
+
'''
|
|
438
|
+
return self._reversed()
|
|
439
|
+
|
|
440
|
+
__str__ = __repr__
|
|
441
|
+
|
|
442
|
+
def count(self, latlon):
|
|
443
|
+
'''Count the number of rows with a specific lat-/longitude.
|
|
444
|
+
|
|
445
|
+
@arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
446
|
+
C{(lat, lon)}).
|
|
447
|
+
|
|
448
|
+
@return: Count (C{int}).
|
|
449
|
+
|
|
450
|
+
@raise TypeError: Invalid B{C{latlon}}.
|
|
451
|
+
'''
|
|
452
|
+
return self._count(latlon)
|
|
453
|
+
|
|
454
|
+
def find(self, latlon, *start_end):
|
|
455
|
+
'''Find the first row with a specific lat-/longitude.
|
|
456
|
+
|
|
457
|
+
@arg latlon: Point (C{LatLon}) or 2-tuple (lat, lon).
|
|
458
|
+
@arg start_end: Optional C{[start[, end]]} index (integers).
|
|
459
|
+
|
|
460
|
+
@return: Index or -1 if not found (C{int}).
|
|
461
|
+
|
|
462
|
+
@raise TypeError: Invalid B{C{latlon}}.
|
|
463
|
+
'''
|
|
464
|
+
return self._find(latlon, start_end)
|
|
465
|
+
|
|
466
|
+
def _findall(self, latlon, start_end):
|
|
467
|
+
'''(INTERNAL) Yield indices of all matching rows.
|
|
468
|
+
'''
|
|
469
|
+
try:
|
|
470
|
+
lat, lon = latlon.lat, latlon.lon
|
|
471
|
+
except AttributeError:
|
|
472
|
+
try:
|
|
473
|
+
lat, lon = latlon
|
|
474
|
+
except (TypeError, ValueError):
|
|
475
|
+
raise _IsnotError(_valid_, latlon=latlon)
|
|
476
|
+
|
|
477
|
+
_ilat, _ilon = self._ilat, self._ilon
|
|
478
|
+
_array, _eps = self._array, self._epsilon
|
|
479
|
+
for i in self._range(*start_end):
|
|
480
|
+
row = _array[i]
|
|
481
|
+
if fabs(row[_ilat] - lat) <= _eps and \
|
|
482
|
+
fabs(row[_ilon] - lon) <= _eps:
|
|
483
|
+
yield i
|
|
484
|
+
|
|
485
|
+
def findall(self, latlon, *start_end):
|
|
486
|
+
'''Yield indices of all rows with a specific lat-/longitude.
|
|
487
|
+
|
|
488
|
+
@arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
489
|
+
C{(lat, lon)}).
|
|
490
|
+
@arg start_end: Optional C{[start[, end]]} index (C{int}).
|
|
491
|
+
|
|
492
|
+
@return: Indices (C{iterable}).
|
|
493
|
+
|
|
494
|
+
@raise TypeError: Invalid B{C{latlon}}.
|
|
495
|
+
'''
|
|
496
|
+
return self._findall(latlon, start_end)
|
|
497
|
+
|
|
498
|
+
def index(self, latlon, *start_end): # PYCHOK Python 2- issue
|
|
499
|
+
'''Find index of the first row with a specific lat-/longitude.
|
|
500
|
+
|
|
501
|
+
@arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
502
|
+
C{(lat, lon)}).
|
|
503
|
+
@arg start_end: Optional C{[start[, end]]} index (C{int}).
|
|
504
|
+
|
|
505
|
+
@return: Index (C{int}).
|
|
506
|
+
|
|
507
|
+
@raise IndexError: Point not found.
|
|
508
|
+
|
|
509
|
+
@raise TypeError: Invalid B{C{latlon}}.
|
|
510
|
+
'''
|
|
511
|
+
return self._index(latlon, start_end)
|
|
512
|
+
|
|
513
|
+
@Property_RO
|
|
514
|
+
def ilat(self):
|
|
515
|
+
'''Get the latitudes column index (C{int}).
|
|
516
|
+
'''
|
|
517
|
+
return self._ilat
|
|
518
|
+
|
|
519
|
+
@Property_RO
|
|
520
|
+
def ilon(self):
|
|
521
|
+
'''Get the longitudes column index (C{int}).
|
|
522
|
+
'''
|
|
523
|
+
return self._ilon
|
|
524
|
+
|
|
525
|
+
# next = __iter__
|
|
526
|
+
|
|
527
|
+
def point(self, row): # PYCHOK *attrs
|
|
528
|
+
'''Instantiate a point C{LatLon}.
|
|
529
|
+
|
|
530
|
+
@arg row: Array row (numpy.array).
|
|
531
|
+
|
|
532
|
+
@return: Point (C{LatLon}).
|
|
533
|
+
'''
|
|
534
|
+
return self._LatLon(row[self._ilat], row[self._ilon])
|
|
535
|
+
|
|
536
|
+
def rfind(self, latlon, *start_end):
|
|
537
|
+
'''Find the last row with a specific lat-/longitude.
|
|
538
|
+
|
|
539
|
+
@arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
540
|
+
C{(lat, lon)}).
|
|
541
|
+
@arg start_end: Optional C{[start[, end]]} index (C{int}).
|
|
542
|
+
|
|
543
|
+
@note: Keyword order, first stop, then start.
|
|
544
|
+
|
|
545
|
+
@return: Index or -1 if not found (C{int}).
|
|
546
|
+
|
|
547
|
+
@raise TypeError: Invalid B{C{latlon}}.
|
|
548
|
+
'''
|
|
549
|
+
return self._rfind(latlon, start_end)
|
|
550
|
+
|
|
551
|
+
def _slicekwds(self):
|
|
552
|
+
'''(INTERNAL) Slice kwds.
|
|
553
|
+
'''
|
|
554
|
+
return dict(ilat=self._ilat, ilon=self._ilon)
|
|
555
|
+
|
|
556
|
+
@Property_RO
|
|
557
|
+
def shape(self):
|
|
558
|
+
'''Get the shape of the C{NumPy} array or the C{Tuples} as
|
|
559
|
+
L{Shape2Tuple}C{(nrows, ncols)}.
|
|
560
|
+
'''
|
|
561
|
+
return self._shape
|
|
562
|
+
|
|
563
|
+
def _subset(self, indices): # PYCHOK no cover
|
|
564
|
+
'''(INTERNAL) I{Must be implemented/overloaded}.'''
|
|
565
|
+
notImplemented(self, indices)
|
|
566
|
+
|
|
567
|
+
def subset(self, indices):
|
|
568
|
+
'''Return a subset of the C{NumPy} array.
|
|
569
|
+
|
|
570
|
+
@arg indices: Row indices (C{range} or C{int}[]).
|
|
571
|
+
|
|
572
|
+
@note: A C{subset} is different from a C{slice} in 2 ways:
|
|
573
|
+
(a) the C{subset} is typically specified as a list of
|
|
574
|
+
(un-)ordered indices and (b) the C{subset} allocates
|
|
575
|
+
a new, separate C{NumPy} array while a C{slice} is
|
|
576
|
+
just an other C{view} of the original C{NumPy} array.
|
|
577
|
+
|
|
578
|
+
@return: Sub-array (C{numpy.array}).
|
|
579
|
+
|
|
580
|
+
@raise IndexError: Out-of-range B{C{indices}} value.
|
|
581
|
+
|
|
582
|
+
@raise TypeError: If B{C{indices}} is not a C{range}
|
|
583
|
+
nor an C{int}[].
|
|
584
|
+
'''
|
|
585
|
+
if not issequence(indices, tuple): # NO tuple, only list
|
|
586
|
+
# and range work properly to get Numpy array sub-sets
|
|
587
|
+
raise _IsnotError(_valid_, indices=type(indices))
|
|
588
|
+
|
|
589
|
+
n = len(self)
|
|
590
|
+
for i, v in enumerate(indices):
|
|
591
|
+
if not isint(v):
|
|
592
|
+
raise _TypeError(Fmt.SQUARE(indices=i), v)
|
|
593
|
+
elif not 0 <= v < n:
|
|
594
|
+
raise _IndexError(Fmt.SQUARE(indices=i), v)
|
|
595
|
+
|
|
596
|
+
return self._subset(indices)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class LatLon2psxy(_Basequence):
|
|
600
|
+
'''Wrapper for C{LatLon} points as "on-the-fly" pseudo-xy coordinates.
|
|
601
|
+
'''
|
|
602
|
+
_closed = False
|
|
603
|
+
_len = 0
|
|
604
|
+
_deg2m = None # default, keep degrees
|
|
605
|
+
_radius = None
|
|
606
|
+
_wrap = True
|
|
607
|
+
|
|
608
|
+
def __init__(self, latlons, closed=False, radius=None, wrap=True):
|
|
609
|
+
'''Handle C{LatLon} points as pseudo-xy coordinates.
|
|
610
|
+
|
|
611
|
+
@note: The C{LatLon} latitude is considered the I{pseudo-y}
|
|
612
|
+
and longitude the I{pseudo-x} coordinate, likewise
|
|
613
|
+
for L{LatLon2Tuple}. However, 2-tuples C{(x, y)} are
|
|
614
|
+
considered as I{(longitude, latitude)}.
|
|
615
|
+
|
|
616
|
+
@arg latlons: Points C{list}, C{sequence}, C{set}, C{tuple},
|
|
617
|
+
etc. (C{LatLon[]}).
|
|
618
|
+
@kwarg closed: Optionally, close the polygon (C{bool}).
|
|
619
|
+
@kwarg radius: Mean earth radius (C{meter}).
|
|
620
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll
|
|
621
|
+
the B{C{latlons}} points (C{bool}).
|
|
622
|
+
|
|
623
|
+
@raise PointsError: Insufficient number of B{C{latlons}}.
|
|
624
|
+
|
|
625
|
+
@raise TypeError: Some B{C{points}} are not B{C{base}}.
|
|
626
|
+
'''
|
|
627
|
+
self._closed = closed
|
|
628
|
+
self._len, self._array = points2(latlons, closed=closed)
|
|
629
|
+
if radius:
|
|
630
|
+
self._radius = r = Radius(radius)
|
|
631
|
+
self._deg2m = degrees2m(_1_0, r)
|
|
632
|
+
if not wrap:
|
|
633
|
+
self._wrap = False
|
|
634
|
+
|
|
635
|
+
def __contains__(self, xy):
|
|
636
|
+
'''Check for a matching point.
|
|
637
|
+
|
|
638
|
+
@arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
639
|
+
C{(x, y)}) in (C{degrees}.
|
|
640
|
+
|
|
641
|
+
@return: C{True} if B{C{xy}} is present, C{False} otherwise.
|
|
642
|
+
|
|
643
|
+
@raise TypeError: Invalid B{C{xy}}.
|
|
644
|
+
'''
|
|
645
|
+
return self._contains(xy)
|
|
646
|
+
|
|
647
|
+
def __getitem__(self, index):
|
|
648
|
+
'''Return the pseudo-xy or return a L{LatLon2psxy} slice.
|
|
649
|
+
'''
|
|
650
|
+
return self._getitem(index)
|
|
651
|
+
|
|
652
|
+
def __iter__(self):
|
|
653
|
+
'''Yield all pseudo-xy's.
|
|
654
|
+
'''
|
|
655
|
+
return self._iter()
|
|
656
|
+
|
|
657
|
+
def __len__(self):
|
|
658
|
+
'''Return the number of pseudo-xy's.
|
|
659
|
+
'''
|
|
660
|
+
return self._len
|
|
661
|
+
|
|
662
|
+
def __repr__(self):
|
|
663
|
+
'''Return a string representation.
|
|
664
|
+
'''
|
|
665
|
+
return self._repr()
|
|
666
|
+
|
|
667
|
+
def __reversed__(self): # PYCHOK false
|
|
668
|
+
'''Yield all pseudo-xy's in reverse order.
|
|
669
|
+
'''
|
|
670
|
+
return self._reversed()
|
|
671
|
+
|
|
672
|
+
__str__ = __repr__
|
|
673
|
+
|
|
674
|
+
def count(self, xy):
|
|
675
|
+
'''Count the number of matching points.
|
|
676
|
+
|
|
677
|
+
@arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
678
|
+
C{(x, y)}) in (C{degrees}.
|
|
679
|
+
|
|
680
|
+
@return: Count (C{int}).
|
|
681
|
+
|
|
682
|
+
@raise TypeError: Invalid B{C{xy}}.
|
|
683
|
+
'''
|
|
684
|
+
return self._count(xy)
|
|
685
|
+
|
|
686
|
+
def find(self, xy, *start_end):
|
|
687
|
+
'''Find the first matching point.
|
|
688
|
+
|
|
689
|
+
@arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
690
|
+
C{(x, y)}) in (C{degrees}.
|
|
691
|
+
@arg start_end: Optional C{[start[, end]]} index (C{int}).
|
|
692
|
+
|
|
693
|
+
@return: Index or -1 if not found (C{int}).
|
|
694
|
+
|
|
695
|
+
@raise TypeError: Invalid B{C{xy}}.
|
|
696
|
+
'''
|
|
697
|
+
return self._find(xy, start_end)
|
|
698
|
+
|
|
699
|
+
def _findall(self, xy, start_end):
|
|
700
|
+
'''(INTERNAL) Yield indices of all matching points.
|
|
701
|
+
'''
|
|
702
|
+
try:
|
|
703
|
+
x, y = xy.lon, xy.lat
|
|
704
|
+
|
|
705
|
+
def _x_y_ll3(ll): # match LatLon
|
|
706
|
+
return ll.lon, ll.lat, ll
|
|
707
|
+
|
|
708
|
+
except AttributeError:
|
|
709
|
+
try:
|
|
710
|
+
x, y = xy[:2]
|
|
711
|
+
except (IndexError, TypeError, ValueError):
|
|
712
|
+
raise _IsnotError(_valid_, xy=xy)
|
|
713
|
+
|
|
714
|
+
_x_y_ll3 = self.point # PYCHOK expected
|
|
715
|
+
|
|
716
|
+
_array, _eps = self._array, self._epsilon
|
|
717
|
+
for i in self._range(*start_end):
|
|
718
|
+
xi, yi, _ = _x_y_ll3(_array[i])
|
|
719
|
+
if fabs(xi - x) <= _eps and \
|
|
720
|
+
fabs(yi - y) <= _eps:
|
|
721
|
+
yield i
|
|
722
|
+
|
|
723
|
+
def findall(self, xy, *start_end):
|
|
724
|
+
'''Yield indices of all matching points.
|
|
725
|
+
|
|
726
|
+
@arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
727
|
+
C{(x, y)}) in (C{degrees}.
|
|
728
|
+
@arg start_end: Optional C{[start[, end]]} index (C{int}).
|
|
729
|
+
|
|
730
|
+
@return: Indices (C{iterator}).
|
|
731
|
+
|
|
732
|
+
@raise TypeError: Invalid B{C{xy}}.
|
|
733
|
+
'''
|
|
734
|
+
return self._findall(xy, start_end)
|
|
735
|
+
|
|
736
|
+
def index(self, xy, *start_end): # PYCHOK Python 2- issue
|
|
737
|
+
'''Find the first matching point.
|
|
738
|
+
|
|
739
|
+
@arg xy: Point (C{LatLon}) or 2-tuple (x, y) in (C{degrees}).
|
|
740
|
+
@arg start_end: Optional C{[start[, end]]} index (C{int}).
|
|
741
|
+
|
|
742
|
+
@return: Index (C{int}).
|
|
743
|
+
|
|
744
|
+
@raise IndexError: Point not found.
|
|
745
|
+
|
|
746
|
+
@raise TypeError: Invalid B{C{xy}}.
|
|
747
|
+
'''
|
|
748
|
+
return self._index(xy, start_end)
|
|
749
|
+
|
|
750
|
+
@property_RO
|
|
751
|
+
def isPoints2(self):
|
|
752
|
+
'''Is this a LatLon2 wrapper/converter?
|
|
753
|
+
'''
|
|
754
|
+
return True # isinstance(self, (LatLon2psxy, ...))
|
|
755
|
+
|
|
756
|
+
def point(self, ll): # PYCHOK *attrs
|
|
757
|
+
'''Create a pseudo-xy.
|
|
758
|
+
|
|
759
|
+
@arg ll: Point (C{LatLon}).
|
|
760
|
+
|
|
761
|
+
@return: An L{Point3Tuple}C{(x, y, ll)}.
|
|
762
|
+
'''
|
|
763
|
+
x, y = ll.lon, ll.lat # note, x, y = lon, lat
|
|
764
|
+
if self._wrap:
|
|
765
|
+
y, x = _Wrap.latlon(y, x)
|
|
766
|
+
d = self._deg2m
|
|
767
|
+
if d: # convert degrees to meter (or radians)
|
|
768
|
+
x *= d
|
|
769
|
+
y *= d
|
|
770
|
+
return Point3Tuple(x, y, ll)
|
|
771
|
+
|
|
772
|
+
def rfind(self, xy, *start_end):
|
|
773
|
+
'''Find the last matching point.
|
|
774
|
+
|
|
775
|
+
@arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple
|
|
776
|
+
C{(x, y)}) in (C{degrees}.
|
|
777
|
+
@arg start_end: Optional C{[start[, end]]} index (C{int}).
|
|
778
|
+
|
|
779
|
+
@return: Index or -1 if not found (C{int}).
|
|
780
|
+
|
|
781
|
+
@raise TypeError: Invalid B{C{xy}}.
|
|
782
|
+
'''
|
|
783
|
+
return self._rfind(xy, start_end)
|
|
784
|
+
|
|
785
|
+
def _slicekwds(self):
|
|
786
|
+
'''(INTERNAL) Slice kwds.
|
|
787
|
+
'''
|
|
788
|
+
return dict(closed=self._closed, radius=self._radius, wrap=self._wrap)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
class Numpy2LatLon(_Array2LatLon): # immutable, on purpose
|
|
792
|
+
'''Wrapper for C{NumPy} arrays as "on-the-fly" C{LatLon} points.
|
|
793
|
+
'''
|
|
794
|
+
def __init__(self, array, ilat=0, ilon=1, LatLon=None):
|
|
795
|
+
'''Handle a C{NumPy} array as a sequence of C{LatLon} points.
|
|
796
|
+
|
|
797
|
+
@arg array: C{NumPy} array (C{numpy.array}).
|
|
798
|
+
@kwarg ilat: Optional index of the latitudes column (C{int}).
|
|
799
|
+
@kwarg ilon: Optional index of the longitudes column (C{int}).
|
|
800
|
+
@kwarg LatLon: Optional C{LatLon} class to use (L{LatLon_}).
|
|
801
|
+
|
|
802
|
+
@raise IndexError: If B{C{array.shape}} is not (1+, 2+).
|
|
803
|
+
|
|
804
|
+
@raise TypeError: If B{C{array}} is not a C{NumPy} array or
|
|
805
|
+
C{LatLon} is not a class with C{lat}
|
|
806
|
+
and C{lon} attributes.
|
|
807
|
+
|
|
808
|
+
@raise ValueError: If the B{C{ilat}} and/or B{C{ilon}} values
|
|
809
|
+
are the same or out of range.
|
|
810
|
+
|
|
811
|
+
@example:
|
|
812
|
+
|
|
813
|
+
>>> type(array)
|
|
814
|
+
<type 'numpy.ndarray'> # <class ...> in Python 3+
|
|
815
|
+
>>> points = Numpy2LatLon(array, lat=0, lon=1)
|
|
816
|
+
>>> simply = simplifyRDP(points, ...)
|
|
817
|
+
>>> type(simply)
|
|
818
|
+
<type 'numpy.ndarray'> # <class ...> in Python 3+
|
|
819
|
+
>>> sliced = points[1:-1]
|
|
820
|
+
>>> type(sliced)
|
|
821
|
+
<class '...Numpy2LatLon'>
|
|
822
|
+
'''
|
|
823
|
+
try: # get shape and check some other numpy.array attrs
|
|
824
|
+
s, _, _ = array.shape, array.nbytes, array.ndim # PYCHOK expected
|
|
825
|
+
except AttributeError:
|
|
826
|
+
raise _IsnotError('NumPy', array=type(array))
|
|
827
|
+
|
|
828
|
+
_Array2LatLon.__init__(self, array, ilat=ilat, ilon=ilon,
|
|
829
|
+
LatLon=LatLon, shape=s)
|
|
830
|
+
|
|
831
|
+
@property_RO
|
|
832
|
+
def isNumpy2(self):
|
|
833
|
+
'''Is this a Numpy2 wrapper?
|
|
834
|
+
'''
|
|
835
|
+
return True # isinstance(self, (Numpy2LatLon, ...))
|
|
836
|
+
|
|
837
|
+
def _subset(self, indices):
|
|
838
|
+
return self._array[indices] # NumPy special
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
class Shape2Tuple(_NamedTuple):
|
|
842
|
+
'''2-Tuple C{(nrows, ncols)}, the number of rows and columns,
|
|
843
|
+
both C{int}.
|
|
844
|
+
'''
|
|
845
|
+
_Names_ = (_nrows_, _ncols_)
|
|
846
|
+
_Units_ = ( Number_, Number_)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
class Tuple2LatLon(_Array2LatLon):
|
|
850
|
+
'''Wrapper for tuple sequences as "on-the-fly" C{LatLon} points.
|
|
851
|
+
'''
|
|
852
|
+
def __init__(self, tuples, ilat=0, ilon=1, LatLon=None):
|
|
853
|
+
'''Handle a list of tuples, each containing a lat- and longitude
|
|
854
|
+
and perhaps other values as a sequence of C{LatLon} points.
|
|
855
|
+
|
|
856
|
+
@arg tuples: The C{list}, C{tuple} or C{sequence} of tuples (C{tuple}[]).
|
|
857
|
+
@kwarg ilat: Optional index of the latitudes value (C{int}).
|
|
858
|
+
@kwarg ilon: Optional index of the longitudes value (C{int}).
|
|
859
|
+
@kwarg LatLon: Optional C{LatLon} class to use (L{LatLon_}).
|
|
860
|
+
|
|
861
|
+
@raise IndexError: If C{(len(B{tuples}), min(len(t) for t
|
|
862
|
+
in B{tuples}))} is not (1+, 2+).
|
|
863
|
+
|
|
864
|
+
@raise TypeError: If B{C{tuples}} is not a C{list}, C{tuple}
|
|
865
|
+
or C{sequence} or if B{C{LatLon}} is not a
|
|
866
|
+
C{LatLon} with C{lat}, C{lon} and C{name}
|
|
867
|
+
attributes.
|
|
868
|
+
|
|
869
|
+
@raise ValueError: If the B{C{ilat}} and/or B{C{ilon}} values
|
|
870
|
+
are the same or out of range.
|
|
871
|
+
|
|
872
|
+
@example:
|
|
873
|
+
|
|
874
|
+
>>> tuples = [(0, 1), (2, 3), (4, 5)]
|
|
875
|
+
>>> type(tuples)
|
|
876
|
+
<type 'list'> # <class ...> in Python 3+
|
|
877
|
+
>>> points = Tuple2LatLon(tuples, lat=0, lon=1)
|
|
878
|
+
>>> simply = simplifyRW(points, 0.5, ...)
|
|
879
|
+
>>> type(simply)
|
|
880
|
+
<type 'list'> # <class ...> in Python 3+
|
|
881
|
+
>>> simply
|
|
882
|
+
[(0, 1), (4, 5)]
|
|
883
|
+
>>> sliced = points[1:-1]
|
|
884
|
+
>>> type(sliced)
|
|
885
|
+
<class '...Tuple2LatLon'>
|
|
886
|
+
>>> sliced
|
|
887
|
+
...Tuple2LatLon([(2, 3), ...][1], ilat=0, ilon=1)
|
|
888
|
+
|
|
889
|
+
>>> closest, _ = nearestOn2(LatLon_(2, 1), points, adjust=False)
|
|
890
|
+
>>> closest
|
|
891
|
+
LatLon_(lat=1.0, lon=2.0)
|
|
892
|
+
|
|
893
|
+
>>> closest, _ = nearestOn2(LatLon_(3, 2), points)
|
|
894
|
+
>>> closest
|
|
895
|
+
LatLon_(lat=2.001162, lon=3.001162)
|
|
896
|
+
'''
|
|
897
|
+
_xinstanceof(list, tuple, tuples=tuples)
|
|
898
|
+
s = len(tuples), min(len(_) for _ in tuples)
|
|
899
|
+
_Array2LatLon.__init__(self, tuples, ilat=ilat, ilon=ilon,
|
|
900
|
+
LatLon=LatLon, shape=s)
|
|
901
|
+
|
|
902
|
+
@property_RO
|
|
903
|
+
def isTuple2(self):
|
|
904
|
+
'''Is this a Tuple2 wrapper?
|
|
905
|
+
'''
|
|
906
|
+
return True # isinstance(self, (Tuple2LatLon, ...))
|
|
907
|
+
|
|
908
|
+
def _subset(self, indices):
|
|
909
|
+
return type(self._array)(self._array[i] for i in indices)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def _area2(points, adjust, wrap):
|
|
913
|
+
'''(INTERNAL) Approximate the area in radians squared, I{signed}.
|
|
914
|
+
'''
|
|
915
|
+
if adjust:
|
|
916
|
+
# approximate trapezoid by a rectangle, adjusting
|
|
917
|
+
# the top width by the cosine of the latitudinal
|
|
918
|
+
# average and bottom width by some fudge factor
|
|
919
|
+
def _adjust(w, h):
|
|
920
|
+
c = cos(h) if fabs(h) < PI_2 else _0_0
|
|
921
|
+
return w * h * (c + 1.2876) * _0_5
|
|
922
|
+
else:
|
|
923
|
+
def _adjust(w, h): # PYCHOK expected
|
|
924
|
+
return w * h
|
|
925
|
+
|
|
926
|
+
# setting radius=1 converts degrees to radians
|
|
927
|
+
Ps = LatLon2PsxyIter(points, loop=1, radius=_1_0, wrap=wrap)
|
|
928
|
+
x1, y1, ll = Ps[0]
|
|
929
|
+
pts = [ll] # for _areaError
|
|
930
|
+
|
|
931
|
+
A2 = Fsum() # trapezoidal area in radians**2
|
|
932
|
+
for p in Ps.iterate(closed=True):
|
|
933
|
+
x2, y2, ll = p
|
|
934
|
+
if len(pts) < 4:
|
|
935
|
+
pts.append(ll)
|
|
936
|
+
w, x2 = unrollPI(x1, x2, wrap=wrap and not Ps.looped)
|
|
937
|
+
A2 += _adjust(w, (y2 + y1) * _0_5)
|
|
938
|
+
x1, y1 = x2, y2
|
|
939
|
+
|
|
940
|
+
return A2.fsum(), tuple(pts)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _areaError(pts, near_=NN): # in .ellipsoidalKarney
|
|
944
|
+
'''(INTERNAL) Area issue.
|
|
945
|
+
'''
|
|
946
|
+
t = _ELLIPSIS_(pts[:3], NN)
|
|
947
|
+
return _ValueError(NN(near_, 'zero or polar area'), txt=t)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def areaOf(points, adjust=True, radius=R_M, wrap=True):
|
|
951
|
+
'''Approximate the area of a polygon or composite.
|
|
952
|
+
|
|
953
|
+
@arg points: The polygon points or clips (C{LatLon}[],
|
|
954
|
+
L{BooleanFHP} or L{BooleanGH}).
|
|
955
|
+
@kwarg adjust: Adjust the wrapped, unrolled longitudinal delta
|
|
956
|
+
by the cosine of the mean latitude (C{bool}).
|
|
957
|
+
@kwarg radius: Mean earth radius (C{meter}) or C{None}.
|
|
958
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll
|
|
959
|
+
the B{C{points}} (C{bool}).
|
|
960
|
+
|
|
961
|
+
@return: Approximate area (I{square} C{meter}, same units as
|
|
962
|
+
B{C{radius}} or C{radians} I{squared} if B{C{radius}}
|
|
963
|
+
is C{None}).
|
|
964
|
+
|
|
965
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
966
|
+
|
|
967
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
968
|
+
|
|
969
|
+
@raise ValueError: Invalid B{C{radius}}.
|
|
970
|
+
|
|
971
|
+
@note: This area approximation has limited accuracy and is
|
|
972
|
+
ill-suited for regions exceeding several hundred Km
|
|
973
|
+
or Miles or with near-polar latitudes.
|
|
974
|
+
|
|
975
|
+
@see: L{sphericalNvector.areaOf}, L{sphericalTrigonometry.areaOf},
|
|
976
|
+
L{ellipsoidalExact.areaOf} and L{ellipsoidalKarney.areaOf}.
|
|
977
|
+
'''
|
|
978
|
+
if _MODS.booleans.isBoolean(points):
|
|
979
|
+
a = points._sum1(areaOf, adjust=adjust, radius=None, wrap=wrap)
|
|
980
|
+
else:
|
|
981
|
+
a, _ = _area2(points, adjust, wrap)
|
|
982
|
+
return fabs(a if radius is None else (Radius(radius)**2 * a))
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def boundsOf(points, wrap=False, LatLon=None): # was=True
|
|
986
|
+
'''Determine the bottom-left SW and top-right NE corners of a
|
|
987
|
+
path or polygon.
|
|
988
|
+
|
|
989
|
+
@arg points: The path or polygon points (C{LatLon}[]).
|
|
990
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll
|
|
991
|
+
the B{C{points}} (C{bool}).
|
|
992
|
+
@kwarg LatLon: Optional class to return the C{bounds}
|
|
993
|
+
corners (C{LatLon}) or C{None}.
|
|
994
|
+
|
|
995
|
+
@return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} as
|
|
996
|
+
B{C{LatLon}}s if B{C{LatLon}} is C{None} a
|
|
997
|
+
L{Bounds4Tuple}C{(latS, lonW, latN, lonE)}.
|
|
998
|
+
|
|
999
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1000
|
+
|
|
1001
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
1002
|
+
|
|
1003
|
+
@see: Function L{quadOf}.
|
|
1004
|
+
'''
|
|
1005
|
+
Ps = LatLon2PsxyIter(points, loop=1, wrap=wrap)
|
|
1006
|
+
w, s, _ = e, n, _ = Ps[0]
|
|
1007
|
+
|
|
1008
|
+
v = w
|
|
1009
|
+
for x, y, _ in Ps.iterate(closed=False): # [1:]
|
|
1010
|
+
if wrap:
|
|
1011
|
+
_, x = unroll180(v, x, wrap=True)
|
|
1012
|
+
v = x
|
|
1013
|
+
|
|
1014
|
+
if w > x:
|
|
1015
|
+
w = x
|
|
1016
|
+
elif e < x:
|
|
1017
|
+
e = x
|
|
1018
|
+
|
|
1019
|
+
if s > y:
|
|
1020
|
+
s = y
|
|
1021
|
+
elif n < y:
|
|
1022
|
+
n = y
|
|
1023
|
+
|
|
1024
|
+
return Bounds4Tuple(s, w, n, e) if LatLon is None else \
|
|
1025
|
+
Bounds2Tuple(LatLon(s, w), LatLon(n, e)) # PYCHOK inconsistent
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def centroidOf(points, wrap=False, LatLon=None): # was=True
|
|
1029
|
+
'''Determine the centroid of a polygon.
|
|
1030
|
+
|
|
1031
|
+
@arg points: The polygon points (C{LatLon}[]).
|
|
1032
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
1033
|
+
B{C{points}} (C{bool}).
|
|
1034
|
+
@kwarg LatLon: Optional class to return the centroid (C{LatLon})
|
|
1035
|
+
or C{None}.
|
|
1036
|
+
|
|
1037
|
+
@return: Centroid (B{C{LatLon}}) or a L{LatLon2Tuple}C{(lat, lon)}
|
|
1038
|
+
if C{B{LatLon} is None}.
|
|
1039
|
+
|
|
1040
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1041
|
+
|
|
1042
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
1043
|
+
|
|
1044
|
+
@raise ValueError: The B{C{points}} enclose a pole or
|
|
1045
|
+
near-zero area.
|
|
1046
|
+
|
|
1047
|
+
@see: U{Centroid<https://WikiPedia.org/wiki/Centroid#Of_a_polygon>} and
|
|
1048
|
+
Paul Bourke's U{Calculating The Area And Centroid Of A Polygon
|
|
1049
|
+
<https://www.SEAS.UPenn.edu/~ese502/lab-content/extra_materials/
|
|
1050
|
+
Polygon%20Area%20and%20Centroid.pdf>}, 1988.
|
|
1051
|
+
'''
|
|
1052
|
+
A, X, Y = Fsum(), Fsum(), Fsum()
|
|
1053
|
+
|
|
1054
|
+
# setting radius=1 converts degrees to radians
|
|
1055
|
+
Ps = LatLon2PsxyIter(points, loop=1, radius=_1_0, wrap=wrap)
|
|
1056
|
+
x1, y1, ll = Ps[0]
|
|
1057
|
+
pts = [ll] # for _areaError
|
|
1058
|
+
for p in Ps.iterate(closed=True):
|
|
1059
|
+
x2, y2, ll = p
|
|
1060
|
+
if len(pts) < 4:
|
|
1061
|
+
pts.append(ll)
|
|
1062
|
+
if wrap and not Ps.looped:
|
|
1063
|
+
_, x2 = unrollPI(x1, x2, wrap=True)
|
|
1064
|
+
t = x1 * y2 - x2 * y1
|
|
1065
|
+
A += t
|
|
1066
|
+
X += t * (x1 + x2)
|
|
1067
|
+
Y += t * (y1 + y2)
|
|
1068
|
+
# XXX more elaborately:
|
|
1069
|
+
# t1, t2 = x1 * y2, -(x2 * y1)
|
|
1070
|
+
# A.fadd_(t1, t2)
|
|
1071
|
+
# X.fadd_(t1 * x1, t1 * x2, t2 * x1, t2 * x2)
|
|
1072
|
+
# Y.fadd_(t1 * y1, t1 * y2, t2 * y1, t2 * y2)
|
|
1073
|
+
x1, y1 = x2, y2
|
|
1074
|
+
|
|
1075
|
+
a = A.fmul(_6_0).fover(_2_0)
|
|
1076
|
+
if isnear0(a):
|
|
1077
|
+
raise _areaError(pts, near_=_near_)
|
|
1078
|
+
y, x = degrees90(Y.fover(a)), degrees180(X.fover(a))
|
|
1079
|
+
return LatLon2Tuple(y, x) if LatLon is None else LatLon(y, x)
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _distanceTo(Error, **name_points): # .frechet, .hausdorff, .heights
|
|
1083
|
+
'''(INTERNAL) Check all callable C{distanceTo} methods.
|
|
1084
|
+
'''
|
|
1085
|
+
name, ps = _xkwds_item2(name_points)
|
|
1086
|
+
for i, p in enumerate(ps):
|
|
1087
|
+
if not callable(_xattr(p, distanceTo=None)):
|
|
1088
|
+
n = _distanceTo.__name__[1:]
|
|
1089
|
+
t = _SPACE_(_no_, callable.__name__, n)
|
|
1090
|
+
raise Error(Fmt.SQUARE(name, i), p, txt=t)
|
|
1091
|
+
return ps
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def fractional(points, fi, j=None, wrap=None, LatLon=None, Vector=None, **kwds):
|
|
1095
|
+
'''Return the point at a given I{fractional} index.
|
|
1096
|
+
|
|
1097
|
+
@arg points: The points (C{LatLon}[], L{Numpy2LatLon}[],
|
|
1098
|
+
L{Tuple2LatLon}[], C{Cartesian}[], C{Vector3d}[],
|
|
1099
|
+
L{Vector3Tuple}[]).
|
|
1100
|
+
@arg fi: The fractional index (L{FIx}, C{float} or C{int}).
|
|
1101
|
+
@kwarg j: Optionally, index of the other point (C{int}).
|
|
1102
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
1103
|
+
B{{points}} (C{bool}) or C{None} for a backward
|
|
1104
|
+
compatible L{LatLon2Tuple} or B{C{LatLon}} with
|
|
1105
|
+
averaged lat- and longitudes. Use C{True} or
|
|
1106
|
+
C{False} to get the I{fractional} point computed
|
|
1107
|
+
by method C{B{points}[fi].intermediateTo}.
|
|
1108
|
+
@kwarg LatLon: Optional class to return the I{intermediate},
|
|
1109
|
+
I{fractional} point (C{LatLon}) or C{None}.
|
|
1110
|
+
@kwarg Vector: Optional class to return the I{intermediate},
|
|
1111
|
+
I{fractional} point (C{Cartesian}, C{Vector3d})
|
|
1112
|
+
or C{None}.
|
|
1113
|
+
@kwarg kwds: Optional, additional B{C{LatLon}} I{or} B{C{Vector}}
|
|
1114
|
+
keyword arguments, ignored if both C{B{LatLon}} and
|
|
1115
|
+
C{B{Vector}} are C{None}.
|
|
1116
|
+
|
|
1117
|
+
@return: A L{LatLon2Tuple}C{(lat, lon)} if B{C{wrap}}, B{C{LatLon}}
|
|
1118
|
+
and B{C{Vector}} all are C{None}, the defaults.
|
|
1119
|
+
|
|
1120
|
+
An instance of B{C{LatLon}} if not C{None} I{or} an instance
|
|
1121
|
+
of B{C{Vector}} if not C{None}.
|
|
1122
|
+
|
|
1123
|
+
Otherwise with B{C{wrap}} either C{True} or C{False} and
|
|
1124
|
+
B{C{LatLon}} and B{C{Vector}} both C{None}, an instance of
|
|
1125
|
+
B{C{points}}' (sub-)class C{intermediateTo} I{fractional}.
|
|
1126
|
+
|
|
1127
|
+
Summarized as follows:
|
|
1128
|
+
|
|
1129
|
+
>>> wrap | LatLon | Vector | returned type/value
|
|
1130
|
+
# -------+--------+--------+--------------+------
|
|
1131
|
+
# | | | LatLon2Tuple | favg
|
|
1132
|
+
# None | None | None | or** |
|
|
1133
|
+
# | | | Vector3Tuple | favg
|
|
1134
|
+
# None | LatLon | None | LatLon | favg
|
|
1135
|
+
# None | None | Vector | Vector | favg
|
|
1136
|
+
# -------+--------+--------+--------------+------
|
|
1137
|
+
# True | None | None | points' | .iTo
|
|
1138
|
+
# True | LatLon | None | LatLon | .iTo
|
|
1139
|
+
# True | None | Vector | Vector | .iTo
|
|
1140
|
+
# -------+--------+--------+--------------+------
|
|
1141
|
+
# False | None | None | points' | .iTo
|
|
1142
|
+
# False | LatLon | None | LatLon | .iTo
|
|
1143
|
+
# False | None | Vector | Vector | .iTo
|
|
1144
|
+
# _____
|
|
1145
|
+
# favg) averaged lat, lon or x, y, z values
|
|
1146
|
+
# .iTo) value from points[fi].intermediateTo
|
|
1147
|
+
# **) depends on base class of points[fi]
|
|
1148
|
+
|
|
1149
|
+
@raise IndexError: Fractional index B{C{fi}} invalid or B{C{points}}
|
|
1150
|
+
not subscriptable or not closed.
|
|
1151
|
+
|
|
1152
|
+
@raise TypeError: Invalid B{C{LatLon}}, B{C{Vector}} or B{C{kwds}}
|
|
1153
|
+
argument.
|
|
1154
|
+
|
|
1155
|
+
@see: Class L{FIx} and method L{FIx.fractional}.
|
|
1156
|
+
'''
|
|
1157
|
+
if LatLon and Vector: # PYCHOK no cover
|
|
1158
|
+
kwds = _xkwds(kwds, fi=fi, LatLon=LatLon, Vector=Vector)
|
|
1159
|
+
raise _TypeError(txt=fractional.__name__, **kwds)
|
|
1160
|
+
w = wrap if LatLon else False # intermediateTo
|
|
1161
|
+
try:
|
|
1162
|
+
if not isscalar(fi) or fi < 0:
|
|
1163
|
+
raise IndexError
|
|
1164
|
+
n = _xattr(fi, fin=0)
|
|
1165
|
+
p = _fractional(points, fi, j, fin=n, wrap=w) # see .units.FIx
|
|
1166
|
+
if LatLon:
|
|
1167
|
+
p = LatLon(p.lat, p.lon, **kwds)
|
|
1168
|
+
elif Vector:
|
|
1169
|
+
p = Vector(p.x, p.y, p.z, **kwds)
|
|
1170
|
+
except (IndexError, TypeError):
|
|
1171
|
+
raise _IndexError(fi=fi, points=points, wrap=w, txt=fractional.__name__)
|
|
1172
|
+
return p
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def _fractional(points, fi, j, fin=None, wrap=None): # in .frechet.py
|
|
1176
|
+
'''(INTERNAL) Compute point at L{fractional} index C{fi} and C{j}.
|
|
1177
|
+
'''
|
|
1178
|
+
i = int(fi)
|
|
1179
|
+
p = points[i]
|
|
1180
|
+
r = fi - float(i)
|
|
1181
|
+
if r > EPS: # EPS0?
|
|
1182
|
+
if j is None: # in .frechet.py
|
|
1183
|
+
j = i + 1
|
|
1184
|
+
if fin:
|
|
1185
|
+
j %= fin
|
|
1186
|
+
q = points[j]
|
|
1187
|
+
if r >= EPS1: # PYCHOK no cover
|
|
1188
|
+
p = q
|
|
1189
|
+
elif wrap is not None: # in (True, False)
|
|
1190
|
+
p = p.intermediateTo(q, r, wrap=wrap)
|
|
1191
|
+
elif _isLatLon(p): # backward compatible default
|
|
1192
|
+
p = LatLon2Tuple(favg(p.lat, q.lat, f=r),
|
|
1193
|
+
favg(p.lon, q.lon, f=r),
|
|
1194
|
+
name=fractional.__name__)
|
|
1195
|
+
else: # assume p and q are cartesian or vectorial
|
|
1196
|
+
z = p.z if p.z is q.z else favg(p.z, q.z, f=r)
|
|
1197
|
+
p = Vector3Tuple(favg(p.x, q.x, f=r),
|
|
1198
|
+
favg(p.y, q.y, f=r), z,
|
|
1199
|
+
name=fractional.__name__)
|
|
1200
|
+
return p
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def isclockwise(points, adjust=False, wrap=True):
|
|
1204
|
+
'''Determine the direction of a path or polygon.
|
|
1205
|
+
|
|
1206
|
+
@arg points: The path or polygon points (C{LatLon}[]).
|
|
1207
|
+
@kwarg adjust: Adjust the wrapped, unrolled longitudinal delta
|
|
1208
|
+
by the cosine of the mean latitude (C{bool}).
|
|
1209
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
1210
|
+
B{C{points}} (C{bool}).
|
|
1211
|
+
|
|
1212
|
+
@return: C{True} if B{C{points}} are clockwise, C{False} otherwise.
|
|
1213
|
+
|
|
1214
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1215
|
+
|
|
1216
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
1217
|
+
|
|
1218
|
+
@raise ValueError: The B{C{points}} enclose a pole or zero area.
|
|
1219
|
+
'''
|
|
1220
|
+
a, pts = _area2(points, adjust, wrap)
|
|
1221
|
+
if a > 0: # opposite of ellipsoidalExact and -Karney
|
|
1222
|
+
return True
|
|
1223
|
+
elif a < 0:
|
|
1224
|
+
return False
|
|
1225
|
+
# <https://blog.Element84.com/determining-if-a-spherical-polygon-contains-a-pole.html>
|
|
1226
|
+
raise _areaError(pts)
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def isconvex(points, adjust=False, wrap=False): # was=True
|
|
1230
|
+
'''Determine whether a polygon is convex.
|
|
1231
|
+
|
|
1232
|
+
@arg points: The polygon points (C{LatLon}[]).
|
|
1233
|
+
@kwarg adjust: Adjust the wrapped, unrolled longitudinal delta
|
|
1234
|
+
by the cosine of the mean latitude (C{bool}).
|
|
1235
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
1236
|
+
B{C{points}} (C{bool}).
|
|
1237
|
+
|
|
1238
|
+
@return: C{True} if B{C{points}} are convex, C{False} otherwise.
|
|
1239
|
+
|
|
1240
|
+
@raise CrossError: Some B{C{points}} are colinear.
|
|
1241
|
+
|
|
1242
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1243
|
+
|
|
1244
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
1245
|
+
'''
|
|
1246
|
+
return bool(isconvex_(points, adjust=adjust, wrap=wrap))
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def isconvex_(points, adjust=False, wrap=False): # was=True
|
|
1250
|
+
'''Determine whether a polygon is convex I{and clockwise}.
|
|
1251
|
+
|
|
1252
|
+
@arg points: The polygon points (C{LatLon}[]).
|
|
1253
|
+
@kwarg adjust: Adjust the wrapped, unrolled longitudinal delta
|
|
1254
|
+
by the cosine of the mean latitude (C{bool}).
|
|
1255
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
1256
|
+
B{C{points}} (C{bool}).
|
|
1257
|
+
|
|
1258
|
+
@return: C{+1} if B{C{points}} are convex clockwise, C{-1} for
|
|
1259
|
+
convex counter-clockwise B{C{points}}, C{0} otherwise.
|
|
1260
|
+
|
|
1261
|
+
@raise CrossError: Some B{C{points}} are colinear.
|
|
1262
|
+
|
|
1263
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1264
|
+
|
|
1265
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
1266
|
+
'''
|
|
1267
|
+
if adjust:
|
|
1268
|
+
def _unroll2(x1, x2, w, y1, y2):
|
|
1269
|
+
x21, x2 = unroll180(x1, x2, wrap=w)
|
|
1270
|
+
y = radians(y1 + y2) * _0_5
|
|
1271
|
+
x21 *= cos(y) if fabs(y) < PI_2 else _0_0
|
|
1272
|
+
return x21, x2
|
|
1273
|
+
else:
|
|
1274
|
+
def _unroll2(x1, x2, w, *unused): # PYCHOK expected
|
|
1275
|
+
return unroll180(x1, x2, wrap=w)
|
|
1276
|
+
|
|
1277
|
+
c, s = crosserrors(), 0
|
|
1278
|
+
|
|
1279
|
+
Ps = LatLon2PsxyIter(points, loop=2, wrap=wrap)
|
|
1280
|
+
x1, y1, _ = Ps[0]
|
|
1281
|
+
x2, y2, _ = Ps[1]
|
|
1282
|
+
|
|
1283
|
+
x21, x2 = _unroll2(x1, x2, False, y1, y2)
|
|
1284
|
+
for i, p in Ps.enumerate(closed=True):
|
|
1285
|
+
x3, y3, ll = p
|
|
1286
|
+
x32, x3 = _unroll2(x2, x3, bool(wrap and not Ps.looped), y2, y3)
|
|
1287
|
+
|
|
1288
|
+
# get the sign of the distance from point
|
|
1289
|
+
# x3, y3 to the line from x1, y1 to x2, y2
|
|
1290
|
+
# <https://WikiPedia.org/wiki/Distance_from_a_point_to_a_line>
|
|
1291
|
+
s3 = fdot((x3, y3, x1, y1), y2 - y1, -x21, -y2, x2)
|
|
1292
|
+
if s3 > 0: # x3, y3 on the right
|
|
1293
|
+
if s < 0: # non-convex
|
|
1294
|
+
return 0
|
|
1295
|
+
s = +1
|
|
1296
|
+
|
|
1297
|
+
elif s3 < 0: # x3, y3 on the left
|
|
1298
|
+
if s > 0: # non-convex
|
|
1299
|
+
return 0
|
|
1300
|
+
s = -1
|
|
1301
|
+
|
|
1302
|
+
elif c and fdot((x32, y1 - y2), y3 - y2, -x21) < 0: # PYCHOK no cover
|
|
1303
|
+
# colinear u-turn: x3, y3 not on the
|
|
1304
|
+
# opposite side of x2, y2 as x1, y1
|
|
1305
|
+
t = Fmt.SQUARE(points=i)
|
|
1306
|
+
raise CrossError(t, ll, txt=_colinear_)
|
|
1307
|
+
|
|
1308
|
+
x1, y1, x2, y2, x21 = x2, y2, x3, y3, x32
|
|
1309
|
+
|
|
1310
|
+
return s # all points on the same side
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def isenclosedBy(point, points, wrap=False): # MCCABE 15
|
|
1314
|
+
'''Determine whether a point is enclosed by a polygon or composite.
|
|
1315
|
+
|
|
1316
|
+
@arg point: The point (C{LatLon} or 2-tuple C{(lat, lon)}).
|
|
1317
|
+
@arg points: The polygon points or clips (C{LatLon}[], L{BooleanFHP}
|
|
1318
|
+
or L{BooleanGH}).
|
|
1319
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
1320
|
+
B{C{points}} (C{bool}).
|
|
1321
|
+
|
|
1322
|
+
@return: C{True} if the B{C{point}} is inside the polygon or
|
|
1323
|
+
composite, C{False} otherwise.
|
|
1324
|
+
|
|
1325
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1326
|
+
|
|
1327
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
1328
|
+
|
|
1329
|
+
@raise ValueError: Invalid B{C{point}}, lat- or longitude.
|
|
1330
|
+
|
|
1331
|
+
@see: Functions L{pygeodesy.isconvex} and L{pygeodesy.ispolar} especially
|
|
1332
|
+
if the B{C{points}} may enclose a pole or wrap around the earth
|
|
1333
|
+
I{longitudinally}, methods L{sphericalNvector.LatLon.isenclosedBy},
|
|
1334
|
+
L{sphericalTrigonometry.LatLon.isenclosedBy} and U{MultiDop
|
|
1335
|
+
GeogContainPt<https://GitHub.com/NASA/MultiDop>} (U{Shapiro et.al. 2009,
|
|
1336
|
+
JTECH<https://Journals.AMetSoc.org/doi/abs/10.1175/2009JTECHA1256.1>}
|
|
1337
|
+
and U{Potvin et al. 2012, JTECH <https://Journals.AMetSoc.org/doi/abs/
|
|
1338
|
+
10.1175/JTECH-D-11-00019.1>}).
|
|
1339
|
+
'''
|
|
1340
|
+
try:
|
|
1341
|
+
y0, x0 = point.lat, point.lon
|
|
1342
|
+
except AttributeError:
|
|
1343
|
+
try:
|
|
1344
|
+
y0, x0 = map(float, point[:2])
|
|
1345
|
+
except (IndexError, TypeError, ValueError) as x:
|
|
1346
|
+
raise _ValueError(point=point, cause=x)
|
|
1347
|
+
|
|
1348
|
+
if wrap:
|
|
1349
|
+
y0, x0 = _Wrap.latlon(y0, x0)
|
|
1350
|
+
|
|
1351
|
+
def _dxy3(x, x2, y2, Ps):
|
|
1352
|
+
dx, x2 = unroll180(x, x2, wrap=not Ps.looped)
|
|
1353
|
+
return dx, x2, y2
|
|
1354
|
+
|
|
1355
|
+
else:
|
|
1356
|
+
x0 = fmod(x0, _360_0) # not x0 % 360!
|
|
1357
|
+
x0_180_ = x0 - _180_0
|
|
1358
|
+
x0_180 = x0 + _180_0
|
|
1359
|
+
|
|
1360
|
+
def _dxy3(x1, x, y, unused): # PYCHOK expected
|
|
1361
|
+
x = _umod_360(float(x))
|
|
1362
|
+
if x < x0_180_:
|
|
1363
|
+
x += _360_0
|
|
1364
|
+
elif x >= x0_180:
|
|
1365
|
+
x -= _360_0
|
|
1366
|
+
return (x - x1), x, y
|
|
1367
|
+
|
|
1368
|
+
if _MODS.booleans.isBoolean(points):
|
|
1369
|
+
return points._encloses(y0, x0, wrap=wrap)
|
|
1370
|
+
|
|
1371
|
+
Ps = LatLon2PsxyIter(points, loop=1, wrap=wrap)
|
|
1372
|
+
p = Ps[0]
|
|
1373
|
+
e = m = False
|
|
1374
|
+
S = Fsum()
|
|
1375
|
+
|
|
1376
|
+
_, x1, y1 = _dxy3(x0, p.x, p.y, False)
|
|
1377
|
+
for p in Ps.iterate(closed=True):
|
|
1378
|
+
dx, x2, y2 = _dxy3(x1, p.x, p.y, Ps)
|
|
1379
|
+
# ignore duplicate and near-duplicate pts
|
|
1380
|
+
if fabs(dx) > EPS or fabs(y2 - y1) > EPS:
|
|
1381
|
+
# determine if polygon edge (x1, y1)..(x2, y2) straddles
|
|
1382
|
+
# point (lat, lon) or is on boundary, but do not count
|
|
1383
|
+
# edges on boundary as more than one crossing
|
|
1384
|
+
if fabs(dx) < 180 and (x1 < x0 <= x2 or x2 < x0 <= x1):
|
|
1385
|
+
m = not m
|
|
1386
|
+
dy = (x0 - x1) * (y2 - y1) - (y0 - y1) * dx
|
|
1387
|
+
if (dy > 0 and dx >= 0) or (dy < 0 and dx <= 0):
|
|
1388
|
+
e = not e
|
|
1389
|
+
|
|
1390
|
+
S += sin(radians(y2))
|
|
1391
|
+
x1, y1 = x2, y2
|
|
1392
|
+
|
|
1393
|
+
# An odd number of meridian crossings means, the polygon
|
|
1394
|
+
# contains a pole. Assume it is the pole on the hemisphere
|
|
1395
|
+
# containing the polygon mean point and if the polygon does
|
|
1396
|
+
# contain the North Pole, flip the result.
|
|
1397
|
+
if m and S.fsum() > 0:
|
|
1398
|
+
e = not e
|
|
1399
|
+
return e
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def ispolar(points, wrap=False):
|
|
1403
|
+
'''Check whether a polygon encloses a pole.
|
|
1404
|
+
|
|
1405
|
+
@arg points: The polygon points (C{LatLon}[]).
|
|
1406
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll
|
|
1407
|
+
the B{C{points}} (C{bool}).
|
|
1408
|
+
|
|
1409
|
+
@return: C{True} if the polygon encloses a pole, C{False}
|
|
1410
|
+
otherwise.
|
|
1411
|
+
|
|
1412
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1413
|
+
|
|
1414
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon} or don't
|
|
1415
|
+
have C{bearingTo2}, C{initialBearingTo}
|
|
1416
|
+
and C{finalBearingTo} methods.
|
|
1417
|
+
'''
|
|
1418
|
+
def _cds(ps, w): # iterate over course deltas
|
|
1419
|
+
Ps = PointsIter(ps, loop=2, wrap=w)
|
|
1420
|
+
p2, p1 = Ps[0:2]
|
|
1421
|
+
b1, _ = _bearingTo2(p2, p1, wrap=False)
|
|
1422
|
+
for p2 in Ps.iterate(closed=True):
|
|
1423
|
+
if not p2.isequalTo(p1, EPS):
|
|
1424
|
+
if w and not Ps.looped:
|
|
1425
|
+
p2 = _unrollon(p1, p2)
|
|
1426
|
+
b, b2 = _bearingTo2(p1, p2, wrap=False)
|
|
1427
|
+
yield wrap180(b - b1) # (b - b1 + 540) % 360 - 180
|
|
1428
|
+
yield wrap180(b2 - b) # (b2 - b + 540) % 360 - 180
|
|
1429
|
+
p1, b1 = p2, b2
|
|
1430
|
+
|
|
1431
|
+
# summation of course deltas around pole is 0° rather than normally ±360°
|
|
1432
|
+
# <https://blog.Element84.com/determining-if-a-spherical-polygon-contains-a-pole.html>
|
|
1433
|
+
s = fsum(_cds(points, wrap), floats=True)
|
|
1434
|
+
# XXX fix (intermittant) edge crossing pole - eg (85,90), (85,0), (85,-90)
|
|
1435
|
+
return fabs(s) < 90 # "zero-ish"
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def luneOf(lon1, lon2, closed=False, LatLon=LatLon_, **LatLon_kwds):
|
|
1439
|
+
'''Generate an ellipsoidal or spherical U{lune
|
|
1440
|
+
<https://WikiPedia.org/wiki/Spherical_lune>}-shaped path or polygon.
|
|
1441
|
+
|
|
1442
|
+
@arg lon1: Left longitude (C{degrees90}).
|
|
1443
|
+
@arg lon2: Right longitude (C{degrees90}).
|
|
1444
|
+
@kwarg closed: Optionally, close the path (C{bool}).
|
|
1445
|
+
@kwarg LatLon: Class to use (L{LatLon_}).
|
|
1446
|
+
@kwarg LatLon_kwds: Optional, additional B{C{LatLon}}
|
|
1447
|
+
keyword arguments.
|
|
1448
|
+
|
|
1449
|
+
@return: A tuple of 4 or 5 B{C{LatLon}} instances outlining
|
|
1450
|
+
the lune shape.
|
|
1451
|
+
|
|
1452
|
+
@see: U{Latitude-longitude quadrangle
|
|
1453
|
+
<https://www.MathWorks.com/help/map/ref/areaquad.html>}.
|
|
1454
|
+
'''
|
|
1455
|
+
t = (LatLon( _0_0, lon1, **LatLon_kwds),
|
|
1456
|
+
LatLon( _90_0, lon1, **LatLon_kwds),
|
|
1457
|
+
LatLon( _0_0, lon2, **LatLon_kwds),
|
|
1458
|
+
LatLon(_N_90_0, lon2, **LatLon_kwds))
|
|
1459
|
+
if closed:
|
|
1460
|
+
t += t[:1]
|
|
1461
|
+
return t
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def nearestOn5(point, points, closed=False, wrap=False, adjust=True,
|
|
1465
|
+
limit=9, **LatLon_and_kwds):
|
|
1466
|
+
'''Locate the point on a path or polygon closest to a reference point.
|
|
1467
|
+
|
|
1468
|
+
The closest point on each polygon edge is either the nearest of that
|
|
1469
|
+
edge's end points or a point in between.
|
|
1470
|
+
|
|
1471
|
+
@arg point: The reference point (C{LatLon}).
|
|
1472
|
+
@arg points: The path or polygon points (C{LatLon}[]).
|
|
1473
|
+
@kwarg closed: Optionally, close the path or polygon (C{bool}).
|
|
1474
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
1475
|
+
B{C{points}} (C{bool}).
|
|
1476
|
+
@kwarg adjust: See function L{pygeodesy.equirectangular_} (C{bool}).
|
|
1477
|
+
@kwarg limit: See function L{pygeodesy.equirectangular_} (C{degrees}),
|
|
1478
|
+
default C{9 degrees} is about C{1,000 Kmeter} (for mean
|
|
1479
|
+
spherical earth radius L{R_KM}).
|
|
1480
|
+
@kwarg LatLon_and_kwds: Optional, C{B{LatLon}=None} class to use for
|
|
1481
|
+
the closest point and additional B{C{LatLon}} keyword
|
|
1482
|
+
arguments, ignored if C{B{LatLon}=None} or not given.
|
|
1483
|
+
|
|
1484
|
+
@return: A L{NearestOn3Tuple}C{(closest, distance, angle)} with the
|
|
1485
|
+
{closest} point (B{C{LatLon}}) or if C{B{LatLon} is None},
|
|
1486
|
+
a L{NearestOn5Tuple}C{(lat, lon, distance, angle, height)}.
|
|
1487
|
+
The C{distance} is the L{pygeodesy.equirectangular} distance
|
|
1488
|
+
between the C{closest} and reference B{C{point}} in C{degrees}.
|
|
1489
|
+
The C{angle} from the B{C{point}} to the C{closest} is in
|
|
1490
|
+
compass C{degrees}, like function L{pygeodesy.compassAngle}.
|
|
1491
|
+
|
|
1492
|
+
@raise LimitError: Lat- and/or longitudinal delta exceeds the B{C{limit}},
|
|
1493
|
+
see function L{pygeodesy.equirectangular_}.
|
|
1494
|
+
|
|
1495
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1496
|
+
|
|
1497
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
1498
|
+
|
|
1499
|
+
@note: Distances are I{approximated} by function L{pygeodesy.equirectangular_}.
|
|
1500
|
+
For more accuracy use one of the C{LatLon.nearestOn6} methods.
|
|
1501
|
+
|
|
1502
|
+
@see: Function L{pygeodesy.degrees2m}.
|
|
1503
|
+
'''
|
|
1504
|
+
def _d2yx4(p2, p1, u, alw):
|
|
1505
|
+
# w = wrap if (i < (n - 1) or not closed) else False
|
|
1506
|
+
# equirectangular_ returns a Distance4Tuple(distance
|
|
1507
|
+
# in degrees squared, delta lat, delta lon, p2.lon
|
|
1508
|
+
# unroll/wrap'd); the previous p2.lon unroll/wrap'd
|
|
1509
|
+
# is also applied to the next edge's p1.lon
|
|
1510
|
+
return equirectangular_(p1.lat, p1.lon + u,
|
|
1511
|
+
p2.lat, p2.lon, **alw)
|
|
1512
|
+
|
|
1513
|
+
def _h(p): # get height or default 0
|
|
1514
|
+
return _xattr(p, height=0) or 0
|
|
1515
|
+
|
|
1516
|
+
# 3-D version used in .vector3d._nearestOn2
|
|
1517
|
+
#
|
|
1518
|
+
# point (x, y) on axis rotated ccw by angle a:
|
|
1519
|
+
# x' = x * cos(a) + y * sin(a)
|
|
1520
|
+
# y' = y * cos(a) - x * sin(a)
|
|
1521
|
+
#
|
|
1522
|
+
# distance (w) along and (h) perpendicular to
|
|
1523
|
+
# a line thru point (dx, dy) and the origin:
|
|
1524
|
+
# d = hypot(dx, dy)
|
|
1525
|
+
# w = (x * dx + y * dy) / d
|
|
1526
|
+
# h = (y * dx - x * dy) / d
|
|
1527
|
+
#
|
|
1528
|
+
# closest point on that line thru (dx, dy):
|
|
1529
|
+
# xc = dx * w / d
|
|
1530
|
+
# yc = dy * w / d
|
|
1531
|
+
# or
|
|
1532
|
+
# xc = dx * f
|
|
1533
|
+
# yc = dy * f
|
|
1534
|
+
# with
|
|
1535
|
+
# f = w / d
|
|
1536
|
+
# or
|
|
1537
|
+
# f = (y * dy + x * dx) / hypot2(dx, dy)
|
|
1538
|
+
#
|
|
1539
|
+
# i.e. no need for sqrt or hypot
|
|
1540
|
+
|
|
1541
|
+
Ps = PointsIter(points, loop=1, wrap=wrap)
|
|
1542
|
+
p1 = c = Ps[0]
|
|
1543
|
+
u1 = u = _0_0
|
|
1544
|
+
kw = dict(adjust=adjust, limit=limit, wrap=False)
|
|
1545
|
+
d, dy, dx, _ = _d2yx4(p1, point, u1, kw)
|
|
1546
|
+
for p2 in Ps.iterate(closed=closed):
|
|
1547
|
+
# iff wrapped, unroll lon1 (actually previous
|
|
1548
|
+
# lon2) like function unroll180/-PI would've
|
|
1549
|
+
if wrap:
|
|
1550
|
+
kw.update(wrap=not (closed and Ps.looped))
|
|
1551
|
+
d21, y21, x21, u2 = _d2yx4(p2, p1, u1, kw)
|
|
1552
|
+
if d21 > EPS:
|
|
1553
|
+
# distance point to p1, y01 and x01 negated
|
|
1554
|
+
d2, y01, x01, _ = _d2yx4(point, p1, u1, kw)
|
|
1555
|
+
if d2 > EPS:
|
|
1556
|
+
w2 = y01 * y21 + x01 * x21
|
|
1557
|
+
if w2 > 0:
|
|
1558
|
+
if w2 < d21:
|
|
1559
|
+
# closest is between p1 and p2, use
|
|
1560
|
+
# original delta's, not y21 and x21
|
|
1561
|
+
f = w2 / d21
|
|
1562
|
+
p1 = LatLon_(favg(p1.lat, p2.lat, f=f),
|
|
1563
|
+
favg(p1.lon, p2.lon + u2, f=f),
|
|
1564
|
+
height=favg(_h(p1), _h(p2), f=f))
|
|
1565
|
+
u1 = _0_0
|
|
1566
|
+
else: # p2 is closest
|
|
1567
|
+
p1, u1 = p2, u2
|
|
1568
|
+
d2, y01, x01, _ = _d2yx4(point, p1, u1, kw)
|
|
1569
|
+
if d2 < d: # p1 is closer, y01 and x01 negated
|
|
1570
|
+
c, u, d, dy, dx = p1, u1, d2, -y01, -x01
|
|
1571
|
+
p1, u1 = p2, u2
|
|
1572
|
+
|
|
1573
|
+
a = atan2b(dx, dy) # azimuth
|
|
1574
|
+
d = hypot( dx, dy)
|
|
1575
|
+
h = _h(c)
|
|
1576
|
+
n = nameof(point) or nearestOn5.__name__
|
|
1577
|
+
if LatLon_and_kwds:
|
|
1578
|
+
LL, kwds = _xkwds_pop2(LatLon_and_kwds, LatLon=None)
|
|
1579
|
+
if LL is not None:
|
|
1580
|
+
r = LL(c.lat, c.lon + u, **_xkwds(kwds, height=h, name=n))
|
|
1581
|
+
return NearestOn3Tuple(r, d, a, name=n)
|
|
1582
|
+
return NearestOn5Tuple(c.lat, c.lon + u, d, a, h, name=n) # PYCHOK expected
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def perimeterOf(points, closed=False, adjust=True, radius=R_M, wrap=True):
|
|
1586
|
+
'''I{Approximate} the perimeter of a path, polygon. or composite.
|
|
1587
|
+
|
|
1588
|
+
@arg points: The path or polygon points or clips (C{LatLon}[],
|
|
1589
|
+
L{BooleanFHP} or L{BooleanGH}).
|
|
1590
|
+
@kwarg closed: Optionally, close the path or polygon (C{bool}).
|
|
1591
|
+
@kwarg adjust: Adjust the wrapped, unrolled longitudinal delta
|
|
1592
|
+
by the cosine of the mean latitude (C{bool}).
|
|
1593
|
+
@kwarg radius: Mean earth radius (C{meter}).
|
|
1594
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
1595
|
+
B{C{points}} (C{bool}).
|
|
1596
|
+
|
|
1597
|
+
@return: Approximate perimeter (C{meter}, same units as
|
|
1598
|
+
B{C{radius}}).
|
|
1599
|
+
|
|
1600
|
+
@raise PointsError: Insufficient number of B{C{points}}
|
|
1601
|
+
|
|
1602
|
+
@raise TypeError: Some B{C{points}} are not C{LatLon}.
|
|
1603
|
+
|
|
1604
|
+
@raise ValueError: Invalid B{C{radius}} or C{B{closed}=False} with
|
|
1605
|
+
C{B{points}} a composite.
|
|
1606
|
+
|
|
1607
|
+
@note: This perimeter is based on the L{pygeodesy.equirectangular_}
|
|
1608
|
+
distance approximation and is ill-suited for regions exceeding
|
|
1609
|
+
several hundred Km or Miles or with near-polar latitudes.
|
|
1610
|
+
|
|
1611
|
+
@see: Functions L{sphericalTrigonometry.perimeterOf} and
|
|
1612
|
+
L{ellipsoidalKarney.perimeterOf}.
|
|
1613
|
+
'''
|
|
1614
|
+
def _degs(ps, c, a, w): # angular edge lengths in degrees
|
|
1615
|
+
Ps = LatLon2PsxyIter(ps, loop=1) # wrap=w
|
|
1616
|
+
p1, u = Ps[0], _0_0 # previous x2's unroll/wrap
|
|
1617
|
+
for p2 in Ps.iterate(closed=c):
|
|
1618
|
+
if w and c:
|
|
1619
|
+
w = not Ps.looped
|
|
1620
|
+
# apply previous x2's unroll/wrap'd to new x1
|
|
1621
|
+
_, dy, dx, u = equirectangular_(p1.y, p1.x + u,
|
|
1622
|
+
p2.y, p2.x,
|
|
1623
|
+
adjust=a, limit=None,
|
|
1624
|
+
wrap=w) # PYCHOK non-seq
|
|
1625
|
+
yield hypot(dx, dy)
|
|
1626
|
+
p1 = p2
|
|
1627
|
+
|
|
1628
|
+
if _MODS.booleans.isBoolean(points):
|
|
1629
|
+
if not closed:
|
|
1630
|
+
notImplemented(None, closed=closed, points=_composite_)
|
|
1631
|
+
d = points._sum1(perimeterOf, closed=True, adjust=adjust,
|
|
1632
|
+
radius=radius, wrap=wrap)
|
|
1633
|
+
else:
|
|
1634
|
+
d = fsum(_degs(points, closed, adjust, wrap), floats=True)
|
|
1635
|
+
return degrees2m(d, radius=radius)
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
def quadOf(latS, lonW, latN, lonE, closed=False, LatLon=LatLon_, **LatLon_kwds):
|
|
1639
|
+
'''Generate a quadrilateral path or polygon from two points.
|
|
1640
|
+
|
|
1641
|
+
@arg latS: Souther-nmost latitude (C{degrees90}).
|
|
1642
|
+
@arg lonW: Western-most longitude (C{degrees180}).
|
|
1643
|
+
@arg latN: Norther-nmost latitude (C{degrees90}).
|
|
1644
|
+
@arg lonE: Eastern-most longitude (C{degrees180}).
|
|
1645
|
+
@kwarg closed: Optionally, close the path (C{bool}).
|
|
1646
|
+
@kwarg LatLon: Class to use (L{LatLon_}).
|
|
1647
|
+
@kwarg LatLon_kwds: Optional, additional B{C{LatLon}}
|
|
1648
|
+
keyword arguments.
|
|
1649
|
+
|
|
1650
|
+
@return: Return a tuple of 4 or 5 B{C{LatLon}} instances
|
|
1651
|
+
outlining the quadrilateral.
|
|
1652
|
+
|
|
1653
|
+
@see: Function L{boundsOf}.
|
|
1654
|
+
'''
|
|
1655
|
+
t = (LatLon(latS, lonW, **LatLon_kwds),
|
|
1656
|
+
LatLon(latN, lonW, **LatLon_kwds),
|
|
1657
|
+
LatLon(latN, lonE, **LatLon_kwds),
|
|
1658
|
+
LatLon(latS, lonE, **LatLon_kwds))
|
|
1659
|
+
if closed:
|
|
1660
|
+
t += t[:1]
|
|
1661
|
+
return t
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
__all__ += _ALL_DOCS(_Array2LatLon, _Basequence)
|
|
1665
|
+
|
|
1666
|
+
# **) MIT License
|
|
1667
|
+
#
|
|
1668
|
+
# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
|
|
1669
|
+
#
|
|
1670
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
1671
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
1672
|
+
# to deal in the Software without restriction, including without limitation
|
|
1673
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
1674
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
1675
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
1676
|
+
#
|
|
1677
|
+
# The above copyright notice and this permission notice shall be included
|
|
1678
|
+
# in all copies or substantial portions of the Software.
|
|
1679
|
+
#
|
|
1680
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
1681
|
+
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
1682
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
1683
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
1684
|
+
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
1685
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
1686
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|