Coverage for pygeodesy/datums.py: 94%

251 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-29 12:40 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Datums and transformations thereof. 

5 

6Classes L{Datum} and L{Transform} and registries L{Datums} and L{Transforms}, respectively. 

7 

8Pure Python implementation of geodesy tools for ellipsoidal earth models, including datums 

9and ellipsoid parameters for different geographic coordinate systems and methods for 

10converting between them and to cartesian coordinates. Transcoded from JavaScript originals by 

11I{(C) Chris Veness 2005-2024} and published under the same MIT Licence**, see U{latlon-ellipsoidal.js 

12<https://www.Movable-Type.co.UK/scripts/geodesy/docs/latlon-ellipsoidal.js.html>}. 

13 

14Historical geodetic datums: a latitude/longitude point defines a geographic location on, above 

15or below the earth’s surface. Latitude is measured in degrees from the equator, lomgitude from 

16the International Reference Meridian and height in meters above an ellipsoid based on the given 

17datum. The datum in turn is based on a reference ellipsoid and tied to geodetic survey 

18reference points. 

19 

20Modern geodesy is generally based on the WGS84 datum (as used for instance by GPS systems), but 

21previously various other reference ellipsoids and datum references were used. 

22 

23The UK Ordnance Survey National Grid References are still based on the otherwise historical OSGB36 

24datum, q.v. U{"A Guide to Coordinate Systems in Great Britain", Section 6 

25<https://www.OrdnanceSurvey.co.UK/docs/support/guide-coordinate-systems-great-britain.pdf>}. 

26 

27@var Datums.BD72: Datum(name='BD72', ellipsoid=Ellipsoids.Intl1924, transform=Transforms.BD72) 

28@var Datums.DHDN: Datum(name='DHDN', ellipsoid=Ellipsoids.Bessel1841, transform=Transforms.DHDN) 

29@var Datums.ED50: Datum(name='ED50', ellipsoid=Ellipsoids.Intl1924, transform=Transforms.ED50) 

30@var Datums.GDA2020: Datum(name='GDA2020', ellipsoid=Ellipsoids.GRS80, transform=Transforms.WGS84) 

31@var Datums.GRS80: Datum(name='GRS80', ellipsoid=Ellipsoids.GRS80, transform=Transforms.WGS84) 

32@var Datums.Irl1975: Datum(name='Irl1975', ellipsoid=Ellipsoids.AiryModified, transform=Transforms.Irl1975) 

33@var Datums.Krassovski1940: Datum(name='Krassovski1940', ellipsoid=Ellipsoids.Krassovski1940, transform=Transforms.Krassovski1940) 

34@var Datums.Krassowsky1940: Datum(name='Krassowsky1940', ellipsoid=Ellipsoids.Krassowsky1940, transform=Transforms.Krassowsky1940) 

35@var Datums.MGI: Datum(name='MGI', ellipsoid=Ellipsoids.Bessel1841, transform=Transforms.MGI) 

36@var Datums.NAD27: Datum(name='NAD27', ellipsoid=Ellipsoids.Clarke1866, transform=Transforms.NAD27) 

37@var Datums.NAD83: Datum(name='NAD83', ellipsoid=Ellipsoids.GRS80, transform=Transforms.NAD83) 

38@var Datums.NTF: Datum(name='NTF', ellipsoid=Ellipsoids.Clarke1880IGN, transform=Transforms.NTF) 

39@var Datums.OSGB36: Datum(name='OSGB36', ellipsoid=Ellipsoids.Airy1830, transform=Transforms.OSGB36) 

40@var Datums.Potsdam: Datum(name='Potsdam', ellipsoid=Ellipsoids.Bessel1841, transform=Transforms.Bessel1841) 

41@var Datums.Sphere: Datum(name='Sphere', ellipsoid=Ellipsoids.Sphere, transform=Transforms.WGS84) 

42@var Datums.TokyoJapan: Datum(name='TokyoJapan', ellipsoid=Ellipsoids.Bessel1841, transform=Transforms.TokyoJapan) 

43@var Datums.WGS72: Datum(name='WGS72', ellipsoid=Ellipsoids.WGS72, transform=Transforms.WGS72) 

44@var Datums.WGS84: Datum(name='WGS84', ellipsoid=Ellipsoids.WGS84, transform=Transforms.WGS84) 

45 

46@var Transforms.BD72: Transform(name='BD72', tx=106.87, ty=-52.298, tz=103.72, s1=1.0, rx=-1.6317e-06, ry=-2.2154e-06, rz=-8.9311e-06, s=1.2727, sx=-0.33657, sy=-0.45696, sz=-1.8422) 

47@var Transforms.Bessel1841: Transform(name='Bessel1841', tx=-582, ty=-105, tz=-414, s1=0.99999, rx=-5.0421e-06, ry=-1.6968e-06, rz=1.4932e-05, s=-8.3, sx=-1.04, sy=-0.35, sz=3.08) 

48@var Transforms.Clarke1866: Transform(name='Clarke1866', tx=8.0, ty=-160, tz=-176, s1=1.0, rx=0.0, ry=0.0, rz=0.0, s=0.0, sx=0.0, sy=0.0, sz=0.0) 

49@var Transforms.DHDN: Transform(name='DHDN', tx=-591.28, ty=-81.35, tz=-396.39, s1=0.99999, rx=7.1607e-06, ry=-3.5682e-07, rz=-7.0686e-06, s=-9.82, sx=1.477, sy=-0.0736, sz=-1.458) 

50@var Transforms.DHDNE: Transform(name='DHDNE', tx=-612.4, ty=-77, tz=-440.2, s1=1.0, rx=2.618e-07, ry=-2.7634e-07, rz=1.356e-05, s=-2.55, sx=0.054, sy=-0.057, sz=2.797) 

51@var Transforms.DHDNW: Transform(name='DHDNW', tx=-598.1, ty=-73.7, tz=-418.2, s1=0.99999, rx=-9.7932e-07, ry=-2.1817e-07, rz=1.1902e-05, s=-6.7, sx=-0.202, sy=-0.045, sz=2.455) 

52@var Transforms.ED50: Transform(name='ED50', tx=89.5, ty=93.8, tz=123.1, s1=1.0, rx=0.0, ry=0.0, rz=7.5631e-07, s=-1.2, sx=0.0, sy=0.0, sz=0.156) 

53@var Transforms.Identity: Transform(name='Identity', tx=0.0, ty=0.0, tz=0.0, s1=1.0, rx=0.0, ry=0.0, rz=0.0, s=0.0, sx=0.0, sy=0.0, sz=0.0) 

54@var Transforms.Irl1965: Transform(name='Irl1965', tx=-482.53, ty=130.6, tz=-564.56, s1=0.99999, rx=5.0518e-06, ry=1.0375e-06, rz=3.0592e-06, s=-8.15, sx=1.042, sy=0.214, sz=0.631) 

55@var Transforms.Irl1975: Transform(name='Irl1975', tx=-482.53, ty=130.6, tz=-564.56, s1=0.99999, rx=5.0518e-06, ry=1.0375e-06, rz=3.0592e-06, s=-8.15, sx=1.042, sy=0.214, sz=0.631) 

56@var Transforms.Krassovski1940: Transform(name='Krassovski1940', tx=-24, ty=123.0, tz=94.0, s1=1.0, rx=-9.6963e-08, ry=1.2605e-06, rz=6.3026e-07, s=-2.423, sx=-0.02, sy=0.26, sz=0.13) 

57@var Transforms.Krassowsky1940: Transform(name='Krassowsky1940', tx=-24, ty=123.0, tz=94.0, s1=1.0, rx=-9.6963e-08, ry=1.2605e-06, rz=6.3026e-07, s=-2.423, sx=-0.02, sy=0.26, sz=0.13) 

58@var Transforms.MGI: Transform(name='MGI', tx=-577.33, ty=-90.129, tz=-463.92, s1=1.0, rx=2.4905e-05, ry=7.1462e-06, rz=2.5681e-05, s=-2.423, sx=5.137, sy=1.474, sz=5.297) 

59@var Transforms.NAD27: Transform(name='NAD27', tx=8.0, ty=-160, tz=-176, s1=1.0, rx=0.0, ry=0.0, rz=0.0, s=0.0, sx=0.0, sy=0.0, sz=0.0) 

60@var Transforms.NAD83: Transform(name='NAD83', tx=1.004, ty=-1.91, tz=-0.515, s1=1.0, rx=1.2945e-07, ry=1.6484e-09, rz=5.333e-08, s=-0.0015, sx=0.0267, sy=0.00034, sz=0.011) 

61@var Transforms.NTF: Transform(name='NTF', tx=-168, ty=-60, tz=320.0, s1=1.0, rx=0.0, ry=0.0, rz=0.0, s=0.0, sx=0.0, sy=0.0, sz=0.0) 

62@var Transforms.OSGB36: Transform(name='OSGB36', tx=-446.45, ty=125.16, tz=-542.06, s1=1.0, rx=-7.2819e-07, ry=-1.1975e-06, rz=-4.0826e-06, s=20.489, sx=-0.1502, sy=-0.247, sz=-0.8421) 

63@var Transforms.TokyoJapan: Transform(name='TokyoJapan', tx=148.0, ty=-507, tz=-685, s1=1.0, rx=0.0, ry=0.0, rz=0.0, s=0.0, sx=0.0, sy=0.0, sz=0.0) 

64@var Transforms.WGS72: Transform(name='WGS72', tx=0.0, ty=0.0, tz=-4.5, s1=1.0, rx=0.0, ry=0.0, rz=2.6859e-06, s=-0.22, sx=0.0, sy=0.0, sz=0.554) 

65@var Transforms.WGS84: Transform(name='WGS84', tx=0.0, ty=0.0, tz=0.0, s1=1.0, rx=0.0, ry=0.0, rz=0.0, s=0.0, sx=0.0, sy=0.0, sz=0.0) 

66''' 

67# make sure int/int division yields float quotient, see .basics 

68from __future__ import division as _; del _ # noqa: E702 ; 

69 

70from pygeodesy.basics import _isin, islistuple, map2, neg, _xinstanceof, _zip 

71from pygeodesy.constants import R_M, _float as _F, _0_0, _1_0, _2_0, _8_0, _3600_0 

72# from pygeodesy.ellipsoidalBase import CartesianEllipsoidalBase as _CEB, \ 

73# LatLonEllipsoidalBase as _LLEB # MODS 

74from pygeodesy.ellipsoids import a_f2Tuple, Ellipsoid, Ellipsoid2, Ellipsoids, _EWGS84, \ 

75 Vector3Tuple 

76from pygeodesy.errors import _IsnotError, _TypeError, _xellipsoidall, _xkwds, _xkwds_pop2 

77from pygeodesy.fmath import fdot, fmean, Fmt, _operator 

78from pygeodesy.internals import _passarg, _under 

79from pygeodesy.interns import NN, _a_, _Airy1830_, _AiryModified_, _BAR_, _Bessel1841_, \ 

80 _Clarke1866_, _Clarke1880IGN_, _COMMASPACE_, _DMAIN_,_DOT_, \ 

81 _earth_, _ellipsoid_, _ellipsoidal_, _GRS80_, _Intl1924_, \ 

82 _MINUS_, _Krassovski1940_, _Krassowsky1940_, _NAD27_, _NAD83_, \ 

83 _s_, _PLUS_, _Sphere_, _spherical_, _transform_, _UNDER_, \ 

84 _WGS72_, _WGS84_ 

85from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

86from pygeodesy.named import _lazyNamedEnumItem as _lazy, _name__, _name2__, _NamedEnum, \ 

87 _NamedEnumItem 

88# from pygeodesy.namedTuples import Vector3Tuple # from .ellipsoids 

89from pygeodesy.props import Property_RO, property_RO 

90# from pygeodesy.streprs import Fmt # from .fmath 

91from pygeodesy.units import _isRadius, Radius_, radians 

92 

93# from math import radians # from .units 

94# import operator as _operator # from .fmath 

95 

96__all__ = _ALL_LAZY.datums 

97__version__ = '25.05.12' 

98 

99_a_ellipsoid_ = _UNDER_(_a_, _ellipsoid_) 

100_BD72_ = 'BD72' 

101_DHDN_ = 'DHDN' 

102_DHDNE_ = 'DHDNE' 

103_DHDNW_ = 'DHDNW' 

104_ED50_ = 'ED50' 

105_GDA2020_ = 'GDA2020' # in .trf 

106_Identity_ = 'Identity' 

107_Irl1965_ = 'Irl1965' 

108_Irl1975_ = 'Irl1975' 

109_MGI_ = 'MGI' 

110_Names7 = 'tx', 'ty', 'tz', _s_, 'sx', 'sy', 'sz' # in .trf 

111_Names11 = _Names7[:3] + ('s1', 'rx', 'ry', 'rz') + _Names7[3:] 

112_NTF_ = 'NTF' 

113_OSGB36_ = 'OSGB36' 

114_Potsdam_ = 'Potsdam' 

115_RPS = radians(_1_0 / _3600_0) # radians per arc-second 

116_S1_S = 1.e-6 # in .trf 

117_TokyoJapan_ = 'TokyoJapan' 

118 

119 

120class Transform(_NamedEnumItem): 

121 '''Helmert I{datum} transformation. 

122 

123 @see: L{TransformXform<trf.TransformXform>}. 

124 ''' 

125 tx = _0_0 # x translation (C{meter}) 

126 ty = _0_0 # y translation (C{meter}) 

127 tz = _0_0 # z translation (C{meter}) 

128 

129 rx = _0_0 # x rotation (C{radians}) 

130 ry = _0_0 # y rotation (C{radians}) 

131 rz = _0_0 # z rotation (C{radians}) 

132 

133 s = _0_0 # scale ppm (C{float}) 

134 s1 = _1_0 # scale + 1 (C{float}) 

135 

136 sx = _0_0 # x rotation (C{arc-seconds}) 

137 sy = _0_0 # y rotation (C{arc-seconds}) 

138 sz = _0_0 # z rotation (C{arc-seconds}) 

139 

140 def __init__(self, name=NN, tx=0, ty=0, tz=0, 

141 s=0, sx=0, sy=0, sz=0): 

142 '''New L{Transform}. 

143 

144 @kwarg name: Optional, unique name (C{str}). 

145 @kwarg tx: X translation (C{meter}). 

146 @kwarg ty: Y translation (C{meter}). 

147 @kwarg tz: Z translation (C{meter}). 

148 @kwarg s: Scale (C{float}), ppm. 

149 @kwarg sx: X rotation (C{arc-seconds}). 

150 @kwarg sy: Y rotation (C{arc-seconds}). 

151 @kwarg sz: Z rotation (C{arc-seconds}). 

152 

153 @raise NameError: Transform with that B{C{name}} already exists. 

154 ''' 

155 if tx: 

156 self.tx = tx 

157 if ty: 

158 self.ty = ty 

159 if tz: 

160 self.tz = tz 

161 if s: 

162 self.s = s 

163 self.s1 = _F(s * _S1_S + _1_0) # normalize ppM to (s + 1) 

164 if sx: # secs to rads 

165 self.rx, self.sx = self._rps2(sx) 

166 if sy: 

167 self.ry, self.sy = self._rps2(sy) 

168 if sz: 

169 self.rz, self.sz = self._rps2(sz) 

170 

171 self._register(Transforms, name) 

172 

173 def __eq__(self, other): 

174 '''Compare this and an other transform. 

175 

176 @arg other: The other transform (L{Transform}). 

177 

178 @return: C{True} if equal, C{False} otherwise. 

179 ''' 

180 return self is other or (isinstance(other, Transform) 

181 and _equall(other, self)) 

182 

183 def __hash__(self): 

184 return hash(tuple(self)) 

185 

186 def __iter__(self): 

187 '''Yield the initial attribute values, I{in order}. 

188 ''' 

189 for n in _Names7: 

190 yield getattr(self, n) 

191 

192 def __matmul__(self, point): # PYCHOK Python 3.5+ 

193 '''Transform an I{ellipsoidal} B{C{point}} with this Helmert. 

194 

195 @return: A transformed copy of B{C{point}}. 

196 

197 @raise TypeError: Invalid B{C{point}}. 

198 

199 @see: Method C{B{point}.toTransform}. 

200 ''' 

201 _ = _xellipsoidall(point) 

202 return point.toTransform(self) 

203 

204 def __neg__(self): 

205 return self.inverse() 

206 

207 def inverse(self, **name): 

208 '''Return the inverse of this transform. 

209 

210 @kwarg name: Optional, unique name (C{str}). 

211 

212 @return: Inverse (L{Transform}), unregistered. 

213 ''' 

214 r = type(self)(**dict(self.items(inverse=True))) 

215 n = _name__(**name) or _negastr(self.name) 

216 if n: 

217 r.name = n # unregistered 

218 return r 

219 

220 @Property_RO 

221 def isunity(self): 

222 '''Is this a C{unity, identity} transform (C{bool}), like 

223 WGS84 with translation, scale and rotation all zero? 

224 ''' 

225 return not any(self) 

226 

227 def items(self, inverse=False): 

228 '''Yield the initial attributes, each as 2-tuple C{(name, value)}. 

229 

230 @kwarg inverse: If C{True}, negate the values (C{bool}). 

231 ''' 

232 _p = neg if inverse else _passarg 

233 for n, x in _zip(_Names7, self): 

234 yield n, _p(x) 

235 

236 def _rps2(self, s_): 

237 '''(INTERNAL) Rotation in C{radians} and C{arc-seconds}. 

238 ''' 

239 # _MR == _RPS * 1.e-3 # radians per milli-arc-second, equ (2) 

240 # <https://www.NGS.NOAA.gov/CORS/Articles/SolerSnayASCE.pdf> 

241 return (_RPS * s_), s_ 

242 

243 def _s_s1(self, s1): # in .trf 

244 '''(INTERNAL) Set C{s1} and C{s}. 

245 ''' 

246 Transform.isunity._update(self) 

247 self.s1 = s1 

248 self.s = s = (s1 - _1_0) / _S1_S 

249 return s 

250 

251 def toStr(self, prec=5, fmt=Fmt.g, **sep_name): # PYCHOK expected 

252 '''Return this transform as a string. 

253 

254 @kwarg prec: Number of (decimal) digits, unstripped (C{int}). 

255 @kwarg fmt: Optional C{float} format (C{letter}). 

256 @kwarg sep_name: Optional C{B{name}=NN} (C{str}) or C{None} 

257 to exclude this transform's name and separater 

258 C{B{sep}=", "} to join the items (C{str}). 

259 

260 @return: Transform attributes (C{str}). 

261 ''' 

262 return self._instr(*_Names11, fmt=fmt, prec=prec, **sep_name) 

263 

264 def transform(self, x, y, z, inverse=False, **Vector_and_kwds): 

265 '''Transform a (cartesian) position, forward or inverse. 

266 

267 @arg x: X coordinate (C{meter}). 

268 @arg y: Y coordinate (C{meter}). 

269 @arg z: Z coordinate (C{meter}). 

270 @kwarg inverse: If C{True}, apply the inverse transform (C{bool}). 

271 @kwarg Vector_and_kwds: An optional, (3-D) C{B{Vector}=None} or 

272 cartesian class and additional C{B{Vector}} keyword 

273 arguments to return the transformed position. 

274 

275 @return: The transformed position (L{Vector3Tuple}C{(x, y, z)}) 

276 unless some B{C{Vector_and_kwds}} are specified. 

277 ''' 

278 if self.isunity: 

279 r = Vector3Tuple(x, y, z, name=self.name) # == inverse 

280 else: 

281 xyz1 = x, y, z, _1_0 

282 s1 = self.s1 

283 if inverse: 

284 xyz1 = map2(neg, xyz1) 

285 s1 -= _2_0 # = s * 1e-6 - 1 = (s1 - 1) - 1 

286 # x', y', z' = (x * .s1 - y * .rz + z * .ry + .tx, 

287 # x * .rz + y * .s1 - z * .rx + .ty, 

288 # -x * .ry + y * .rx + z * .s1 + .tz) 

289 r = Vector3Tuple(fdot(xyz1, s1, -self.rz, self.ry, self.tx), 

290 fdot(xyz1, self.rz, s1, -self.rx, self.ty), 

291 fdot(xyz1, -self.ry, self.rx, s1, self.tz), 

292 name=self.name) 

293 if Vector_and_kwds: 

294 V, kwds = _xkwds_pop2(Vector_and_kwds, Vector=None) 

295 if V: 

296 r = V(r, **_xkwds(kwds, name=self.name)) 

297 return r 

298 

299 

300class Transforms(_NamedEnum): 

301 '''(INTERNAL) L{Transform} registry, I{must} be a sub-class 

302 to accommodate the L{_LazyNamedEnumItem} properties. 

303 ''' 

304 def _Lazy(self, **name_tx_ty_tz_s_sx_sy_sz): 

305 '''(INTERNAL) Instantiate the C{Transform}. 

306 ''' 

307 return Transform(**name_tx_ty_tz_s_sx_sy_sz) 

308 

309Transforms = Transforms(Transform) # PYCHOK singleton 

310'''Some pre-defined L{Transform}s, all I{lazily} instantiated.''' 

311# <https://WikiPedia.org/wiki/Helmert_transformation> from WGS84 to ... 

312Transforms._assert( 

313 BD72 = _lazy(_BD72_, tx=_F(106.868628), ty=_F(-52.297783), tz=_F(103.723893), s=_F(1.2727), 

314 # <https://www.NGI.Be/FR/FR4-4.shtm> ETRS89 == WG84 

315 # <https://EPSG.org/transformation_15929/BD72-to-WGS-84-3.html> 

316 sx=_F( -0.33657), sy=_F( -0.456955), sz=_F( -1.84218)), 

317 

318 Bessel1841 = _lazy(_Bessel1841_, tx=_F(-582.0), ty=_F(-105.0), tz=_F(-414.0), s=_F(-8.3), 

319 sx=_F( -1.04), sy=_F( -0.35), sz=_F( 3.08)), 

320 

321 Clarke1866 = _lazy(_Clarke1866_, tx=_F(8), ty=_F(-160), tz=_F(-176)), 

322 

323 DHDN = _lazy(_DHDN_, tx=_F(-591.28), ty=_F(-81.35), tz=_F(-396.39), s=_F(-9.82), 

324 sx=_F( 1.477), sy=_F( -0.0736), sz=_F( -1.458)), # Germany 

325 

326 DHDNE = _lazy(_DHDNE_, tx=_F(-612.4), ty=_F(-77.0), tz=_F(-440.2), s=_F(-2.55), 

327 # <https://EPSG.org/transformation_15869/DHDN-to-WGS-84-3.html> 

328 sx=_F( 0.054), sy=_F( -0.057), sz=_F( 2.797)), # East Germany 

329 

330 DHDNW = _lazy(_DHDNW_, tx=_F(-598.1), ty=_F(-73.7), tz=_F(-418.2), s=_F(-6.7), 

331 # <https://EPSG.org/transformation_1777/DHDN-to-WGS-84-2.html> 

332 sx=_F( -0.202), sy=_F( -0.045), sz=_F( 2.455)), # West Germany 

333 

334 ED50 = _lazy(_ED50_, tx=_F(89.5), ty=_F(93.8), tz=_F(123.1), s=_F(-1.2), 

335 # <https://GeoNet.ESRI.com/thread/36583> sz=_F(-0.156) 

336 # <https://GitHub.com/ChrisVeness/geodesy/blob/master/latlon-ellipsoidal.js> 

337 # <https://www.Gov.UK/guidance/oil-and-gas-petroleum-operations-notices#pon-4> 

338 sz=_F( 0.156)), 

339 Identity = _lazy(_Identity_), 

340 

341 Irl1965 = _lazy(_Irl1965_, tx=_F(-482.530), ty=_F(130.596), tz=_F(-564.557), s=_F(-8.15), 

342 # <https://EPSG.org/transformation_1641/TM65-to-WGS-84-2.html> 

343 sx=_F( 1.042), sy=_F( 0.214), sz=_F( 0.631)), 

344 Irl1975 = _lazy(_Irl1975_, tx=_F(-482.530), ty=_F(130.596), tz=_F(-564.557), s=_F(-8.15), 

345 # <https://EPSG.org/transformation_1954/TM75-to-WGS-84-2.html> 

346 sx=_F( 1.042), sy=_F( 0.214), sz=_F( 0.631)), 

347 

348 Krassovski1940 = _lazy(_Krassovski1940_, tx=_F(-24.0), ty=_F(123.0), tz=_F(94.0), s=_F(-2.423), 

349 sx=_F( -0.02), sy=_F( 0.26), sz=_F( 0.13)), # spelling 

350 

351 Krassowsky1940 = _lazy(_Krassowsky1940_, tx=_F(-24.0), ty=_F(123.0), tz=_F(94.0), s=_F(-2.423), 

352 sx=_F( -0.02), sy=_F( 0.26), sz=_F( 0.13)), # spelling 

353 

354 MGI = _lazy(_MGI_, tx=_F(-577.326), ty=_F(-90.129), tz=_F(-463.920), s=_F(-2.423), 

355 sx=_F( 5.137), sy=_F( 1.474), sz=_F( 5.297)), # Austria 

356 

357 NAD27 = _lazy(_NAD27_, tx=_8_0, ty=_F(-160), tz=_F(-176)), 

358 

359 NAD83 = _lazy(_NAD83_, tx=_F(1.004), ty=_F(-1.910), tz=_F(-0.515), s=_F(-0.0015), 

360 sx=_F(0.0267), sy=_F( 0.00034), sz=_F( 0.011)), 

361 

362 NTF = _lazy(_NTF_, tx=_F(-168), ty=_F(-60), tz=_F(320)), # XXX verify 

363 

364 OSGB36 = _lazy(_OSGB36_, tx=_F(-446.448), ty=_F(125.157), tz=_F(-542.060), s=_F(20.4894), 

365 # <https://EPSG.org/transformation_1314/OSGB36-to-WGS-84-6.html> 

366 sx=_F( -0.1502), sy=_F( -0.2470), sz=_F( -0.8421)), 

367 

368 TokyoJapan = _lazy(_TokyoJapan_, tx=_F(148), ty=_F(-507), tz=_F(-685)), 

369 

370 WGS72 = _lazy(_WGS72_, tz=_F(-4.5), s=_F(-0.22), sz=_F(0.554)), 

371 

372 WGS84 = _lazy(_WGS84_), # unity 

373) 

374 

375 

376class Datum(_NamedEnumItem): 

377 '''Ellipsoid and transform parameters for an earth model. 

378 ''' 

379 _ellipsoid = Ellipsoids.WGS84 # default ellipsoid (L{Ellipsoid}, L{Ellipsoid2}) 

380 _transform = Transforms.WGS84 # default transform (L{Transform}) 

381 

382 def __init__(self, ellipsoid, transform=None, **name): 

383 '''New L{Datum}. 

384 

385 @arg ellipsoid: The ellipsoid (L{Ellipsoid} or L{Ellipsoid2}). 

386 @kwarg transform: Optional transform (L{Transform}). 

387 @kwarg name: Optional, unique C{B{name}=NN} (C{str}). 

388 

389 @raise NameError: Datum with that B{C{name}} already exists. 

390 

391 @raise TypeError: If B{C{ellipsoid}} is not an L{Ellipsoid} 

392 nor L{Ellipsoid2} or B{C{transform}} is 

393 not a L{Transform}. 

394 ''' 

395 self._ellipsoid = ellipsoid or Datum._ellipsoid 

396 _xinstanceof(Ellipsoid, ellipsoid=self.ellipsoid) 

397 

398 self._transform = transform or Datum._transform 

399 _xinstanceof(Transform, transform=self.transform) 

400 

401 self._register(Datums, _name__(name) or self.transform.name 

402 or self.ellipsoid.name) 

403 

404 def __eq__(self, other): 

405 '''Compare this and an other datum. 

406 

407 @arg other: The other datum (L{Datum}). 

408 

409 @return: C{True} if equal, C{False} otherwise. 

410 ''' 

411 return self is other or (isinstance(other, Datum) and 

412 self.ellipsoid == other.ellipsoid and 

413 self.transform == other.transform) 

414 

415 def __hash__(self): 

416 return self._hash # memoized 

417 

418 def __matmul__(self, point): # PYCHOK Python 3.5+ 

419 '''Convert an I{ellipsoidal} B{C{point}} to this datum. 

420 

421 @raise TypeError: Invalid B{C{point}}. 

422 ''' 

423 _ = _xellipsoidall(point) 

424 return point.toDatum(self) 

425 

426 def ecef(self, Ecef=None): 

427 '''Return U{ECEF<https://WikiPedia.org/wiki/ECEF>} converter. 

428 

429 @kwarg Ecef: ECEF class to use, default L{EcefKarney}. 

430 

431 @return: An ECEF converter for this C{datum}. 

432 

433 @raise TypeError: Invalid B{C{Ecef}}. 

434 

435 @see: Module L{pygeodesy.ecef}. 

436 ''' 

437 return _MODS.ecef._4Ecef(self, Ecef) 

438 

439 @Property_RO 

440 def ellipsoid(self): 

441 '''Get this datum's ellipsoid (L{Ellipsoid} or L{Ellipsoid2}). 

442 ''' 

443 return self._ellipsoid 

444 

445 @Property_RO 

446 def exactTM(self): 

447 '''Get the C{ExactTM} projection (L{ExactTransverseMercator}). 

448 ''' 

449 return _MODS.etm.ExactTransverseMercator(datum=self) 

450 

451 @Property_RO 

452 def _hash(self): 

453 return hash(self.ellipsoid) + hash(self.transform) 

454 

455 @property_RO 

456 def isEllipsoidal(self): 

457 '''Check whether this datum is ellipsoidal (C{bool}). 

458 ''' 

459 return self.ellipsoid.isEllipsoidal 

460 

461 @property_RO 

462 def isOblate(self): 

463 '''Check whether this datum's ellipsoidal is I{oblate} (C{bool}). 

464 ''' 

465 return self.ellipsoid.isOblate 

466 

467 @property_RO 

468 def isProlate(self): 

469 '''Check whether this datum's ellipsoidal is I{prolate} (C{bool}). 

470 ''' 

471 return self.ellipsoid.isProlate 

472 

473 @property_RO 

474 def isSpherical(self): 

475 '''Check whether this datum is (near-)spherical (C{bool}). 

476 ''' 

477 return self.ellipsoid.isSpherical 

478 

479 def toStr(self, sep=_COMMASPACE_, **name): # PYCHOK expected 

480 '''Return this datum as a string. 

481 

482 @kwarg sep: Separator to join (C{str}). 

483 @kwarg name: Optional, override C{B{name}=NN} (C{str}) or 

484 C{None} to exclude this datum's name. 

485 

486 @return: Datum attributes (C{str}). 

487 ''' 

488 name, _ = _name2__(**name) # name=None 

489 t = [] if name is None else \ 

490 [Fmt.EQUAL(name=repr(name or self.named))] 

491 for a in (_ellipsoid_, _transform_): 

492 v = getattr(self, a) 

493 t.append(NN(Fmt.EQUAL(a, v.classname), _s_, _DOT_, v.name)) 

494 return sep.join(t) 

495 

496 @Property_RO 

497 def transform(self): 

498 '''Get this datum's transform (L{Transform}). 

499 ''' 

500 return self._transform 

501 

502 

503def _earth_datum(inst, a_earth, f=None, raiser=_a_ellipsoid_, **name): # in .karney, .trf, ... 

504 '''(INTERNAL) Set C{inst._datum} from C{(B{a_..}, B{f})} or C{B{.._ellipsoid}} 

505 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum}, C{a_f2Tuple} or C{scalar} earth radius). 

506 

507 @note: C{B{raiser}='a_ellipsoid'} for backward naming compatibility. 

508 ''' 

509 if f is not None: 

510 E, n, D = _EnD3((a_earth, f), name) 

511 if raiser and not E: 

512 raise _TypeError(f=f, **{raiser: a_earth}) 

513 elif _isin(a_earth, None, _EWGS84, _WGS84) and inst._datum is _WGS84: 

514 return 

515 elif isinstance(a_earth, Datum): 

516 E, n, D = None, NN, a_earth 

517 else: 

518 E, n, D = _EnD3(a_earth, name) 

519 if raiser and not E: 

520 _xinstanceof(Ellipsoid, Ellipsoid2, a_f2Tuple, Datum, **{raiser: a_earth}) 

521 if D is None: 

522 D = Datum(E, transform=Transforms.Identity, name=_under(n)) 

523 inst._datum = D 

524 

525 

526def _earth_ellipsoid(earth, **name_raiser): 

527 '''(INTERAL) Return the ellipsoid for the given C{earth} model. 

528 ''' 

529 return Ellipsoids.Sphere if earth is R_M else ( 

530 _EWGS84 if earth is _EWGS84 or earth is _WGS84 else 

531 _spherical_datum(earth, **name_raiser).ellipsoid) 

532 

533 

534def _ED2(radius, name): 

535 '''(INTERNAL) Helper for C{_EnD3} and C{_spherical_datum}. 

536 ''' 

537 D = Datums.Sphere 

538 E = D.ellipsoid 

539 if name or radius != E.a: # != E.b 

540 n = _under(_name__(name, _or_nameof=D)) 

541 E = Ellipsoid(radius, radius, name=n) 

542 D = Datum(E, transform=Transforms.Identity, name=n) 

543 return E, D 

544 

545 

546def _ellipsoidal_datum(earth, Error=TypeError, raiser=NN, **name): 

547 '''(INTERNAL) Create a L{Datum} from an L{Ellipsoid} or L{Ellipsoid2}, 

548 C{a_f2Tuple}, 2-tuple or 2-list B{C{earth}} model. 

549 

550 @kwarg raiser: If not C{NN}, raise an B{C{Error}} if not ellipsoidal. 

551 ''' 

552 if isinstance(earth, Datum): 

553 D = earth 

554 else: 

555 E, n, D = _EnD3(earth, name) 

556 if not E: 

557 n = raiser or _earth_ 

558 _xinstanceof(Datum, Ellipsoid, Ellipsoid2, a_f2Tuple, **{n: earth}) 

559 if D is None: 

560 D = Datum(E, transform=Transforms.Identity, name=_under(n)) 

561 if raiser and not D.isEllipsoidal: 

562 raise _IsnotError(_ellipsoidal_, Error=Error, **{raiser: earth}) 

563 return D 

564 

565 

566def _EnD3(earth, name): 

567 '''(INTERNAL) Helper for C{_earth_datum} and C{_ellipsoidal_datum}. 

568 ''' 

569 D, n = None, _under(_name__(name, _or_nameof=earth)) 

570 if isinstance(earth, (Ellipsoid, Ellipsoid2)): 

571 E = earth 

572 elif isinstance(earth, Datum): 

573 E = earth.ellipsoid 

574 D = earth 

575 elif _isRadius(earth): 

576 E, D = _ED2(Radius_(earth), n) 

577 n = E.name 

578 elif isinstance(earth, a_f2Tuple): 

579 E = earth.ellipsoid(name=n) 

580 elif islistuple(earth, minum=2): 

581 E = Ellipsoids.Sphere 

582 a, f = earth[:2] 

583 if f or a != E.a: # != E.b 

584 E = Ellipsoid(a, f=f, name=n) 

585 else: 

586 n = E.name 

587 D = Datums.Sphere 

588 else: 

589 E, n = None, NN 

590 return E, n, D 

591 

592 

593def _equall(t1, t2): # in .trf 

594 '''(INTERNAL) Return L{Transform} C{t1 == t2}. 

595 ''' 

596 return all(map(_operator.eq, t1, t2)) 

597 

598 

599def _mean_radius(radius, *lats): 

600 '''(INTERNAL) Compute the mean radius of a L{Datum} from an L{Ellipsoid}, 

601 L{Ellipsoid2} or scalar earth C{radius} over several latitudes. 

602 ''' 

603 if radius is R_M: 

604 r = radius 

605 elif _isRadius(radius): 

606 r = Radius_(radius, low=0, Error=TypeError) 

607 else: 

608 E = _ellipsoidal_datum(radius).ellipsoid 

609 r = fmean(map(E.Rgeocentric, lats)) if lats else E.Rmean 

610 return r 

611 

612 

613def _negastr(name): # in .trf, test/testTrf 

614 '''(INTERNAL) Negate a C{Transform/-Xform} name. 

615 ''' 

616 b, m, p = _BAR_, _MINUS_, _PLUS_ 

617 n = name.replace(m, b).replace(p, m).replace(b, p) 

618 # as good and fast as (in Python 3+ only) ... 

619 # _MINUSxPLUS = str.maketrans({_MINUS_: _PLUS_, _PLUS_: _MINUS_}) 

620 # def _negastr(name): 

621 # n = name.translate(_MINUSxPLUS) 

622 # ... 

623 return n.lstrip(p) if n.startswith(p) else NN(m, n) 

624 

625 

626def _spherical_datum(earth, Error=TypeError, raiser=NN, **name): 

627 '''(INTERNAL) Create a L{Datum} from an L{Ellipsoid}, L{Ellipsoid2}, 

628 C{a_f2Tuple}, 2-tuple, 2-list B{C{earth}} model or C{scalar} radius. 

629 

630 @kwarg raiser: If not C{NN}, raise an B{C{Error}} if not spherical. 

631 ''' 

632 if isinstance(earth, Datum): 

633 D = earth 

634 elif _isRadius(earth): 

635 _, D = _ED2(Radius_(earth, Error=Error), name) 

636 else: 

637 D = _ellipsoidal_datum(earth, Error=Error, **name) 

638 if raiser and not D.isSpherical: 

639 raise _IsnotError(_spherical_, Error=Error, **{raiser: earth}) 

640 return D 

641 

642 

643class Datums(_NamedEnum): 

644 '''(INTERNAL) L{Datum} registry, I{must} be a sub-class 

645 to accommodate the L{_LazyNamedEnumItem} properties. 

646 ''' 

647 def _Lazy(self, ellipsoid_name, transform_name, **name): 

648 '''(INTERNAL) Instantiate the L{Datum}. 

649 ''' 

650 return Datum(Ellipsoids.get(ellipsoid_name), 

651 Transforms.get(transform_name), **name) 

652 

653Datums = Datums(Datum) # PYCHOK singleton 

654'''Some pre-defined L{Datum}s, all I{lazily} instantiated.''' 

655# Datums with associated ellipsoid and Helmert transform parameters 

656# to convert from WGS84 into the given datum. More are available at 

657# <https://Earth-Info.NGA.mil/GandG/coordsys/datums/NATO_DT.pdf> and 

658# <XXX://www.FieldenMaps.info/cconv/web/cconv_params.js>. 

659Datums._assert( 

660 # Belgian Datum 1972, based on Hayford ellipsoid. 

661 # <https://NL.WikiPedia.org/wiki/Belgian_Datum_1972> 

662 # <https://SpatialReference.org/ref/sr-org/7718/html/> 

663 BD72 = _lazy(_BD72_, _Intl1924_, _BD72_), 

664 

665 # Germany <https://WikiPedia.org/wiki/Bessel-Ellipsoid> 

666 # <https://WikiPedia.org/wiki/Helmert_transformation> 

667 DHDN = _lazy(_DHDN_, _Bessel1841_, _DHDN_), 

668 

669 # <https://www.Gov.UK/guidance/oil-and-gas-petroleum-operations-notices#pon-4> 

670 ED50 = _lazy(_ED50_, _Intl1924_, _ED50_), 

671 

672 # Australia <https://ICSM.Gov.AU/datum/gda2020-and-gda94-technical-manuals> 

673# ADG66 = _lazy(_ADG66_, _ANS_, _WGS84_), # XXX Transform? 

674# ADG84 = _lazy(_ADG84_, _ANS_, _WGS84_), # XXX Transform? 

675# GDA94 = _lazy(_GDA94_, _GRS80_, _WGS84_), 

676 GDA2020 = _lazy(_GDA2020_, _GRS80_, _WGS84_), # XXX Transform? 

677 

678 # <https://WikiPedia.org/wiki/GRS_80> 

679 GRS80 = _lazy(_GRS80_, _GRS80_, _WGS84_), 

680 

681 # <https://OSI.IE/wp-content/uploads/2015/05/transformations_booklet.pdf> Table 2 

682# Irl1975 = _lazy(_Irl1965_, _AiryModified_, _Irl1965_), 

683 Irl1975 = _lazy(_Irl1975_, _AiryModified_, _Irl1975_), 

684 

685 # Germany <https://WikiPedia.org/wiki/Helmert_transformation> 

686 Krassovski1940 = _lazy(_Krassovski1940_, _Krassovski1940_, _Krassovski1940_), # XXX spelling? 

687 Krassowsky1940 = _lazy(_Krassowsky1940_, _Krassowsky1940_, _Krassowsky1940_), # XXX spelling? 

688 

689 # Austria <https://DE.WikiPedia.org/wiki/Datum_Austria> 

690 MGI = _lazy(_MGI_, _Bessel1841_, _MGI_), 

691 

692 # <https://WikiPedia.org/wiki/Helmert_transformation> 

693 NAD27 = _lazy(_NAD27_, _Clarke1866_, _NAD27_), 

694 

695 # NAD83 (2009) == WGS84 - <https://www.UVM.edu/giv/resources/WGS84_NAD83.pdf> 

696 # (If you *really* must convert WGS84<->NAD83, you need more than this!) 

697 NAD83 = _lazy(_NAD83_, _GRS80_, _NAD83_), 

698 

699 # Nouvelle Triangulation Francaise (Paris) XXX verify 

700 NTF = _lazy(_NTF_, _Clarke1880IGN_, _NTF_), 

701 

702 # <https://www.OrdnanceSurvey.co.UK/docs/support/guide-coordinate-systems-great-britain.pdf> 

703 OSGB36 = _lazy(_OSGB36_, _Airy1830_, _OSGB36_), 

704 

705 # Germany <https://WikiPedia.org/wiki/Helmert_transformation> 

706 Potsdam = _lazy(_Potsdam_, _Bessel1841_, _Bessel1841_), 

707 

708 # XXX psuedo-ellipsoids for spherical LatLon 

709 Sphere = _lazy(_Sphere_, _Sphere_, _WGS84_), 

710 

711 # <https://www.GeoCachingToolbox.com?page=datumEllipsoidDetails> 

712 TokyoJapan = _lazy(_TokyoJapan_, _Bessel1841_, _TokyoJapan_), 

713 

714 # <https://www.ICAO.int/safety/pbn/documentation/eurocontrol/eurocontrol%20wgs%2084%20implementation%20manual.pdf> 

715 WGS72 = _lazy(_WGS72_, _WGS72_, _WGS72_), 

716 

717 WGS84 = _lazy(_WGS84_, _WGS84_, _WGS84_), 

718) 

719 

720_WGS84 = Datums.WGS84 

721assert _WGS84.ellipsoid is _EWGS84 

722# assert _WGS84.transform.isunity 

723 

724if __name__ == _DMAIN_: 

725 

726 from pygeodesy.interns import _COMMA_, _NL_, _NLATvar_ 

727 from pygeodesy import printf 

728 

729 # __doc__ of this file, force all into registery 

730 for r in (Datums, Transforms): 

731 t = [NN] + r.toRepr(all=True, asorted=True).split(_NL_) 

732 printf(_NLATvar_.join(i.strip(_COMMA_) for i in t)) 

733 

734# **) MIT License 

735# 

736# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

737# 

738# Permission is hereby granted, free of charge, to any person obtaining a 

739# copy of this software and associated documentation files (the "Software"), 

740# to deal in the Software without restriction, including without limitation 

741# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

742# and/or sell copies of the Software, and to permit persons to whom the 

743# Software is furnished to do so, subject to the following conditions: 

744# 

745# The above copyright notice and this permission notice shall be included 

746# in all copies or substantial portions of the Software. 

747# 

748# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

749# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

750# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

751# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

752# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

753# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

754# OTHER DEALINGS IN THE SOFTWARE.