From a652b54f189507b4bf9e3717152bf7c612fa5c7e Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Sat, 28 Mar 2020 16:50:51 +0000 Subject: [PATCH 1/4] Fix forcing zones around equator and add force_northern in from_latlon Logic in from_latlon has been changed to be similar to to_latlon, including adding a force_northern option to mirror northern option in to_latlon. This resolves the issue with northing coordinate incorrectly being set when pasing in -ve or +ve latitude when forcing northern or southern zone respectively. Closes #35 - alternative fix for issue. --- test/test_utm.py | 43 +++++++++++++++++++++++++++++++++++++++++++ utm/conversion.py | 33 +++++++++++++++++---------------- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/test/test_utm.py b/test/test_utm.py index 8511b2a..6435005 100755 --- a/test/test_utm.py +++ b/test/test_utm.py @@ -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) @@ -396,6 +402,43 @@ 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") +@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]), 31, zone) + for expected_lat, easting, northing in zip(lats, *result[:2]): + assert_equal_lat( + (easting, northing, result[2], result[3]), expected_lat) + + +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__ diff --git a/utm/conversion.py b/utm/conversion.py index d3d6a6f..e03628e 100644 --- a/utm/conversion.py +++ b/utm/conversion.py @@ -62,16 +62,6 @@ def check_valid_zone(zone_number, zone_letter): 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 @@ -97,7 +87,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 @@ -184,7 +175,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 @@ -204,6 +195,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 @@ -227,6 +223,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) @@ -243,11 +241,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) @@ -275,9 +278,7 @@ def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=N 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): + if not northern: northing += 10000000 return easting, northing, zone_number, zone_letter From c32fab87d8e7fa0edacff94d65fc660aa5cb393e Mon Sep 17 00:00:00 2001 From: Heath Henley Date: Mon, 21 Oct 2024 18:01:11 -0400 Subject: [PATCH 2/4] Keep default behavior and new option --- test/test_utm.py | 17 +++++++++++++++-- utm/conversion.py | 9 +++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/test/test_utm.py b/test/test_utm.py index 6435005..401eb00 100755 --- a/test/test_utm.py +++ b/test/test_utm.py @@ -426,10 +426,23 @@ 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]), 31, zone) + with pytest.raises(ValueError, + match="latitudes must all have the same sign"): + UTM.from_latlon(lats, np.array([0, 0]), 31, zone) + + +@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) + (easting, northing, result[2], result[3]), expected_lat, + northern=force_northern) def test_force_both(): diff --git a/utm/conversion.py b/utm/conversion.py index e03628e..fea30ef 100644 --- a/utm/conversion.py +++ b/utm/conversion.py @@ -62,6 +62,10 @@ def check_valid_zone(zone_number, zone_letter): 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 mod_angle(value): """Returns angle in radians to be between -pi and pi""" return (value + mathlib.pi) % (2 * mathlib.pi) - mathlib.pi @@ -107,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') @@ -278,7 +281,9 @@ def from_latlon(latitude, longitude, force_zone_number=None, force_zone_letter=N a4 / 24 * (5 - lat_tan2 + 9 * c + 4 * c**2) + a6 / 720 * (61 - 58 * lat_tan2 + lat_tan4 + 600 * c - 330 * E_P2))) - if not northern: + if force_northern is None and mixed_signs(latitude): + raise ValueError("latitudes must all have the same sign") + elif not northern: northing += 10000000 return easting, northing, zone_number, zone_letter From 6c2a9b4abfd045c2df4a78e36fdf594afc8474ef Mon Sep 17 00:00:00 2001 From: Heath Henley Date: Tue, 22 Oct 2024 07:22:09 -0400 Subject: [PATCH 3/4] Add test to cover default and fix forced zone behavior --- test/test_utm.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/test_utm.py b/test/test_utm.py index 401eb00..b7946d4 100755 --- a/test/test_utm.py +++ b/test/test_utm.py @@ -420,15 +420,26 @@ def test_force_south(): 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]) - with pytest.raises(ValueError, - match="latitudes must all have the same sign"): - UTM.from_latlon(lats, np.array([0, 0]), 31, zone) + 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") From b6d666af35856733201c3a3082753041b816e50a Mon Sep 17 00:00:00 2001 From: Heath Henley Date: Tue, 22 Oct 2024 07:38:41 -0400 Subject: [PATCH 4/4] Skip check if force northern or force zone is given --- utm/conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utm/conversion.py b/utm/conversion.py index fea30ef..a549bc9 100644 --- a/utm/conversion.py +++ b/utm/conversion.py @@ -280,8 +280,8 @@ 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 force_northern is None and 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 not northern: northing += 10000000