diff --git a/test/test_utm.py b/test/test_utm.py index 8511b2a..b7946d4 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,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__ diff --git a/utm/conversion.py b/utm/conversion.py index d3d6a6f..a549bc9 100644 --- a/utm/conversion.py +++ b/utm/conversion.py @@ -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 @@ -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 @@ -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') @@ -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 @@ -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 @@ -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) @@ -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) @@ -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