Skip to content

Commit

Permalink
Merge pull request #124 from heathhenley/north_south_fix
Browse files Browse the repository at this point in the history
Rebase and tweaks to "Fix forcing zones around equator and add force_northern in from_latlon"
  • Loading branch information
bartvanandel authored Dec 15, 2024
2 parents f87796d + b6d666a commit 9c7cc8f
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 13 deletions.
67 changes: 67 additions & 0 deletions test/test_utm.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ def test_force_zone(lat, lon, utm, utm_kw, expected_number, expected_letter):
assert result[3].upper() == expected_letter.upper()


def assert_equal_lat(result, expected_lat, northern=None):
args = result[:3] if northern else result[:4]
lat, _ = UTM.to_latlon(*args, northern=northern, strict=False)
assert lat == pytest.approx(expected_lat, abs=0.001)


def assert_equal_lon(result, expected_lon):
_, lon = UTM.to_latlon(*result[:4], strict=False)
assert lon == pytest.approx(expected_lon, abs=0.001)
Expand All @@ -396,6 +402,67 @@ def test_force_west():
assert_equal_lon(UTM.from_latlon(0, -179.9, 60, "N"), -179.9)


def test_force_north():
# Force southern point to northern zone letter
assert_equal_lat(UTM.from_latlon(-0.1, 0, 31, 'N'), -0.1)

# Again, using force northern
assert_equal_lat(
UTM.from_latlon(-0.1, 0, 31, force_northern=True), -0.1, northern=True)


def test_force_south():
# Force northern point to southern zone letter
assert_equal_lat(UTM.from_latlon(0.1, 0, 31, 'M'), 0.1)

# Again, using force northern as False
assert_equal_lat(
UTM.from_latlon(0.1, 0, 31, force_northern=True), 0.1, northern=True)


@pytest.mark.skipif(not use_numpy, reason="numpy not installed")
def test_no_force_numpy():
# Point above and below equator
lats = np.array([-0.1, 0.1])
with pytest.raises(ValueError,
match="latitudes must all have the same sign"):
UTM.from_latlon(lats, np.array([0, 0]))


@pytest.mark.skipif(not use_numpy, reason="numpy not installed")
@pytest.mark.parametrize("zone", ('N', 'M'))
def test_force_numpy(zone):
# Point above and below equator
lats = np.array([-0.1, 0.1])

result = UTM.from_latlon(
lats, np.array([0, 0]), force_zone_letter=zone)
for expected_lat, easting, northing in zip(lats, *result[:2]):
assert_equal_lat(
(easting, northing, result[2], result[3]), expected_lat)


@pytest.mark.skipif(not use_numpy, reason="numpy not installed")
@pytest.mark.parametrize("force_northern", (True, False))
def test_force_numpy_force_northern_true(force_northern):
# Point above and below equator
lats = np.array([-0.1, 0.1])

result = UTM.from_latlon(
lats, np.array([0, 0]), force_northern=force_northern)
for expected_lat, easting, northing in zip(lats, *result[:2]):
assert_equal_lat(
(easting, northing, result[2], result[3]), expected_lat,
northern=force_northern)


def test_force_both():
# Force both letter and northern not allowed
with pytest.raises(ValueError, match="set either force_zone_letter or "
"force_northern, but not both"):
UTM.from_latlon(-0.1, 0, 31, 'N', True)


def test_version():
assert isinstance(UTM.__version__, str) and "." in UTM.__version__

Expand Down
32 changes: 19 additions & 13 deletions utm/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,6 @@ 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
Expand All @@ -97,7 +91,8 @@ def to_latlon(easting, northing, zone_number, zone_letter=None, northern=None, s
designators can be seen in [1]_
northern: bool
You can set True or False to set this parameter. Default is None
You can set True (North) or False (South) as an alternative to
providing a zone letter. Default is None
strict: bool
Raise an OutOfRangeError if outside of bounds
Expand All @@ -116,7 +111,6 @@ def to_latlon(easting, northing, zone_number, zone_letter=None, northern=None, s
"""
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')

Expand Down Expand Up @@ -184,7 +178,7 @@ def to_latlon(easting, northing, zone_number, zone_letter=None, northern=None, s
mathlib.degrees(longitude))


def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=None):
def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=None, force_northern=None):
"""This function converts Latitude and Longitude to UTM coordinate
Parameters
Expand All @@ -204,6 +198,11 @@ def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=N
You may force conversion to be included within one UTM zone
letter. For more information see utmzones [1]_
force_northern: bool
You can set True (North) or False (South) as an alternative to
forcing with a zone letter. When set, the returned zone_letter will
be None. Default is None
Returns
-------
easting: float or NumPy array
Expand All @@ -227,6 +226,8 @@ def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=N
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_letter and force_northern is not None:
raise ValueError('set either force_zone_letter or force_northern, but not both')
if force_zone_number is not None:
check_valid_zone(force_zone_number, force_zone_letter)

Expand All @@ -243,11 +244,16 @@ def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=N
else:
zone_number = force_zone_number

if force_zone_letter is None:
if force_zone_letter is None and force_northern is None:
zone_letter = latitude_to_zone_letter(latitude)
else:
zone_letter = force_zone_letter

if force_northern is None:
northern = (zone_letter >= 'N')
else:
northern = force_northern

lon_rad = mathlib.radians(longitude)
central_lon = zone_number_to_central_longitude(zone_number)
central_lon_rad = mathlib.radians(central_lon)
Expand All @@ -274,10 +280,10 @@ def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=N
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):
check_signs = force_northern is None and force_zone_letter is None
if check_signs and mixed_signs(latitude):
raise ValueError("latitudes must all have the same sign")
elif negative(latitude):
elif not northern:
northing += 10000000

return easting, northing, zone_number, zone_letter
Expand Down

0 comments on commit 9c7cc8f

Please sign in to comment.