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/geoids.py ADDED
@@ -0,0 +1,1884 @@
1
+
2
+ # -*- coding: utf-8 -*-
3
+
4
+ u'''Geoid models and geoid height interpolations.
5
+
6
+ Classes L{GeoidG2012B}, L{GeoidKarney} and L{GeoidPGM} to interpolate the
7
+ height of various U{geoid<https://WikiPedia.org/wiki/Geoid>}s at C{LatLon}
8
+ locations or separate lat-/longitudes using different interpolation methods
9
+ and C{geoid} model files.
10
+
11
+ L{GeoidKarney} is a transcoding of I{Charles Karney}'s C++ class U{Geoid
12
+ <https://GeographicLib.SourceForge.io/C++/doc/geoid.html>} to pure Python.
13
+ The L{GeoidG2012B} and L{GeoidPGM} interpolators both depend on U{scipy
14
+ <https://SciPy.org>} and U{numpy<https://PyPI.org/project/numpy>} and
15
+ require those packages to be installed.
16
+
17
+ In addition, each geoid interpolator needs C{grid knots} (down)loaded from
18
+ a C{geoid} model file, I{specific to the interpolator}, more details below
19
+ and in the documentation of the interpolator class. For each interpolator,
20
+ there are several interpolation choices, like I{linear}, I{cubic}, etc.
21
+
22
+ Typical usage
23
+ =============
24
+
25
+ 1. Choose one of the interpolator classes L{GeoidG2012B}, L{GeoidKarney}
26
+ or L{GeoidPGM} and download a C{geoid} model file, containing locations with
27
+ known heights also referred to as the C{grid knots}. See the documentation
28
+ of the interpolator class for references to available C{grid} models.
29
+
30
+ C{>>> from pygeodesy import GeoidG2012B # or -Karney or -PGM as GeoidXyz}
31
+
32
+ 2. Instantiate an interpolator with the C{geoid} model file and use keyword
33
+ arguments to select different interpolation options
34
+
35
+ C{>>> ginterpolator = GeoidXyz(geoid_model_file, **options)}
36
+
37
+ 3. Get the interpolated geoid height of other C{LatLon} location(s) with
38
+
39
+ C{>>> ll = LatLon(1, 2, ...)}
40
+
41
+ C{>>> h = ginterpolator(ll)}
42
+
43
+ or
44
+
45
+ C{>>> h0, h1, h2, ... = ginterpolator(ll0, ll1, ll2, ...)}
46
+
47
+ or a list, tuple, generator, etc. of C{LatLon}s
48
+
49
+ C{>>> hs = ginterpolator(lls)}
50
+
51
+ 4. For separate lat- and longitudes invoke the C{.height} method as
52
+
53
+ C{>>> h = ginterpolator.height(lat, lon)}
54
+
55
+ or as 2 lists, 2 tuples, etc.
56
+
57
+ C{>>> hs = ginterpolator.height(lats, lons)}
58
+
59
+ 5. An example is in U{issue #64<https://GitHub.com/mrJean1/PyGeodesy/issues/64>},
60
+ courtesy of SBFRF.
61
+
62
+ @note: Classes L{GeoidG2012B} and L{GeoidPGM} require both U{numpy
63
+ <https://PyPI.org/project/numpy>} and U{scipy<https://PyPI.org/project/scipy>}
64
+ to be installed.
65
+
66
+ @note: Errors from C{scipy} are raised as L{SciPyError}s. Warnings issued by
67
+ C{scipy} can be thrown as L{SciPyWarning} exceptions, provided Python
68
+ C{warnings} are filtered accordingly, see L{SciPyWarning}.
69
+
70
+ @see: I{Karney}'s U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/index.html>},
71
+ U{Geoid height<https://GeographicLib.SourceForge.io/C++/doc/geoid.html>} and U{Installing
72
+ the Geoid datasets<https://GeographicLib.SourceForge.io/C++/doc/geoid.html#geoidinst>},
73
+ U{SciPy<https://docs.SciPy.org/doc/scipy/reference/interpolate.html>} interpolation
74
+ U{RectBivariateSpline<https://docs.SciPy.org/doc/scipy/reference/generated/scipy.
75
+ interpolate.RectBivariateSpline.html>} and U{interp2d<https://docs.SciPy.org/doc/scipy/
76
+ reference/generated/scipy.interpolate.interp2d.html>}, functions L{elevations.elevation2}
77
+ and L{elevations.geoidHeight2}, U{I{Ellispoid vs Orthometric Elevations}<https://
78
+ www.YouTube.com/watch?v=dX6a6kCk3Po>} and U{I{Pitfalls Related to Ellipsoid Height
79
+ and Height Above Mean Sea Level (AMSL)}<https://Wiki.ROS.org/mavros#mavros.2FPlugins.
80
+ Avoiding_Pitfalls_Related_to_Ellipsoid_Height_and_Height_Above_Mean_Sea_Level>}.
81
+ '''
82
+ # make sure int/int division yields float quotient, see .basics
83
+ from __future__ import division as _; del _ # PYCHOK semicolon
84
+
85
+ from pygeodesy.basics import len2, map1, isodd, ub2str as _ub2str
86
+ from pygeodesy.constants import EPS, _float as _F, _0_0, _1_0, _180_0, _360_0
87
+ # from pygeodesy.datums import _ellipsoidal_datum # from .heights
88
+ # from pygeodesy.dms import parseDMS2 # _MODS
89
+ from pygeodesy.errors import _incompatible, LenError, RangeError, SciPyError, \
90
+ _SciPyIssue
91
+ from pygeodesy.fmath import favg, Fdot, fdot, Fhorner, frange
92
+ # from pygoedesy.formy import heightOrthometric # _MODS
93
+ from pygeodesy.heights import _as_llis2, _ascalar, _height_called, HeightError, \
94
+ _HeightsBase, _ellipsoidal_datum, _Wrap
95
+ from pygeodesy.interns import MISSING, NN, _4_, _COLONSPACE_, _COMMASPACE_, \
96
+ _cubic_, _E_, _height_, _in_, _kind_, _knots_, \
97
+ _lat_, _linear_, _lon_, _mean_, _N_, _n_a_, \
98
+ _not_, _numpy_, _on_, _outside_, _S_, _s_, \
99
+ _scipy_, _SPACE_, _stdev_, _supported_, _tbd_, \
100
+ _W_, _width_, _version2
101
+ from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, _FOR_DOCS
102
+ from pygeodesy.named import _Named, _NamedTuple, notOverloaded
103
+ # from pygeodesy.namedTuples import LatLon3Tuple # _MODS
104
+ from pygeodesy.props import deprecated_method, Property_RO, property_RO
105
+ from pygeodesy.streprs import attrs, Fmt, fstr, pairs
106
+ from pygeodesy.units import Height, Int_, Lat, Lon
107
+ # from pygeodesy.utily import _Wrap # from .heights
108
+
109
+ from math import floor
110
+ import os.path as _os_path
111
+ from os import SEEK_CUR as _SEEK_CUR, SEEK_SET as _SEEK_SET
112
+ from struct import calcsize as _calcsize, unpack as _unpack
113
+ try:
114
+ from StringIO import StringIO as _BytesIO # reads bytes
115
+ _ub2str = str # PYCHOK convert bytes to str for egm*.pgm text
116
+
117
+ except ImportError: # Python 3+
118
+ from io import BytesIO as _BytesIO # PYCHOK expected
119
+
120
+ __all__ = _ALL_LAZY.geoids
121
+ __version__ = '24.03.22'
122
+
123
+ _assert_ = 'assert'
124
+ _bHASH_ = b'#'
125
+ _endian_ = 'endian'
126
+ _format_ = '%s %r'
127
+ _header_ = 'header'
128
+ # temporarily hold a single instance for each int value
129
+ _intCs = {}
130
+ _interp2d_ks = {-2: _linear_,
131
+ -3: _cubic_,
132
+ -5: 'quintic'}
133
+ _lli_ = 'lli'
134
+ _non_increasing_ = 'non-increasing'
135
+ _rb_ = 'rb'
136
+
137
+
138
+ class _GeoidBase(_HeightsBase):
139
+ '''(INTERNAL) Base class for C{Geoid...}s.
140
+ '''
141
+ _cropped = None
142
+ # _datum = _WGS84 # from _HeightsBase
143
+ _egm = None # open C{egm*.pgm} geoid file
144
+ _endian = _tbd_
145
+ _geoid = _n_a_
146
+ _hs_y_x = None # numpy 2darray, row-major order
147
+ _interp2d = None # interp2d interpolation
148
+ _kind = 3 # order for interp2d, RectBivariateSpline
149
+ # _kmin = 2 # min number of knots
150
+ _knots = 0 # nlat * nlon
151
+ _mean = None # fixed in GeoidKarney
152
+ # _name = NN # _Named
153
+ _nBytes = 0 # numpy size in bytes, float64
154
+ _pgm = None # PGM attributes, C{_PGM} or C{None}
155
+ _sizeB = 0 # geoid file size in bytes
156
+ _smooth = 0 # used only for RectBivariateSpline
157
+ _stdev = None # fixed in GeoidKarney
158
+ _u2B = 0 # np.itemsize or undefined
159
+
160
+ _lat_d = _0_0 # increment, +tive
161
+ _lat_lo = _0_0 # lower lat, south
162
+ _lat_hi = _0_0 # upper lat, noth
163
+ _lon_d = _0_0 # increment, +tive
164
+ _lon_lo = _0_0 # left lon, west
165
+ _lon_hi = _0_0 # right lon, east
166
+ _lon_of = _0_0 # forward lon offset
167
+ _lon_og = _0_0 # reverse lon offset
168
+
169
+ _center = None # (lat, lon, height)
170
+ _yx_hits = None # cache hits, ala Karney
171
+
172
+ def __init__(self, hs, p):
173
+ '''(INTERNAL) Set up the grid axes, the C{SciPy} interpolator
174
+ and several internal geoid attributes.
175
+
176
+ @arg hs: Grid knots with known height (C{numpy 2darray}).
177
+ @arg p: The C{slat, wlon, nlat, nlon, dlat, dlon} and
178
+ other geoid parameters (C{INTERNAL}).
179
+
180
+ @raise GeoidError: Incompatible grid B{C{hs}} shape or
181
+ invalid B{C{kind}}.
182
+
183
+ @raise LenError: Mismatch grid B{C{hs}} axis.
184
+
185
+ @raise SciPyError: A C{scipy.interpolate.inter2d} or
186
+ C{-.RectBivariateSpline} issue.
187
+
188
+ @raise SciPyWarning: A C{scipy.interpolate.inter2d} or
189
+ C{-.RectBivariateSpline} warning as
190
+ exception.
191
+
192
+ @note: C{scipy.interpolate.interp2d} has been C{DEPRECATED},
193
+ specify keyword argument C{B{kind}=1..5} to use
194
+ C{scipy.interpolate.RectBivariateSpline}.
195
+ '''
196
+ spi = self.scipy_interpolate
197
+ # for 2d scipy.interpolate.interp2d(xs, ys, hs, ...) and
198
+ # scipy.interpolate.RectBivariateSpline(ys, xs, hs, ...)
199
+ # require the shape of hs to be (len(ys), len(xs)), note
200
+ # the different (xs, ys, ...) and (ys, xs, ...) orders
201
+ if (p.nlat, p.nlon) != hs.shape:
202
+ raise GeoidError(shape=hs.shape, txt=_incompatible((p.nlat, p.nlon)))
203
+
204
+ # both axes and bounding box
205
+ ys, self._lat_d = self._gaxis2(p.slat, p.dlat, p.nlat, _lat_ + _s_)
206
+ xs, self._lon_d = self._gaxis2(p.wlon, p.dlon, p.nlon, _lon_ + _s_)
207
+
208
+ bb = ys[0], ys[-1], xs[0], xs[-1] + p.dlon # fudge lon_hi
209
+ # geoid grids are typically stored in row-major order, some
210
+ # with rows (90..-90) reversed and columns (0..360) wrapped
211
+ # to Easten longitude, 0 <= east < 180 and 180 <= west < 360
212
+ k = self.kind
213
+ if k in _interp2d_ks: # .interp2d DEPRECATED since scipy 1.10
214
+ if self._scipy_version() < (1, 10):
215
+ self._interp2d = spi.interp2d(xs, ys, hs, kind=_interp2d_ks[k])
216
+ else: # call and overwrite the DEPRECATED .interp2d
217
+ self._interp2d = self._interp2d(xs, ys, hs, k)
218
+ elif 1 <= k <= 5:
219
+ self._ev = spi.RectBivariateSpline(ys, xs, hs, bbox=bb, ky=k, kx=k,
220
+ s=self._smooth).ev
221
+ else:
222
+ raise GeoidError(kind=k)
223
+
224
+ self._hs_y_x = hs # numpy 2darray, row-major
225
+ self._nBytes = hs.nbytes # numpy size in bytes
226
+ self._knots = p.knots # grid knots
227
+ self._lon_of = float(p.flon) # forward offset
228
+ self._lon_og = float(p.glon) # reverse offset
229
+ # shrink the box by 1 unit on every side
230
+ # bb += self._lat_d, -self._lat_d, self._lon_d, -self._lon_d
231
+ self._lat_lo = float(bb[0])
232
+ self._lat_hi = float(bb[1])
233
+ self._lon_lo = float(bb[2] - p.glon)
234
+ self._lon_hi = float(bb[3] - p.glon)
235
+
236
+ def __call__(self, *llis, **wrap_H):
237
+ '''Interpolate the geoid height for one or several locations.
238
+
239
+ @arg llis: One or more locations (C{LatLon}s), all positional.
240
+ @kwarg wrap_H: Keyword arguments C{B{wrap}=False, B{H}=False}.
241
+ If C{B{wrap} is True}, wrap or I{normalize} all
242
+ B{C{llis}} locations (C{bool}). If C{B{H} is True},
243
+ return the I{orthometric} height instead of the
244
+ I{geoid} height at each location (C{bool}).
245
+
246
+ @return: A single interpolated geoid (or orthometric) height
247
+ (C{float}) or a list or tuple of interpolated geoid
248
+ (or orthometric) heights (C{float}s).
249
+
250
+ @raise GeoidError: Insufficient number of B{C{llis}}, an
251
+ invalid B{C{lli}} or the C{egm*.pgm}
252
+ geoid file is closed.
253
+
254
+ @raise RangeError: An B{C{lli}} is outside this geoid's lat-
255
+ or longitude range.
256
+
257
+ @raise SciPyError: A C{scipy.interpolate.inter2d} or
258
+ C{-.RectBivariateSpline} issue.
259
+
260
+ @raise SciPyWarning: A C{scipy.interpolate.inter2d} or
261
+ C{-.RectBivariateSpline} warning as
262
+ exception.
263
+
264
+ @note: To obtain I{orthometric} heights, each B{C{llis}}
265
+ location must have an ellipsoid C{height} or C{h}
266
+ attribute, otherwise C{height=0} is used.
267
+
268
+ @see: Function L{pygeodesy.heightOrthometric}.
269
+ '''
270
+ return self._called(llis, True, **wrap_H)
271
+
272
+ def __enter__(self):
273
+ '''Open context.
274
+ '''
275
+ return self
276
+
277
+ def __exit__(self, *unused): # PYCHOK exc_type, exc_value, exc_traceback)
278
+ '''Close context.
279
+ '''
280
+ self.close()
281
+ # return None # XXX False
282
+
283
+ def __repr__(self):
284
+ return self.toStr()
285
+
286
+ def __str__(self):
287
+ return Fmt.PAREN(self.classname, repr(self.name))
288
+
289
+ def _called(self, llis, scipy, wrap=False, H=False):
290
+ # handle __call__
291
+ _H = _MODS.formy.heightOrthometric if H else None
292
+ _as, llis = _as_llis2(llis, Error=GeoidError)
293
+ hs, _w = [], _Wrap._latlonop(wrap)
294
+ _a, _h = hs.append, self._hGeoid
295
+ try:
296
+ for i, lli in enumerate(llis):
297
+ N = _h(*_w(lli.lat, lli.lon))
298
+ # orthometric or geoid height
299
+ _a(_H(lli, N) if _H else N)
300
+ return _as(hs)
301
+
302
+ except (GeoidError, RangeError) as x:
303
+ # XXX avoid str(LatLon()) degree symbols
304
+ t = _lli_ if _as is _ascalar else Fmt.INDEX(llis=i)
305
+ lli = fstr((lli.lat, lli.lon), strepr=repr)
306
+ raise type(x)(t, lli, wrap=wrap, H=H, cause=x)
307
+ except Exception as x:
308
+ if scipy and self.scipy:
309
+ raise _SciPyIssue(x)
310
+ else:
311
+ raise
312
+
313
+ @Property_RO
314
+ def _center(self):
315
+ ''' Cache for method L{center}.
316
+ '''
317
+ return self._llh3(favg(self._lat_lo, self._lat_hi),
318
+ favg(self._lon_lo, self._lon_hi))
319
+
320
+ def center(self, LatLon=None):
321
+ '''Return the center location and height of this geoid.
322
+
323
+ @kwarg LatLon: Optional class to return the location and height
324
+ (C{LatLon}) or C{None}.
325
+
326
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
327
+ lon, height)} otherwise a B{C{LatLon}} instance
328
+ with the lat-, longitude and geoid height of the
329
+ center grid location.
330
+ '''
331
+ return self._llh3LL(self._center, LatLon)
332
+
333
+ def close(self):
334
+ '''Close the C{egm*.pgm} geoid file if open (and applicable).
335
+ '''
336
+ if not self.closed:
337
+ self._egm.close()
338
+ self._egm = None
339
+
340
+ @property_RO
341
+ def closed(self):
342
+ '''Get the C{egm*.pgm} geoid file status.
343
+ '''
344
+ return self._egm is None
345
+
346
+ @Property_RO
347
+ def cropped(self):
348
+ '''Is geoid cropped (C{bool} or C{None} if crop not supported).
349
+ '''
350
+ return self._cropped
351
+
352
+ @Property_RO
353
+ def dtype(self):
354
+ '''Get the grid C{scipy} U{dtype<https://docs.SciPy.org/doc/numpy/
355
+ reference/generated/numpy.ndarray.dtype.html>} (C{numpy.dtype}).
356
+ '''
357
+ return self._hs_y_x.dtype
358
+
359
+ @Property_RO
360
+ def endian(self):
361
+ '''Get the geoid endianess and U{dtype<https://docs.SciPy.org/
362
+ doc/numpy/reference/generated/numpy.dtype.html>} (C{str}).
363
+ '''
364
+ return self._endian
365
+
366
+ def _ev(self, y, x): # PYCHOK expected
367
+ # only used for .interpolate.interp2d, but
368
+ # overwritten for .RectBivariateSpline,
369
+ # note (y, x) must be flipped!
370
+ return self._interp2d(x, y)
371
+
372
+ def _gaxis2(self, lo, d, n, name):
373
+ # build grid axis, hi = lo + (n - 1) * d
374
+ m, a = len2(frange(lo, n, d))
375
+ if m != n:
376
+ raise LenError(self.__class__, grid=m, **{name: n})
377
+ if d < 0:
378
+ d, a = -d, list(reversed(a))
379
+ for i in range(1, m):
380
+ e = a[i] - a[i-1]
381
+ if e < EPS: # non-increasing axis
382
+ i = Fmt.INDEX(name, i)
383
+ raise GeoidError(i, e, txt=_non_increasing_)
384
+ return self.numpy.array(a), d
385
+
386
+ def _g2ll2(self, lat, lon): # PYCHOK no cover
387
+ '''(INTERNAL) I{Must be overloaded}.'''
388
+ notOverloaded(self, lat, lon)
389
+
390
+ def _gyx2g2(self, y, x):
391
+ # convert grid (y, x) indices to grid (lat, lon)
392
+ return ((self._lat_lo + self._lat_d * y),
393
+ (self._lon_lo + self._lon_of + self._lon_d * x))
394
+
395
+ def height(self, lats, lons, **wrap):
396
+ '''Interpolate the geoid height for one or several lat-/longitudes.
397
+
398
+ @arg lats: Latitude or latitudes (C{degrees} or C{degrees}s).
399
+ @arg lons: Longitude or longitudes (C{degrees} or C{degrees}s).
400
+ @kwarg wrap: If C{True}, wrap or I{normalize} all B{C{lats}}
401
+ and B{C{lons}} locations (C{bool}).
402
+
403
+ @return: A single interpolated geoid height (C{float}) or a
404
+ list of interpolated geoid heights (C{float}s).
405
+
406
+ @raise GeoidError: Insufficient or non-matching number of
407
+ B{C{lats}} and B{C{lons}}.
408
+
409
+ @raise RangeError: A B{C{lat}} or B{C{lon}} is outside this
410
+ geoid's lat- or longitude range.
411
+
412
+ @raise SciPyError: A C{scipy.interpolate.inter2d} or
413
+ C{-.RectBivariateSpline} issue.
414
+
415
+ @raise SciPyWarning: A C{scipy.interpolate.inter2d} or
416
+ C{-.RectBivariateSpline} warning as
417
+ exception.
418
+ '''
419
+ return _height_called(self, lats, lons, Error=GeoidError, **wrap)
420
+
421
+ def _hGeoid(self, lat, lon):
422
+ out = self.outside(lat, lon)
423
+ if out:
424
+ lli = fstr((lat, lon), strepr=repr)
425
+ raise RangeError(lli=lli, txt=_SPACE_(_outside_, _on_, out))
426
+ return float(self._ev(*self._ll2g2(lat, lon)))
427
+
428
+ @Property_RO
429
+ def _highest(self):
430
+ '''(INTERNAL) Cache for L{highest} method.
431
+ '''
432
+ return self._llh3minmax(True)
433
+
434
+ def highest(self, LatLon=None, **unused):
435
+ '''Return the location and largest height of this geoid.
436
+
437
+ @kwarg LatLon: Optional class to return the location and height
438
+ (C{LatLon}) or C{None}.
439
+
440
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
441
+ lon, height)} otherwise a B{C{LatLon}} instance
442
+ with the lat-, longitude and geoid height of the
443
+ highest grid location.
444
+ '''
445
+ return self._llh3LL(self._highest, LatLon)
446
+
447
+ @Property_RO
448
+ def hits(self):
449
+ '''Get the number of cache hits (C{int} or C{None}).
450
+ '''
451
+ return self._yx_hits
452
+
453
+ @deprecated_method
454
+ def _interp2d(self, xs, ys, hs=(), k=0): # overwritten in .__init__ above
455
+ '''DEPRECATED on 23.01.06, use keyword argument C{B{kind}=1..5}.'''
456
+ # assert k in _interp2d_ks # and len(hs) == len(xs) == len(ys)
457
+ try:
458
+ return self.scipy_interpolate.interp2d(xs, ys, hs, kind=_interp2d_ks[k])
459
+ except AttributeError as x:
460
+ raise SciPyError(interp2d=MISSING, kind=k, cause=x)
461
+
462
+ @Property_RO
463
+ def kind(self):
464
+ '''Get the interpolator kind and order (C{int}).
465
+ '''
466
+ return self._kind
467
+
468
+ @Property_RO
469
+ def knots(self):
470
+ '''Get the number of grid knots (C{int}).
471
+ '''
472
+ return self._knots
473
+
474
+ def _ll2g2(self, lat, lon): # PYCHOK no cover
475
+ '''(INTERNAL) I{Must be overloaded}.'''
476
+ notOverloaded(self, lat, lon)
477
+
478
+ @property_RO
479
+ def _LL3T(self):
480
+ '''(INTERNAL) Get L{LatLon3Tuple}, I{once}.
481
+ '''
482
+ _GeoidBase._LL3T = T = _MODS.namedTuples.LatLon3Tuple # overwrite poperty_RO
483
+ return T
484
+
485
+ def _llh3(self, lat, lon):
486
+ return self._LL3T(lat, lon, self._hGeoid(lat, lon), name=self.name)
487
+
488
+ def _llh3LL(self, llh, LatLon):
489
+ return llh if LatLon is None else self._xnamed(LatLon(*llh))
490
+
491
+ def _llh3minmax(self, highest=True, *unused):
492
+ hs, np = self._hs_y_x, self.numpy
493
+ # <https://docs.SciPy.org/doc/numpy/reference/generated/
494
+ # numpy.argmin.html#numpy.argmin>
495
+ arg = np.argmax if highest else np.argmin
496
+ y, x = np.unravel_index(arg(hs, axis=None), hs.shape)
497
+ return self._g2ll2(*self._gyx2g2(y, x)) + (float(hs[y, x]),)
498
+
499
+ def _load(self, g, dtype, n, offset=0):
500
+ # numpy.fromfile, like .frombuffer
501
+ g.seek(offset, _SEEK_SET)
502
+ return self.numpy.fromfile(g, dtype, n)
503
+
504
+ @Property_RO
505
+ def _lowerleft(self):
506
+ '''(INTERNAL) Cache for L{lowerleft}.
507
+ '''
508
+ return self._llh3(self._lat_lo, self._lon_lo)
509
+
510
+ def lowerleft(self, LatLon=None):
511
+ '''Return the lower-left location and height of this geoid.
512
+
513
+ @kwarg LatLon: Optional class to return the location
514
+ (C{LatLon}) and height or C{None}.
515
+
516
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
517
+ lon, height)} otherwise a B{C{LatLon}} instance
518
+ with the lat-, longitude and geoid height of the
519
+ lower-left, SW grid corner.
520
+ '''
521
+ return self._llh3LL(self._lowerleft, LatLon)
522
+
523
+ @Property_RO
524
+ def _loweright(self):
525
+ '''(INTERNAL) Cache for L{loweright}.
526
+ '''
527
+ return self._llh3(self._lat_lo, self._lon_hi)
528
+
529
+ def loweright(self, LatLon=None):
530
+ '''Return the lower-right location and height of this geoid.
531
+
532
+ @kwarg LatLon: Optional class to return the location and height
533
+ (C{LatLon}) or C{None}.
534
+
535
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
536
+ lon, height)} otherwise a B{C{LatLon}} instance
537
+ with the lat-, longitude and geoid height of the
538
+ lower-right, SE grid corner.
539
+ '''
540
+
541
+ return self._llh3LL(self._loweright, LatLon)
542
+
543
+ lowerright = loweright # synonymous
544
+
545
+ @Property_RO
546
+ def _lowest(self):
547
+ '''(INTERNAL) Cache for L{lowest}.
548
+ '''
549
+ return self._llh3minmax(False)
550
+
551
+ def lowest(self, LatLon=None, **unused):
552
+ '''Return the location and lowest height of this geoid.
553
+
554
+ @kwarg LatLon: Optional class to return the location and height
555
+ (C{LatLon}) or C{None}.
556
+
557
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
558
+ lon, height)} otherwise a B{C{LatLon}} instance
559
+ with the lat-, longitude and geoid height of the
560
+ lowest grid location.
561
+ '''
562
+ return self._llh3LL(self._lowest, LatLon)
563
+
564
+ @Property_RO
565
+ def mean(self):
566
+ '''Get the mean of this geoid's heights (C{float}).
567
+ '''
568
+ if self._mean is None: # see GeoidKarney
569
+ self._mean = float(self.numpy.mean(self._hs_y_x))
570
+ return self._mean
571
+
572
+ @property_RO
573
+ def name(self):
574
+ '''Get the name of this geoid (C{str}).
575
+ '''
576
+ return _HeightsBase.name.fget(self) or self._geoid # recursion
577
+
578
+ @Property_RO
579
+ def nBytes(self):
580
+ '''Get the grid in-memory size in bytes (C{int}).
581
+ '''
582
+ return self._nBytes
583
+
584
+ def _open(self, geoid, datum, kind, name, smooth):
585
+ # open the geoid file
586
+ try:
587
+ self._geoid = _os_path.basename(geoid)
588
+ self._sizeB = _os_path.getsize(geoid)
589
+ g = open(geoid, _rb_)
590
+ except (IOError, OSError) as x:
591
+ raise GeoidError(geoid=geoid, cause=x)
592
+
593
+ if datum not in (None, self._datum):
594
+ self._datum = _ellipsoidal_datum(datum, name=name)
595
+ self._kind = int(kind)
596
+ if name:
597
+ _HeightsBase.name.fset(self, name) # rename
598
+ if smooth:
599
+ self._smooth = Int_(smooth=smooth, Error=GeoidError, low=0)
600
+
601
+ return g
602
+
603
+ def outside(self, lat, lon):
604
+ '''Check whether a location is outside this geoid's
605
+ lat-/longitude or crop range.
606
+
607
+ @arg lat: The latitude (C{degrees}).
608
+ @arg lon: The longitude (C{degrees}).
609
+
610
+ @return: A 1- or 2-character C{str} if outside or an
611
+ empty C{str} if inside.
612
+ '''
613
+ return (_S_ if lat < self._lat_lo else
614
+ (_N_ if lat > self._lat_hi else NN)) + \
615
+ (_W_ if lon < self._lon_lo else
616
+ (_E_ if lon > self._lon_hi else NN))
617
+
618
+ @Property_RO
619
+ def pgm(self):
620
+ '''Get the PGM attributes (C{_PGM} or C{None} if not available/applicable).
621
+ '''
622
+ return self._pgm
623
+
624
+ @Property_RO
625
+ def sizeB(self):
626
+ '''Get the geoid grid file size in bytes (C{int}).
627
+ '''
628
+ return self._sizeB
629
+
630
+ @Property_RO
631
+ def smooth(self):
632
+ '''Get the C{RectBivariateSpline} smoothing (C{int}).
633
+ '''
634
+ return self._smooth
635
+
636
+ @Property_RO
637
+ def stdev(self):
638
+ '''Get the standard deviation of this geoid's heights (C{float}) or C{None}.
639
+ '''
640
+ if self._stdev is None: # see GeoidKarney
641
+ self._stdev = float(self.numpy.std(self._hs_y_x))
642
+ return self._stdev
643
+
644
+ def _swne(self, crop):
645
+ # crop box to 4-tuple (s, w, n, e)
646
+ try:
647
+ if len(crop) == 2:
648
+ try: # sw, ne LatLons
649
+ swne = (crop[0].lat, crop[0].lon,
650
+ crop[1].lat, crop[1].lon)
651
+ except AttributeError: # (s, w), (n, e)
652
+ swne = tuple(crop[0]) + tuple(crop[1])
653
+ else: # (s, w, n, e)
654
+ swne = crop
655
+ if len(swne) == 4:
656
+ s, w, n, e = map(float, swne)
657
+ if -90 <= s <= (n - _1_0) <= 89 and \
658
+ -180 <= w <= (e - _1_0) <= 179:
659
+ return s, w, n, e
660
+ except (IndexError, TypeError, ValueError):
661
+ pass
662
+ raise GeoidError(crop=crop)
663
+
664
+ def toStr(self, prec=3, sep=_COMMASPACE_): # PYCHOK signature
665
+ '''This geoid and all geoid attributes as a string.
666
+
667
+ @kwarg prec: Number of decimal digits (0..9 or C{None} for
668
+ default). Trailing zero decimals are stripped
669
+ for B{C{prec}} values of 1 and above, but kept
670
+ for negative B{C{prec}} values.
671
+ @kwarg sep: Separator to join (C{str}).
672
+
673
+ @return: Geoid name and attributes (C{str}).
674
+ '''
675
+ s = 1 if self.kind < 0 else 2
676
+ t = tuple(Fmt.PAREN(m.__name__, fstr(m(), prec=prec)) for m in
677
+ (self.lowerleft, self.upperright,
678
+ self.center,
679
+ self.highest, self.lowest)) + \
680
+ attrs( _mean_, _stdev_, prec=prec, Nones=False) + \
681
+ attrs((_kind_, 'smooth')[:s], prec=prec, Nones=False) + \
682
+ attrs( 'cropped', 'dtype', _endian_, 'hits', _knots_, 'nBytes',
683
+ 'sizeB', _scipy_, _numpy_, prec=prec, Nones=False)
684
+ return _COLONSPACE_(self, sep.join(t))
685
+
686
+ @Property_RO
687
+ def u2B(self):
688
+ '''Get the PGM itemsize in bytes (C{int}).
689
+ '''
690
+ return self._u2B
691
+
692
+ @Property_RO
693
+ def _upperleft(self):
694
+ '''(INTERNAL) Cache for method L{upperleft}.
695
+ '''
696
+ return self._llh3(self._lat_hi, self._lon_lo)
697
+
698
+ def upperleft(self, LatLon=None):
699
+ '''Return the upper-left location and height of this geoid.
700
+
701
+ @kwarg LatLon: Optional class to return the location and height
702
+ (C{LatLon}) or C{None}.
703
+
704
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
705
+ lon, height)} otherwise a B{C{LatLon}} instance
706
+ with the lat-, longitude and geoid height of the
707
+ upper-left, NW grid corner.
708
+ '''
709
+ return self._llh3LL(self._upperleft, LatLon)
710
+
711
+ @Property_RO
712
+ def _upperright(self):
713
+ '''(INTERNAL) Cache for method L{upperright}.
714
+ '''
715
+ return self._llh3(self._lat_hi, self._lon_hi)
716
+
717
+ def upperright(self, LatLon=None):
718
+ '''Return the upper-right location and height of this geoid.
719
+
720
+ @kwarg LatLon: Optional class to return the location and height
721
+ (C{LatLon}) or C{None}.
722
+
723
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
724
+ lon, height)} otherwise a B{C{LatLon}} instance
725
+ with the lat-, longitude and geoid height of the
726
+ upper-right, NE grid corner.
727
+ '''
728
+ return self._llh3LL(self._upperright, LatLon)
729
+
730
+
731
+ class GeoidError(HeightError):
732
+ '''Geoid interpolator C{Geoid...} or interpolation issue.
733
+ '''
734
+ pass
735
+
736
+
737
+ class GeoidG2012B(_GeoidBase):
738
+ '''Geoid height interpolator for U{GEOID12B Model
739
+ <https://www.NGS.NOAA.gov/GEOID/GEOID12B/>} grids U{CONUS
740
+ <https://www.NGS.NOAA.gov/GEOID/GEOID12B/GEOID12B_CONUS.shtml>},
741
+ U{Alaska<https://www.NGS.NOAA.gov/GEOID/GEOID12B/GEOID12B_AK.shtml>},
742
+ U{Hawaii<https://www.NGS.NOAA.gov/GEOID/GEOID12B/GEOID12B_HI.shtml>},
743
+ U{Guam and Northern Mariana Islands
744
+ <https://www.NGS.NOAA.gov/GEOID/GEOID12B/GEOID12B_GMNI.shtml>},
745
+ U{Puerto Rico and U.S. Virgin Islands
746
+ <https://www.NGS.NOAA.gov/GEOID/GEOID12B/GEOID12B_PRVI.shtml>} and
747
+ U{American Samoa<https://www.NGS.NOAA.gov/GEOID/GEOID12B/GEOID12B_AS.shtml>}
748
+ based on C{SciPy} U{RectBivariateSpline<https://docs.SciPy.org/doc/
749
+ scipy/reference/generated/scipy.interpolate.RectBivariateSpline.html>}
750
+ or U{interp2d<https://docs.SciPy.org/doc/scipy/reference/generated/
751
+ scipy.interpolate.interp2d.html>} interpolation.
752
+
753
+ Use any of the binary C{le} (little endian) or C{be} (big endian)
754
+ C{g2012b*.bin} grid files.
755
+ '''
756
+ def __init__(self, g2012b_bin, crop=None, datum=None, # NAD 83 Ellipsoid
757
+ kind=3, name=NN, smooth=0):
758
+ '''New L{GeoidG2012B} interpolator.
759
+
760
+ @arg g2012b_bin: A C{GEOID12B} grid file name (C{.bin}).
761
+ @kwarg crop: Optional crop box, not supported (C{None}).
762
+ @kwarg datum: Optional grid datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
763
+ or L{a_f2Tuple}), default C{WGS84}.
764
+ @kwarg kind: C{scipy.interpolate} order (C{int}), use 1..5 for
765
+ U{RectBivariateSpline<https://docs.SciPy.org/doc/scipy/
766
+ reference/generated/scipy.interpolate.RectBivariateSpline.html>},
767
+ -2 for U{interp2d linear<https://docs.SciPy.org/doc/scipy/
768
+ reference/generated/scipy.interpolate.interp2d.html>}, -3
769
+ for C{interp2d cubic} or -5 for C{interp2d quintic}.
770
+ @kwarg name: Optional geoid name (C{str}).
771
+ @kwarg smooth: Smoothing factor for U{RectBivariateSpline
772
+ <https://docs.SciPy.org/doc/scipy/reference/generated/
773
+ scipy.interpolate.RectBivariateSpline.html>}
774
+ only (C{int}).
775
+
776
+ @raise GeoidError: G2012B grid file B{C{g2012b_bin}} issue or invalid
777
+ B{C{crop}}, B{C{kind}} or B{C{smooth}}.
778
+
779
+ @raise ImportError: Package C{numpy} or C{scipy} not found or
780
+ not installed.
781
+
782
+ @raise LenError: Grid file B{C{g2012b_bin}} axis mismatch.
783
+
784
+ @raise SciPyError: A C{RectBivariateSpline} or C{inter2d} issue.
785
+
786
+ @raise SciPyWarning: A C{RectBivariateSpline} or C{inter2d}
787
+ warning as exception.
788
+
789
+ @raise TypeError: Invalid B{C{datum}}.
790
+
791
+ @note: C{scipy.interpolate.interp2d} has been C{DEPRECATED}, specify
792
+ C{B{kind}=1..5} for C{scipy.interpolate.RectBivariateSpline}.
793
+ '''
794
+ if crop is not None:
795
+ raise GeoidError(crop=crop, txt=_not_(_supported_))
796
+
797
+ g = self._open(g2012b_bin, datum, kind, name, smooth)
798
+ _ = self.numpy # import numpy for ._load and
799
+
800
+ try:
801
+ p = _Gpars()
802
+ n = (self.sizeB // 4) - 11 # number of f4 heights
803
+ # U{numpy dtype formats are different from Python struct formats
804
+ # <https://docs.SciPy.org/doc/numpy-1.15.0/reference/arrays.dtypes.html>}
805
+ for en_ in ('<', '>'):
806
+ # skip 4xf8, get 3xi4
807
+ p.nlat, p.nlon, ien = map(int, self._load(g, en_+'i4', 3, 32))
808
+ if ien == 1: # correct endian
809
+ p.knots = p.nlat * p.nlon
810
+ if p.knots == n and 1 < p.nlat < n \
811
+ and 1 < p.nlon < n:
812
+ self._endian = en_+'f4'
813
+ break
814
+ else: # couldn't validate endian
815
+ raise GeoidError(_endian_)
816
+
817
+ # get the first 4xf8
818
+ p.slat, p.wlon, p.dlat, p.dlon = map(float, self._load(g, en_+'f8', 4))
819
+ # read all f4 heights, ignoring the first 4xf8 and 3xi4
820
+ hs = self._load(g, self._endian, n, 44).reshape(p.nlat, p.nlon)
821
+ p.wlon -= _360_0 # western-most East longitude to earth (..., lon)
822
+ _GeoidBase.__init__(self, hs, p)
823
+
824
+ except Exception as x:
825
+ raise _SciPyIssue(x, _in_, repr(g2012b_bin))
826
+ finally:
827
+ g.close()
828
+
829
+ def _g2ll2(self, lat, lon):
830
+ # convert grid (lat, lon) to earth (lat, lon)
831
+ return lat, lon
832
+
833
+ def _ll2g2(self, lat, lon):
834
+ # convert earth (lat, lon) to grid (lat, lon)
835
+ return lat, lon
836
+
837
+ if _FOR_DOCS:
838
+ __call__ = _GeoidBase.__call__
839
+ height = _GeoidBase.height
840
+
841
+
842
+ class GeoidHeight5Tuple(_NamedTuple): # .geoids.py
843
+ '''5-Tuple C{(lat, lon, egm84, egm96, egm2008)} for U{GeoidHeights.dat
844
+ <https://SourceForge.net/projects/geographiclib/files/testdata/>}
845
+ tests with the heights for 3 different EGM grids at C{degrees90}
846
+ and C{degrees180} degrees (after converting C{lon} from original
847
+ C{0 <= EasterLon <= 360}).
848
+ '''
849
+ _Names_ = (_lat_, _lon_, 'egm84', 'egm96', 'egm2008')
850
+ _Units_ = ( Lat, Lon, Height, Height, Height)
851
+
852
+
853
+ def _I(i):
854
+ '''(INTERNAL) Cache a single C{int} constant.
855
+ '''
856
+ return _intCs.setdefault(i, i) # PYCHOK undefined due to del _intCs
857
+
858
+
859
+ def _T(*cs):
860
+ '''(INTERNAL) Cache a tuple of single C{int} constants.
861
+ '''
862
+ return map1(_I, *cs)
863
+
864
+ _T0s12 = (_I(0),) * 12 # PYCHOK _T(0, 0, ..., 0)
865
+
866
+
867
+ class GeoidKarney(_GeoidBase):
868
+ '''Geoid height interpolator for I{Karney}'s U{GeographicLib Earth
869
+ Gravitational Model (EGM)<https://GeographicLib.SourceForge.io/C++/doc/
870
+ geoid.html>} geoid U{egm*.pgm<https://GeographicLib.SourceForge.io/
871
+ C++/doc/geoid.html#geoidinst>} datasets using bilinear or U{cubic
872
+ <https://dl.ACM.org/citation.cfm?id=368443>} interpolation and U{caching
873
+ <https://GeographicLib.SourceForge.io/C++/doc/geoid.html#geoidcache>}
874
+ in pure Python, transcoded from I{Karney}'s U{C++ class Geoid
875
+ <https://GeographicLib.SourceForge.io/C++/doc/geoid.html#geoidinterp>}.
876
+
877
+ Use any of the geoid U{egm84-, egm96- or egm2008-*.pgm
878
+ <https://GeographicLib.SourceForge.io/C++/doc/geoid.html#geoidinst>}
879
+ datasets.
880
+ '''
881
+ _C0 = _F(372), _F(240), _F(372) # n, _ and s common denominators
882
+ # matrices c3n_, c3, c3s_, transposed from GeographicLib/Geoid.cpp
883
+ _C3 = ((_T(0, 0, 62, 124, 124, 62, 0, 0, 0, 0, 0, 0),
884
+ _T0s12,
885
+ _T(-131, 7, -31, -62, -62, -31, 45, 216, 156, -45, -55, -7),
886
+ _T0s12,
887
+ _T(138, -138, 0, 0, 0, 0, -183, 33, 153, -3, 48, -48), # PYCHOK indent
888
+ _T(144, 42, -62, -124, -124, -62, -9, 87, 99, 9, 42, -42),
889
+ _T0s12,
890
+ _T(0, 0, 0, 0, 0, 0, 93, -93, -93, 93, 0, 0),
891
+ _T(-102, 102, 0, 0, 0, 0, 18, 12, -12, -18, -84, 84),
892
+ _T(-31, -31, 31, 62, 62, 31, 0, -93, -93, 0, 31, 31)), # PYCHOK indent
893
+
894
+ (_T(9, -9, 9, 186, 54, -9, -9, 54, -54, 9, -9, 9),
895
+ _T(-18, 18, -88, -42, 162, -32, 8, -78, 78, -8, 18, -18),
896
+ _T(-88, 8, -18, -42, -78, 18, 18, 162, 78, -18, -32, -8),
897
+ _T(0, 0, 90, -150, 30, 30, 30, -90, 90, -30, 0, 0),
898
+ _T(96, -96, 96, -96, -24, 24, -96, -24, 144, -24, 24, -24), # PYCHOK indent
899
+ _T(90, 30, 0, -150, -90, 0, 0, 30, 90, 0, 30, -30),
900
+ _T(0, 0, -20, 60, -60, 20, -20, 60, -60, 20, 0, 0),
901
+ _T(0, 0, -60, 60, 60, -60, 60, -60, -60, 60, 0, 0),
902
+ _T(-60, 60, 0, 60, -60, 0, 0, 60, -60, 0, -60, 60),
903
+ _T(-20, -20, 0, 60, 60, 0, 0, -60, -60, 0, 20, 20)),
904
+
905
+ (_T(18, -18, 36, 210, 162, -36, 0, 0, 0, 0, -18, 18), # PYCHOK indent
906
+ _T(-36, 36, -165, 45, 141, -21, 0, 0, 0, 0, 36, -36),
907
+ _T(-122, -2, -27, -111, -75, 27, 62, 124, 124, 62, -64, 2),
908
+ _T(0, 0, 93, -93, -93, 93, 0, 0, 0, 0, 0, 0),
909
+ _T(120, -120, 147, -57, -129, 39, 0, 0, 0, 0, 66, -66), # PYCHOK indent
910
+ _T(135, 51, -9, -192, -180, 9, 31, 62, 62, 31, 51, -51),
911
+ _T0s12,
912
+ _T(0, 0, -93, 93, 93, -93, 0, 0, 0, 0, 0, 0),
913
+ _T(-84, 84, 18, 12, -12, -18, 0, 0, 0, 0, -102, 102),
914
+ _T(-31, -31, 0, 93, 93, 0, -31, -62, -62, -31, 31, 31)))
915
+
916
+ _BT = (_T(0, 0), # bilinear 4-tuple [i, j] indices
917
+ _T(1, 0),
918
+ _T(0, 1),
919
+ _T(1, 1))
920
+
921
+ _CM = (_T( 0, -1), # 10x12 cubic matrix [i, j] indices
922
+ _T( 1, -1),
923
+ _T(-1, 0),
924
+ _T( 0, 0),
925
+ _T( 1, 0),
926
+ _T( 2, 0),
927
+ _T(-1, 1),
928
+ _T( 0, 1),
929
+ _T( 1, 1),
930
+ _T( 2, 1),
931
+ _T( 0, 2),
932
+ _T( 1, 2))
933
+
934
+ _endian = '>H' # struct.unpack 1 ushort (big endian, unsigned short)
935
+ _4endian = '>4H' # struct.unpack 4 ushorts
936
+ _Rendian = NN # struct.unpack a row of ushorts
937
+ # _highest = (-8.4, 147.367, 85.839) if egm2008-1.pgm else (
938
+ # (-8.167, 147.25, 85.422) if egm96-5.pgm else
939
+ # (-4.5, 148.75, 81.33)) # egm84-15.pgm
940
+ # _lowest = (4.7, 78.767, -106.911) if egm2008-1.pgm else (
941
+ # (4.667, 78.833, -107.043) if egm96-5.pgm else
942
+ # (4.75, 79.25, -107.34)) # egm84-15.pgm
943
+ _mean = _F(-1.317) # from egm2008-1, -1.438 egm96-5, -0.855 egm84-15
944
+ _nBytes = None # not applicable
945
+ _nterms = len(_C3[0]) # columns length, number of row
946
+ _smooth = None # not applicable
947
+ _stdev = _F(29.244) # from egm2008-1, 29.227 egm96-5, 29.183 egm84-15
948
+ _u2B = _calcsize(_endian) # pixelsize_ in bytes
949
+ _4u2B = _calcsize(_4endian) # 4 pixelsize_s in bytes
950
+ _Ru2B = 0 # row of pixelsize_s in bytes
951
+ _yxH = () # cache (y, x) indices
952
+ _yxHt = () # cached 4- or 10-tuple for _ev2H resp. _ev3H
953
+ _yx_hits = 0 # cache hits
954
+
955
+ def __init__(self, egm_pgm, crop=None, datum=None, # WGS84
956
+ kind=3, name=NN, smooth=None):
957
+ '''New L{GeoidKarney} interpolator.
958
+
959
+ @arg egm_pgm: An U{EGM geoid dataset<https://GeographicLib.SourceForge.io/
960
+ C++/doc/geoid.html#geoidinst>} file name (C{egm*.pgm}), see
961
+ note below.
962
+ @kwarg crop: Optional box to limit geoid locations, a 4-tuple (C{south,
963
+ west, north, east}), 2-tuple (C{(south, west), (north,
964
+ east)}) or 2, in C{degrees90} lat- and C{degrees180}
965
+ longitudes or a 2-tuple (C{LatLonSW, LatLonNE}) of
966
+ C{LatLon} instances.
967
+ @kwarg datum: Optional grid datum (C{Datum}, L{Ellipsoid}, L{Ellipsoid2}
968
+ or L{a_f2Tuple}), default C{WGS84}.
969
+ @kwarg kind: Interpolation order (C{int}), 2 for C{bilinear} or 3
970
+ for C{cubic}.
971
+ @kwarg name: Optional geoid name (C{str}).
972
+ @kwarg smooth: Smoothing factor, unsupported (C{None}).
973
+
974
+ @raise GeoidError: EGM dataset B{C{egm_pgm}} issue or invalid
975
+ B{C{crop}}, B{C{kind}} or B{C{smooth}}.
976
+
977
+ @raise TypeError: Invalid B{C{datum}}.
978
+
979
+ @see: Class L{GeoidPGM} and function L{egmGeoidHeights}.
980
+
981
+ @note: Geoid file B{C{egm_pgm}} remains open and must be closed
982
+ by calling the C{close} method or by using this instance
983
+ in a C{with B{GeoidKarney}(...) as ...} context.
984
+ '''
985
+ if smooth is not None:
986
+ raise GeoidError(smooth=smooth, txt=_not_(_supported_))
987
+
988
+ if kind in (2,):
989
+ self._evH = self._ev2H
990
+ elif kind not in (3,):
991
+ raise GeoidError(kind=kind)
992
+
993
+ self._egm = g = self._open(egm_pgm, datum, kind, name, smooth)
994
+ self._pgm = p = _PGM(g, pgm=egm_pgm, itemsize=self.u2B, sizeB=self.sizeB)
995
+
996
+ self._Rendian = self._4endian.replace(_4_, str(p.nlon))
997
+ self._Ru2B = _calcsize(self._Rendian)
998
+
999
+ self._knots = p.knots # grid knots
1000
+ self._lon_of = float(p.flon) # forward offset
1001
+ self._lon_og = float(p.glon) # reverse offset
1002
+ # set earth (lat, lon) limits (s, w, n, e)
1003
+ self._lat_lo, self._lon_lo, \
1004
+ self._lat_hi, self._lon_hi = self._swne(crop if crop else p.crop4)
1005
+ self._cropped = True if crop else False
1006
+
1007
+ def __call__(self, *llis, **wrap_H):
1008
+ '''Interpolate the geoid height for one or several locations.
1009
+
1010
+ @arg llis: One or more locations (C{LatLon}s), all positional.
1011
+ @kwarg wrap_H: Keyword arguments C{B{wrap}=False, B{H}=False}.
1012
+ If C{B{wrap} is True}, wrap or I{normalize} all
1013
+ B{C{llis}} locations (C{bool}). If C{B{H} is True},
1014
+ return the I{orthometric} height instead of the
1015
+ I{geoid} height at each location (C{bool}).
1016
+
1017
+ @return: A single interpolated geoid (or orthometric) height
1018
+ (C{float}) or a list or tuple of interpolated geoid
1019
+ (or orthometric) heights (C{float}s).
1020
+
1021
+ @raise GeoidError: Insufficient number of B{C{llis}}, an
1022
+ invalid B{C{lli}} or the C{egm*.pgm}
1023
+ geoid file is closed.
1024
+
1025
+ @raise RangeError: An B{C{lli}} is outside this geoid's lat-
1026
+ or longitude range.
1027
+
1028
+ @note: To obtain I{orthometric} heights, each B{C{llis}}
1029
+ location must have an ellipsoid C{height} or C{h}
1030
+ attribute, otherwise C{height=0} is used.
1031
+
1032
+ @see: Function L{pygeodesy.heightOrthometric}.
1033
+ '''
1034
+ return self._called(llis, False, **wrap_H)
1035
+
1036
+ def _c0c3v(self, y, x):
1037
+ # get the common denominator, the 10x12 cubic matrix and
1038
+ # the 12 cubic v-coefficients around geoid index (y, x)
1039
+ p = self._pgm
1040
+ if 0 < x < (p.nlon - 2) and 0 < y < (p.nlat - 2):
1041
+ # read 4x4 ushorts, drop the 4 corners
1042
+ g = self._egm
1043
+ e = self._4endian
1044
+ n = self._4u2B
1045
+ R = self._Ru2B
1046
+
1047
+ b = self._seek(y - 1, x - 1)
1048
+ v = _unpack(e, g.read(n))[1:3]
1049
+ b += R
1050
+ g.seek(b, _SEEK_SET)
1051
+ v += _unpack(e, g.read(n))
1052
+ b += R
1053
+ g.seek(b, _SEEK_SET)
1054
+ v += _unpack(e, g.read(n))
1055
+ b += R
1056
+ g.seek(b, _SEEK_SET)
1057
+ v += _unpack(e, g.read(n))[1:3]
1058
+ j = 1
1059
+
1060
+ else: # likely some wrapped y and/or x's
1061
+ v = self._raws(y, x, GeoidKarney._CM)
1062
+ j = 0 if y < 1 else (1 if y < (p.nlat - 2) else 2)
1063
+
1064
+ return GeoidKarney._C0[j], GeoidKarney._C3[j], v
1065
+
1066
+ @Property_RO
1067
+ def dtype(self):
1068
+ '''Get the geoid's grid data type (C{str}).
1069
+ '''
1070
+ return 'ushort'
1071
+
1072
+ def _ev(self, lat, lon): # PYCHOK expected
1073
+ # interpolate the geoid height at grid (lat, lon)
1074
+ fy, fx = self._g2yx2(lat, lon)
1075
+ y, x = int(floor(fy)), int(floor(fx))
1076
+ fy -= y
1077
+ fx -= x
1078
+ H = self._evH(fy, fx, y, x) # ._ev3H or ._ev2H
1079
+ H *= self._pgm.Scale # H.fmul(self._pgm.Scale)
1080
+ H += self._pgm.Offset # H.fadd(self._pgm.Offset)
1081
+ return H.fsum()
1082
+
1083
+ def _ev2H(self, fy, fx, *yx):
1084
+ # compute the bilinear 4-tuple and interpolate raw H
1085
+ if self._yxH == yx:
1086
+ t = self._yxHt
1087
+ self._yx_hits += 1
1088
+ else:
1089
+ y, x = self._yxH = yx
1090
+ self._yxHt = t = self._raws(y, x, GeoidKarney._BT)
1091
+ v = _1_0, -fx, fx
1092
+ H = Fdot(v, t[0], t[0], t[1]).fmul(_1_0 - fy) # c = a * (1 - fy)
1093
+ H += Fdot(v, t[2], t[2], t[3]).fmul(fy) # c += b * fy
1094
+ return H
1095
+
1096
+ def _ev3H(self, fy, fx, *yx):
1097
+ # compute the cubic 10-tuple and interpolate raw H
1098
+ if self._yxH == yx:
1099
+ t = self._yxHt
1100
+ self._yx_hits += 1
1101
+ else:
1102
+ self._yxH = yx
1103
+ c0, c3, v = self._c0c3v(*yx)
1104
+ t = [fdot(v, *c3[i]) / c0 for i in range(self._nterms)]
1105
+ self._yxHt = t = tuple(t)
1106
+ # GeographicLib/Geoid.cpp Geoid::height(lat, lon) ...
1107
+ # real h = t[0] + fx * (t[1] + fx * (t[3] + fx * t[6])) +
1108
+ # fy * (t[2] + fx * (t[4] + fx * t[7]) +
1109
+ # fy * (t[5] + fx * t[8] + fy * t[9]));
1110
+ v = _1_0, fx, fy
1111
+ H = Fdot(v, t[5], t[8], t[9])
1112
+ H *= fy
1113
+ H += Fhorner(fx, t[2], t[4], t[7])
1114
+ H *= fy
1115
+ H += Fhorner(fx, t[0], t[1], t[3], t[6])
1116
+ return H
1117
+
1118
+ _evH = _ev3H # overriden for kind == 2
1119
+
1120
+ def _g2ll2(self, lat, lon):
1121
+ # convert grid (lat, lon) to earth (lat, lon), uncropped
1122
+ while lon > _180_0:
1123
+ lon -= _360_0
1124
+ return lat, lon
1125
+
1126
+ def _g2yx2(self, lat, lon):
1127
+ # convert grid (lat, lon) to grid (y, x) indices
1128
+ p = self._pgm
1129
+ # note, slat = +90, rlat < 0 makes y >=0
1130
+ return ((lat - p.slat) * p.rlat), ((lon - p.wlon) * p.rlon)
1131
+
1132
+ def _gyx2g2(self, y, x):
1133
+ # convert grid (y, x) indices to grid (lat, lon)
1134
+ p = self._pgm
1135
+ return (p.slat + p.dlat * y), (p.wlon + p.dlon * x)
1136
+
1137
+ def height(self, lats, lons, **wrap):
1138
+ '''Interpolate the geoid height for one or several lat-/longitudes.
1139
+
1140
+ @arg lats: Latitude or latitudes (C{degrees} or C{degrees}s).
1141
+ @arg lons: Longitude or longitudes (C{degrees} or C{degrees}s).
1142
+ @kwarg wrap: If C{True}, wrap or I{normalize} all B{C{lats}}
1143
+ and B{C{lons}} locations (C{bool}).
1144
+
1145
+ @return: A single interpolated geoid height (C{float}) or a
1146
+ list of interpolated geoid heights (C{float}s).
1147
+
1148
+ @raise GeoidError: Insufficient or non-matching number of
1149
+ B{C{lats}} and B{C{lons}} or the C{egm*.pgm}
1150
+ geoid file is closed.
1151
+
1152
+ @raise RangeError: A B{C{lat}} or B{C{lon}} is outside this
1153
+ geoid's lat- or longitude range.
1154
+ '''
1155
+ return _height_called(self, lats, lons, Error=GeoidError, **wrap)
1156
+
1157
+ @Property_RO
1158
+ def _highest_ltd(self):
1159
+ '''(INTERNAL) Cache for L{highest} mesthod.
1160
+ '''
1161
+ return self._llh3minmax(True, -12, -4)
1162
+
1163
+ def highest(self, LatLon=None, full=False): # PYCHOK full
1164
+ '''Return the location and largest height of this geoid.
1165
+
1166
+ @kwarg LatLon: Optional class to return the location and height
1167
+ (C{LatLon}) or C{None}.
1168
+ @kwarg full: Search the full or limited latitude range (C{bool}).
1169
+
1170
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
1171
+ lon, height)} otherwise a B{C{LatLon}} instance
1172
+ with the lat-, longitude and geoid height at the
1173
+ highest grid location.
1174
+ '''
1175
+ llh = self._highest if full or self.cropped else self._highest_ltd
1176
+ return self._llh3LL(llh, LatLon)
1177
+
1178
+ def _lat2y2(self, lat2):
1179
+ # convert earth lat(s) to min and max grid y indices
1180
+ ys, m = [], self._pgm.nlat - 1
1181
+ for lat in lat2:
1182
+ y, _ = self._g2yx2(*self._ll2g2(lat, 0))
1183
+ ys.append(max(min(int(y), m), 0))
1184
+ return min(ys), max(ys) + 1
1185
+
1186
+ def _ll2g2(self, lat, lon):
1187
+ # convert earth (lat, lon) to grid (lat, lon), uncropped
1188
+ while lon < 0:
1189
+ lon += _360_0
1190
+ return lat, lon
1191
+
1192
+ def _llh3minmax(self, highest=True, *lat2):
1193
+ # find highest or lowest, takes 10+ secs for egm2008-1.pgm geoid
1194
+ # (Python 2.7.16, macOS 10.13.6 High Sierra, iMac 3 GHz Core i3)
1195
+ y = x = 0
1196
+ h = self._raw(y, x)
1197
+ if highest:
1198
+ for j, r in self._raw2(*lat2):
1199
+ m = max(r)
1200
+ if m > h:
1201
+ h, y, x = m, j, r.index(m)
1202
+ else: # lowest
1203
+ for j, r in self._raw2(*lat2):
1204
+ m = min(r)
1205
+ if m < h:
1206
+ h, y, x = m, j, r.index(m)
1207
+ h *= self._pgm.Scale
1208
+ h += self._pgm.Offset
1209
+ return self._g2ll2(*self._gyx2g2(y, x)) + (h,)
1210
+
1211
+ @Property_RO
1212
+ def _lowest_ltd(self):
1213
+ '''(INTERNAL) Cache for L{lowest}.
1214
+ '''
1215
+ return self._llh3minmax(False, 0, 8)
1216
+
1217
+ def lowest(self, LatLon=None, full=False): # PYCHOK full
1218
+ '''Return the location and lowest height of this geoid.
1219
+
1220
+ @kwarg LatLon: Optional class to return the location and height
1221
+ (C{LatLon}) or C{None}.
1222
+ @kwarg full: Search the full or limited latitude range (C{bool}).
1223
+
1224
+ @return: If B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat,
1225
+ lon, height)} otherwise a B{C{LatLon}} instance
1226
+ with the lat-, longitude and geoid height of the
1227
+ lowest grid location.
1228
+ '''
1229
+ llh = self._lowest if full or self.cropped else self._lowest_ltd
1230
+ return self._llh3LL(llh, LatLon)
1231
+
1232
+ def _raw(self, y, x):
1233
+ # get the ushort geoid height at geoid index (y, x),
1234
+ # like GeographicLib/Geoid.hpp real rawval(is, iy)
1235
+ p = self._pgm
1236
+ if x < 0:
1237
+ x += p.nlon
1238
+ elif x >= p.nlon:
1239
+ x -= p.nlon
1240
+ h = p.nlon // 2
1241
+ if y < 0:
1242
+ y = -y
1243
+ elif y >= p.nlat:
1244
+ y = (p.nlat - 1) * 2 - y
1245
+ else:
1246
+ h = 0
1247
+ x += h if x < h else -h
1248
+ self._seek(y, x)
1249
+ h = _unpack(self._endian, self._egm.read(self._u2B))
1250
+ return h[0]
1251
+
1252
+ def _raws(self, y, x, ijs):
1253
+ # get bilinear 4-tuple or 10x12 cubic matrix
1254
+ return tuple(self._raw(y + j, x + i) for i, j in ijs)
1255
+
1256
+ def _raw2(self, *lat2):
1257
+ # yield a 2-tuple (y, ushorts) for each row or for
1258
+ # the rows between two (or more) earth lat values
1259
+ p = self._pgm
1260
+ g = self._egm
1261
+ e = self._Rendian
1262
+ n = self._Ru2B
1263
+ # min(lat2) <= lat <= max(lat2) or 0 <= y < p.nlat
1264
+ s, t = self._lat2y2(lat2) if lat2 else (0, p.nlat)
1265
+ self._seek(s, 0) # to start of row s
1266
+ for y in range(s, t):
1267
+ yield y, _unpack(e, g.read(n))
1268
+
1269
+ def _seek(self, y, x):
1270
+ # position geoid to grid index (y, x)
1271
+ p, g = self._pgm, self._egm
1272
+ if g:
1273
+ b = p.skip + (y * p.nlon + x) * self._u2B
1274
+ g.seek(b, _SEEK_SET)
1275
+ return b # position
1276
+ raise GeoidError('closed file', txt=repr(p.egm)) # IOError
1277
+
1278
+
1279
+ class GeoidPGM(_GeoidBase):
1280
+ '''Geoid height interpolator for I{Karney}'s U{GeographicLib Earth
1281
+ Gravitational Model (EGM)<https://GeographicLib.SourceForge.io/C++/doc/
1282
+ geoid.html>} geoid U{egm*.pgm<https://GeographicLib.SourceForge.io/
1283
+ C++/doc/geoid.html#geoidinst>} datasets but based on C{SciPy}
1284
+ U{RectBivariateSpline<https://docs.SciPy.org/doc/scipy/reference/
1285
+ generated/scipy.interpolate.RectBivariateSpline.html>} or
1286
+ U{interp2d<https://docs.SciPy.org/doc/scipy/reference/generated/
1287
+ scipy.interpolate.interp2d.html>} interpolation.
1288
+
1289
+ Use any of the U{egm84-, egm96- or egm2008-*.pgm
1290
+ <https://GeographicLib.SourceForge.io/C++/doc/geoid.html#geoidinst>}
1291
+ datasets. However, unless cropped, an entire C{egm*.pgm} dataset
1292
+ is loaded into the C{SciPy} U{RectBivariateSpline<https://docs.SciPy.org/
1293
+ doc/scipy/reference/generated/scipy.interpolate.RectBivariateSpline.html>}
1294
+ or U{interp2d<https://docs.SciPy.org/doc/scipy/reference/generated/
1295
+ scipy.interpolate.interp2d.html>} interpolator and converted from
1296
+ 2-byte C{int} to 8-byte C{dtype float64}. Therefore, internal memory
1297
+ usage is 4x the U{egm*.pgm<https://GeographicLib.SourceForge.io/C++/doc/
1298
+ geoid.html#geoidinst>} file size and may exceed the available memory,
1299
+ especially with 32-bit Python, see properties C{.nBytes} and C{.sizeB}.
1300
+ '''
1301
+ _cropped = False
1302
+ _endian = '>u2'
1303
+
1304
+ def __init__(self, egm_pgm, crop=None, datum=None, # WGS84
1305
+ kind=3, name=NN, smooth=0):
1306
+ '''New L{GeoidPGM} interpolator.
1307
+
1308
+ @arg egm_pgm: An U{EGM geoid dataset<https://GeographicLib.SourceForge.io/
1309
+ C++/doc/geoid.html#geoidinst>} file name (C{egm*.pgm}).
1310
+ @kwarg crop: Optional box to crop B{C{egm_pgm}}, a 4-tuple (C{south, west,
1311
+ north, east}) or 2-tuple (C{(south, west), (north, east)}),
1312
+ in C{degrees90} lat- and C{degrees180} longitudes or a
1313
+ 2-tuple (C{LatLonSW, LatLonNE}) of C{LatLon} instances.
1314
+ @kwarg datum: Optional grid datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
1315
+ or L{a_f2Tuple}), default C{WGS84}.
1316
+ @kwarg kind: C{scipy.interpolate} order (C{int}), use 1..5 for
1317
+ U{RectBivariateSpline<https://docs.SciPy.org/doc/scipy/
1318
+ reference/generated/scipy.interpolate.RectBivariateSpline.html>},
1319
+ -2 for U{interp2d linear<https://docs.SciPy.org/doc/scipy/
1320
+ reference/generated/scipy.interpolate.interp2d.html>}, -3
1321
+ for C{interp2d cubic} or -5 for C{interp2d quintic}.
1322
+ @kwarg name: Optional geoid name (C{str}).
1323
+ @kwarg smooth: Smoothing factor for U{RectBivariateSpline
1324
+ <https://docs.SciPy.org/doc/scipy/reference/generated/
1325
+ scipy.interpolate.RectBivariateSpline.html>}
1326
+ only (C{int}).
1327
+
1328
+ @raise GeoidError: EGM dataset B{C{egm_pgm}} issue or invalid B{C{crop}},
1329
+ B{C{kind}} or B{C{smooth}}.
1330
+
1331
+ @raise ImportError: Package C{numpy} or C{scipy} not found or not installed.
1332
+
1333
+ @raise LenError: EGM dataset B{C{egm_pgm}} axis mismatch.
1334
+
1335
+ @raise SciPyError: A C{RectBivariateSpline} or C{inter2d} issue.
1336
+
1337
+ @raise SciPyWarning: A C{RectBivariateSpline} or C{inter2d}
1338
+ warning as exception.
1339
+
1340
+ @raise TypeError: Invalid B{C{datum}}.
1341
+
1342
+ @note: C{scipy.interpolate.interp2d} has been C{DEPRECATED}, specify
1343
+ C{B{kind}=1..5} for C{scipy.interpolate.RectBivariateSpline}.
1344
+
1345
+ @note: The U{GeographicLib egm*.pgm<https://GeographicLib.SourceForge.io/
1346
+ C++/doc/geoid.html#geoidinst>} file sizes are based on a 2-byte
1347
+ C{int} height converted to 8-byte C{dtype float64} for C{scipy}
1348
+ interpolators. Therefore, internal memory usage is 4 times the
1349
+ C{egm*.pgm} file size and may exceed the available memory,
1350
+ especially with 32-bit Python. To reduce memory usage, set
1351
+ keyword argument B{C{crop}} to the region of interest. For example
1352
+ C{B{crop}=(20, -125, 50, -65)} covers the U{conterminous US<https://
1353
+ www.NGS.NOAA.gov/GEOID/GEOID12B/maps/GEOID12B_CONUS_grids.png>}
1354
+ (CONUS), less than 3% of the entire C{egm2008-1.pgm} dataset.
1355
+
1356
+ @see: Class L{GeoidKarney} and function L{egmGeoidHeights}.
1357
+ '''
1358
+ np = self.numpy
1359
+ self._u2B = np.dtype(self.endian).itemsize
1360
+
1361
+ g = self._open(egm_pgm, datum, kind, name, smooth)
1362
+ self._pgm = p = _PGM(g, pgm=egm_pgm, itemsize=self.u2B, sizeB=self.sizeB)
1363
+ if crop:
1364
+ g = p._cropped(g, abs(kind) + 1, *self._swne(crop))
1365
+ if _version2(np.__version__) < (1, 9):
1366
+ g = open(g.name, _rb_) # reopen tempfile for numpy 1.8.0-
1367
+ self._cropped = True
1368
+ try:
1369
+ # U{numpy dtype formats are different from Python struct formats
1370
+ # <https://docs.SciPy.org/doc/numpy-1.15.0/reference/arrays.dtypes.html>}
1371
+ # read all heights, skipping the PGM header lines, converted to float
1372
+ hs = self._load(g, self.endian, p.knots, p.skip).reshape(p.nlat, p.nlon) * p.Scale
1373
+ if p.Offset: # offset
1374
+ hs = p.Offset + hs
1375
+ if p.dlat < 0: # flip the rows
1376
+ hs = np.flipud(hs)
1377
+ _GeoidBase.__init__(self, hs, p)
1378
+ except Exception as x:
1379
+ raise _SciPyIssue(x, _in_, repr(egm_pgm))
1380
+ finally:
1381
+ g.close()
1382
+
1383
+ def _g2ll2(self, lat, lon):
1384
+ # convert grid (lat, lon) to earth (lat, lon), un-/cropped
1385
+ if self._cropped:
1386
+ lon -= self._lon_of
1387
+ else:
1388
+ while lon > _180_0:
1389
+ lon -= _360_0
1390
+ return lat, lon
1391
+
1392
+ def _ll2g2(self, lat, lon):
1393
+ # convert earth (lat, lon) to grid (lat, lon), un-/cropped
1394
+ if self._cropped:
1395
+ lon += self._lon_of
1396
+ else:
1397
+ while lon < 0:
1398
+ lon += _360_0
1399
+ return lat, lon
1400
+
1401
+ if _FOR_DOCS:
1402
+ __call__ = _GeoidBase.__call__
1403
+ height = _GeoidBase.height
1404
+
1405
+
1406
+ class _Gpars(_Named):
1407
+ '''(INTERNAL) Basic geoid parameters.
1408
+ '''
1409
+ # interpolator parameters
1410
+ dlat = 0 # +/- latitude resolution in C{degrees}
1411
+ dlon = 0 # longitude resolution in C{degrees}
1412
+ nlat = 1 # number of latitude knots (C{int})
1413
+ nlon = 0 # number of longitude knots (C{int})
1414
+ rlat = 0 # +/- latitude resolution in C{float}, 1 / .dlat
1415
+ rlon = 0 # longitude resolution in C{float}, 1 / .dlon
1416
+ slat = 0 # nothern- or southern most latitude (C{degrees90})
1417
+ wlon = 0 # western-most longitude in Eastern lon (C{degrees360})
1418
+
1419
+ flon = 0 # forward, earth to grid longitude offset
1420
+ glon = 0 # reverse, grid to earth longitude offset
1421
+
1422
+ knots = 0 # number of knots, nlat * nlon (C{int})
1423
+ skip = 0 # header bytes to skip (C{int})
1424
+
1425
+ def __repr__(self):
1426
+ t = _COMMASPACE_.join(pairs((a, getattr(self, a)) for
1427
+ a in dir(self.__class__)
1428
+ if a[:1].isupper()))
1429
+ return _COLONSPACE_(self, t)
1430
+
1431
+ def __str__(self):
1432
+ return Fmt.PAREN(self.classname, repr(self.name))
1433
+
1434
+
1435
+ class _PGM(_Gpars):
1436
+ '''(INTERNAL) Parse an C{egm*.pgm} geoid dataset file.
1437
+
1438
+ # Geoid file in PGM format for the GeographicLib::Geoid class
1439
+ # Description WGS84 EGM96, 5-minute grid
1440
+ # URL https://Earth-Info.NGA.mil/GandG/wgs84/gravitymod/egm96/egm96.html
1441
+ # DateTime 2009-08-29 18:45:03
1442
+ # MaxBilinearError 0.140
1443
+ # RMSBilinearError 0.005
1444
+ # MaxCubicError 0.003
1445
+ # RMSCubicError 0.001
1446
+ # Offset -108
1447
+ # Scale 0.003
1448
+ # Origin 90N 0E
1449
+ # AREA_OR_POINT Point
1450
+ # Vertical_Datum WGS84
1451
+ <width> <height>
1452
+ <pixel>
1453
+ ...
1454
+ '''
1455
+ crop4 = () # 4-tuple (C{south, west, north, east}).
1456
+ egm = None
1457
+ glon = 180 # reverse offset, uncropped
1458
+ # pgm = NN # name
1459
+ sizeB = 0
1460
+ u2B = 2 # item size of grid height (C{int}).
1461
+
1462
+ @staticmethod
1463
+ def _llstr2floats(latlon):
1464
+ # llstr to (lat, lon) floats
1465
+ lat, lon = latlon.split()
1466
+ return _MODS.dms.parseDMS2(lat, lon)
1467
+
1468
+ # PGM file attributes, CamelCase but not .istitle()
1469
+ AREA_OR_POINT = str
1470
+ DateTime = str
1471
+ Description = str # 'WGS84 EGM96, 5-minute grid'
1472
+ Geoid = str # 'file in PGM format for the GeographicLib::Geoid class'
1473
+ MaxBilinearError = float
1474
+ MaxCubicError = float
1475
+ Offset = float
1476
+ Origin = _llstr2floats
1477
+ Pixel = 0
1478
+ RMSBilinearError = float
1479
+ RMSCubicError = float
1480
+ Scale = float
1481
+ URL = str # 'https://Earth-Info.NGA.mil/GandG/wgs84/...'
1482
+ Vertical_Datum = str
1483
+
1484
+ def __init__(self, g, pgm=NN, itemsize=0, sizeB=0): # MCCABE 22
1485
+ '''(INTERNAL) New C{_PGM} parsed C{egm*.pgm} geoid dataset.
1486
+ '''
1487
+ self.name = pgm # geoid file name
1488
+ if itemsize:
1489
+ self._u2B = itemsize
1490
+ if sizeB:
1491
+ self.sizeB = sizeB
1492
+
1493
+ t = g.readline() # make sure newline == '\n'
1494
+ if t != b'P5\n' and t.strip() != b'P5':
1495
+ raise self._Errorf(_format_, _header_, t)
1496
+
1497
+ while True: # read all # Attr ... lines,
1498
+ try: # ignore empty ones or comments
1499
+ t = g.readline().strip()
1500
+ if t.startswith(_bHASH_):
1501
+ t = t.lstrip(_bHASH_).lstrip()
1502
+ a, v = map(_ub2str, t.split(None, 1))
1503
+ f = getattr(_PGM, a, None)
1504
+ if callable(f) and a[:1].isupper():
1505
+ setattr(self, a, f(v))
1506
+ elif t:
1507
+ break
1508
+ except (TypeError, ValueError):
1509
+ raise self._Errorf(_format_, 'Attr', t)
1510
+ else: # should never get here
1511
+ raise self._Errorf(_format_, _header_, g.tell())
1512
+
1513
+ try: # must be (even) width and (odd) height
1514
+ nlon, nlat = map(int, t.split())
1515
+ if nlon < 2 or nlon > (360 * 60) or isodd(nlon) or \
1516
+ nlat < 2 or nlat > (181 * 60) or not isodd(nlat):
1517
+ raise ValueError
1518
+ except (TypeError, ValueError):
1519
+ raise self._Errorf(_format_, _SPACE_(_width_, _height_), t)
1520
+
1521
+ try: # must be 16 bit pixel height
1522
+ t = g.readline().strip()
1523
+ self.Pixel = int(t)
1524
+ if not 255 < self.Pixel < 65536: # >u2 or >H only
1525
+ raise ValueError
1526
+ except (TypeError, ValueError):
1527
+ raise self._Errorf(_format_, 'pixel', t)
1528
+
1529
+ for a in dir(_PGM): # set undefined # Attr ... to None
1530
+ if a[:1].isupper() and callable(getattr(self, a)):
1531
+ setattr(self, a, None)
1532
+
1533
+ if self.Origin is None:
1534
+ raise self._Errorf(_format_, 'Origin', self.Origin)
1535
+ if self.Offset is None or self.Offset > 0:
1536
+ raise self._Errorf(_format_, 'Offset', self.Offset)
1537
+ if self.Scale is None or self.Scale < EPS:
1538
+ raise self._Errorf(_format_, 'Scale', self.Scale)
1539
+
1540
+ self.skip = g.tell()
1541
+ self.knots = nlat * nlon
1542
+
1543
+ self.nlat, self.nlon = nlat, nlon
1544
+ self.slat, self.wlon = self.Origin
1545
+ # note, negative .dlat and .rlat since rows
1546
+ # are from .slat 90N down in decreasing lat
1547
+ self.dlat, self.dlon = _180_0 / (1 - nlat), _360_0 / nlon
1548
+ self.rlat, self.rlon = (1 - nlat) / _180_0, nlon / _360_0
1549
+
1550
+ # grid corners in earth (lat, lon), .slat = 90, .dlat < 0
1551
+ n = float(self.slat)
1552
+ s = n + self.dlat * (nlat - 1)
1553
+ w = self.wlon - self.glon
1554
+ e = w + self.dlon * nlon
1555
+ self.crop4 = s, w, n, e
1556
+
1557
+ n = self.sizeB - self.skip
1558
+ if n > 0 and n != (self.knots * self.u2B):
1559
+ raise self._Errorf('%s(%s x %s != %s)', _assert_, nlat, nlon, n)
1560
+
1561
+ def _cropped(self, g, k1, south, west, north, east): # MCCABE 15
1562
+ '''Crop the geoid to (south, west, north, east) box.
1563
+ '''
1564
+ # flon offset for both west and east
1565
+ f = 360 if west < 0 else 0
1566
+ # earth (lat, lon) to grid indices (y, x),
1567
+ # note y is decreasing, i.e. n < s
1568
+ s, w = self._lle2yx2(south, west, f)
1569
+ n, e = self._lle2yx2(north, east, f)
1570
+ s += 1 # s > n
1571
+ e += 1 # e > w
1572
+
1573
+ hi, wi = self.nlat, self.nlon
1574
+ # handle special cases
1575
+ if (s - n) > hi:
1576
+ n, s = 0, hi # entire lat range
1577
+ if (e - w) > wi:
1578
+ w, e, f = 0, wi, 180 # entire lon range
1579
+ if s == hi and w == n == 0 and e == wi:
1580
+ return g # use entire geoid as-is
1581
+
1582
+ if (e - w) < k1 or (s - n) < (k1 + 1):
1583
+ raise self._Errorf(_format_, 'swne', (north - south, east - west))
1584
+
1585
+ if e > wi > w: # wrap around
1586
+ # read w..wi and 0..e
1587
+ r, p = (wi - w), (e - wi)
1588
+ elif e > w:
1589
+ r, p = (e - w), 0
1590
+ else:
1591
+ raise self._Errorf('%s(%s < %s)', _assert_, w, e)
1592
+
1593
+ # convert to bytes
1594
+ r *= self.u2B
1595
+ p *= self.u2B
1596
+ q = wi * self.u2B # stride
1597
+ # number of rows and cols to skip from
1598
+ # the original (.slat, .wlon) origin
1599
+ z = self.skip + (n * wi + w) * self.u2B
1600
+ # sanity check
1601
+ if r < 2 or p < 0 or q < 2 or z < self.skip \
1602
+ or z > self.sizeB:
1603
+ raise self._Errorf(_format_, _assert_, (r, p, q, z))
1604
+
1605
+ # can't use _BytesIO since numpy
1606
+ # needs .fileno attr in .fromfile
1607
+ t, c = 0, self._tmpfile()
1608
+ # reading (s - n) rows, forward
1609
+ for y in range(n, s): # PYCHOK y unused
1610
+ g.seek(z, _SEEK_SET)
1611
+ # Python 2 tmpfile.write returns None
1612
+ t += c.write(g.read(r)) or r
1613
+ if p: # wrap around to start of row
1614
+ g.seek(-q, _SEEK_CUR)
1615
+ # assert(g.tell() == (z - w * self.u2B))
1616
+ # Python 2 tmpfile.write returns None
1617
+ t += c.write(g.read(p)) or p
1618
+ z += q
1619
+ c.flush()
1620
+ g.close()
1621
+
1622
+ s -= n # nlat
1623
+ e -= w # nlon
1624
+ k = s * e # knots
1625
+ z = k * self.u2B
1626
+ if t != z:
1627
+ raise self._Errorf('%s(%s != %s) %s', _assert_, t, z, self)
1628
+
1629
+ # update the _Gpars accordingly, note attributes
1630
+ # .dlat, .dlon, .rlat and .rlon remain unchanged
1631
+ self.slat += n * self.dlat
1632
+ self.wlon += w * self.dlon
1633
+ self.nlat = s
1634
+ self.nlon = e
1635
+ self.flon = self.glon = f
1636
+
1637
+ self.crop4 = south, west, north, east
1638
+ self.knots = k
1639
+ self.skip = 0 # no header lines in c
1640
+
1641
+ c.seek(0, _SEEK_SET)
1642
+ # c = open(c.name, _rb_) # reopen for numpy 1.8.0-
1643
+ return c
1644
+
1645
+ def _Errorf(self, fmt, *args): # PYCHOK no cover
1646
+ t = fmt % args
1647
+ e = self.pgm or NN
1648
+ if e:
1649
+ t = _SPACE_(t, _in_, repr(e))
1650
+ return PGMError(t)
1651
+
1652
+ def _lle2yx2(self, lat, lon, flon):
1653
+ # earth (lat, lon) to grid indices (y, x)
1654
+ # with .dlat decreasing from 90N .slat
1655
+ lat -= self.slat
1656
+ lon += flon - self.wlon
1657
+ return (min(self.nlat - 1, max(0, int(lat * self.rlat))),
1658
+ max(0, int(lon * self.rlon)))
1659
+
1660
+ def _tmpfile(self):
1661
+ # create a tmpfile to hold the cropped geoid grid
1662
+ try:
1663
+ from tempfile import NamedTemporaryFile as tmpfile
1664
+ except ImportError: # Python 2.7.16-
1665
+ from os import tmpfile
1666
+ t = _os_path.splitext(_os_path.basename(self.pgm))[0]
1667
+ f = tmpfile(mode='w+b', prefix=t or 'egm')
1668
+ f.seek(0, _SEEK_SET) # force overwrite
1669
+ return f
1670
+
1671
+ @Property_RO
1672
+ def pgm(self):
1673
+ '''Get the geoid file name (C{str}).
1674
+ '''
1675
+ return self.name
1676
+
1677
+
1678
+ class PGMError(GeoidError):
1679
+ '''Issue parsing or cropping an C{egm*.pgm} geoid dataset.
1680
+ '''
1681
+ pass
1682
+
1683
+
1684
+ def egmGeoidHeights(GeoidHeights_dat):
1685
+ '''Generate geoid U{egm*.pgm<https://GeographicLib.SourceForge.io/
1686
+ C++/doc/geoid.html#geoidinst>} height tests from U{GeoidHeights.dat
1687
+ <https://SourceForge.net/projects/geographiclib/files/testdata/>}
1688
+ U{Test data for Geoids<https://GeographicLib.SourceForge.io/C++/doc/
1689
+ geoid.html#testgeoid>}.
1690
+
1691
+ @arg GeoidHeights_dat: The un-gz-ed C{GeoidHeights.dat} file
1692
+ (C{str} or C{file} handle).
1693
+
1694
+ @return: For each test, yield a L{GeoidHeight5Tuple}C{(lat, lon,
1695
+ egm84, egm96, egm2008)}.
1696
+
1697
+ @raise GeoidError: Invalid B{C{GeoidHeights_dat}}.
1698
+
1699
+ @note: Function L{egmGeoidHeights} is used to test the geoids
1700
+ L{GeoidKarney} and L{GeoidPGM}, see PyGeodesy module
1701
+ C{test/testGeoids.py}.
1702
+ '''
1703
+ dat = GeoidHeights_dat
1704
+ if isinstance(dat, bytes):
1705
+ dat = _BytesIO(dat)
1706
+
1707
+ try:
1708
+ dat.seek(0, _SEEK_SET) # reset
1709
+ except AttributeError as x:
1710
+ raise GeoidError(GeoidHeights_dat=type(dat), cause=x)
1711
+
1712
+ for t in dat.readlines():
1713
+ t = t.strip()
1714
+ if t and not t.startswith(_bHASH_):
1715
+ lat, lon, egm84, egm96, egm2008 = map(float, t.split())
1716
+ while lon > _180_0: # EasternLon to earth lon
1717
+ lon -= _360_0
1718
+ yield GeoidHeight5Tuple(lat, lon, egm84, egm96, egm2008)
1719
+
1720
+
1721
+ __all__ += _ALL_DOCS(_GeoidBase)
1722
+
1723
+ if __name__ == '__main__':
1724
+
1725
+ from pygeodesy.lazily import printf, _sys
1726
+
1727
+ _crop = ()
1728
+ _GeoidEGM = GeoidKarney
1729
+ _kind = 3
1730
+
1731
+ geoids = _sys.argv[1:]
1732
+ while geoids:
1733
+ geoid = geoids.pop(0)
1734
+
1735
+ if '-crop'.startswith(geoid.lower()):
1736
+ _crop = 20, -125, 50, -65 # CONUS
1737
+
1738
+ elif '-karney'.startswith(geoid.lower()):
1739
+ _GeoidEGM = GeoidKarney
1740
+
1741
+ elif '-kind'.startswith(geoid.lower()):
1742
+ _kind = int(geoids.pop(0))
1743
+
1744
+ elif '-pgm'.startswith(geoid.lower()):
1745
+ _GeoidEGM = GeoidPGM
1746
+
1747
+ elif geoid[-4:].lower() in ('.pgm',):
1748
+ g = _GeoidEGM(geoid, crop=_crop, kind=_kind)
1749
+ printf(g.toStr(), nt=1, nl=1)
1750
+ printf(repr(g.pgm), nt=1)
1751
+ # <https://GeographicLib.SourceForge.io/cgi-bin/GeoidEval>:
1752
+ # The height of the EGM96 geoid at Timbuktu
1753
+ # echo 16:46:33N 3:00:34W | GeoidEval
1754
+ # => 28.7068 -0.02e-6 -1.73e-6
1755
+ # The 1st number is the height of the geoid, the 2nd and
1756
+ # 3rd are its slopes in northerly and easterly direction
1757
+ t = 'Timbuktu %s' % (g,)
1758
+ k = {'egm84-15.pgm': '31.2979',
1759
+ 'egm96-5.pgm': '28.7067',
1760
+ 'egm2008-1.pgm': '28.7880'}.get(g.name.lower(), '28.7880')
1761
+ ll = _MODS.dms.parseDMS2('16:46:33N', '3:00:34W', sep=':')
1762
+ for ll in (ll, (16.776, -3.009),):
1763
+ try:
1764
+ h, ll = g.height(*ll), fstr(ll, prec=6)
1765
+ printf('%s.height(%s): %.4F vs %s', t, ll, h, k)
1766
+ except (GeoidError, RangeError) as x:
1767
+ printf(_COLONSPACE_(t, str(x)))
1768
+
1769
+ elif geoid[-4:].lower() in ('.bin',):
1770
+ g = GeoidG2012B(geoid, kind=_kind)
1771
+ printf(g.toStr())
1772
+
1773
+ else:
1774
+ raise GeoidError(grid=repr(geoid))
1775
+
1776
+ _I = int # PYCHOK unused _I
1777
+ del _intCs # trash ints cache
1778
+
1779
+ # **) MIT License
1780
+ #
1781
+ # Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
1782
+ #
1783
+ # Permission is hereby granted, free of charge, to any person obtaining a
1784
+ # copy of this software and associated documentation files (the "Software"),
1785
+ # to deal in the Software without restriction, including without limitation
1786
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
1787
+ # and/or sell copies of the Software, and to permit persons to whom the
1788
+ # Software is furnished to do so, subject to the following conditions:
1789
+ #
1790
+ # The above copyright notice and this permission notice shall be included
1791
+ # in all copies or substantial portions of the Software.
1792
+ #
1793
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1794
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1795
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1796
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1797
+ # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1798
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1799
+ # OTHER DEALINGS IN THE SOFTWARE.
1800
+
1801
+ # <https://GeographicLib.SourceForge.io/cgi-bin/GeoidEval>
1802
+ # _lowerleft = -90, -179, -30.1500 # egm2008-1.pgm
1803
+ # _lowerleft = -90, -179, -29.5350 # egm96-5.pgm
1804
+ # _lowerleft = -90, -179, -29.7120 # egm84-15.pgm
1805
+
1806
+ # _center = 0, 0, 17.2260 # egm2008-1.pgm
1807
+ # _center = 0, 0, 17.1630 # egm96-5.pgm
1808
+ # _center = 0, 0, 18.3296 # egm84-15.pgm
1809
+
1810
+ # _upperright = 90, 180, 14.8980 # egm2008-1.pgm
1811
+ # _upperright = 90, 180, 13.6050 # egm96-5.pgm
1812
+ # _upperright = 90, 180, 13.0980 # egm84-15.pgm
1813
+
1814
+
1815
+ # % python3 -m pygeodesy.geoids [-Karney] ../testGeoids/egm*.pgm
1816
+ #
1817
+ # GeoidKarney('egm2008-1.pgm'): lowerleft(-90.0, -180.0, -30.15), upperright(90.0, 180.0, 14.898), center(0.0, 0.0, 17.226), highest(-8.4, 147.367, 85.839), lowest(4.7, 78.767, -106.911)
1818
+ #
1819
+ # _PGM('../testGeoids/egm2008-1.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-31 06:54:00', Description='WGS84 EGM2008, 1-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.025, MaxCubicError=0.003, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.001, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008', Vertical_Datum='WGS84'
1820
+ #
1821
+ # Timbuktu GeoidKarney('egm2008-1.pgm').height(16.775833, -3.009444): 28.7881 vs 28.7880
1822
+ # Timbuktu GeoidKarney('egm2008-1.pgm').height(16.776, -3.009): 28.7880 vs 28.7880
1823
+ #
1824
+ # GeoidKarney('egm84-15.pgm'): lowerleft(-90.0, -180.0, -29.712), upperright(90.0, 180.0, 13.098), center(0.0, 0.0, 18.33), highest(-4.5, 148.75, 81.33), lowest(4.75, 79.25, -107.34)
1825
+ #
1826
+ # _PGM('../testGeoids/egm84-15.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-29 18:45:02', Description='WGS84 EGM84, 15-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.413, MaxCubicError=0.02, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.018, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/wgs84_180/wgs84_180.html', Vertical_Datum='WGS84'
1827
+ #
1828
+ # Timbuktu GeoidKarney('egm84-15.pgm').height(16.775833, -3.009444): 31.2983 vs 31.2979
1829
+ # Timbuktu GeoidKarney('egm84-15.pgm').height(16.776, -3.009): 31.2979 vs 31.2979
1830
+ #
1831
+ # GeoidKarney('egm96-5.pgm'): lowerleft(-90.0, -180.0, -29.535), upperright(90.0, 180.0, 13.605), center(0.0, 0.0, 17.163), highest(-8.167, 147.25, 85.422), lowest(4.667, 78.833, -107.043)
1832
+ #
1833
+ # _PGM('../testGeoids/egm96-5.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-29 18:45:03', Description='WGS84 EGM96, 5-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.14, MaxCubicError=0.003, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.005, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm96/egm96.html', Vertical_Datum='WGS84'
1834
+ #
1835
+ # Timbuktu GeoidKarney('egm96-5.pgm').height(16.775833, -3.009444): 28.7068 vs 28.7067
1836
+ # Timbuktu GeoidKarney('egm96-5.pgm').height(16.776, -3.009): 28.7067 vs 28.7067
1837
+
1838
+
1839
+ # % python3 -m pygeodesy.geoids -Karney ../testGeoids/egm*.pgm
1840
+ #
1841
+ # GeoidKarney('egm2008-1.pgm'): lowerleft(-90.0, -180.0, -30.15), upperright(90.0, 180.0, 14.898), center(0.0, 0.0, 17.226), highest(-8.4, 147.367, 85.839), lowest(4.7, 78.767, -106.911)
1842
+ #
1843
+ # _PGM('../testGeoids/egm2008-1.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-31 06:54:00', Description='WGS84 EGM2008, 1-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.025, MaxCubicError=0.003, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.001, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008', Vertical_Datum='WGS84'
1844
+ #
1845
+ # Timbuktu GeoidKarney('egm2008-1.pgm').height(16.775833, -3.009444): 28.7881 vs 28.7880
1846
+ # Timbuktu GeoidKarney('egm2008-1.pgm').height(16.776, -3.009): 28.7880 vs 28.7880
1847
+ #
1848
+ # GeoidKarney('egm84-15.pgm'): lowerleft(-90.0, -180.0, -29.712), upperright(90.0, 180.0, 13.098), center(0.0, 0.0, 18.33), highest(-4.5, 148.75, 81.33), lowest(4.75, 79.25, -107.34)
1849
+ #
1850
+ # _PGM('../testGeoids/egm84-15.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-29 18:45:02', Description='WGS84 EGM84, 15-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.413, MaxCubicError=0.02, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.018, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/wgs84_180/wgs84_180.html', Vertical_Datum='WGS84'
1851
+ #
1852
+ # Timbuktu GeoidKarney('egm84-15.pgm').height(16.775833, -3.009444): 31.2983 vs 31.2979
1853
+ # Timbuktu GeoidKarney('egm84-15.pgm').height(16.776, -3.009): 31.2979 vs 31.2979
1854
+ #
1855
+ # GeoidKarney('egm96-5.pgm'): lowerleft(-90.0, -180.0, -29.535), upperright(90.0, 180.0, 13.605), center(0.0, 0.0, 17.163), highest(-8.167, 147.25, 85.422), lowest(4.667, 78.833, -107.043)
1856
+ #
1857
+ # _PGM('../testGeoids/egm96-5.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-29 18:45:03', Description='WGS84 EGM96, 5-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.14, MaxCubicError=0.003, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.005, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm96/egm96.html', Vertical_Datum='WGS84'
1858
+ #
1859
+ # Timbuktu GeoidKarney('egm96-5.pgm').height(16.775833, -3.009444): 28.7068 vs 28.7067
1860
+ # Timbuktu GeoidKarney('egm96-5.pgm').height(16.776, -3.009): 28.7067 vs 28.7067
1861
+
1862
+
1863
+ # % python2 -m pygeodesy.geoids -PGM ../testGeoids/egm*.pgm
1864
+ #
1865
+ # GeoidPGM('egm2008-1.pgm'): lowerleft(-90.0, -180.0, -30.15), upperright(90.0, 180.0, 14.898), center(0.0, 0.0, 17.226), highest(-8.4, -32.633, 85.839), lowest(4.683, -101.25, -106.911)
1866
+ #
1867
+ # _PGM('../testGeoids/egm2008-1.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-31 06:54:00', Description='WGS84 EGM2008, 1-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.025, MaxCubicError=0.003, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.001, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008', Vertical_Datum='WGS84'
1868
+ #
1869
+ # Timbuktu GeoidPGM('egm2008-1.pgm').height(16.775833, -3.009444): 28.7881 vs 28.7880
1870
+ # Timbuktu GeoidPGM('egm2008-1.pgm').height(16.776, -3.009): 28.7880 vs 28.7880
1871
+ #
1872
+ # GeoidPGM('egm84-15.pgm'): lowerleft(-90.0, -180.0, -29.712), upperright(90.0, 180.0, 13.098), center(0.0, 0.0, 18.33), highest(-4.5, -31.25, 81.33), lowest(4.75, -100.75, -107.34)
1873
+ #
1874
+ # _PGM('../testGeoids/egm84-15.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-29 18:45:02', Description='WGS84 EGM84, 15-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.413, MaxCubicError=0.02, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.018, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/wgs84_180/wgs84_180.html', Vertical_Datum='WGS84'
1875
+ #
1876
+ # Timbuktu GeoidPGM('egm84-15.pgm').height(16.775833, -3.009444): 31.2979 vs 31.2979
1877
+ # Timbuktu GeoidPGM('egm84-15.pgm').height(16.776, -3.009): 31.2975 vs 31.2979
1878
+ #
1879
+ # GeoidPGM('egm96-5.pgm'): lowerleft(-90.0, -180.0, -29.535), upperright(90.0, 180.0, 13.605), center(0.0, -0.0, 17.179), highest(-8.167, -32.75, 85.422), lowest(4.667, -101.167, -107.043)
1880
+ #
1881
+ # _PGM('../testGeoids/egm96-5.pgm'): AREA_OR_POINT='Point', DateTime='2009-08-29 18:45:03', Description='WGS84 EGM96, 5-minute grid', Geoid='file in PGM format for the GeographicLib::Geoid class', MaxBilinearError=0.14, MaxCubicError=0.003, Offset=-108.0, Origin=LatLon2Tuple(lat=90.0, lon=0.0), Pixel=65535, RMSBilinearError=0.005, RMSCubicError=0.001, Scale=0.003, URL='http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm96/egm96.html', Vertical_Datum='WGS84'
1882
+ #
1883
+ # Timbuktu GeoidPGM('egm96-5.pgm').height(16.775833, -3.009444): 28.7065 vs 28.7067
1884
+ # Timbuktu GeoidPGM('egm96-5.pgm').height(16.776, -3.009): 28.7064 vs 28.7067