pax_global_header00006660000000000000000000000064137574413610014525gustar00rootroot0000000000000052 comment=670875367bed009bd5384e1b1abdc11c1acdb2cf utm-0.7.0/000077500000000000000000000000001375744136100123365ustar00rootroot00000000000000utm-0.7.0/.github/000077500000000000000000000000001375744136100136765ustar00rootroot00000000000000utm-0.7.0/.github/workflows/000077500000000000000000000000001375744136100157335ustar00rootroot00000000000000utm-0.7.0/.github/workflows/ci.yml000066400000000000000000000024031375744136100170500ustar00rootroot00000000000000name: CI on: push: branches: - master - "v*" tags: pull_request: schedule: - cron: '0 3 * * *' # daily, at 3am jobs: tests: strategy: fail-fast: true matrix: python-version: - "3.9" - "3.8" - "3.7" - "3.6" - "3.5" - "2.7" numpy: - true - false name: "Tests (Python v${{ matrix.python-version }}, NumPy: ${{ matrix.numpy }})" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - run: pip install -r requirements.txt - run: pip install -r requirements-numpy.txt if: matrix.numpy == true - 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@v2 - uses: actions/setup-python@v1 with: python-version: 3.9 - run: python setup.py sdist - uses: pypa/gh-action-pypi-publish@v1.4.1 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} utm-0.7.0/.gitignore000066400000000000000000000005021375744136100143230ustar00rootroot00000000000000*.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.7.0/CHANGELOG.rst000066400000000000000000000030721375744136100143610ustar00rootroot00000000000000Changelog ========= 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.7.0/LICENSE000066400000000000000000000021141375744136100133410ustar00rootroot00000000000000MIT 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.7.0/MANIFEST.in000066400000000000000000000000431375744136100140710ustar00rootroot00000000000000include README.rst include LICENSE utm-0.7.0/README.rst000066400000000000000000000107551375744136100140350ustar00rootroot00000000000000utm === .. image:: https://travis-ci.org/Turbo87/utm.png .. image:: https://img.shields.io/badge/License-MIT-yellow.svg :target: https://github.com/Turbo87/utm/blob/master/LICENSE 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.7.0/requirements-numpy.txt000066400000000000000000000000151375744136100167640ustar00rootroot00000000000000numpy==1.16.6utm-0.7.0/requirements.txt000066400000000000000000000000411375744136100156150ustar00rootroot00000000000000pytest==4.6.11 pytest-cov==2.10.1utm-0.7.0/scripts/000077500000000000000000000000001375744136100140255ustar00rootroot00000000000000utm-0.7.0/scripts/utm-converter000077500000000000000000000027061375744136100165720ustar00rootroot00000000000000#!/usr/bin/env python 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.7.0/setup.py000066400000000000000000000013771375744136100140600ustar00rootroot00000000000000from distutils.core import setup setup( name='utm', version='0.7.0', author='Tobias Bieniek', author_email='Tobias.Bieniek@gmx.de', url='https://github.com/Turbo87/utm', description='Bidirectional UTM-WGS84 converter for python', 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'], scripts=['scripts/utm-converter'], ) utm-0.7.0/test/000077500000000000000000000000001375744136100133155ustar00rootroot00000000000000utm-0.7.0/test/__init__.py000066400000000000000000000000001375744136100154140ustar00rootroot00000000000000utm-0.7.0/test/test_utm.py000077500000000000000000000325151375744136100155440ustar00rootroot00000000000000import utm as UTM import unittest try: import numpy as np use_numpy = True except ImportError: use_numpy = False class UTMTestCase(unittest.TestCase): def assert_utm_equal(self, a, b): if use_numpy and isinstance(b[0], np.ndarray): self.assertTrue(np.allclose(a[0], b[0])) self.assertTrue(np.allclose(a[1], b[1])) else: self.assertAlmostEqual(a[0], b[0], 0) self.assertAlmostEqual(a[1], b[1], 0) self.assertEqual(a[2], b[2]) self.assertEqual(a[3].upper(), b[3].upper()) def assert_latlon_equal(self, a, b): if use_numpy and isinstance(b[0], np.ndarray): self.assertTrue(np.allclose(a[0], b[0], rtol=1e-4, atol=1e-4)) self.assertTrue(np.allclose(a[1], b[1], rtol=1e-4, atol=1e-4)) else: self.assertAlmostEqual(a[0], b[0], 4) self.assertAlmostEqual(a[1], b[1], 4) class KnownValues(UTMTestCase): 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}, ), # Latitude 84 ( (84, -5.00601), (476594, 9328501, 30, 'X'), {'northern': True}, ), ] def test_from_latlon(self): '''from_latlon should give known result with known input''' for latlon, utm, _ in self.known_values: result = UTM.from_latlon(*latlon) self.assert_utm_equal(utm, result) def test_from_latlon_numpy(self): if not use_numpy: return lats = np.array([0.0, 3.0, 6.0]) lons = np.array([0.0, 1.0, 3.4]) result = UTM.from_latlon(lats, lons) self.assert_utm_equal((np.array([166021.44317933032, 277707.83075574087, 544268.12794623]), np.array([0.0, 331796.29167519242, 663220.7198366751]), 31, 'N'), result) for latlon, utm, _ in self.known_values: result = UTM.from_latlon(*[np.array([x]) for x in latlon]) self.assert_utm_equal(utm, result) def test_to_latlon(self): '''to_latlon should give known result with known input''' for latlon, utm, utm_kw in self.known_values: result = UTM.to_latlon(*utm) self.assert_latlon_equal(latlon, result) result = UTM.to_latlon(*utm[0:3], **utm_kw) self.assert_latlon_equal(latlon, result) def test_to_latlon_numpy(self): if not use_numpy: return result = UTM.to_latlon(np.array([166021.44317933032, 277707.83075574087, 544268.12794623]), np.array([0.0, 331796.29167519242, 663220.7198366751]), 31, northern=True) self.assert_latlon_equal((np.array([0.0, 3.0, 6.0]), np.array([0.0, 1.0, 3.4])), result) for latlon, utm, utm_kw in self.known_values: utm = [np.array([x]) for x in utm[:2]] + list(utm[2:]) result = UTM.to_latlon(*utm) self.assert_latlon_equal(latlon, result) class BadInput(UTMTestCase): def test_from_latlon_range_checks(self): '''from_latlon should fail with out-of-bounds input''' self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, -100, 0) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, -80.1, 0) for i in range(-8000, 8400): UTM.from_latlon(i / 100.0, 0) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 84.1, 0) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 100, 0) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 0, -300) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 0, -180.1) for i in range(-18000, 18000): UTM.from_latlon(0, i / 100.0) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 0, 180.1) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 0, 300) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, -100, -300) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 100, -300) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, -100, 300) self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 100, 300) # test forcing zone ranges # NYC should be zone 18T self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 40.71435, -74.00597, 70, 'T') self.assertRaises(UTM.OutOfRangeError, UTM.from_latlon, 40.71435, -74.00597, 18, 'A') def test_to_latlon_range_checks(self): '''to_latlon should fail with out-of-bounds input''' # test easting range self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 0, 5000000, 32, 'U') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 99999, 5000000, 32, 'U') for i in range(100000, 999999, 1000): UTM.to_latlon(i, 5000000, 32, 'U') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 1000000, 5000000, 32, 'U') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 100000000000, 5000000, 32, 'U') # test northing range self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, -100000, 32, 'U') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, -1, 32, 'U') for i in range(10, 10000000, 1000): UTM.to_latlon(500000, i, 32, 'U') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 10000001, 32, 'U') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 50000000, 32, 'U') # test zone numbers self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 0, 'U') for i in range(1, 60): UTM.to_latlon(500000, 5000000, i, 'U') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 61, 'U') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 1000, 'U') # test zone letters self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 32, 'A') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 32, 'B') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 32, 'I') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 32, 'O') for i in range(ord('C'), ord('X')): i = chr(i) if i != 'I' and i != 'O': UTM.to_latlon(500000, 5000000, 32, i) self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 32, 'Y') self.assertRaises( UTM.OutOfRangeError, UTM.to_latlon, 500000, 5000000, 32, 'Z') class Zone32V(unittest.TestCase): def assert_zone_equal(self, result, expected_number, expected_letter): self.assertEqual(result[2], expected_number) self.assertEqual(result[3].upper(), expected_letter.upper()) def test_inside(self): self.assert_zone_equal(UTM.from_latlon(56, 3), 32, 'V') self.assert_zone_equal(UTM.from_latlon(56, 6), 32, 'V') self.assert_zone_equal(UTM.from_latlon(56, 9), 32, 'V') self.assert_zone_equal(UTM.from_latlon(56, 11.999999), 32, 'V') self.assert_zone_equal(UTM.from_latlon(60, 3), 32, 'V') self.assert_zone_equal(UTM.from_latlon(60, 6), 32, 'V') self.assert_zone_equal(UTM.from_latlon(60, 9), 32, 'V') self.assert_zone_equal(UTM.from_latlon(60, 11.999999), 32, 'V') self.assert_zone_equal(UTM.from_latlon(63.999999, 3), 32, 'V') self.assert_zone_equal(UTM.from_latlon(63.999999, 6), 32, 'V') self.assert_zone_equal(UTM.from_latlon(63.999999, 9), 32, 'V') self.assert_zone_equal(UTM.from_latlon(63.999999, 11.999999), 32, 'V') def test_left_of(self): self.assert_zone_equal(UTM.from_latlon(55.999999, 2.999999), 31, 'U') self.assert_zone_equal(UTM.from_latlon(56, 2.999999), 31, 'V') self.assert_zone_equal(UTM.from_latlon(60, 2.999999), 31, 'V') self.assert_zone_equal(UTM.from_latlon(63.999999, 2.999999), 31, 'V') self.assert_zone_equal(UTM.from_latlon(64, 2.999999), 31, 'W') def test_right_of(self): self.assert_zone_equal(UTM.from_latlon(55.999999, 12), 33, 'U') self.assert_zone_equal(UTM.from_latlon(56, 12), 33, 'V') self.assert_zone_equal(UTM.from_latlon(60, 12), 33, 'V') self.assert_zone_equal(UTM.from_latlon(63.999999, 12), 33, 'V') self.assert_zone_equal(UTM.from_latlon(64, 12), 33, 'W') def test_below(self): self.assert_zone_equal(UTM.from_latlon(55.999999, 3), 31, 'U') self.assert_zone_equal(UTM.from_latlon(55.999999, 6), 32, 'U') self.assert_zone_equal(UTM.from_latlon(55.999999, 9), 32, 'U') self.assert_zone_equal(UTM.from_latlon(55.999999, 11.999999), 32, 'U') self.assert_zone_equal(UTM.from_latlon(55.999999, 12), 33, 'U') def test_above(self): self.assert_zone_equal(UTM.from_latlon(64, 3), 31, 'W') self.assert_zone_equal(UTM.from_latlon(64, 6), 32, 'W') self.assert_zone_equal(UTM.from_latlon(64, 9), 32, 'W') self.assert_zone_equal(UTM.from_latlon(64, 11.999999), 32, 'W') self.assert_zone_equal(UTM.from_latlon(64, 12), 33, 'W') class TestRightBoundaries(unittest.TestCase): def assert_zone_equal(self, result, expected_number): self.assertEqual(result[2], expected_number) def test_limits(self): self.assert_zone_equal(UTM.from_latlon(40, 0), 31) self.assert_zone_equal(UTM.from_latlon(40, 5.999999), 31) self.assert_zone_equal(UTM.from_latlon(40, 6), 32) self.assert_zone_equal(UTM.from_latlon(72, 0), 31) self.assert_zone_equal(UTM.from_latlon(72, 5.999999), 31) self.assert_zone_equal(UTM.from_latlon(72, 6), 31) self.assert_zone_equal(UTM.from_latlon(72, 8.999999), 31) self.assert_zone_equal(UTM.from_latlon(72, 9), 33) class TestValidZones(unittest.TestCase): def test_valid_zones(self): # should not raise any exceptions UTM.check_valid_zone(10, 'C') UTM.check_valid_zone(10, 'X') UTM.check_valid_zone(10, 'p') UTM.check_valid_zone(10, 'q') UTM.check_valid_zone(20, 'X') UTM.check_valid_zone(1, 'X') UTM.check_valid_zone(60, 'e') def test_invalid_zones(self): self.assertRaises(UTM.OutOfRangeError, UTM.check_valid_zone, -100, 'C') self.assertRaises(UTM.OutOfRangeError, UTM.check_valid_zone, 20, 'I') self.assertRaises(UTM.OutOfRangeError, UTM.check_valid_zone, 20, 'O') self.assertRaises(UTM.OutOfRangeError, UTM.check_valid_zone, 0, 'O') class TestForcingZones(unittest.TestCase): def assert_zone_equal(self, result, expected_number, expected_letter): self.assertEqual(result[2], expected_number) self.assertEqual(result[3].upper(), expected_letter.upper()) def test_force_zone(self): # test forcing zone ranges # NYC should be zone 18T self.assert_zone_equal(UTM.from_latlon(40.71435, -74.00597, 19, 'T'), 19, 'T') self.assert_zone_equal(UTM.from_latlon(40.71435, -74.00597, 17, 'T'), 17, 'T') self.assert_zone_equal(UTM.from_latlon(40.71435, -74.00597, 18, 'u'), 18, 'U') self.assert_zone_equal(UTM.from_latlon(40.71435, -74.00597, 18, 'S'), 18, 'S') class TestForcingAntiMeridian(unittest.TestCase): def assert_equal_lon(self, result, expected_lon): _, lon = UTM.to_latlon(*result[:4], strict=False) self.assertAlmostEqual(lon, expected_lon, 4) def test_force_east(self): # Force point just west of anti-meridian to east zone 1 self.assert_equal_lon( UTM.from_latlon(0, 179.9, 1, 'N'), 179.9) def test_force_west(self): # Force point just east of anti-meridian to west zone 60 self.assert_equal_lon( UTM.from_latlon(0, -179.9, 60, 'N'), -179.9) if __name__ == '__main__': unittest.main() utm-0.7.0/utm/000077500000000000000000000000001375744136100131435ustar00rootroot00000000000000utm-0.7.0/utm/__init__.py000066400000000000000000000002321375744136100152510ustar00rootroot00000000000000from utm.conversion import to_latlon, from_latlon, latlon_to_zone_number, latitude_to_zone_letter, check_valid_zone from utm.error import OutOfRangeError utm-0.7.0/utm/conversion.py000066400000000000000000000232551375744136100157110ustar00rootroot00000000000000from 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.0 - 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.0, 84.0): raise OutOfRangeError('latitude out of range (must be between 80 deg S and 84 deg N)') if not in_bounds(longitude, -180.0, 180.0): 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 utm-0.7.0/utm/error.py000066400000000000000000000000541375744136100146450ustar00rootroot00000000000000class OutOfRangeError(ValueError): pass