Coverage for pyrdnap / rdnap2018.py: 93%

242 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-06-10 10:52 -0400

1 

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

3 

4u'''Main classes L{RDNAP2018v1} and L{RDNAP2018v2} follow C{variant 1} respectively C{variant 2} of 

5the U{RDNAPTRANS(tm)2018_v220627<https://formulieren.kadaster.nl/aanvragen_rdnaptrans>} specification. 

6Each provide a C{forward} method to convert geodetic lat-/longitudes and heights to C{RD} coodinates 

7and C{NAP} heights and a C{reverse} method for converting the other way. 

8 

9The L{RDNAP2018v1.forward} and C{.reverse} results are within the C{RDNAPTRANS(tm)2018_v220627} 

10self-validation requirements of C{0.000000010 degrees} respectively C{0.0010 meter} for points inside 

11the C{RD} region, see B{C{Note below}}. 

12 

13@note: L{RDNAP2018v1}, L{RDNAP2018v2}, C{PyRDNAP} and C{pyrdnap} have B{not been formally validated} 

14 and are B{not certified} to carry the trademark C{RDNAPTRANS(tm)}. 

15''' 

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

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

18 

19from pyrdnap.rd0 import _RD, _RD0 as A0, RDNAP7Tuple 

20from pyrdnap.v_grids import RDNAPError, _V_grid, _v_gridz_import 

21from pyrdnap.__pygeodesy import (_0_0, _0_5, _1_0, _2_0, 

22 _isNAN, _isNAN0, _earth_datum, 

23 _ALL_DOCS, _ALL_OTHER, _FOR_DOCS, 

24 _NamedBase) 

25from pygeodesy import (map1, EPS0, EPS1, NAN, PI_2, PI, PI2, # "consterns" 

26 Datum, Ellipsoid, LatLonDatum3Tuple, # datums, ellipsoids 

27 Property_RO, property_RO, property_ROnce, # props 

28 Lamd, Lat, Lon, Phid, # units 

29 sincos2, sincos2d) # utily 

30 

31from math import asin, atan, copysign, degrees, exp, \ 

32 fabs, floor, hypot, radians, sin, sqrt 

33 

34__all__ = () 

35__version__ = '26.06.09' 

36 

37_TOL_D = 1e-9 # degrees 2.3.3f+ 

38_TOL_M = 1e-6 # meter 

39_TOL_R = radians(_TOL_D) # 2e-11 

40_TRIPS = 16 # 5..6 sufficient 

41 

42 

43class _RDNAPbase(_NamedBase): # in .rd0._RD.regionB 

44 '''(INTERNAL) L{RDNAP2018v1}C{/-v2} base class. 

45 ''' 

46 _datum = None # forward, v1 reverse Datum, lazily (GRS80) 

47 _EETRS = None # forward, v1 reverse Ellipsoid, lazily 

48 _raiser = False 

49 

50 def __init__(self, a_ellipsoid=None, f=None, raiser=False, **name): 

51 '''New C{RDNAP2018v1} or C{-v2} instance. 

52 

53 @kwarg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or the ellipsoid's equatorial 

54 radius (C{scalar}, conventionally in C{meter}), see B{C{f}} 

55 or a datum (L{Datum}). Default C{Datums.GRS80} for ETRS89. 

56 @kwarg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}} is 

57 specified as C{scalar}, ignored otherwise. 

58 @kwarg raiser: If C{True} raise an L{RDNAPError} for lat-/longitudes outside 

59 the C{RD} region (C{bool}). 

60 @kwarg name: Optional name (C{str}). 

61 

62 @raise RDNAPError: Ellipsoid (or datum) is not oblate (i.e. is spherical or 

63 prolate) or the datum's C{transform} is not C{unity}. 

64 ''' 

65 if a_ellipsoid is f is None: 

66 self._datum = A0.D80 # GRS80 (ETRS89) 

67 else: 

68 _earth_datum(self, a_ellipsoid, f, **name) # sets self._datum 

69 self._EETRS = E = self._datum.ellipsoid 

70 if not E.isOblate: 

71 raise RDNAPError('not oblate: %r' % (E,)) 

72 if raiser: # PYCHOK no cover 

73 T = self._datum.transform 

74 if not T.isunity: 

75 raise RDNAPError('not unity: %r' % (T,)) 

76 self._raiser = True 

77 if name: 

78 self.name = name 

79 

80 def forward(self, lat, lon, height=0, raiser=None, name='forward'): 

81 '''Convert GRS80 (ETRS98) geodetic C{(B{lat}, B{lon})} and B{C{height}} 

82 to local C{(RDx, RDy)} coordinates and C{NAPh} quasi-geoid-height. 

83 

84 @arg lat: Latitude (C{degrees} geodetic). 

85 @arg lon: Longitude (C{degrees} geodetic). 

86 @kwarg height: Height, optional (C{meter} above geoid) or C{NAN} 

87 to ignore C{NAPh} interpolation. 

88 @kwarg raiser: If C{True} raise an L{RDNAPError} if B{C{lat}} or 

89 B{C{lon}} is outside the C{RD} region (C{bool}), 

90 if C{False} don't, overriding property C{raiser}. 

91 @kwarg name: Optional name (C{str}). 

92 

93 @return: An L{RDNAP7Tuple}C{(RDx, RDy, NAPh, lat, lon, height, datum)} 

94 with local C{RDx}, C{RDy} coordinates and C{NAPh} height. 

95 ''' 

96 lat, lon = Lat(lat), Lon(lon) 

97 lat0, lon0 = \ 

98 lat_, lon_ = self._forwardXform2(raiser, lat, lon) 

99 for _ in range(_TRIPS): # 2.3.3a-f, 1..2 

100 latc, lonc = self._rdlatlon2(lat_, lon_, lat0, lon0) 

101 if fabs(latc - lat_) < _TOL_D and \ 

102 fabs(lonc - lon_) < _TOL_D: 

103 break 

104 lat_, lon_ = latc, lonc 

105 

106 phiClamC = _ellipsoidal2spherical(latc, lonc) 

107 RDx, RDy = _spherical2oblique(*phiClamC) 

108 NAPh = NAN if _isNAN(height) else (height - # NOT lat0, lon0 

109 self._rdNAPh_v(lat, lon, latc, lonc)) # 2.5.2 

110 return RDNAP7Tuple(RDx, RDy, NAPh, 

111 lat, lon, height, self.forwardDatum, name=name) 

112 

113 def forward3(self, lat, lon, name='forward3'): 

114 '''Datum transform C{(B{lat}, B{lon})} from GRS80 (ETRS98) to Bessel1841 (RD-Bessel). 

115 

116 @return: A L{LatLonDatum3Tuple}C{(lat, lon, datum)}. 

117 ''' 

118 x, y, z = _geodetic2cartesian(lat, lon, A0.H0_ETRS, self._EETRS) 

119 x, y, z = _RD._xETRS2RD.transform(x, y, z) # pseudo 

120 lat, lon = _cartesian2geodetic(x, y, z, A0.E0) # pseudo 

121 return LatLonDatum3Tuple(lat, lon, A0.D0, name=name) 

122 

123 @property_RO 

124 def forwardDatum(self): 

125 '''Get the C{forward} datum (L{Datum}, default GRS80). 

126 ''' 

127 return self._datum 

128 

129 def _forwardXform2(self, *args): # PYCHOK no cover 

130 return self._notOverloaded(*args) 

131 

132 def _inside2(self, raiser, lat, lon, **as89): 

133 # if RD-Bessel C{(lat, lon)} is not inside C{RD} region 

134 # raise an error if C{raiser} or self.raiser is True 

135 if (raiser or (raiser is None and self._raiser)) and \ 

136 not _RD.isinside(lat, lon, **as89): 

137 raise self._outsidError(lat, lon) 

138 return lat, lon 

139 

140 def isinside(self, lat, lon, as89=False, eps=0): 

141 '''Is geodetic C{(B{lat}, B{lon})} inside the C{RD} region[89] (C{bool})? 

142 

143 @kwarg as89: Use C{B{as89}=True} for C{RD} region89 and in case 

144 C{(B{lat}, B{lon})} are ETRS89 (GRS80), not Bessel1841. 

145 @kwarg eps: Over-/undersize the C{RD} region (C{degrees}). 

146 ''' 

147 return _RD.isinside(Lat(lat), Lon(lon), as89, eps) 

148 

149 def _outsidError(self, *lat_lon): 

150 # format an RDNAPError for C{lat_lon} outside C{RD} region 

151 return RDNAPError('%r outside %s' % (lat_lon, self.region)) 

152 

153 @property_RO 

154 def _rdgrid(self): # PYCHOK no cover 

155 return self._notOverloaded() 

156 

157 def _rdlatlon2(self, lat, lon, lat0=None, lon0=None): # 2.3.2 

158 # return the RD-corrected C{(lat, lon)} 

159 if _RD.isinside(lat, lon): 

160 c_f_N_f6 = _RD._c_f_N_f6(lat, lon) 

161 lat_corr = _bilinear(self._rdgrid._lat_corr, *c_f_N_f6) 

162 lon_corr = _bilinear(self._rdgrid._lon_corr, *c_f_N_f6) 

163 

164 if lat0 is lon0 is None: # reverse 

165 lat += lat_corr 

166 lon += lon_corr 

167 else: # forward 

168 lat = lat0 - lat_corr 

169 lon = lon0 - lon_corr 

170 return lat, lon # NAN, NAN? 

171 

172 def rdNAPh(self, lat, lon, raiser=False): # 2.5.1 and 3.5 

173 '''Interpolate the C{NAPh} quasi-geoid-height I{within} the C{RD} region. 

174 

175 @arg lat: Latitude (C{degrees} geodetic). 

176 @arg lon: Longitude (C{degrees} geodetic). 

177 @kwarg raiser: If C{True} raise an L{RDNAPError} if B{C{lat}} or 

178 B{C{lon}} is outside the C{RD} region (C{bool}), 

179 otherwise don't and return C{NAN}. 

180 

181 @return: C{NAPh} (C{meter}) or C{NAN} if C{B{raiser} is False} and 

182 B{C{lat}} or B{C{lon}} is outside the C{RD} region. 

183 ''' 

184 return self._rdNAPh(Lat(lat), Lon(lon), raiser) 

185 

186 def _rdNAPh(self, lat, lon, raiser): 

187 # return C{NAPh} at C{(lat, lon)} or C{NAN} if outside 

188 if _RD.isinside(lat, lon): # eps=0 

189 c_f_N_f6 = _RD._c_f_N_f6(lat, lon) 

190 return _bilinear(self._rdgrid._NAP_h, *c_f_N_f6) 

191 elif raiser: 

192 raise self._outsidError(lat, lon) 

193 return NAN # c0 2.5.1e+ 

194 

195 def _rdNAPh_v(self, lat1, lon1, lat2, lon2): 

196 # get C{NAPh} at geodetic C{lat1, lon1} for variant 1 or at the 

197 # RD-corrected or inverse-projected C{lat2, lon2} for variant 2 

198 if self.variant == 2: 

199 lat1, lon1 = lat2, lon2 

200 return self._rdNAPh(lat1, lon1, False) 

201 

202 @property_ROnce 

203 def region(self): 

204 '''Get the C{RD} region as L{Bounds4Tuple}C{(latS, lonW, latN, lonE)}, all C{RD-Bessel degrees}. 

205 ''' 

206 return _RD.region 

207 

208 @Property_RO 

209 def region89(self): # in .rd0._RD.region89 

210 '''Get the C{RD} region as L{Bounds4Tuple}C{(latS, lonW, latN, lonE)}, all C{ETRS89 (GRS80) degrees}. 

211 ''' 

212 S, W, N, E = r = self.region # Bounds4Tuple 

213 _R = _RDNAPbase 

214 _r = _R().reverse3 

215 nB = r.name.replace(_R.region.name, _R.region89.name) 

216 s, w, _ = _r(S, W) 

217 n, e, _ = _r(N, E) 

218 return r.classof(s, w, n, e, name=nB) # r.dup(latS=S, ...) 

219 

220 def _reverse(self, RDx, RDy, NAPh, asRD, raiser=None, name='reverse', toRD=None): 

221 '''(INTERNAL) Convert local C{(B{RDx}, B{RDy})} coordinates and 

222 B{C{NAPh}} quasi-geoid-height to GRS80 (ETRS89) or Bessel1841 

223 (RD-Bessel) geodetic C{lat}, C{lon} and C{height}. 

224 ''' 

225 phiClamC = _oblique2spherical(RDx, RDy) 

226 latlon = _spherical2ellipsoidal(*phiClamC) 

227 

228 latc, lonc = self._rdlatlon2(*latlon) 

229 lat, lon, d = self._reverseXform3(raiser, latc, lonc) 

230 h = NAN if _isNAN(NAPh) else (NAPh + 

231 self._rdNAPh_v(lat, lon, *latlon)) 

232 

233 if (asRD if toRD is None else toRD): # backward comp'y toRD 

234 lat, lon, d = latc, lonc, A0.D0 

235 return RDNAP7Tuple(RDx, RDy, NAPh, 

236 lat, lon, h, d, name=name) 

237 

238 def reverse3(self, lat, lon, name='reverse3'): 

239 '''Datum transform C{(B{lat}, B{lon})} from Bessel1841 (RD-Bessel) to GRS80 (ETRS98). 

240 

241 @return: A L{LatLon3Tuple}C{(lat, lon, datum)}. 

242 ''' 

243 x, y, z = _geodetic2cartesian(lat, lon, A0.H0, A0.E0) 

244 x, y, z = _RD._xRD2ETRS.transform(x, y, z) 

245 lat, lon = _cartesian2geodetic(x, y, z, self._EETRS) 

246 return LatLonDatum3Tuple(lat, lon, self.forwardDatum, name=name) 

247 

248 @property_RO 

249 def reverseDatum(self): 

250 '''Get the I{default} C{reverse} datum (L{Datum}), GRS80 or Bessel1841. 

251 ''' 

252 return {1: self._datum, # self.forwardDatum 

253 2: A0.D0}.get(self.variant) 

254 

255 def _reverseXform3(self, *raiser_lat_lon): 

256 # datum transform C{(lat, lon)} from RD-Bessel to GRS80 (ETRS89) 

257 # and raise an C{RDNAPError} if outside the C{RD} region 

258 lat, lon = self._inside2(*raiser_lat_lon) 

259 return self.reverse3(lat, lon) 

260 

261 def similarity(self, inverse=None): # PYCHOK no cover 

262 return self._notOverloaded(inverse=inverse) 

263 

264 def toStr(self, prec=9, **unused): # PYCHOK signature 

265 '''Return this C{RDNAP20181v1} or C{-v2} instance as a string. 

266 

267 @kwarg prec: Precision, number of decimal digits (0..9). 

268 

269 @return: This C{RDNAP2018v1} or C{-v2} (C{str}). 

270 ''' 

271 return self.attrs('name', 'variant', 'forwardDatum', prec=prec) # _ellipsoid_, _name__ 

272 

273 @property_RO 

274 def variant(self): # PYCHOK no cover 

275 return self._notOverloaded() 

276 

277 

278class RDNAP2018v1(_RDNAPbase): 

279 '''Transformer implementing C{variant 1} of U{RD NAP 2018 v220627 

280 <https://formulieren.kadaster.nl/aanvragen_rdnaptrans>}. 

281 

282 @note: L{RDNAP2018v1} has B{not been formally validated} and is 

283 B{not certified} to carry the trademark C{RDNAPTRANS(tm)}. 

284 ''' 

285 if _FOR_DOCS: 

286 __init__ = _RDNAPbase.__init__ 

287 forward = _RDNAPbase.forward 

288 forward3 = _RDNAPbase.forward3 

289 reverse3 = _RDNAPbase.reverse3 

290 

291 def _forwardXform2(self, raiser, *lat_lon): # PYCHOK signature 

292 # datum transform C{(lat, lon)} from ETRS89 to RD-Bessel 

293 # and raise an C{RDNAPError} if outside the C{RD} region 

294 lat, lon, _ = self.forward3(*lat_lon) 

295 return self._inside2(raiser, lat, lon) 

296 

297 @property_ROnce 

298 def _rdgrid(self): 

299 try: 

300 from pyrdnap import v1grid 

301 except (AttributeError, ImportError, RDNAPError): 

302 v1grid = _v_gridz_import(self.variant) 

303 return v1grid 

304 

305 def reverse(self, RDx, RDy, NAPh=0, asRD=False, **raiser_name): 

306 '''Convert a local C{(B{RDx}, B{RDy})} point and B{C{NAPh}} height to 

307 B{GRS80 (ETRS89)} geodetic C{(lat, lon, height)}, by default. 

308 

309 @arg RDx: Local C{RD} X (C{meter}, conventionally). 

310 @arg RDy: Local C{RD} Y (C{meter}, conventionally). 

311 @kwarg NAPh: C{NAP} quasi-geoid-height (C{meter}, conventionally) 

312 or C{NAN} to ignore C{NAPh} interpolation. 

313 @kwarg asRD: Use C{B{asRD}=True} to return Bessel1841 (RD-Bessel) lat- 

314 and longitudes instead of GRS80 (ETRS89) (C{bool}). 

315 @kwarg raiser_name: Like the C{forward} method, C{B{raiser}=None} 

316 (C{bool}) and optional C{B{name}='reverse'} (C{str}). 

317 

318 @return: An L{RDNAP7Tuple}C{(RDx, RDy, NAPh, lat, lon, height, datum)} 

319 with geodetic C{lat} and C{lon}, C{height} and C{datum} 

320 B{GRS80 (ETRS89)} or C{Bessel1841 (RD-Bessel)}. 

321 ''' 

322 return self._reverse(RDx, RDy, NAPh, asRD, **raiser_name) 

323 

324 def similarity(self, inverse=False): 

325 '''Get the similarity transform (C{Similarity}). 

326 

327 @kwarg inverse: Use C{True} for the C{reverse} or C{False} 

328 for the C{forward} transform (C{bool}). 

329 ''' 

330 return _RD._xRD2ETRS if inverse else _RD._xETRS2RD 

331 

332 @property_ROnce 

333 def variant(self): 

334 '''Get this C{RDNAP2018}'s variant (C{int}). 

335 ''' 

336 return 1 

337 

338 

339class RDNAP2018v2(_RDNAPbase): 

340 '''Transformer implementing C{variant 2} of U{RD NAP 2018 v220627 

341 <https://formulieren.kadaster.nl/aanvragen_rdnaptrans>}. 

342 

343 @note: Method L{RDNAP2018v2.reverse} returns by default GRS80 

344 (ETRS89) geodetic lat- and longitudes and datum. 

345 

346 @note: L{RDNAP2018v2} has B{not been formally validated} and is 

347 B{not certified} to carry the trademark C{RDNAPTRANS(tm)}. 

348 ''' 

349 if _FOR_DOCS: 

350 __init__ = _RDNAPbase.__init__ 

351 forward = _RDNAPbase.forward 

352 forward3 = _RDNAPbase.forward3 

353 reverse3 = _RDNAPbase.reverse3 

354 

355 def _forwardXform2(self, *raiser_lat_lon): 

356 # no datum transform C{(lat, lon)} to RD-Bessel, but 

357 # raise an C{RDNAPError} if outside the C{RD} region 

358 return self._inside2(*raiser_lat_lon) # as89=True 

359 

360 @property_ROnce 

361 def _rdgrid(self): 

362 try: 

363 from pyrdnap import v2grid 

364 except (AttributeError, ImportError, RDNAPError): 

365 v2grid = _v_gridz_import(self.variant) 

366 return v2grid 

367 

368 def reverse(self, RDx, RDy, NAPh=0, asRD=True, **raiser_name): 

369 '''Convert a local C{(B{RDx}, B{RDy})} point and B{C{NAPh}} height to 

370 B{GRS80 (ETRS89)} geodetic C{(lat, lon, height)}, by default. 

371 

372 @arg RDx: Local C{RD} X (C{meter}, conventionally). 

373 @arg RDy: Local C{RD} Y (C{meter}, conventionally). 

374 @kwarg NAPh: C{NAP} quasi-geoid-height (C{meter}, conventionally) 

375 or C{NAN} to ignore C{NAPh} interpolation. 

376 @kwarg asRD: Use C{B{asRD}=False} to return GRS80 (ETRS89) lat- and 

377 longitudes instead of Bessel1841 (RD-Bessel) (C{bool}). 

378 @kwarg raiser_name: Like the C{forward} method, C{B{raiser}=None} 

379 (C{bool}) and optional C{B{name}='reverse'} (C{str}). 

380 

381 @return: An L{RDNAP7Tuple}C{(RDx, RDy, NAPh, lat, lon, height, datum)} 

382 with geodetic C{lat} and C{lon}, C{height} and C{datum} 

383 B{Bessel1841 (RD-Bessel)} or C{GRS80 (ETRS89)}. 

384 ''' 

385 return self._reverse(RDx, RDy, NAPh, asRD, **raiser_name) 

386 

387 def similarity(self, *unused): # PYCHOK signature 

388 '''Get the similarity transform, always C{None}. 

389 ''' 

390 return None 

391 

392 @property_ROnce 

393 def variant(self): 

394 '''Get this C{RDNAP2018}'s variant (C{int}). 

395 ''' 

396 return 2 

397 

398 

399def _atan3(y, x, x0): # 2.2.3e and 3.1.1i 

400 # equiv to math.atan2 iff x0 is y 

401 if x > 0: 

402 r = atan(y / x) 

403 elif x < 0: 

404 r = atan(y / x) + copysign(PI, x0) 

405 else: 

406 r = copysign(PI_2, x0) if x0 else _0_0 

407 return r 

408 

409 

410def _atan_exp(w): # 2.4.1c 

411 return atan(exp(w)) * _2_0 - PI_2 

412 

413 

414def _bilinear(v_grid, c_latI, f_latI, latN_f, # 2.3.1f and g 

415 c_lonI, f_lonI, lonN_f): 

416 # interpolate a lat_corr_, lon_corr_ or NAP_h... 

417 assert isinstance(v_grid, _V_grid), v_grid 

418 ne = v_grid(c_latI, c_lonI) 

419 nw = v_grid(c_latI, f_lonI) 

420 se = v_grid(f_latI, c_lonI) 

421 sw = v_grid(f_latI, f_lonI) 

422 lonN_f1 = _1_0 - lonN_f # == 1 - (lonN - f_lonN) 

423 return (ne * lonN_f + nw * lonN_f1) * latN_f + \ 

424 (se * lonN_f + sw * lonN_f1) * (_1_0 - latN_f) 

425 

426 

427def _cartesian2geodetic(x, y, z, E): # 2.2.3 == EcefUPC.reverse? 

428 # convert cartesian C{(x, y, z)} to C{E}-geodetic C{(lat, lon)} 

429 r = hypot(x, y) 

430 if r > _TOL_M: 

431 a = E.a * E.e2 

432 phi_ = atan(z / r) # atan2(z, r) 

433 for _ in range(_TRIPS): # 4..6 

434 s = sin(phi_) 

435 s *= a / sqrt(_1_0 - s**2 * E.e2) 

436 phi = atan((z + s) / r) # atan2(z + s, r) 

437 if fabs(phi - phi_) < _TOL_R: 

438 break 

439 phi_ = phi 

440 else: 

441 phi = copysign(PI_2, z) 

442 lam = _atan3(y, x, y) 

443 return map1(degrees, phi, lam) # lat, lon 

444 

445 

446def _ellipsoidal2spherical(lat, lon): # 2.4.1 

447 # convert RD-Bessel C{(lat, lon)} to spherical C{(𝛷, 𝛬)} 

448 phiC = phi = Phid(lat) 

449 if PI_2 > phi > -PI_2: # 2.4.1c 

450 q = A0.log_tan(phi) - A0.log_e_2(phi) 

451 w = A0.N0 * q + A0.M0 # 2.4.1b 

452 phiC = _atan_exp(w) 

453 lamC = (Lamd(lon) - A0.LAM0) * A0.N0 + A0.LAM0C # 2.4.1d 

454 return phiC, lamC # -Capital 𝛷, 𝛬 

455 

456 

457def _eq0(r, r0=_0_0): 

458 return fabs(r - r0) < _TOL_R 

459 

460 

461# def _eq0d(d, d0=_0_0): 

462# return fabs(d - d0) < _TOL_D 

463 

464 

465def _geodetic2cartesian(lat, lon, h, E): # 2.2.1 

466 # convert C{E}-geodetic C{(lat, lon)} to cartesian C{(x, y, z)} 

467 y, x = sincos2d(lon) 

468 z, c = sincos2d(lat) 

469 n = E.a / sqrt(_1_0 - z**2 * E.e2) 

470 H = _isNAN0(h) 

471 c *= n + H 

472 x *= c 

473 y *= c 

474 z *= n * (_1_0 - E.e2) + H 

475 return x, y, z 

476 

477 

478def _ne0(r, r0=_0_0): 

479 return fabs(r - r0) > _TOL_R 

480 

481 

482# def _ne0d(d, d0=_0_0): 

483# return fabs(d - d0) > _TOL_D 

484 

485 

486def _oblique2spherical(x, y): # 3.1.1 

487 # inverse oblique stereographic conformal projection from 

488 # C{RD (x, y)} to spherical C{(𝛷, 𝛬)}, see C++ function 

489 # sterea_e_inverse in U{Proj/src/projections/sterea.cpp 

490 # <https://Proj.org/en/stable/operations/projections/sterea.html>} 

491 x -= A0.X0 

492 y -= A0.Y0 

493 r = hypot(x, y) 

494 if r > _TOL_M: # x and y 

495 s0, c0 = A0.sincos2PHI0C 

496 sp, cp = sincos2(atan(r / A0.RK2) * _2_0) # psi atan2(r, A0.RK2) 

497 ca = sp * y / r 

498 xN = cp * c0 - ca * s0 

499 yN = sp * x / r 

500 zN = cp * s0 + ca * c0 

501 phiC = asin(zN) 

502 else: 

503 _, xN = A0.sincos2PHI0C 

504 yN = _0_0 

505 phiC = A0.PHI0C # asin(sin(PHI0C)) 

506 lamC = _atan3(yN, xN, x) + A0.LAM0C 

507 return phiC, lamC # -Capital 𝛷, 𝛬 

508 

509 

510def _spherical2ellipsoidal(phiC, lamC): # 3.1.2 

511 # inverse Gauss conformal projection from 

512 # spherical C{(𝛷, 𝛬)} to RD-Bessel C{(lat, lon)} 

513 phi = phiC 

514 if PI_2 > phi > -PI_2: 

515 q = (A0.log_tan(phi) - A0.M0) / A0.N0 

516# w = A0.log_tan(phi) 

517 for _ in range(_TRIPS): # 3..6 

518 phi_ = phi 

519 phi = _atan_exp(A0.log_e_2(phi) + q) 

520 if fabs(phi - phi_) < _TOL_R: 

521 break 

522 lam = (lamC - A0.LAM0C) / A0.N0 + A0.LAM0 

523 lam += floor((PI - lam) / PI2) * PI2 

524 return map1(degrees, phi, lam) # lat, lon 

525 

526 

527def _spherical2oblique(phiC, lamC): # 2.4.2 

528 # oblique stereographic conformal projection 

529 # from spherical C{(𝛷, 𝛬)} to C{RD (x, y)} 

530 x = A0.X0 # 2.4.2g 

531 y = A0.Y0 # 2.4.2h 

532 a = phiC - A0.PHI0C # 𝛷 - 𝛷0 

533 b = lamC - A0.LAM0C # 𝛬 - 𝛬0 

534 if (_ne0(a) or _ne0(b)) and (_ne0(phiC, -A0.PHI0C) or 

535 _ne0(lamC, -A0.LAM0C + PI)): 

536 s0, c0 = A0.sincos2PHI0C # sin(𝛷0), cos(𝛷0) 

537 s, c = sincos2(phiC) # sin(𝛷), cos(𝛷) 

538 sp_22 = sin(a * _0_5)**2 + \ 

539 sin(b * _0_5)**2 * c * c0 # sin(𝜓/2)**2 

540 if EPS0 < sp_22 < EPS1: 

541 # r = 2kR * tan(𝜓/2) 

542 # q = r / (sin(𝜓/2) * cos(𝜓/2) * 2) 

543 # = 2kR * sin(𝜓/2) / (sin(𝜓/2) * cos(𝜓/2)**2 * 2) 

544 # = 2kR / (cos(𝜓/2)**2 * 2) 

545 # = 2kR / ((1 - sin(𝜓/2)**2) * 2) 

546 # = 2kR / (2 - sin(𝜓/2)**2 * 2) 

547 t = sp_22 * _2_0 # 0 < t < 2 

548 q = A0.RK2 / (_2_0 - t) 

549 x += q * (c * sin(b)) 

550 y += q * (s - s0 + s0 * t) / c0 

551 elif _eq0(a) and _eq0(b): 

552 pass 

553 else: # if _eq0(phiC, -A0.PHI0C) and _eq0(lamC, A0.LAM0C - PI): 

554 x = y = NAN 

555# else: 

556# raise RDNAPError(str((phiC, lamC))) 

557 return x, y 

558 

559 

560__all__ += _ALL_DOCS(_RDNAPbase) 

561__all__ += _ALL_OTHER(RDNAP2018v1, RDNAP2018v2, RDNAPError, 

562 Datum, Ellipsoid, LatLonDatum3Tuple) # passed along from PyGeodesy 

563 

564# **) MIT License 

565# 

566# Copyright (C) 2026-2026 -- mrJean1 at Gmail -- All Rights Reserved. 

567# 

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

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

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

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

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

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

574# 

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

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

577# 

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

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

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

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

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

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

584# OTHER DEALINGS IN THE SOFTWARE.