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.
Files changed (115) hide show
  1. PyGeodesy-24.3.24.dist-info/METADATA +272 -0
  2. PyGeodesy-24.3.24.dist-info/RECORD +115 -0
  3. PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
  4. PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
  5. pygeodesy/LICENSE +21 -0
  6. pygeodesy/__init__.py +615 -0
  7. pygeodesy/__main__.py +103 -0
  8. pygeodesy/albers.py +867 -0
  9. pygeodesy/auxilats/_CX_4.py +218 -0
  10. pygeodesy/auxilats/_CX_6.py +314 -0
  11. pygeodesy/auxilats/_CX_8.py +475 -0
  12. pygeodesy/auxilats/__init__.py +54 -0
  13. pygeodesy/auxilats/__main__.py +86 -0
  14. pygeodesy/auxilats/auxAngle.py +548 -0
  15. pygeodesy/auxilats/auxDLat.py +302 -0
  16. pygeodesy/auxilats/auxDST.py +296 -0
  17. pygeodesy/auxilats/auxLat.py +848 -0
  18. pygeodesy/auxilats/auxily.py +272 -0
  19. pygeodesy/azimuthal.py +1150 -0
  20. pygeodesy/basics.py +892 -0
  21. pygeodesy/booleans.py +2031 -0
  22. pygeodesy/cartesianBase.py +1062 -0
  23. pygeodesy/clipy.py +704 -0
  24. pygeodesy/constants.py +516 -0
  25. pygeodesy/css.py +660 -0
  26. pygeodesy/datums.py +752 -0
  27. pygeodesy/deprecated/__init__.py +61 -0
  28. pygeodesy/deprecated/bases.py +40 -0
  29. pygeodesy/deprecated/classes.py +262 -0
  30. pygeodesy/deprecated/consterns.py +54 -0
  31. pygeodesy/deprecated/datum.py +40 -0
  32. pygeodesy/deprecated/functions.py +375 -0
  33. pygeodesy/deprecated/nvector.py +48 -0
  34. pygeodesy/deprecated/rhumbBase.py +32 -0
  35. pygeodesy/deprecated/rhumbaux.py +33 -0
  36. pygeodesy/deprecated/rhumbsolve.py +33 -0
  37. pygeodesy/deprecated/rhumbx.py +33 -0
  38. pygeodesy/dms.py +986 -0
  39. pygeodesy/ecef.py +1348 -0
  40. pygeodesy/elevations.py +279 -0
  41. pygeodesy/ellipsoidalBase.py +1224 -0
  42. pygeodesy/ellipsoidalBaseDI.py +913 -0
  43. pygeodesy/ellipsoidalExact.py +343 -0
  44. pygeodesy/ellipsoidalGeodSolve.py +343 -0
  45. pygeodesy/ellipsoidalKarney.py +403 -0
  46. pygeodesy/ellipsoidalNvector.py +685 -0
  47. pygeodesy/ellipsoidalVincenty.py +590 -0
  48. pygeodesy/ellipsoids.py +2476 -0
  49. pygeodesy/elliptic.py +1198 -0
  50. pygeodesy/epsg.py +243 -0
  51. pygeodesy/errors.py +804 -0
  52. pygeodesy/etm.py +1190 -0
  53. pygeodesy/fmath.py +1013 -0
  54. pygeodesy/formy.py +1818 -0
  55. pygeodesy/frechet.py +865 -0
  56. pygeodesy/fstats.py +760 -0
  57. pygeodesy/fsums.py +1898 -0
  58. pygeodesy/gars.py +358 -0
  59. pygeodesy/geodesicw.py +581 -0
  60. pygeodesy/geodesicx/_C4_24.py +1699 -0
  61. pygeodesy/geodesicx/_C4_27.py +2395 -0
  62. pygeodesy/geodesicx/_C4_30.py +3301 -0
  63. pygeodesy/geodesicx/__init__.py +48 -0
  64. pygeodesy/geodesicx/__main__.py +91 -0
  65. pygeodesy/geodesicx/gx.py +1382 -0
  66. pygeodesy/geodesicx/gxarea.py +535 -0
  67. pygeodesy/geodesicx/gxbases.py +154 -0
  68. pygeodesy/geodesicx/gxline.py +669 -0
  69. pygeodesy/geodsolve.py +426 -0
  70. pygeodesy/geohash.py +914 -0
  71. pygeodesy/geoids.py +1884 -0
  72. pygeodesy/hausdorff.py +892 -0
  73. pygeodesy/heights.py +1155 -0
  74. pygeodesy/interns.py +687 -0
  75. pygeodesy/iters.py +545 -0
  76. pygeodesy/karney.py +919 -0
  77. pygeodesy/ktm.py +633 -0
  78. pygeodesy/latlonBase.py +1766 -0
  79. pygeodesy/lazily.py +960 -0
  80. pygeodesy/lcc.py +684 -0
  81. pygeodesy/ltp.py +1107 -0
  82. pygeodesy/ltpTuples.py +1563 -0
  83. pygeodesy/mgrs.py +721 -0
  84. pygeodesy/named.py +1324 -0
  85. pygeodesy/namedTuples.py +683 -0
  86. pygeodesy/nvectorBase.py +695 -0
  87. pygeodesy/osgr.py +781 -0
  88. pygeodesy/points.py +1686 -0
  89. pygeodesy/props.py +628 -0
  90. pygeodesy/resections.py +1048 -0
  91. pygeodesy/rhumb/__init__.py +46 -0
  92. pygeodesy/rhumb/aux_.py +397 -0
  93. pygeodesy/rhumb/bases.py +1148 -0
  94. pygeodesy/rhumb/ekx.py +563 -0
  95. pygeodesy/rhumb/solve.py +572 -0
  96. pygeodesy/simplify.py +647 -0
  97. pygeodesy/solveBase.py +472 -0
  98. pygeodesy/sphericalBase.py +724 -0
  99. pygeodesy/sphericalNvector.py +1264 -0
  100. pygeodesy/sphericalTrigonometry.py +1447 -0
  101. pygeodesy/streprs.py +627 -0
  102. pygeodesy/trf.py +2079 -0
  103. pygeodesy/triaxials.py +1484 -0
  104. pygeodesy/units.py +969 -0
  105. pygeodesy/unitsBase.py +349 -0
  106. pygeodesy/ups.py +538 -0
  107. pygeodesy/utily.py +1231 -0
  108. pygeodesy/utm.py +762 -0
  109. pygeodesy/utmups.py +318 -0
  110. pygeodesy/utmupsBase.py +517 -0
  111. pygeodesy/vector2d.py +785 -0
  112. pygeodesy/vector3d.py +968 -0
  113. pygeodesy/vector3dBase.py +1049 -0
  114. pygeodesy/webmercator.py +383 -0
  115. 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.