1

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

3

4 u'''I{Karney}'s UTM and UPS utilities.

5

6 Functions L{parseUTMUPS5}, L{toUtmUps8}, L{UtmUps} and L{utmupsZoneBand5}

7 to handle both I{Universal Transverse Mercator} (U{UTM

8<https://WikiPedia.org/wiki/Universal_Transverse_Mercator_coordinate_system>})

9 and I{Universal Polar Stereographic} (U{UPS

10<https://WikiPedia.org/wiki/Universal_polar_stereographic_coordinate_system>})

11 coordinates.

12

13 A pure Python implementation, partially transcoded from C++ class U{UTMUPS

14<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UTMUPS.html>}

15 by I{Charles Karney}.

16'''

17 from pygeodesy.basics import _isin, map1

18# from pygeodesy.datums import _WGS84 # from .utmupsBase

19 from pygeodesy.errors import _IsnotError, RangeError, _ValueError, _xkwds_pop2

20 from pygeodesy.interns import NN, _easting_, _MGRS_, _northing_, _NS_, \

21 _outside_, _range_, _SPACE_, _UPS_, _UTM_

22 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

23 from pygeodesy.named import modulename

24 from pygeodesy.namedTuples import UtmUps5Tuple, UtmUps8Tuple

25# from pygeodesy.streprs import Fmt # from .utmupsBase

26 from pygeodesy.units import Northing, _100km

27 from pygeodesy.ups import parseUPS5, toUps8, Ups, UPSError, upsZoneBand5

28 from pygeodesy.utm import parseUTM5, toUtm8, Utm, UTMError, utmZoneBand5

29 from pygeodesy.utmupsBase import Fmt, _to4lldn, _to3zBhp, _UPS_ZONE, \

30 _UPS_ZONE_STR, _UTMUPS_ZONE_MIN, \

31 _UTMUPS_ZONE_MAX, _WGS84

32

33__all__ = _ALL_LAZY.utmups

34__version__ = '25.04.14'

35

36_MGRS_TILE = _100km # in .mgrs.Mgrs.tile

37

38_UPS_N_MAX = Northing( 27 * _MGRS_TILE)

39_UPS_N_MIN = Northing( 13 * _MGRS_TILE)

40_UPS_S_MAX = Northing( 32 * _MGRS_TILE)

41_UPS_S_MIN = Northing( 8 * _MGRS_TILE)

42

43_UTM_C_MAX = Northing( 9 * _MGRS_TILE)

44_UTM_C_MIN = Northing( 1 * _MGRS_TILE)

45_UTM_N_MAX = Northing( 95 * _MGRS_TILE)

46_UTM_N_MIN = Northing( 0 * _MGRS_TILE)

47_UTM_S_MAX = Northing(100 * _MGRS_TILE)

48_UTM_S_MIN = Northing( 10 * _MGRS_TILE)

49

50_UTM_N_SHIFT = _UTM_S_MAX - _UTM_N_MIN # South minus North UTM northing

51

52

53 class _UpsMinMax(object): # XXX _NamedEnum or _NamedTuple

54 # UPS ranges for North, South pole

55 eMax = _UPS_N_MAX, _UPS_S_MAX

56 eMin = _UPS_N_MIN, _UPS_S_MIN

57 nMax = _UPS_N_MAX, _UPS_S_MAX

58 nMin = _UPS_N_MIN, _UPS_S_MIN

59

60

61 class _UtmMinMax(object): # XXX _NamedEnum or _NamedTuple

62 # UTM ranges for Northern, Southern hemisphere

63 eMax = _UTM_C_MAX, _UTM_C_MAX

64 eMin = _UTM_C_MIN, _UTM_C_MIN

65 nMax = _UTM_N_MAX, Northing(_UTM_N_MAX + _UTM_N_SHIFT)

66 nMin = Northing(_UTM_S_MIN - _UTM_N_SHIFT), _UTM_S_MIN

67

68

69 class UTMUPSError(_ValueError): # XXX (UTMError, UPSError)

70 '''Universal Transverse Mercator/Universal Polar Stereographic

71 (UTM/UPS) parse, validate or other issue.

72 '''

73 pass

74

75

76 def parseUTMUPS5(strUTMUPS, datum=_WGS84, Utm=Utm, Ups=Ups, **name):

77 '''Parse a string representing a UTM or UPS coordinate, consisting

78 of C{"zone[band] hemisphere/pole easting northing"}.

79

80 @arg strUTMUPS: A UTM or UPS coordinate (C{str}).

81 @kwarg datum: Optional datum to use (L{Datum}).

82 @kwarg Utm: Optional class to return the UTM coordinate (L{Utm})

83 or C{None}.

84 @kwarg Ups: Optional class to return the UPS coordinate (L{Ups})

85 or C{None}.

86 @kwarg name: Optional B{C{Utm}} or B{C{Ups}} C{B{name}=NN} (C{str}).

87

88 @return: The UTM or UPS instance (B{C{Utm}} or B{C{Ups}}) or a

89 L{UtmUps5Tuple}C{(zone, hemipole, easting, northing, band)}

90 if B{C{Utm}} respectively B{C{Ups}} or both are C{None}.

91 The C{hemipole} is C{'N'|'S'}, the UTM hemisphere or UPS

92 pole, the UPS projection top/center.

93

94 @raise UTMUPSError: Invalid B{C{strUTMUPS}}.

95

96 @see: Functions L{pygeodesy.parseUTM5} and L{pygeodesy.parseUPS5}.

97 '''

98 try:

99 try:

100 u = parseUTM5(strUTMUPS, datum=datum, Utm=Utm, name=name)

101 except UTMError:

102 u = parseUPS5(strUTMUPS, datum=datum, Ups=Ups, name=name)

103 except (UTMError, UPSError) as x:

104 raise UTMUPSError(strUTMUPS=strUTMUPS, cause=x)

105 return u

106

107

108 def toUtmUps8(latlon, lon=None, datum=None, falsed=True, Utm=Utm, Ups=Ups,

109 pole=NN, **name_cmoff):

110 '''Convert a lat-/longitude point to a UTM or UPS coordinate.

111

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

113 geodetic C{LatLon} point.

114 @kwarg lon: Longitude (C{degrees}), required if B{C{latlon}} is C{degrees},

115 ignored otherwise.

116 @kwarg datum: Optional datum to use this UTM coordinate, overriding the

117 B{C{latlon}}'s datum (C{Datum}).

118 @kwarg falsed: If C{True}, false both easting and northing (C{bool}).

119 @kwarg Utm: Optional class to return the UTM coordinate (L{Utm}) or C{None}.

120 @kwarg Ups: Optional class to return the UPS coordinate (L{Ups}) or C{None}.

121 @kwarg pole: Optional top/center of UPS (stereographic) projection (C{str},

122 C{'N[orth]'} or C{'S[outh]'}).

123 @kwarg name_cmoff: Optional C{B{name}=NN} (C{str}) and DEPRECATED keyword

124 argument C{B{cmoff}=True} to offset the longitude from the zone's

125 central meridian (C{bool}), use B{C{falsed}} instead and I{for

126 UTM only}.

127

128 @return: The UTM or UPS coordinate (B{C{Utm}} respectively B{C{Ups}}) or a

129 L{UtmUps8Tuple}C{(zone, hemipole, easting, northing, band, datum,

130 gamma, scale)} if B{C{Utm}} respectively C{B{Ups} is None} or if

131 C{B{falsed} is False}.

132

133 @raise RangeError: If B{C{lat}} outside the valid UTM or UPS bands or if

134 B{C{lat}} or B{C{lon}} outside the valid range and

135 L{rangerrors<pygeodesy.rangerrors>} is C{True}.

136

137 @raise TypeError: If B{C{latlon}} is not ellipsoidal or B{C{lon}} is missing

138 or B{C{datum}} is invalid.

139

140 @raise UTMUPSError: UTM or UPS validation failed.

141

142 @raise ValueError: Invalid B{C{lat}} or B{C{lon}}.

143

144 @see: Functions L{pygeodesy.toUtm8} and L{pygeodesy.toUps8}.

145 '''

146 f, name = _xkwds_pop2(name_cmoff, cmoff=falsed)

147 lat, lon, d, n = _to4lldn(latlon, lon, datum, name)

148 z, _, p, lat, lon = utmupsZoneBand5(lat, lon)

149 if z == _UPS_ZONE:

150 u = toUps8(lat, lon, datum=d, falsed=f, Ups=Ups, name=n, pole=pole or p)

151 else:

152 u = toUtm8(lat, lon, datum=d, falsed=f, Utm=Utm, name=n)

153 return u

154

155

156 def UtmUps(zone, hemipole, easting, northing, band=NN, datum=_WGS84, falsed=True, **name):

157 '''Class-like function to create a UTM/UPS coordinate.

158

159 @kwarg zone: The UTM zone with/-out I{longitudinal} Band or UPS zone C{0}

160 or C{"00"} with/-out I{polar} Band (C{str} or C{int}).

161 @kwarg hemipole: UTM hemisphere or UPS top/center of projection (C{str},

162 C{'N[orth]'} or C{'S[outh]'}).

163 @arg easting: Easting, see B{C{falsed}} (C{meter}).

164 @arg northing: Northing, see B{C{falsed}} (C{meter}).

165 @kwarg band: Optional, UTM I{latitudinal} C{'C'|'D'|..|'W'|'X'} or UPS

166 I{polar} Band letter C{'A'|'B'|'Y'|'Z'} Band letter (C{str}).

167 @kwarg datum: The coordinate's datum (L{Datum}).

168 @kwarg falsed: If C{True}, both B{C{easting}} and B{C{northing}} are falsed

169 (C{bool}).

170 @kwarg name: Optional L{Utm} or L{Ups} C{B{name}=NN} (C{str}).

171

172 @return: New UTM or UPS instance (L{Utm} or L{Ups}).

173

174 @raise TypeError: Invalid B{C{datum}}.

175

176 @raise UTMUPSError: UTM or UPS validation failed.

177

178 @see: Classes L{Utm} and L{Ups} and I{Karney}'s U{UTMUPS

179 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UTMUPS.html>}.

180 '''

181 z, B, hp = _to3zBhp(zone, band, hemipole=hemipole)

182 U = Ups if _isin(z, _UPS_ZONE, _UPS_ZONE_STR) else Utm

183 return U(z, hp, easting, northing, band=B, datum=datum, falsed=falsed, **name)

184

185

186 def utmupsValidate(coord, falsed=False, MGRS=False, Error=UTMUPSError):

187 '''Check a UTM or UPS coordinate.

188

189 @arg coord: The UTM or UPS coordinate (L{Utm}, L{Etm}, L{Ups} or

190 C{5+Tuple}).

191 @kwarg falsed: If C{True}, easting and northing are falsed in the

192 C{B{coord} 5+Tuple} (C{bool}), ignored otherwise.

193 @kwarg MGRS: Increase easting and northing ranges (C{bool}).

194 @kwarg Error: Optional error to raise, overriding the default

195 (L{UTMUPSError}).

196

197 @return: C{None} if validation passed.

198

199 @raise Error: Validation failed.

200

201 @see: Function L{utmupsValidateOK}.

202 '''

203

204 def _en(en, lo, hi, ename): # U, Error

205 try:

206 if lo <= float(en) <= hi:

207 return

208 except (TypeError, ValueError):

209 pass

210 t = _SPACE_(_outside_, U, _range_, _range_(lo, hi))

211 raise Error(ename, en, txt=t)

212

213 if isinstance(coord, (Ups, Utm)):

214 hemi = coord.hemisphere

215 enMM = coord.falsed

216 elif isinstance(coord, (UtmUps5Tuple, UtmUps8Tuple)):

217 hemi = coord.hemipole

218 enMM = falsed

219 else:

220 raise _IsnotError(Error=Error, coord=coord, *map1(modulename,

221 Utm, Ups, UtmUps5Tuple, UtmUps8Tuple))

222 band = coord.band

223 zone = coord.zone

224

225 z, B, h = _to3zBhp(zone, band, hemipole=hemi)

226

227 if z == _UPS_ZONE: # UPS

228 u, U, M = _MODS.ups, _UPS_, _UpsMinMax

229 else: # UTM

230 u, U, M = _MODS.utm, _UTM_, _UtmMinMax

231

232 if MGRS:

233 U, s = _MGRS_, _MGRS_TILE

234 else:

235 s = 0

236

237 i = _NS_.find(h)

238 if i < 0 or z < _UTMUPS_ZONE_MIN \

239 or z > _UTMUPS_ZONE_MAX \

240 or B not in u._Bands:

241 t = Fmt.PAREN(U, repr(_SPACE_(NN(Fmt.zone(z), B), h)))

242 raise Error(coord=t, zone=zone, band=band, hemisphere=hemi)

243

244 if enMM:

245 _en(coord.easting, M.eMin[i] - s, M.eMax[i] + s, _easting_) # PYCHOK .eMax .eMin

246 _en(coord.northing, M.nMin[i] - s, M.nMax[i] + s, _northing_) # PYCHOK .nMax .nMin

247

248

249 def utmupsValidateOK(coord, falsed=False, ok=True):

250 '''Check a UTM or UPS coordinate.

251

252 @arg coord: The UTM or UPS coordinate (L{Utm}, L{Ups} or C{5+Tuple}).

253 @kwarg falsed: If C{True}, easting and northing are falsed in the

254 C{B{coord} 5+Tuple} (C{bool}), ignored otherwise.

255 @kwarg ok: Result to return if validation passed (B{C{ok}}).

256

257 @return: B{C{ok}} if validation passed, otherwise the L{UTMUPSError}.

258

259 @see: Function L{utmupsValidate}.

260 '''

261 try:

262 utmupsValidate(coord, falsed=falsed)

263 except UTMUPSError as x:

264 return x

265 return ok

266

267

268 def utmupsZoneBand5(lat, lon, cmoff=False, **name):

269 '''Return the UTM/UPS zone number, Band letter, hemisphere/pole

270 and clipped lat- and longitude for a given location.

271

272 @arg lat: Latitude in degrees (C{scalar} or C{str}).

273 @arg lon: Longitude in degrees (C{scalar} or C{str}).

274 @kwarg cmoff: If C{True}, offset longitude from the zone's central

275 meridian, I{for UTM only} (C{bool}).

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

277

278 @return: A L{UtmUpsLatLon5Tuple}C{(zone, band, hemipole, lat, lon)}

279 where C{hemipole} is C{'N'|'S'}, the UTM hemisphere or UPS

280 pole, projection top/center.

281

282 @raise RangeError: If B{C{lat}} outside the valid UTM or UPS bands or

283 if B{C{lat}} or B{C{lon}} outside the valid range

284 and L{rangerrors<pygeodesy.rangerrors>} is C{True}.

285

286 @raise ValueError: Invalid B{C{lat}} or B{C{lon}}.

287

288 @see: Functions L{pygeodesy.utmZoneBand5} and L{pygeodesy.upsZoneBand5}.

289 '''

290 try:

291 return utmZoneBand5(lat, lon, cmoff=cmoff, **name)

292 except RangeError:

293 return upsZoneBand5(lat, lon, **name)

294

295# **) MIT License

296#

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

298#

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

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

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

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

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

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

305#

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

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

308#

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

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

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

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

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

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

315# OTHER DEALINGS IN THE SOFTWARE.