1

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

3

4 u'''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

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

8

9 A 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

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

13

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

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

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

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

18

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

20 Karney'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>},

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

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

24 and 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

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

28

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

30 _splituple, _xsubclassof, typename

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

32 from pygeodesy.datums import Datums, _ellipsoidal_datum, _WGS84

33# from pygeodesy.dms import parseDMS2 # _MODS

34 from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB

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

36 _xkwds, _xkwds_get, _xkwds_pop2

37 from pygeodesy.fmath import Fdot, fpowers

38 from pygeodesy.fsums import _Fsumf_

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

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

41 _COMMASPACE_, _DMAIN_, _DOT_, _not_, \

42 _ellipsoidal_, _latlon_, _SPACE_

43 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

44 from pygeodesy.named import _name2__, _NamedBase, nameof

45 from pygeodesy.namedTuples import EasNor2Tuple, LatLon2Tuple, \

46 LatLonDatum3Tuple

47 from pygeodesy.props import Property_RO, property_RO

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

49 _resolution10, unstr, _xzipairs

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

51 Phid, Scalar, _10um, _100km

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

53

54 from 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

65 class _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

180 class OSGRError(_ValueError):

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

182 '''

183 pass

184

185

186 class 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

466 def _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

480 def _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

492 def _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

508 def 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

579 def 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

674 if __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