Coverage for pygeodesy/osgr.py: 97%

305 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-25 13:15 -0400

1 

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

3 

4u'''Ordnance Survey Grid References (OSGR) references on the UK U{National Grid 

5<https://www.OrdnanceSurvey.co.UK/documents/resources/guide-to-nationalgrid.pdf>}. 

6 

7Classes L{Osgr} and L{OSGRError} and functions L{parseOSGR} and L{toOsgr}. 

8 

9A pure Python implementation, transcoded from I{Chris Veness}' JavaScript originals U{OS National Grid 

10<https://www.Movable-Type.co.UK/scripts/latlong-os-gridref.html>} and U{Module osgridref 

11<https://www.Movable-Type.co.UK/scripts/geodesy/docs/module-osgridref.html>} and I{Charles Karney}'s 

12C++ class U{OSGB<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1OSGB.html>}. 

13 

14OSGR provides geocoordinate references for UK mapping purposes, converted in 2015 to work with the C{WGS84} 

15or the original C{OSGB36} datum. In addition, this implementation includes both the OS recommended and the 

16Krüger-based method to convert between OSGR and geodetic coordinates (with keyword argument C{kTM} of 

17function L{toOsgr}, method L{Osgr.toLatLon} and method C{toOsgr} of any ellipsoidal C{LatLon} class). 

18 

19See U{Transverse Mercator: Redfearn series<https://WikiPedia.org/wiki/Transverse_Mercator:_Redfearn_series>}, 

20Karney's U{"Transverse Mercator with an accuracy of a few nanometers", 2011<https://ArXiv.org/pdf/1002.1417v3.pdf>} 

21(building on U{"Konforme Abbildung des Erdellipsoids in der Ebene", 1912<https://bib.GFZ-Potsdam.DE/pub/digi/krueger2.pdf>}, 

22U{"Die Mathematik der Gauß-Krueger-Abbildung", 2006<https://DE.WikiPedia.org/wiki/Gauß-Krüger-Koordinatensystem>}, 

23U{A Guide<https://www.OrdnanceSurvey.co.UK/documents/resources/guide-coordinate-systems-great-britain.pdf>} 

24and U{Ordnance Survey National Grid<https://WikiPedia.org/wiki/Ordnance_Survey_National_Grid>}. 

25''' 

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

27from __future__ import division as _; del _ # PYCHOK semicolon 

28 

29from pygeodesy.basics import halfs2, isbool, isfloat, map1, \ 

30 _splituple, _xsubclassof, typename 

31from pygeodesy.constants import _1_0, _10_0, _N_2_0 # PYCHOK used! 

32from pygeodesy.datums import Datums, _ellipsoidal_datum, _WGS84 

33# from pygeodesy.dms import parseDMS2 # _MODS 

34from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB 

35from pygeodesy.errors import _parseX, _TypeError, _ValueError, \ 

36 _xkwds, _xkwds_get, _xkwds_pop2 

37from pygeodesy.fmath import Fdot, fpowers 

38from pygeodesy.fsums import _Fsumf_ 

39# from pygeodesy.internals import typename # from .basics 

40from pygeodesy.interns import MISSING, NN, _A_, _COLON_, _COMMA_, \ 

41 _COMMASPACE_, _DMAIN_, _DOT_, _not_, \ 

42 _ellipsoidal_, _latlon_, _SPACE_ 

43from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

44from pygeodesy.named import _name2__, _NamedBase, nameof 

45from pygeodesy.namedTuples import EasNor2Tuple, LatLon2Tuple, \ 

46 LatLonDatum3Tuple 

47from pygeodesy.props import Property_RO, property_RO 

48from pygeodesy.streprs import _EN_WIDE, enstr2, _enstr2m3, Fmt, \ 

49 _resolution10, unstr, _xzipairs 

50from pygeodesy.units import Easting, Lamd, Lat, Lon, Northing, \ 

51 Phid, Scalar, _10um, _100km 

52from pygeodesy.utily import degrees90, degrees180, sincostan3, truncate 

53 

54from math import cos, fabs, radians, sin, sqrt 

55 

56__all__ = _ALL_LAZY.osgr 

57__version__ = '25.04.12' 

58 

59_equivalent_ = 'equivalent' 

60_OSGR_ = 'OSGR' 

61_ord_A = ord(_A_) 

62_TRIPS = 33 # .toLatLon convergence 

63 

64 

65class _NG(object): 

66 '''Ordnance Survey National Grid parameters. 

67 ''' 

68 @Property_RO 

69 def a0(self): # equatoradius, scaled 

70 return self.ellipsoid.a * self.k0 

71 

72 @Property_RO 

73 def b0(self): # polaradius, scaled 

74 return self.ellipsoid.b * self.k0 

75 

76 @Property_RO 

77 def datum(self): # datum, Airy130 ellipsoid 

78 return Datums.OSGB36 

79 

80 @Property_RO 

81 def eas0(self): # False origin easting (C{meter}) 

82 return Easting(4 * _100km) 

83 

84 @Property_RO 

85 def easX(self): # easting [0..extent] (C{meter}) 

86 return Easting(7 * _100km) 

87 

88 @Property_RO 

89 def ellipsoid(self): # ellipsoid, Airy130 

90 return self.datum.ellipsoid 

91 

92 def forward2(self, latlon): # convert C{latlon} to (easting, norting), as I{Karney}'s 

93 # U{Forward<https://GeographicLib.SourceForge.io/C++/doc/OSGB_8hpp_source.html>} 

94 t = self.kTM.forward(latlon.lat, latlon.lon, lon0=self.lon0) 

95 e = t.easting + self.eas0 

96 n = t.northing + self.nor0ffset 

97 return e, n 

98 

99 @Property_RO 

100 def k0(self): # central scale (C{float}), like I{Karney}'s CentralScale 

101 # <https://GeographicLib.SourceForge.io/C++/doc/OSGB_8hpp_source.html> 

102 _0_9998268 = (9998268 - 10000000) / 10000000 

103 return Scalar(_10_0**_0_9998268) # 0.9996012717... 

104 

105 @Property_RO 

106 def kTM(self): # the L{KTransverseMercator} instance, like I{Karney}'s OSGBTM 

107 # <https://GeographicLib.SourceForge.io/C++/doc/OSGB_8cpp_source.html> 

108 return _MODS.ktm.KTransverseMercator(self.datum, lon0=0, k0=self.k0) 

109 

110 @Property_RO 

111 def lam0(self): # True origin longitude C{radians} 

112 return Lamd(self.lon0) 

113 

114 @Property_RO 

115 def lat0(self): # True origin latitude, 49°N 

116 return Lat(49.0) 

117 

118 @Property_RO 

119 def lon0(self): # True origin longitude, 2°W 

120 return Lon(_N_2_0) 

121 

122 @Property_RO 

123 def Mabcd(self): # meridional coefficients (a, b, c, d) 

124 n, n2, n3 = fpowers(self.ellipsoid.n, 3) 

125 M = (_Fsumf_(4, 4 * n, 5 * n2, 5 * n3) / 4, 

126 _Fsumf_( 24 * n, 24 * n2, 21 * n3) / 8, 

127 _Fsumf_( 15 * n2, 15 * n3) / 8, 

128 (35 * n3 / 24)) 

129 return M 

130 

131 def Mabcd0(self, a): # meridional arc, scaled 

132 c = a + self.phi0 

133 s = a - self.phi0 

134 R = Fdot(self.Mabcd, s, -sin(s) * cos(c), 

135 sin(s * 2) * cos(c * 2), 

136 -sin(s * 3) * cos(c * 3)) 

137 return float(R * self.b0) 

138 

139 @Property_RO 

140 def nor0(self): # False origin northing (C{meter}) 

141 return Northing(-_100km) 

142 

143 @Property_RO 

144 def nor0ffset(self): # like I{Karney}'s computenorthoffset 

145 # <https://GeographicLib.SourceForge.io/C++/doc/OSGB_8cpp_source.html> 

146 return self.nor0 - self.kTM.forward(self.lat0, 0).northing 

147 

148 @Property_RO 

149 def norX(self): # northing [0..extent] (C{meter}) 

150 return Northing(13 * _100km) 

151 

152 def nu_rho_eta3(self, sa): # 3-tuple (nu, nu / rho, eta2) 

153 E = self.ellipsoid # rho, nu = E.roc2_(sa) # .k0? 

154 s = E.e2s2(sa) # == 1 - E.e2 * sa**2 

155 v = self.a0 / sqrt(s) # == nu, transverse roc 

156 # rho = .a0 * E.e21 / s**1.5 == v * E.e21 / s 

157 # r = v * E.e21 / s # == rho, meridional roc 

158 # nu / rho == v / (v * E.e21 / s) == s / E.e21 == ... 

159 s *= E._1_e21 # ... s * E._1_e21 == s * E.a2_b2 

160 return v, s, (s - _1_0) # η2 = nu / rho - 1 

161 

162 @Property_RO 

163 def phi0(self): # True origin latitude C{radians} 

164 return Phid(self.lat0) 

165 

166 def reverse(self, osgr): # convert C{osgr} to (ellipsoidal} LatLon, as I{Karney}'s 

167 # U{Reverse<https://GeographicLib.SourceForge.io/C++/doc/OSGB_8hpp_source.html>} 

168 r = osgr._latlonTM 

169 if r is None: 

170 x = osgr.easting - self.eas0 

171 y = osgr.northing - self.nor0ffset 

172 t = self.kTM.reverse(x, y, lon0=self.lon0) 

173 r = _LLEB(t.lat, t.lon, datum=self.datum, name=osgr.name) 

174 osgr._latlonTM = r 

175 return r 

176 

177_NG = _NG() # PYCHOK singleton 

178 

179 

180class OSGRError(_ValueError): 

181 '''Error raised for a L{parseOSGR}, L{Osgr} or other OSGR issue. 

182 ''' 

183 pass 

184 

185 

186class Osgr(_NamedBase): 

187 '''Ordnance Survey Grid References (OSGR) coordinates on 

188 the U{National Grid<https://www.OrdnanceSurvey.co.UK/ 

189 documents/resources/guide-to-nationalgrid.pdf>}. 

190 ''' 

191 _datum = _NG.datum # default datum (L{Datums.OSGB36}) 

192 _easting = 0 # Easting (C{meter}) 

193 _latlon = None # cached B{C{_toLatlon}} 

194 _latlonTM = None # cached B{C{_toLatlon kTM}} 

195 _northing = 0 # Nothing (C{meter}) 

196 _resolution = 0 # from L{parseOSGR} (C{meter}) 

197 

198 def __init__(self, easting, northing, datum=None, resolution=0, **name): 

199 '''New L{Osgr} coordinate. 

200 

201 @arg easting: Easting from the OS C{National Grid} origin (C{meter}). 

202 @arg northing: Northing from the OS C{National Grid} origin (C{meter}). 

203 @kwarg datum: Override default datum (C{Datums.OSGB36}). 

204 @kwarg resolution: Optional resolution (C{meter}), C{0} for default. 

205 @kwarg name: Optional C{B{name}=NN} (C{str}). 

206 

207 @raise OSGRError: Invalid or negative B{C{easting}} or B{C{northing}} 

208 or B{C{datum}} not an C{Datums.OSGB36} equivalent. 

209 ''' 

210 if datum: # PYCHOK no cover 

211 try: 

212 self._datum = _ellipsoidal_datum(datum) 

213 if self.datum != _NG.datum: 

214 raise ValueError(_not_(_NG.datum.name, _equivalent_)) 

215 except (TypeError, ValueError) as x: 

216 raise OSGRError(datum=datum, cause=x) 

217 

218 self._easting = Easting( easting, Error=OSGRError, high=_NG.easX) 

219 self._northing = Northing(northing, Error=OSGRError, high=_NG.norX) 

220 

221 if name: 

222 self.name = name 

223 if resolution: 

224 self._resolution = _resolution10(resolution, Error=OSGRError) 

225 

226 def __str__(self): 

227 return self.toStr(GD=True, sep=_SPACE_) 

228 

229 @Property_RO 

230 def datum(self): 

231 '''Get the datum (L{Datum}). 

232 ''' 

233 return self._datum 

234 

235 @Property_RO 

236 def easting(self): 

237 '''Get the easting (C{meter}). 

238 ''' 

239 return self._easting 

240 

241 @Property_RO 

242 def falsing0(self): 

243 '''Get the C{OS National Grid} falsing (L{EasNor2Tuple}). 

244 ''' 

245 return EasNor2Tuple(_NG.eas0, _NG.nor0, name=_OSGR_) 

246 

247 @property_RO 

248 def iteration(self): 

249 '''Get the most recent C{Osgr.toLatLon} iteration number 

250 (C{int}) or C{None} if not available/applicable. 

251 ''' 

252 return self._iteration 

253 

254 @Property_RO 

255 def latlon0(self): 

256 '''Get the C{OS National Grid} origin (L{LatLon2Tuple}). 

257 ''' 

258 return LatLon2Tuple(_NG.lat, _NG.lon0, name=_OSGR_) 

259 

260 @Property_RO 

261 def northing(self): 

262 '''Get the northing (C{meter}). 

263 ''' 

264 return self._northing 

265 

266 def parse(self, strOSGR, **name): 

267 '''Parse an OSGR reference to a similar L{Osgr} instance. 

268 

269 @arg strOSGR: The OSGR reference (C{str}), see function L{parseOSGR}. 

270 @kwarg name: Optional C{B{name}=NN} (C{str}), overriding this name. 

271 

272 @return: The similar instance (L{Osgr}) 

273 

274 @raise OSGRError: Invalid B{C{strOSGR}}. 

275 ''' 

276 return parseOSGR(strOSGR, Osgr=self.classof, name=self._name__(name)) 

277 

278 @property_RO 

279 def resolution(self): 

280 '''Get the OSGR resolution (C{meter}, power of 10) or C{0} if undefined. 

281 ''' 

282 return self._resolution 

283 

284 def toLatLon(self, LatLon=None, datum=_WGS84, kTM=False, eps=_10um, **LatLon_kwds): 

285 '''Convert this L{Osgr} coordinate to an (ellipsoidal) geodetic 

286 point. 

287 

288 @kwarg LatLon: Optional ellipsoidal class to return the 

289 geodetic point (C{LatLon}) or C{None}. 

290 @kwarg datum: Optional datum to convert to (L{Datum}, 

291 L{Ellipsoid}, L{Ellipsoid2}, L{Ellipsoid2} 

292 or L{a_f2Tuple}). 

293 @kwarg kTM: If C{True}, use I{Karney}'s Krüger method from 

294 module L{ktm}, otherwise use the Ordnance Survey 

295 formulation (C{bool}). 

296 @kwarg eps: Tolerance for OS convergence (C{meter}). 

297 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword 

298 arguments, ignored if C{B{LatLon} is None}. 

299 

300 @return: A B{C{LatLon}} instance or if C{B{LatLon} is None} 

301 a L{LatLonDatum3Tuple}C{(lat, lon, datum)}. 

302 

303 @note: While OS grid references are based on the OSGB36 datum, 

304 the Ordnance Survey have deprecated the use of OSGB36 for 

305 lat-/longitude coordinates (in favour of WGS84). Hence, 

306 this method returns WGS84 by default with OSGB36 as an 

307 option, U{see<https://www.OrdnanceSurvey.co.UK/blog/2014/12/2>}. 

308 

309 @note: The formulation implemented here due to Thomas, Redfearn, 

310 etc. is as published by the Ordnance Survey, but is 

311 inferior to Krüger as used by e.g. Karney 2011. 

312 

313 @raise OSGRError: No convergence. 

314 

315 @raise TypeError: If B{C{LatLon}} is not ellipsoidal or B{C{datum}} 

316 is invalid or conversion to B{C{datum}} failed. 

317 ''' 

318 NG = _NG 

319 if kTM: 

320 r = NG.reverse(self) 

321 

322 elif self._latlon is None: 

323 _F = _Fsumf_ 

324 e0 = self.easting - NG.eas0 

325 n0 = m = self.northing - NG.nor0 

326 

327 a0 = NG.a0 

328 _M = NG.Mabcd0 

329 a = NG.phi0 

330 _a = fabs 

331 _A = _F(a).fsum_ 

332 for self._iteration in range(1, _TRIPS): 

333 a = _A(m / a0) 

334 m = n0 - _M(a) # meridional arc 

335 if _a(m) < eps: 

336 break 

337 else: # PYCHOK no cover 

338 t = str(self) 

339 t = Fmt.PAREN(self.classname, repr(t)) 

340 t = _DOT_(t, typename(self.toLatLon)) 

341 t = unstr(t, eps=eps, kTM=kTM) 

342 raise OSGRError(Fmt.no_convergence(m), txt=t) 

343 

344 sa, ca, ta = sincostan3(a) 

345 v, v_r, n2 = NG.nu_rho_eta3(sa) 

346 

347 ta2 = ta**2 

348 ta4 = ta2**2 

349 

350 ta *= v_r / 2 

351 d = e0 / v 

352 d2 = d**2 

353 

354 a = (d2 * ta * (-1 + # Horner-like 

355 d2 / 12 * (_F( 5, 3 * ta2, -9 * ta2 * n2, n2) - 

356 d2 / 30 * _F(61, 90 * ta2, 45 * ta4)))).fsum_(a) 

357 

358 b = (d / ca * ( 1 - # Horner-like 

359 d2 / 6 * (_F(v_r, 2 * ta2) - 

360 d2 / 20 * (_F( 5, 28 * ta2, 24 * ta4) + 

361 d2 / 42 * _F(61, 662 * ta2, 1320 * ta4, 

362 720 * ta2 * ta4))))).fsum_(NG.lam0) 

363 

364 r = _LLEB(degrees90(a), degrees180(b), datum=self.datum, name=self.name) 

365 r._iteration = self._iteration # only ellipsoidal LatLon 

366 self._latlon = r 

367 else: 

368 r = self._latlon 

369 

370 return _ll2LatLon3(r, LatLon, datum, LatLon_kwds) 

371 

372 @Property_RO 

373 def scale0(self): 

374 '''Get the C{OS National Grid} central scale (C{scalar}). 

375 ''' 

376 return _NG.k0 

377 

378 def toRepr(self, GD=None, fmt=Fmt.SQUARE, sep=_COMMASPACE_, **prec): # PYCHOK expected 

379 '''Return a string representation of this L{Osgr} coordinate. 

380 

381 @kwarg GD: If C{bool}, in- or exclude the 2-letter grid designation and get 

382 the new B{C{prec}} behavior, otherwise if C{None}, default to the 

383 DEPRECATED definition C{B{prec}=5} I{for backward compatibility}. 

384 @kwarg fmt: Enclosing backets format (C{str}). 

385 @kwarg sep: Separator to join (C{str}) or C{None} to return an unjoined 2- or 

386 3-C{tuple} of C{str}s. 

387 @kwarg prec: Precison C{B{prec}=0}, the number of I{decimal} digits (C{int}) or 

388 if negative, the number of I{units to drop}, like MGRS U{PRECISION 

389 <https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html#PRECISION>}. 

390 

391 @return: This OSGR as (C{str}), C{"[G:GD, E:meter, N:meter]"} or if C{B{GD}=False} 

392 C{"[OSGR:easting,northing]"} or C{B{GD}=False} and C{B{prec} > 0} if 

393 C{"[OSGR:easting.d,northing.d]"}. 

394 

395 @note: OSGR easting and northing values are truncated, not rounded. 

396 

397 @raise OSGRError: If C{B{GD} not in (None, True, False)} or if C{B{prec} < -4} 

398 and C{B{GD}=False}. 

399 

400 @raise ValueError: Invalid B{C{prec}}. 

401 ''' 

402 GD, prec = _GD_prec2(GD, fmt=fmt, sep=sep, **prec) 

403 

404 if GD: 

405 t = self.toStr(GD=True, prec=prec, sep=None) 

406 t = _xzipairs('GEN', t, sep=sep, fmt=fmt) 

407 else: 

408 t = _COLON_(_OSGR_, self.toStr(GD=False, prec=prec)) 

409 if fmt: 

410 t = fmt % (t,) 

411 return t 

412 

413 def toStr(self, GD=None, sep=NN, **prec): # PYCHOK expected 

414 '''Return this L{Osgr} coordinate as a string. 

415 

416 @kwarg GD: If C{bool}, in- or exclude the 2-letter grid designation and get 

417 the new B{C{prec}} behavior, otherwise if C{None}, default to the 

418 DEPRECATED definition C{B{prec}=5} I{for backward compatibility}. 

419 @kwarg sep: Separator to join (C{str}) or C{None} to return an unjoined 2- or 

420 3-C{tuple} of C{str}s. 

421 @kwarg prec: Precison C{B{prec}=0}, the number of I{decimal} digits (C{int}) or 

422 if negative, the number of I{units to drop}, like MGRS U{PRECISION 

423 <https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html#PRECISION>}. 

424 

425 @return: This OSGR as (C{str}), C{"GD meter meter"} or if C{B{GD}=False} 

426 C{"easting,northing"} or if C{B{GD}=False} and C{B{prec} > 0} 

427 C{"easting.d,northing.d"} 

428 

429 @note: OSGR easting and northing values are truncated, not rounded. 

430 

431 @raise OSGRError: If C{B{GD} not in (None, True, False)} or if C{B{prec} 

432 < -4} and C{B{GD}=False}. 

433 

434 @raise ValueError: Invalid B{C{prec}}. 

435 ''' 

436 def _i2c(i): 

437 if i > 7: 

438 i += 1 

439 return chr(_ord_A + i) 

440 

441 GD, prec = _GD_prec2(GD, sep=sep, **prec) 

442 

443 if GD: 

444 E, e = divmod(self.easting, _100km) 

445 N, n = divmod(self.northing, _100km) 

446 E, N = int(E), int(N) 

447 if 0 > E or E > 6 or \ 

448 0 > N or N > 12: 

449 raise OSGRError(E=E, e=e, N=N, n=n, prec=prec, sep=sep) 

450 N = 19 - N 

451 EN = _i2c( N - (N % 5) + (E + 10) // 5) + \ 

452 _i2c((N * 5) % 25 + (E % 5)) 

453 t = enstr2(e, n, prec, EN) 

454 s = sep 

455 

456 elif prec <= -_EN_WIDE: 

457 raise OSGRError(GD=GD, prec=prec, sep=sep) 

458 else: 

459 t = enstr2(self.easting, self.northing, prec, dot=True, 

460 wide=_EN_WIDE + 1) 

461 s = sep if sep is None else (sep or _COMMA_) 

462 

463 return t if s is None else s.join(t) 

464 

465 

466def _GD_prec2(GD, **prec_et_al): 

467 '''(INTERNAL) Handle C{prec} backward compatibility. 

468 ''' 

469 if GD is None: # old C{prec} 5+ or neg 

470 prec = _xkwds_get(prec_et_al, prec=_EN_WIDE) 

471 GD = prec > 0 

472 prec = (prec - _EN_WIDE) if GD else -prec 

473 elif isbool(GD): 

474 prec = _xkwds_get(prec_et_al, prec=0) 

475 else: 

476 raise OSGRError(GD=GD, **prec_et_al) 

477 return GD, prec 

478 

479 

480def _ll2datum(ll, datum, name): 

481 '''(INTERNAL) Convert datum if needed. 

482 ''' 

483 if datum: 

484 try: 

485 if ll.datum != datum: 

486 ll = ll.toDatum(datum) 

487 except (AttributeError, TypeError, ValueError) as x: 

488 raise _TypeError(cause=x, datum=datum.name, **{name: ll}) 

489 return ll 

490 

491 

492def _ll2LatLon3(ll, LatLon, datum, LatLon_kwds): 

493 '''(INTERNAL) Convert C{ll} to C{LatLon} 

494 ''' 

495 n = nameof(ll) 

496 if LatLon is None: 

497 r = _ll2datum(ll, datum, typename(LatLonDatum3Tuple)) 

498 r = LatLonDatum3Tuple(r.lat, r.lon, r.datum, name=n) 

499 else: # must be ellipsoidal 

500 _xsubclassof(_LLEB, LatLon=LatLon) 

501 r = _ll2datum(ll, datum, typename(LatLon)) 

502 r = LatLon(r.lat, r.lon, datum=r.datum, **_xkwds(LatLon_kwds, name=n)) 

503 if r._iteration != ll._iteration: 

504 r._iteration = ll._iteration 

505 return r 

506 

507 

508def parseOSGR(strOSGR, Osgr=Osgr, **name_Osgr_kwds): 

509 '''Parse a string representing an OS Grid Reference, consisting of C{"[GD] 

510 easting northing"}. 

511 

512 Accepts standard OS Grid References like "SU 387 148", with or without 

513 whitespace separators, from 2- up to 22-digit references, or all-numeric, 

514 comma-separated references in meters, for example "438700,114800". 

515 

516 @arg strOSGR: An OSGR coordinate (C{str}). 

517 @kwarg Osgr: Optional class to return the OSGR coordinate (L{Osgr}) or C{None}. 

518 @kwarg name_Osgr_kwds: Optional C{B{name}=NN} (C{str}) and optionally, additional 

519 B{C{Osgr}} keyword arguments, ignored if C{B{Osgr} is None}. 

520 

521 @return: An (B{C{Osgr}}) instance or if C{B{Osgr} is None}, an 

522 L{EasNor2Tuple}C{(easting, northing)}. 

523 

524 @raise OSGRError: Invalid B{C{strOSGR}}. 

525 ''' 

526 def _c2i(G): 

527 g = ord(G.upper()) - _ord_A 

528 if g > 7: 

529 g -= 1 

530 if g < 0 or g > 25: 

531 raise ValueError 

532 return g 

533 

534 def _OSGR(strOSGR, Osgr, kwds): 

535 s = _splituple(strOSGR.strip()) 

536 p = len(s) 

537 if not p: 

538 raise ValueError 

539 g = s[0] 

540 if p == 2 and isfloat(g, both=True): # "easting,northing" 

541 e, n, m = _enstr2m3(*s, wide=_EN_WIDE + 1) 

542 

543 else: 

544 if p == 1: # "GReastingnorthing" 

545 s = halfs2(g[2:]) 

546 g = g[:2] 

547 elif p == 2: # "GReasting northing" 

548 s = g[2:], s[1] # for backward ... 

549 g = g[:2] # ... compatibility 

550 elif p != 3: 

551 raise ValueError 

552 else: # "GR easting northing" 

553 s = s[1:] 

554 

555 e, n = map(_c2i, g) 

556 n, m = divmod(n, 5) 

557 E = ((e - 2) % 5) * 5 + m 

558 N = 19 - (e // 5) * 5 - n 

559 if 0 > E or E > 6 or \ 

560 0 > N or N > 12: 

561 raise ValueError 

562 

563 e, n, m = _enstr2m3(*s, wide=_EN_WIDE) 

564 e += E * _100km 

565 n += N * _100km 

566 

567 name, kwds = _name2__(**kwds) 

568 if Osgr is None: 

569 _ = _MODS.osgr.Osgr(e, n, resolution=m) # validate 

570 r = EasNor2Tuple(e, n, name=name) 

571 else: 

572 r = Osgr(e, n, name=name, **_xkwds(kwds, resolution=m)) 

573 return r 

574 

575 return _parseX(_OSGR, strOSGR, Osgr, name_Osgr_kwds, 

576 strOSGR=strOSGR, Error=OSGRError) 

577 

578 

579def toOsgr(latlon, lon=None, kTM=False, datum=_WGS84, Osgr=Osgr, # MCCABE 14 

580 **prec_name_Osgr_kwds): 

581 '''Convert a lat-/longitude point to an OSGR coordinate. 

582 

583 @arg latlon: Latitude (C{degrees}) or an (ellipsoidal) geodetic 

584 C{LatLon} point. 

585 @kwarg lon: Optional longitude in degrees (scalar or C{None}). 

586 @kwarg kTM: If C{True}, use I{Karney}'s Krüger method from 

587 module L{ktm}, otherwise use the Ordnance Survey 

588 formulation (C{bool}). 

589 @kwarg datum: Optional datum to convert B{C{lat, lon}} from 

590 (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or 

591 L{a_f2Tuple}). 

592 @kwarg Osgr: Optional class to return the OSGR coordinate 

593 (L{Osgr}) or C{None}. 

594 @kwarg prec_name_Osgr_kwds: Optional C{B{name}=NN} (C{str}), 

595 optional L{truncate} precision C{B{prec}=ndigits} 

596 and additional B{C{Osgr}} keyword arguments, 

597 ignored if C{B{Osgr} is None}. 

598 

599 @return: An (B{C{Osgr}}) instance or if C{B{Osgr} is None} 

600 an L{EasNor2Tuple}C{(easting, northing)}. 

601 

602 @note: If L{isint}C{(B{prec})} both easting and northing are 

603 L{truncate}d to the given number of digits. 

604 

605 @raise OSGRError: Invalid B{C{latlon}} or B{C{lon}}. 

606 

607 @raise TypeError: Non-ellipsoidal B{C{latlon}} or invalid 

608 B{C{datum}}, B{C{Osgr}}, B{C{Osgr_kwds}} 

609 or conversion to C{Datums.OSGB36} failed. 

610 ''' 

611 if lon is not None: 

612 try: 

613 lat, lon = _MODS.dms.parseDMS2(latlon, lon) 

614 latlon = _LLEB(lat, lon, datum=datum) 

615 except Exception as x: 

616 raise OSGRError(latlon=latlon, lon=lon, datum=datum, cause=x) 

617 elif not isinstance(latlon, _LLEB): 

618 raise _TypeError(latlon=latlon, txt=_not_(_ellipsoidal_)) 

619 

620 NG = _NG 

621 # convert latlon to OSGB36 first 

622 ll = _ll2datum(latlon, NG.datum, _latlon_) 

623 

624 if kTM: 

625 e, n = NG.forward2(ll) 

626 

627 else: 

628 try: 

629 a, b = ll.philam 

630 except AttributeError: 

631 a, b = map1(radians, ll.lat, ll.lon) 

632 

633 sa, ca, ta = sincostan3(a) 

634 v, v_r, n2 = NG.nu_rho_eta3(sa) 

635 

636 m0 = NG.Mabcd0(a) 

637 b -= NG.lam0 

638 t = b * sa * v / 2 

639 d = b * ca 

640 d2 = d**2 

641 

642 ta2 = -(ta**2) 

643 ta4 = ta2**2 

644 

645 e = (d * v * ( 1 + # Horner-like 

646 d2 / 6 * (_Fsumf_(v_r, ta2) + 

647 d2 / 20 * _Fsumf_(5, 18 * ta2, ta4, 14 * n2, 

648 58 * n2 * ta2)))).fsum_(NG.eas0) 

649 

650 n = (d * t * ( 1 + # Horner-like 

651 d2 / 12 * (_Fsumf_( 5, ta2, 9 * n2) + 

652 d2 / 30 * _Fsumf_(61, ta4, 58 * ta2)))).fsum_(m0, NG.nor0) 

653 

654 t, kwds = _name2__(prec_name_Osgr_kwds, _or_nameof=latlon) 

655 if kwds: 

656 p, kwds = _xkwds_pop2(kwds, prec=MISSING) 

657 if p is not MISSING: 

658 e = truncate(e, p) 

659 n = truncate(n, p) 

660 

661 if Osgr is None: 

662 _ = _MODS.osgr.Osgr(e, n) # validate 

663 r = EasNor2Tuple(e, n, name=t) 

664 else: 

665 r = Osgr(e, n, name=t, **kwds) # datum=NG.datum 

666 if lon is None and isinstance(latlon, _LLEB): 

667 if kTM: 

668 r._latlonTM = latlon # XXX weakref(latlon)? 

669 else: 

670 r._latlon = latlon # XXX weakref(latlon)? 

671 return r 

672 

673 

674if __name__ == _DMAIN_: 

675 

676 from pygeodesy import printf 

677 from random import random, seed 

678 from time import localtime 

679 

680 seed(localtime().tm_yday) 

681 

682 def _rnd(X, n): 

683 X -= 2 

684 d = set() 

685 while len(d) < n: 

686 r = 1 + int(random() * X) 

687 if r not in d: 

688 d.add(r) 

689 yield r 

690 

691 D = _NG.datum 

692 i = t = 0 

693 t1 = t2 = 0, 0, 0, 0 

694 for e in _rnd(_NG.easX, 256): 

695 for n in _rnd(_NG.norX, 512): 

696 p = False 

697 t += 1 

698 

699 g = Osgr(e, n) 

700 v = g.toLatLon(kTM=False, datum=D) 

701 k = g.toLatLon(kTM=True, datum=D) 

702 d = max(fabs(v.lat - k.lat), fabs(v.lon - k.lon)) 

703 if d > t1[2]: 

704 t1 = e, n, d, t 

705 p = True 

706 

707 ll = _LLEB((v.lat + k.lat) / 2, 

708 (v.lon + k.lon) / 2, datum=D) 

709 v = ll.toOsgr(kTM=False) 

710 k = ll.toOsgr(kTM=True) 

711 d = max(fabs(v.easting - k.easting), 

712 fabs(v.northing - k.northing)) 

713 if d > t2[2]: 

714 t2 = ll.lat, ll.lon, d, t 

715 p = True 

716 

717 if p: 

718 i += 1 

719 printf('%5d: %s %s', i, 

720 'll(%.2f, %.2f) %.3e %d' % t2, 

721 'en(%d, %d) %.3e %d' % t1) 

722 printf('%d total %s', t, D.name) 

723 

724# **) MIT License 

725# 

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

727# 

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

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

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

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

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

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

734# 

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

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

737# 

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

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

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

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

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

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

744# OTHER DEALINGS IN THE SOFTWARE. 

745 

746# % python3 -m pygeodesy.osgr 

747# 1: ll(53.42, -0.59) 4.672e-07 1 en(493496, 392519) 2.796e-11 1 

748# 2: ll(60.86, -0.28) 2.760e-05 2 en(493496, 1220986) 2.509e-10 2 

749# 3: ll(61.41, -0.25) 3.045e-05 13 en(493496, 1281644) 2.774e-10 13 

750# 4: ll(61.41, -0.25) 3.045e-05 13 en(493496, 1192797) 3.038e-10 20 

751# 5: ll(61.41, -0.25) 3.045e-05 13 en(493496, 1192249) 3.073e-10 120 

752# 6: ll(61.55, -0.24) 3.120e-05 160 en(493496, 1192249) 3.073e-10 120 

753# 7: ll(61.55, -0.24) 3.122e-05 435 en(493496, 1192249) 3.073e-10 120 

754# 8: ll(61.57, -0.24) 3.130e-05 473 en(493496, 1192249) 3.073e-10 120 

755# 9: ll(58.66, -8.56) 8.084e-04 513 en(19711, 993800) 3.020e-06 513 

756# 10: ll(52.83, -7.65) 8.156e-04 518 en(19711, 993800) 3.020e-06 513 

757# 11: ll(51.55, -7.49) 8.755e-04 519 en(19711, 993800) 3.020e-06 513 

758# 12: ll(60.20, -8.87) 9.439e-04 521 en(19711, 1165686) 4.318e-06 521 

759# 13: ll(60.45, -8.92) 9.668e-04 532 en(19711, 1194002) 4.588e-06 532 

760# 14: ll(61.17, -9.08) 1.371e-03 535 en(19711, 1274463) 5.465e-06 535 

761# 15: ll(61.31, -9.11) 1.463e-03 642 en(19711, 1290590) 5.663e-06 642 

762# 16: ll(61.35, -9.12) 1.488e-03 807 en(19711, 1294976) 5.718e-06 807 

763# 17: ll(61.38, -9.13) 1.510e-03 929 en(19711, 1298667) 5.765e-06 929 

764# 18: ll(61.11, -9.24) 1.584e-03 11270 en(10307, 1268759) 6.404e-06 11270 

765# 19: ll(61.20, -9.26) 1.650e-03 11319 en(10307, 1278686) 6.545e-06 11319 

766# 20: ll(61.23, -9.27) 1.676e-03 11383 en(10307, 1282514) 6.600e-06 11383 

767# 21: ll(61.36, -9.30) 1.776e-03 11437 en(10307, 1297037) 6.816e-06 11437 

768# 22: ll(61.38, -9.30) 1.789e-03 11472 en(10307, 1298889) 6.844e-06 11472 

769# 23: ll(61.25, -9.39) 1.885e-03 91137 en(4367, 1285831) 7.392e-06 91137 

770# 24: ll(61.32, -9.40) 1.944e-03 91207 en(4367, 1293568) 7.519e-06 91207 

771# 25: ll(61.34, -9.41) 1.963e-03 91376 en(4367, 1296061) 7.561e-06 91376 

772# 26: ll(61.37, -9.41) 1.986e-03 91595 en(4367, 1298908) 7.608e-06 91595 

773# 131072 total OSGB36