pax_global_header00006660000000000000000000000064147623046100014515gustar00rootroot0000000000000052 comment=ff3ba48ece7796d6c2161eca39a2d72bac80eae9 utm-0.8.1/000077500000000000000000000000001476230461000123305ustar00rootroot00000000000000utm-0.8.1/.github/000077500000000000000000000000001476230461000136705ustar00rootroot00000000000000utm-0.8.1/.github/FUNDING.yml000066400000000000000000000000701476230461000155020ustar00rootroot00000000000000github: Turbo87 custom: https://paypal.me/tobiasbieniek utm-0.8.1/.github/workflows/000077500000000000000000000000001476230461000157255ustar00rootroot00000000000000utm-0.8.1/.github/workflows/ci.yml000066400000000000000000000030471476230461000170470ustar00rootroot00000000000000name: CI on: push: branches: - master - "v*" tags: - "v*" pull_request: schedule: - cron: '0 3 * * *' # daily, at 3am jobs: tests: strategy: fail-fast: true matrix: python-version: - "3.13" - "3.12" - "3.11" - "3.10" - "3.9" - "3.8" numpy-version: - false - "1.x" - "2.x" exclude: - python-version: "3.13" numpy-version: "1.x" - python-version: "3.8" numpy-version: "2.x" name: "Tests (Python v${{ matrix.python-version }}, NumPy: ${{ matrix.numpy-version }})" runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: pip install -r requirements.txt - run: pip install -r numpy-${{ matrix.numpy-version }}-requirements.txt if: matrix.numpy-version != false - run: pytest -v --cov=utm --color=yes release: needs: tests if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') name: Release runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.13" - run: pip install -r release-requirements.txt - run: python -m build - uses: pypa/gh-action-pypi-publish@v1.12.4 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} utm-0.8.1/.gitignore000066400000000000000000000005021476230461000143150ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts var sdist develop-eggs .installed.cfg lib lib64 MANIFEST # Installer logs pip-log.txt # Unit test / coverage reports .pytest_cache .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject utm-0.8.1/CHANGELOG.rst000066400000000000000000000046231476230461000143560ustar00rootroot00000000000000Changelog ========= v0.8.1 ------ * Add python_version to bundle metadata, for pypi (#134, #135) v0.8.0 ------ * Add support for Python 3.10, 3.11, 3.12 and 3.13 * Drop support for Python 2.7, 3.5, 3.6, 3.7 and 3.8 * Add version (#62) * Convert all tests to pytest (#65) * Port to setuptools (#89) * Add long description for PyPi (#99) * Fix numpy array being modified in place (#86) * Fix ``latlon_to_zone_number()`` returning bogus zone 61 for longitude 180 (#110) * Fix forcing zones around equator and add ``force_northern`` in ``from_latlon()`` (#124) * Improve ``to_latlon()`` accuracy (#120) * Update all (test) dependencies, taking into account supported Python versions (e.g. #116, #128) * Add ``zone_letter_to_central_latitude()`` as a counterpart to ``zone_number_to_central_longitude()`` (#130) * Bring CI script into the 2024 realm v0.7.0 ------ * Add support for Python 3.7, 3.8 and 3.9 (#54) * Drop support for Python 3.4 v0.6.0 ------ * Drop support for Python 2.6 and 3.3 (#53) * Improve documentation (#50) * Fix issue near anti-meridian when forcing zones (#47) * Improve ``to_latlon()`` accuracy (#49) v0.5.0 ------ * Add zone checking when forced * Implement numpy support * Fix UTM zones boundaries v0.4.2 ------ * added optional ``strict`` option to ``to_latlon()`` * added ``LICENSE`` file v0.4.1 ------ * fixed missing zone letter for latitude 84 deg. * fixed ``from_lat_lon()`` longitude error message * fixed zone numbers for 32V and related regions v0.4.0 ------ * added optional ``force_zone_number`` parameter to ``from_latlon()`` (`#8 `_) * fixed minor precision error (`#9 `_) v0.3.1 ------ * added optional ``northern`` parameter to ``to_latlon()`` * use `py.test `_ instead of `nosetest` v0.3.0 ------ * return floats from ``from_latlon()`` v0.2.5 ------ * more unit tests v0.2.4 ------ * performance improvements v0.2.3 ------ * `TravisCI `_ support v0.2.2 ------ * support for lowercase zone letters * documentation fixes * raise ``OutOfRangeError`` exception for bad input parameters v0.2.1 ------ * install utm-converter properly v0.2.0 ------ * added unit tests v0.1.0 ------ * initial release utm-0.8.1/LICENSE000066400000000000000000000021141476230461000133330ustar00rootroot00000000000000MIT License Copyright (c) 2012-2017 Tobias Bieniek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. utm-0.8.1/MANIFEST.in000066400000000000000000000000431476230461000140630ustar00rootroot00000000000000include README.rst include LICENSE utm-0.8.1/README.rst000066400000000000000000000104651476230461000140250ustar00rootroot00000000000000utm === Bidirectional UTM-WGS84 converter for python Usage ----- .. code-block:: python >>> import utm Latitude/Longitude to UTM ^^^^^^^^^^^^^^^^^^^^^^^^^ Convert a ``(latitude, longitude)`` tuple into an UTM coordinate: .. code-block:: python >>> utm.from_latlon(51.2, 7.5) (395201.3103811303, 5673135.241182375, 32, 'U') The syntax is ``utm.from_latlon(LATITUDE, LONGITUDE)``. The return has the form ``(EASTING, NORTHING, ZONE_NUMBER, ZONE_LETTER)``. You can also use NumPy arrays for ``LATITUDE`` and ``LONGITUDE``. In the result ``EASTING`` and ``NORTHING`` will have the same shape. ``ZONE_NUMBER`` and ``ZONE_LETTER`` are scalars and will be calculated for the first point of the input. All other points will be set into the same UTM zone. Therefore it's a good idea to make sure all points are near each other. .. code-block:: python >>> utm.from_latlon(np.array([51.2, 49.0]), np.array([7.5, 8.4])) (array([395201.31038113, 456114.59586214]), array([5673135.24118237, 5427629.20426126]), 32, 'U') UTM to Latitude/Longitude ^^^^^^^^^^^^^^^^^^^^^^^^^ Convert an UTM coordinate into a ``(latitude, longitude)`` tuple: .. code-block:: python >>> utm.to_latlon(340000, 5710000, 32, 'U') (51.51852098408468, 6.693872395145327) The syntax is ``utm.to_latlon(EASTING, NORTHING, ZONE_NUMBER, ZONE_LETTER)``. The return has the form ``(LATITUDE, LONGITUDE)``. You can also use NumPy arrays for ``EASTING`` and ``NORTHING``. In the result ``LATITUDE`` and ``LONGITUDE`` will have the same shape. ``ZONE_NUMBER`` and ``ZONE_LETTER`` are scalars. .. code-block:: python >>> utm.to_latlon(np.array([395200, 456100]), np.array([5673100, 5427600]), 32, 'U') (array([51.19968297, 48.99973627]), array([7.49999141, 8.3998036 ])) Since the zone letter is not strictly needed for the conversion you may also the ``northern`` parameter instead, which is a named parameter and can be set to either ``True`` or ``False``. Have a look at the unit tests to see how it can be used. The UTM coordinate system is explained on `this `_ Wikipedia page. Speed ----- The library has been compared to the more generic pyproj library by running the unit test suite through pyproj instead of utm. These are the results: * with pyproj (without projection cache): 4.0 - 4.5 sec * with pyproj (with projection cache): 0.9 - 1.0 sec * with utm: 0.4 - 0.5 sec NumPy arrays bring another speed improvement (on a different computer than the previous test). Using ``utm.from_latlon(x, y)`` to convert one million points: * one million calls (``x`` and ``y`` are floats): 1,000,000 × 90µs = 90s * one call (``x`` and ``y`` are numpy arrays of one million points): 0.26s Development ----------- Create a new ``virtualenv`` and install the library via ``pip install -e .``. After that install the ``pytest`` package via ``pip install pytest`` and run the unit test suite by calling ``pytest``. Changelog --------- see `CHANGELOG.rst `_ file Authors ------- * Bart van Andel * Tobias Bieniek * Torstein I. Bø License ------- Copyright (C) 2012 Tobias Bieniek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. utm-0.8.1/numpy-1.x-requirements.txt000066400000000000000000000001511476230461000173630ustar00rootroot00000000000000numpy==1.24.4; python_version < '3.9' numpy==1.26.4; python_version >= '3.9' and python_version < '3.13' utm-0.8.1/numpy-2.x-requirements.txt000066400000000000000000000002541476230461000173700ustar00rootroot00000000000000numpy==2.0.0; python_version >= '3.9' and python_version < '3.10' numpy==2.1.0; python_version >= '3.10' and python_version < '3.13' numpy==2.2.0; python_version >= '3.13' utm-0.8.1/release-requirements.txt000066400000000000000000000000061476230461000172260ustar00rootroot00000000000000build utm-0.8.1/renovate.json000066400000000000000000000001451476230461000150460ustar00rootroot00000000000000{ "extends": [ "config:base", ":dependencyDashboard", ":semanticCommitsDisabled" ] } utm-0.8.1/requirements.txt000066400000000000000000000001431476230461000156120ustar00rootroot00000000000000pytest==8.3.5 pytest-cov==5.0.0; python_version < '3.9' pytest-cov==6.0.0; python_version >= '3.9' utm-0.8.1/scripts/000077500000000000000000000000001476230461000140175ustar00rootroot00000000000000utm-0.8.1/scripts/utm-converter000077500000000000000000000027641476230461000165700ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import print_function import argparse import utm parser = argparse.ArgumentParser(description='Bidirectional UTM-WGS84 converter for python') subparsers = parser.add_subparsers() parser_latlon = subparsers.add_parser('latlon', help='Convert a latitude/longitude pair WGS84 to UTM') parser_latlon.add_argument('latitude', type=float, help='Latitude of the WGS84 coordinate') parser_latlon.add_argument('longitude', type=float, help='Longitude of the WGS84 coordinate') parser_utm = subparsers.add_parser('utm', help='Convert a UTM coordinate to WGS84') parser_utm.add_argument('easting', type=int, help='Easting component of the UTM coordinate') parser_utm.add_argument('northing', type=int, help='Northing component of the UTM coordinate') parser_utm.add_argument('zone_number', type=int, help='Zone number of the UTM coordinate') parser_utm.add_argument('zone_letter', help='Zone letter of the UTM coordinate') args = parser.parse_args() if all(arg in args for arg in ['easting', 'northing', 'zone_number', 'zone_letter']): if args.zone_letter == '': parser_utm.print_usage() print("utm-converter utm: error: too few arguments") exit() coordinate = utm.to_latlon(args.easting, args.northing, args.zone_number, args.zone_letter) elif all(arg in args for arg in ['latitude', 'longitude']): coordinate = utm.from_latlon(args.latitude, args.longitude) print(','.join(str(component) for component in coordinate)) utm-0.8.1/setup.py000066400000000000000000000020311476230461000140360ustar00rootroot00000000000000from setuptools import setup from utm._version import __version__ from pathlib import Path this_directory = Path(__file__).parent long_description = (this_directory / "README.rst").read_text() setup( name='utm', version=__version__, author='Tobias Bieniek', author_email='Tobias.Bieniek@gmx.de', url='https://github.com/Turbo87/utm', description='Bidirectional UTM-WGS84 converter for python', long_description=long_description, long_description_content_type='text/x-rst', keywords=['utm', 'wgs84', 'coordinate', 'converter'], classifiers=[ 'Programming Language :: Python', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Development Status :: 4 - Beta', 'Environment :: Other Environment', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering :: GIS', ], packages=['utm'], python_requires=">=3.8", scripts=['scripts/utm-converter'], ) utm-0.8.1/test/000077500000000000000000000000001476230461000133075ustar00rootroot00000000000000utm-0.8.1/test/__init__.py000066400000000000000000000000001476230461000154060ustar00rootroot00000000000000utm-0.8.1/test/test_utm.py000077500000000000000000000346621476230461000155430ustar00rootroot00000000000000from __future__ import division import utm as UTM import functools import pytest try: import numpy as np use_numpy = True except ImportError: use_numpy = False def assert_utm_equal(a, b): if use_numpy and isinstance(b[0], np.ndarray): assert np.allclose(a[0], b[0]) assert np.allclose(a[1], b[1]) else: assert a[0] == pytest.approx(b[0], abs=1) assert a[1] == pytest.approx(b[1], abs=1) assert a[2] == b[2] assert a[3].upper() == b[3].upper() def assert_latlon_equal(a, b): if use_numpy and isinstance(b[0], np.ndarray): def longitude_close(lon1, lon2, rtol=1e-4, atol=1e-4): # Check if longitudes are close after normalization is_close = functools.partial(np.isclose, lon1, rtol=rtol, atol=atol) return is_close(lon2) or is_close(lon2 - 360) or is_close(lon2 + 360) assert np.allclose(a[0], b[0], rtol=1e-4, atol=1e-4) if isinstance(a[1], np.ndarray): assert all(longitude_close(lon_a, lon_b) for lon_a, lon_b in zip(a[1].flatten(), b[1].flatten())) else: assert all(longitude_close(a[1], lon_b) for lon_b in b[1].flatten()) else: assert a[0] == pytest.approx(b[0], 4) assert ( a[1] == pytest.approx(b[1], 4) or a[1] == pytest.approx(b[1] - 360, 4) or a[1] == pytest.approx(b[1] + 360, 4) ) known_values = [ # Aachen, Germany ( (50.77535, 6.08389), (294409, 5628898, 32, "U"), {"northern": True}, ), # New York, USA ( (40.71435, -74.00597), (583960, 4507523, 18, "T"), {"northern": True}, ), # Wellington, New Zealand ( (-41.28646, 174.77624), (313784, 5427057, 60, "G"), {"northern": False}, ), # Capetown, South Africa ( (-33.92487, 18.42406), (261878, 6243186, 34, "H"), {"northern": False}, ), # Mendoza, Argentina ( (-32.89018, -68.84405), (514586, 6360877, 19, "h"), {"northern": False}, ), # Fairbanks, Alaska, USA ( (64.83778, -147.71639), (466013, 7190568, 6, "W"), {"northern": True}, ), # Ben Nevis, Scotland, UK ( (56.79680, -5.00601), (377486, 6296562, 30, "V"), {"northern": True}, ), # Bergen, Norway ( (60.38952, 5.320675), (297264, 6700454, 32, "V"), {"northern": True}, ), # Alkefjellet, Spitsbergen, Svalbard ( (79.45574, 18.76338), (576830, 8823320, 33, "X"), {"northern": True}, ), # Latitude 84 ( (84, -5.00601), (476594, 9328501, 30, "X"), {"northern": True}, ), # East-most point on the Equator ( (0, 180), (166021, 0, 1, "N"), {"northern": True}, ), # West-most point on the Equator ( (0, -180), (166021, 0, 1, "N"), {"northern": True} ), ] @pytest.mark.parametrize("latlon, utm, utm_kw", known_values) def test_from_latlon(latlon, utm, utm_kw): """from_latlon should give known result with known input""" result = UTM.from_latlon(*latlon) assert_utm_equal(utm, result) @pytest.mark.skipif(not use_numpy, reason="numpy not installed") @pytest.mark.parametrize("latlon, utm, utm_kw", known_values) def test_from_latlon_numpy(latlon, utm, utm_kw): result = UTM.from_latlon(*[np.array([x]) for x in latlon]) assert_utm_equal(utm, result) @pytest.mark.skipif(not use_numpy, reason="numpy not installed") def test_from_latlon_numpy_static(): lats = np.array([0.0, 3.0, 6.0]) lons = np.array([0.0, 1.0, 3.4]) result = UTM.from_latlon(lats, lons) assert_utm_equal( ( np.array( [166021.44317933032, 277707.83075574087, 544268.12794623] ), np.array([0.0, 331796.29167519242, 663220.7198366751]), 31, "N", ), result, ) @pytest.mark.parametrize("latlon, utm, utm_kw", known_values) def test_to_latlon(latlon, utm, utm_kw): """to_latlon should give known result with known input""" result = UTM.to_latlon(*utm) assert_latlon_equal(latlon, result) result = UTM.to_latlon(*utm[0:3], **utm_kw) assert_latlon_equal(latlon, result) @pytest.mark.skipif(not use_numpy, reason="numpy not installed") @pytest.mark.parametrize("latlon, utm, utm_kw", known_values) def test_to_latlon_numpy(latlon, utm, utm_kw): utm = [np.array([x]) for x in utm[:2]] + list(utm[2:]) result = UTM.to_latlon(*utm) assert_latlon_equal(latlon, result) @pytest.mark.skipif(not use_numpy, reason="numpy not installed") def test_to_latlon_numpy_static(): result = UTM.to_latlon( np.array([166021.44317933032, 277707.83075574087, 544268.12794623]), np.array([0.0, 331796.29167519242, 663220.7198366751]), 31, northern=True, ) assert_latlon_equal( (np.array([0.0, 3.0, 6.0]), np.array([0.0, 1.0, 3.4])), result ) def test_from_latlon_range_ok(): """from_latlon should work for good values""" for i in range(-8000, 8400): assert UTM.from_latlon(i / 100, 0) for i in range(-18000, 18000): assert UTM.from_latlon(0, i / 100) @pytest.mark.parametrize( "lat, lon", [ (-100, 0), (-80.1, 0), (84.1, 0), (100, 0), (0, -300), (0, -180.1), (0, 180.1), (0, 300), (-100, -300), (100, -300), (-100, 300), (100, 300), ], ) def test_from_latlon_range_fails(lat, lon): """from_latlon should fail with out-of-bounds input""" with pytest.raises(UTM.OutOfRangeError): UTM.from_latlon(lat, lon) @pytest.mark.parametrize( "lat, lon, force_zone_number, force_zone_letter", [(40.71435, -74.00597, 70, "T"), (40.71435, -74.00597, 18, "A")], ) def test_from_latlon_range_forced_fails( lat, lon, force_zone_number, force_zone_letter ): """from_latlon should fail with out-of-bounds input""" with pytest.raises(UTM.OutOfRangeError): UTM.from_latlon(lat, lon, force_zone_number, force_zone_letter) def test_to_latlon_range_ok(): """to_latlon should work for good values""" for i in range(100000, 999999, 1000): assert UTM.to_latlon(i, 5000000, 32, "U") for i in range(10, 10000000, 1000): assert UTM.to_latlon(500000, i, 32, "U") for i in range(1, 60): assert UTM.to_latlon(500000, 5000000, i, "U") for i in range(ord("C"), ord("X")): i = chr(i) if i != "I" and i != "O": UTM.to_latlon(500000, 5000000, 32, i) @pytest.mark.parametrize( "easting, northing, zone_number, zone_letter", [ (0, 5000000, 32, "U"), (99999, 5000000, 32, "U"), (1000000, 5000000, 32, "U"), (100000000000, 5000000, 32, "U"), (500000, -100000, 32, "U"), (500000, -1, 32, "U"), (500000, 10000001, 32, "U"), (500000, 50000000, 32, "U"), (500000, 5000000, 0, "U"), (500000, 5000000, 61, "U"), (500000, 5000000, 1000, "U"), (500000, 5000000, 32, "A"), (500000, 5000000, 32, "B"), (500000, 5000000, 32, "I"), (500000, 5000000, 32, "O"), (500000, 5000000, 32, "Y"), (500000, 5000000, 32, "Z"), ], ) def test_to_latlon_range_checks(easting, northing, zone_number, zone_letter): """to_latlon should fail with out-of-bounds input""" with pytest.raises(UTM.OutOfRangeError): UTM.to_latlon(0, 5000000, 32, "U") @pytest.mark.parametrize( "lat, lon, expected_number, expected_letter", [ # test inside: (56, 3, 32, "V"), (56, 6, 32, "V"), (56, 9, 32, "V"), (56, 11.999999, 32, "V"), (60, 3, 32, "V"), (60, 6, 32, "V"), (60, 9, 32, "V"), (60, 11.999999, 32, "V"), (63.999999, 3, 32, "V"), (63.999999, 6, 32, "V"), (63.999999, 9, 32, "V"), (63.999999, 11.999999, 32, "V"), # test left of: (55.999999, 2.999999, 31, "U"), (56, 2.999999, 31, "V"), (60, 2.999999, 31, "V"), (63.999999, 2.999999, 31, "V"), (64, 2.999999, 31, "W"), # test right of: (55.999999, 12, 33, "U"), (56, 12, 33, "V"), (60, 12, 33, "V"), (63.999999, 12, 33, "V"), (64, 12, 33, "W"), # test below: (55.999999, 3, 31, "U"), (55.999999, 6, 32, "U"), (55.999999, 9, 32, "U"), (55.999999, 11.999999, 32, "U"), (55.999999, 12, 33, "U"), # test above: (64, 3, 31, "W"), (64, 6, 32, "W"), (64, 9, 32, "W"), (64, 11.999999, 32, "W"), (64, 12, 33, "W"), # test edge: (0, 180, 1, "N"), (0, -180, 1, "N"), (84, 180, 1, "X"), (84, -180, 1, "X"), ], ) def test_from_latlon_zones(lat, lon, expected_number, expected_letter): result = UTM.from_latlon(lat, lon) assert result[2] == expected_number assert result[3].upper() == expected_letter.upper() @pytest.mark.parametrize( "lat, lon, expected_number", [ (40, 0, 31), (40, 5.999999, 31), (40, 6, 32), (72, 0, 31), (72, 5.999999, 31), (72, 6, 31), (72, 8.999999, 31), (72, 9, 33), ], ) def test_limits(lat, lon, expected_number): assert UTM.from_latlon(lat, lon)[2] == expected_number @pytest.mark.parametrize( "zone_number, zone_letter", [ (10, "C"), (10, "X"), (10, "p"), (10, "q"), (20, "X"), (1, "X"), (60, "e"), ], ) def test_valid_zones(zone_number, zone_letter): # should not raise any exceptions assert UTM.check_valid_zone(zone_number, zone_letter) is None @pytest.mark.parametrize( "zone_number, zone_letter", [(-100, "C"), (20, "I"), (20, "O"), (0, "O")] ) def test_invalid_zones(zone_number, zone_letter): with pytest.raises(UTM.OutOfRangeError): UTM.check_valid_zone(zone_number, zone_letter) @pytest.mark.parametrize( "lat, lon, utm, utm_kw, expected_number, expected_letter", [ (40.71435, -74.00597, 19, "T", 19, "T"), (40.71435, -74.00597, 17, "T", 17, "T"), (40.71435, -74.00597, 18, "u", 18, "U"), (40.71435, -74.00597, 18, "S", 18, "S"), ], ) def test_force_zone(lat, lon, utm, utm_kw, expected_number, expected_letter): # test forcing zone ranges # NYC should be zone 18T result = UTM.from_latlon(lat, lon, utm, utm_kw) assert result[2] == expected_number 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) def test_force_east(): # Force point just west of anti-meridian to east zone 1 assert_equal_lon(UTM.from_latlon(0, 179.9, 1, "N"), 179.9) def test_force_west(): # Force point just east of anti-meridian to west zone 60 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__ @pytest.mark.skipif(not use_numpy, reason="numpy not installed") def test_numpy_args_not_modified(): TEST_EASTING = 387358.0 TEST_NORTHING = 8145567.0 easting = np.array(TEST_EASTING) northing = np.array(TEST_NORTHING) zone = 55 letter = "K" UTM.to_latlon(easting, northing, zone, letter) assert easting == TEST_EASTING assert northing == TEST_NORTHING @pytest.mark.parametrize( "zone_number, expected_lon", [ (1, -177), (12, -111), (16, -87), (31, 3), (37, 39), ], ) def test_zone_number_to_central_longitude(zone_number, expected_lon): lon = UTM.zone_number_to_central_longitude(zone_number) assert lon == expected_lon @pytest.mark.parametrize( "zone_letter, expected_lat", [ ("X", 78), ("C", -76), ("E", -60), ("F", -52), ("Q", 20), ], ) def test_zone_letter_to_central_latitude(zone_letter, expected_lat): lat = UTM.zone_letter_to_central_latitude(zone_letter) assert lat == expected_lat utm-0.8.1/utm/000077500000000000000000000000001476230461000131355ustar00rootroot00000000000000utm-0.8.1/utm/__init__.py000066400000000000000000000004021476230461000152420ustar00rootroot00000000000000from utm.conversion import to_latlon, from_latlon, latlon_to_zone_number, latitude_to_zone_letter, check_valid_zone, zone_number_to_central_longitude, zone_letter_to_central_latitude from utm.error import OutOfRangeError from utm._version import __version__ utm-0.8.1/utm/_version.py000066400000000000000000000000261476230461000153310ustar00rootroot00000000000000__version__ = "0.8.1" utm-0.8.1/utm/conversion.py000066400000000000000000000255231476230461000157030ustar00rootroot00000000000000from __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_letter(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 check_valid_zone_number(zone_number): if not 1 <= zone_number <= 60: raise OutOfRangeError('zone number out of range (must be between 1 and 60)') def check_valid_zone(zone_number, zone_letter): check_valid_zone_number(zone_number) if zone_letter: check_valid_zone_letter(zone_letter) 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 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 (North) or False (South) as an alternative to providing a zone letter. 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 northern else northing - 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, force_northern=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]_ 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 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_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) 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 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) 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))) 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 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] # Normalize longitude to be in the range [-180, 180) longitude = (longitude % 360 + 540) % 360 - 180 # Special zone for Norway if 56 <= latitude < 64 and 3 <= longitude < 12: return 32 # Special zones for Svalbard 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): check_valid_zone_number(zone_number) return (zone_number - 1) * 6 - 180 + 3 def zone_letter_to_central_latitude(zone_letter): check_valid_zone_letter(zone_letter) zone_letter = zone_letter.upper() if zone_letter == 'X': return 78 else: return -76 + (ZONE_LETTERS.index(zone_letter) * 8) utm-0.8.1/utm/error.py000066400000000000000000000000541476230461000146370ustar00rootroot00000000000000class OutOfRangeError(ValueError): pass