Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
13 contributors

Users who have contributed to this file

@Turbo87 @tibnor @psotres @TheBB @arnaldorusso @eumiro @Hubble1942 @sdhiscocks @hogend @paulhovey @astrojuanlu @bartvanandel
from __future__ import division
from utm.error import OutOfRangeError
# For most use cases in this module, numpy is indistinguishable
# from math, except it also works on numpy arrays
try:
import numpy as mathlib
use_numpy = True
except ImportError:
import math as mathlib
use_numpy = False
__all__ = ['to_latlon', 'from_latlon']
K0 = 0.9996
E = 0.00669438
E2 = E * E
E3 = E2 * E
E_P2 = E / (1 - E)
SQRT_E = mathlib.sqrt(1 - E)
_E = (1 - SQRT_E) / (1 + SQRT_E)
_E2 = _E * _E
_E3 = _E2 * _E
_E4 = _E3 * _E
_E5 = _E4 * _E
M1 = (1 - E / 4 - 3 * E2 / 64 - 5 * E3 / 256)
M2 = (3 * E / 8 + 3 * E2 / 32 + 45 * E3 / 1024)
M3 = (15 * E2 / 256 + 45 * E3 / 1024)
M4 = (35 * E3 / 3072)
P2 = (3 / 2 * _E - 27 / 32 * _E3 + 269 / 512 * _E5)
P3 = (21 / 16 * _E2 - 55 / 32 * _E4)
P4 = (151 / 96 * _E3 - 417 / 128 * _E5)
P5 = (1097 / 512 * _E4)
R = 6378137
ZONE_LETTERS = "CDEFGHJKLMNPQRSTUVWXX"
def in_bounds(x, lower, upper, upper_strict=False):
if upper_strict and use_numpy:
return lower <= mathlib.min(x) and mathlib.max(x) < upper
elif upper_strict and not use_numpy:
return lower <= x < upper
elif use_numpy:
return lower <= mathlib.min(x) and mathlib.max(x) <= upper
return lower <= x <= upper
def check_valid_zone(zone_number, zone_letter):
if not 1 <= zone_number <= 60:
raise OutOfRangeError('zone number out of range (must be between 1 and 60)')
if zone_letter:
zone_letter = zone_letter.upper()
if not 'C' <= zone_letter <= 'X' or zone_letter in ['I', 'O']:
raise OutOfRangeError('zone letter out of range (must be between C and X)')
def mixed_signs(x):
return use_numpy and mathlib.min(x) < 0 and mathlib.max(x) >= 0
def negative(x):
if use_numpy:
return mathlib.max(x) < 0
return x < 0
def mod_angle(value):
"""Returns angle in radians to be between -pi and pi"""
return (value + mathlib.pi) % (2 * mathlib.pi) - mathlib.pi
def to_latlon(easting, northing, zone_number, zone_letter=None, northern=None, strict=True):
"""This function converts UTM coordinates to Latitude and Longitude
Parameters
----------
easting: int or NumPy array
Easting value of UTM coordinates
northing: int or NumPy array
Northing value of UTM coordinates
zone_number: int
Zone number is represented with global map numbers of a UTM zone
numbers map. For more information see utmzones [1]_
zone_letter: str
Zone letter can be represented as string values. UTM zone
designators can be seen in [1]_
northern: bool
You can set True or False to set this parameter. Default is None
strict: bool
Raise an OutOfRangeError if outside of bounds
Returns
-------
latitude: float or NumPy array
Latitude between 80 deg S and 84 deg N, e.g. (-80.0 to 84.0)
longitude: float or NumPy array
Longitude between 180 deg W and 180 deg E, e.g. (-180.0 to 180.0).
.. _[1]: http://www.jaworski.ca/utmzones.htm
"""
if not zone_letter and northern is None:
raise ValueError('either zone_letter or northern needs to be set')
elif zone_letter and northern is not None:
raise ValueError('set either zone_letter or northern, but not both')
if strict:
if not in_bounds(easting, 100000, 1000000, upper_strict=True):
raise OutOfRangeError('easting out of range (must be between 100,000 m and 999,999 m)')
if not in_bounds(northing, 0, 10000000):
raise OutOfRangeError('northing out of range (must be between 0 m and 10,000,000 m)')
check_valid_zone(zone_number, zone_letter)
if zone_letter:
zone_letter = zone_letter.upper()
northern = (zone_letter >= 'N')
x = easting - 500000
y = northing
if not northern:
y -= 10000000
m = y / K0
mu = m / (R * M1)
p_rad = (mu +
P2 * mathlib.sin(2 * mu) +
P3 * mathlib.sin(4 * mu) +
P4 * mathlib.sin(6 * mu) +
P5 * mathlib.sin(8 * mu))
p_sin = mathlib.sin(p_rad)
p_sin2 = p_sin * p_sin
p_cos = mathlib.cos(p_rad)
p_tan = p_sin / p_cos
p_tan2 = p_tan * p_tan
p_tan4 = p_tan2 * p_tan2
ep_sin = 1 - E * p_sin2
ep_sin_sqrt = mathlib.sqrt(1 - E * p_sin2)
n = R / ep_sin_sqrt
r = (1 - E) / ep_sin
c = E_P2 * p_cos**2
c2 = c * c
d = x / (n * K0)
d2 = d * d
d3 = d2 * d
d4 = d3 * d
d5 = d4 * d
d6 = d5 * d
latitude = (p_rad - (p_tan / r) *
(d2 / 2 -
d4 / 24 * (5 + 3 * p_tan2 + 10 * c - 4 * c2 - 9 * E_P2)) +
d6 / 720 * (61 + 90 * p_tan2 + 298 * c + 45 * p_tan4 - 252 * E_P2 - 3 * c2))
longitude = (d -
d3 / 6 * (1 + 2 * p_tan2 + c) +
d5 / 120 * (5 - 2 * c + 28 * p_tan2 - 3 * c2 + 8 * E_P2 + 24 * p_tan4)) / p_cos
longitude = mod_angle(longitude + mathlib.radians(zone_number_to_central_longitude(zone_number)))
return (mathlib.degrees(latitude),
mathlib.degrees(longitude))
def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=None):
"""This function converts Latitude and Longitude to UTM coordinate
Parameters
----------
latitude: float or NumPy array
Latitude between 80 deg S and 84 deg N, e.g. (-80.0 to 84.0)
longitude: float or NumPy array
Longitude between 180 deg W and 180 deg E, e.g. (-180.0 to 180.0).
force_zone_number: int
Zone number is represented by global map numbers of an UTM zone
numbers map. You may force conversion to be included within one
UTM zone number. For more information see utmzones [1]_
force_zone_letter: str
You may force conversion to be included within one UTM zone
letter. For more information see utmzones [1]_
Returns
-------
easting: float or NumPy array
Easting value of UTM coordinates
northing: float or NumPy array
Northing value of UTM coordinates
zone_number: int
Zone number is represented by global map numbers of a UTM zone
numbers map. More information see utmzones [1]_
zone_letter: str
Zone letter is represented by a string value. UTM zone designators
can be accessed in [1]_
.. _[1]: http://www.jaworski.ca/utmzones.htm
"""
if not in_bounds(latitude, -80, 84):
raise OutOfRangeError('latitude out of range (must be between 80 deg S and 84 deg N)')
if not in_bounds(longitude, -180, 180):
raise OutOfRangeError('longitude out of range (must be between 180 deg W and 180 deg E)')
if force_zone_number is not None:
check_valid_zone(force_zone_number, force_zone_letter)
lat_rad = mathlib.radians(latitude)
lat_sin = mathlib.sin(lat_rad)
lat_cos = mathlib.cos(lat_rad)
lat_tan = lat_sin / lat_cos
lat_tan2 = lat_tan * lat_tan
lat_tan4 = lat_tan2 * lat_tan2
if force_zone_number is None:
zone_number = latlon_to_zone_number(latitude, longitude)
else:
zone_number = force_zone_number
if force_zone_letter is None:
zone_letter = latitude_to_zone_letter(latitude)
else:
zone_letter = force_zone_letter
lon_rad = mathlib.radians(longitude)
central_lon = zone_number_to_central_longitude(zone_number)
central_lon_rad = mathlib.radians(central_lon)
n = R / mathlib.sqrt(1 - E * lat_sin**2)
c = E_P2 * lat_cos**2
a = lat_cos * mod_angle(lon_rad - central_lon_rad)
a2 = a * a
a3 = a2 * a
a4 = a3 * a
a5 = a4 * a
a6 = a5 * a
m = R * (M1 * lat_rad -
M2 * mathlib.sin(2 * lat_rad) +
M3 * mathlib.sin(4 * lat_rad) -
M4 * mathlib.sin(6 * lat_rad))
easting = K0 * n * (a +
a3 / 6 * (1 - lat_tan2 + c) +
a5 / 120 * (5 - 18 * lat_tan2 + lat_tan4 + 72 * c - 58 * E_P2)) + 500000
northing = K0 * (m + n * lat_tan * (a2 / 2 +
a4 / 24 * (5 - lat_tan2 + 9 * c + 4 * c**2) +
a6 / 720 * (61 - 58 * lat_tan2 + lat_tan4 + 600 * c - 330 * E_P2)))
if mixed_signs(latitude):
raise ValueError("latitudes must all have the same sign")
elif negative(latitude):
northing += 10000000
return easting, northing, zone_number, zone_letter
def latitude_to_zone_letter(latitude):
# If the input is a numpy array, just use the first element
# User responsibility to make sure that all points are in one zone
if use_numpy and isinstance(latitude, mathlib.ndarray):
latitude = latitude.flat[0]
if -80 <= latitude <= 84:
return ZONE_LETTERS[int(latitude + 80) >> 3]
else:
return None
def latlon_to_zone_number(latitude, longitude):
# If the input is a numpy array, just use the first element
# User responsibility to make sure that all points are in one zone
if use_numpy:
if isinstance(latitude, mathlib.ndarray):
latitude = latitude.flat[0]
if isinstance(longitude, mathlib.ndarray):
longitude = longitude.flat[0]
if 56 <= latitude < 64 and 3 <= longitude < 12:
return 32
if 72 <= latitude <= 84 and longitude >= 0:
if longitude < 9:
return 31
elif longitude < 21:
return 33
elif longitude < 33:
return 35
elif longitude < 42:
return 37
return int((longitude + 180) / 6) + 1
def zone_number_to_central_longitude(zone_number):
return (zone_number - 1) * 6 - 180 + 3