├── .github └── workflows │ ├── pypi.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── pygc ├── __init__.py ├── gc.py └── tests │ ├── __init__.py │ ├── test_gc.py │ ├── test_gd.py │ ├── x.npy │ ├── xmask.npy │ ├── y.npy │ └── ymask.npy ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg └── setup.py /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | release: 9 | types: 10 | - published 11 | 12 | jobs: 13 | packages: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: "3.x" 22 | 23 | - name: Get tags 24 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 25 | shell: bash 26 | 27 | - name: Install build tools 28 | run: | 29 | python -m pip install --upgrade pip wheel setuptools setuptools_scm build twine 30 | 31 | shell: bash 32 | 33 | - name: Build binary wheel 34 | run: python -m build --sdist --wheel . --outdir dist 35 | 36 | - name: CheckFiles 37 | run: | 38 | ls dist 39 | shell: bash 40 | 41 | - name: Test wheels 42 | run: | 43 | cd dist && python -m pip install pygc*.whl 44 | python -m twine check * 45 | shell: bash 46 | 47 | - name: Publish a Python distribution to PyPI 48 | if: success() && github.event_name == 'release' 49 | uses: pypa/gh-action-pypi-publish@release/v1 50 | with: 51 | user: __token__ 52 | password: ${{ secrets.PYPI_PASSWORD }} 53 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | run: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | python-version: ["3.8", "3.9", "3.10"] 14 | os: [windows-latest, ubuntu-latest, macos-latest] 15 | fail-fast: false 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Setup Micromamba 21 | uses: mamba-org/setup-micromamba@v1 22 | with: 23 | micromamba-version: latest 24 | 25 | - name: Create and activate environment 26 | shell: bash -l {0} 27 | run: | 28 | micromamba create --name TEST python=${{ matrix.python-version }} --file requirements.txt --file requirements-dev.txt --channel conda-forge 29 | micromamba activate TEST 30 | python -m pip install -e . --no-deps --force-reinstall 31 | 32 | - name: Tests 33 | shell: bash -l {0} 34 | run: | 35 | micromamba activate TEST 36 | python -m pytest -rxs pygc/tests 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg* 2 | *.pyc 3 | .coverage 4 | .pytest_cache/ 5 | build/ 6 | dist/ 7 | pygc/_version.py 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: check-ast 7 | - id: debug-statements 8 | - id: check-added-large-files 9 | - id: requirements-txt-fixer 10 | - id: file-contents-sorter 11 | 12 | - repo: https://gitlab.com/pycqa/flake8 13 | rev: 3.9.2 14 | hooks: 15 | - id: flake8 16 | 17 | - repo: https://github.com/codespell-project/codespell 18 | rev: v2.1.0 19 | hooks: 20 | - id: codespell 21 | exclude: > 22 | (?x)^( 23 | .*\.yaml 24 | )$ 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kyle Wilcox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include README.md 3 | 4 | graft pygc 5 | include pyproject.toml 6 | 7 | prune *.egg-info 8 | 9 | exclude *.yml 10 | exclude .pre-commit-config.yaml 11 | exclude .gitignore 12 | exclude .isort.cfg 13 | exclude pygc/_version.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pygc [![Tests](https://github.com/axiom-data-science/pygc/actions/workflows/tests.yml/badge.svg)](https://github.com/axiom-data-science/pygc/actions/workflows/tests.yml) 2 | 3 | 4 | ## Great Circle calculations for Python 2/3 using Vincenty's formulae 5 | 6 | ### Installation 7 | 8 | **pip** 9 | `pip install pygc` 10 | 11 | **conda** 12 | `conda install -c conda-forge pygc` 13 | 14 | **development** 15 | `pip install git+https://github.com/axiom-data-science/pygc.git` 16 | 17 | 18 | ### Great Circle 19 | ```python 20 | from pygc import great_circle 21 | ``` 22 | 23 | #### New point from initial point, distance, and azimuth 24 | ```python 25 | great_circle(distance=111000, azimuth=65, latitude=30, longitude=-74) 26 | {'latitude': 30.41900364921926, 27 | 'longitude': -72.952930949727573, 28 | 'reverse_azimuth': 245.52686122611451} 29 | ``` 30 | 31 | #### Three new points in three different angles from an initial point 32 | ```python 33 | great_circle(distance=[100000, 200000, 300000], azimuth=[90, 180, -90], latitude=30, longitude=-74) 34 | {'latitude': array([29.99592067, 28.1955554, 29.96329797]), 35 | 'longitude': array([-72.96361148, -74., -77.10848799]), 36 | 'reverse_azimuth': array([270.51817296, 360., 88.44633085])} 37 | ``` 38 | 39 | #### Three point south of three initial points (longitude shouldn't change much) 40 | ```python 41 | great_circle(distance=[100000, 200000, 300000], azimuth=180, latitude=30, longitude=[-74, -75, -76]) 42 | {'latitude': array([29.09783841, 28.1955554, 27.29315337]), 43 | 'longitude': array([-74., -75., -76.]), 44 | 'reverse_azimuth': array([360., 360., 360.])} 45 | ``` 46 | 47 | #### Three point west of three initial points (latitude shouldn't change much) 48 | ```python 49 | great_circle(distance=[100000, 200000, 300000], azimuth=270, latitude=[30, 31, 32], longitude=-74) 50 | {'latitude': array([ 29.99592067, 30.98302388, 31.96029484]), 51 | 'longitude': array([-75.03638852, -76.09390011, -77.17392199]), 52 | 'reverse_azimuth': array([ 89.48182704, 88.92173899, 88.31869938])} 53 | ``` 54 | 55 | 56 | #### Starburst pattern around a point 57 | ```python 58 | great_circle(distance=100000, azimuth=[0, 60, 120, 180, 240, 300], latitude=30, longitude=-74) 59 | {'latitude': array([ 30.90203788, 30.44794729, 29.54590235, 29.09783841, 29.54590235, 30.44794729]), 60 | 'longitude': array([-74., -73.09835956, -73.10647702, -74., -74.89352298, -74.90164044]), 61 | 'reverse_azimuth': array([ 180., 240.45387965, 300.44370186, 360., 59.55629814, 119.54612035])} 62 | ``` 63 | 64 | 65 | ### Great Distance 66 | 67 | Distance between each pair of points is returned in meters. 68 | 69 | ```python 70 | from pygc import great_distance 71 | ``` 72 | 73 | #### Distance and angle between two points 74 | ```python 75 | great_distance(start_latitude=30, start_longitude=-74, end_latitude=40, end_longitude=-74) 76 | {'azimuth': 0.0, 'distance': array(1109415.6324018822), 'reverse_azimuth': 180.0} 77 | ``` 78 | 79 | #### Distance and angle between two sets of points 80 | ```python 81 | great_distance(start_latitude=[30, 35], start_longitude=[-74, -79], end_latitude=[40, 45], end_longitude=[-74, -79]) 82 | {'azimuth': array([0., 0.]), 83 | 'distance': array([1109415.63240188, 1110351.47627673]), 84 | 'reverse_azimuth': array([180., 180.])} 85 | ``` 86 | 87 | #### Distance and angle between initial point and three end points 88 | ```python 89 | great_distance(start_latitude=30, start_longitude=-74, end_latitude=[40, 45, 50], end_longitude=[-74, -74, -74]) 90 | {'azimuth': array([0., 0., 0.]), 91 | 'distance': array([1109415.63240188, 1664830.98002662, 2220733.64373152]), 92 | 'reverse_azimuth': array([180., 180., 180.])} 93 | ``` 94 | 95 | 96 | ## Source 97 | 98 | Algrothims from Geocentric Datum of Australia Technical Manual 99 | 100 | https://www.icsm.gov.au/sites/default/files/2017-09/gda-v_2.4_0.pdf 101 | Computations on the Ellipsoid 102 | 103 | There are a number of formulae that are available 104 | to calculate accurate geodetic positions, 105 | azimuths and distances on the ellipsoid. 106 | 107 | Vincenty's formulae (Vincenty, 1975) may be used 108 | for lines ranging from a few cm to nearly 20,000 km, 109 | with millimetre accuracy. 110 | The formulae have been extensively tested 111 | for the Australian region, by comparison with results 112 | from other formulae (Rainsford, 1955 & Sodano, 1965). 113 | -------------------------------------------------------------------------------- /pygc/__init__.py: -------------------------------------------------------------------------------- 1 | from pygc.gc import great_circle 2 | from pygc.gc import great_distance 3 | 4 | __all__ = ["great_circle", "great_distance"] 5 | 6 | try: 7 | from ._version import __version__ 8 | except ImportError: 9 | __version__ = "unknown" 10 | -------------------------------------------------------------------------------- /pygc/gc.py: -------------------------------------------------------------------------------- 1 | from pyproj import Geod 2 | import numpy as np 3 | 4 | 5 | def great_circle(**kwargs): 6 | """ 7 | Named arguments: 8 | distance = distance to travel, or numpy array of distances 9 | azimuth = angle, in DEGREES of HEADING from NORTH, or numpy array of azimuths 10 | latitude = latitude, in DECIMAL DEGREES, or numpy array of latitudes 11 | longitude = longitude, in DECIMAL DEGREES, or numpy array of longitudes 12 | rmajor = radius of earth's major axis. default=6378137.0 (WGS84) 13 | rminor = radius of earth's minor axis. default=6356752.3142 (WGS84) 14 | 15 | Returns a dictionary with: 16 | 'latitude' in decimal degrees 17 | 'longitude' in decimal degrees 18 | 'reverse_azimuth' in decimal degrees 19 | 20 | """ 21 | distance = kwargs.get('distance') 22 | azimuth = kwargs.get('azimuth') 23 | latitude = kwargs.get('latitude') 24 | longitude = kwargs.get('longitude') 25 | rmajor = kwargs.get('rmajor', 6378137.0) 26 | rminor = kwargs.get('rminor', 6356752.3142) 27 | 28 | # Convert inputs to numpy arrays if they are not already 29 | distance = np.atleast_1d(distance) 30 | azimuth = np.atleast_1d(azimuth) 31 | latitude = np.atleast_1d(latitude) 32 | longitude = np.atleast_1d(longitude) 33 | 34 | # Ensure all arrays have the same length 35 | max_length = max(len(distance), len(azimuth), len(latitude), len(longitude)) 36 | if len(distance) != max_length: 37 | distance = np.full(max_length, distance[0]) 38 | if len(azimuth) != max_length: 39 | azimuth = np.full(max_length, azimuth[0]) 40 | if len(latitude) != max_length: 41 | latitude = np.full(max_length, latitude[0]) 42 | if len(longitude) != max_length: 43 | longitude = np.full(max_length, longitude[0]) 44 | 45 | geod = Geod(a=rmajor, b=rminor) 46 | lon, lat, back_azimuth = geod.fwd(longitude, latitude, azimuth, distance) 47 | 48 | if isinstance(back_azimuth, (list, np.ndarray)): 49 | back_azimuth = np.array([i % 360 for i in back_azimuth]) 50 | else: 51 | back_azimuth = back_azimuth % 360 52 | 53 | return { 54 | 'latitude': np.array(lat) if isinstance(lat, (list, np.ndarray)) else lat, 55 | 'longitude': np.array(lon) if isinstance(lon, (list, np.ndarray)) else lon, 56 | 'reverse_azimuth': back_azimuth if isinstance(back_azimuth, (list, np.ndarray)) else back_azimuth 57 | } 58 | 59 | 60 | def great_distance(**kwargs): 61 | """ 62 | Named arguments: 63 | start_latitude = starting latitude, in DECIMAL DEGREES 64 | start_longitude = starting longitude, in DECIMAL DEGREES 65 | end_latitude = ending latitude, in DECIMAL DEGREES 66 | end_longitude = ending longitude, in DECIMAL DEGREES 67 | rmajor = radius of earth's major axis. default=6378137.0 (WGS84) 68 | rminor = radius of earth's minor axis. default=6356752.3142 (WGS84) 69 | 70 | Returns a dictionary with: 71 | 'distance' in meters 72 | 'azimuth' in decimal degrees 73 | 'reverse_azimuth' in decimal degrees 74 | 75 | """ 76 | final_mask = None 77 | start_latitude = kwargs.get('start_latitude') 78 | start_longitude = kwargs.get('start_longitude') 79 | end_latitude = kwargs.get('end_latitude') 80 | end_longitude = kwargs.get('end_longitude') 81 | rmajor = kwargs.get('rmajor', 6378137.0) 82 | rminor = kwargs.get('rminor', 6356752.3142) 83 | 84 | # Handle cases where inputs are mask arrays 85 | if (np.ma.isMaskedArray(start_latitude) or 86 | np.ma.isMaskedArray(start_longitude) or 87 | np.ma.isMaskedArray(end_latitude) or 88 | np.ma.isMaskedArray(end_longitude) 89 | ): 90 | 91 | try: 92 | assert start_latitude.size == start_longitude.size == end_latitude.size == end_longitude.size 93 | except AttributeError: 94 | raise ValueError("All or none of the inputs should be masked") 95 | except AssertionError: 96 | raise ValueError("When using masked arrays all must be of equal size") 97 | 98 | final_mask = np.logical_not((start_latitude.mask | start_longitude.mask | end_latitude.mask | end_longitude.mask)) 99 | if np.isscalar(final_mask): 100 | final_mask = np.full(start_latitude.size, final_mask, dtype=bool) 101 | start_latitude = start_latitude[final_mask].data 102 | start_longitude = start_longitude[final_mask].data 103 | end_latitude = end_latitude[final_mask].data 104 | end_longitude = end_longitude[final_mask].data 105 | 106 | # Handle cases where either start or end are multiple points 107 | else: 108 | start_latitude = np.atleast_1d(start_latitude) 109 | start_longitude = np.atleast_1d(start_longitude) 110 | end_latitude = np.atleast_1d(end_latitude) 111 | end_longitude = np.atleast_1d(end_longitude) 112 | varlist = [start_latitude, start_longitude, end_latitude, end_longitude] 113 | 114 | max_length = max([len(i) for i in varlist]) 115 | if max_length > 1: 116 | for i in range(len(varlist)): 117 | if len(varlist[i]) == 1: 118 | varlist[i] = np.full(max_length, varlist[i][0]) 119 | else: 120 | varlist[i] = np.array(varlist[i]) 121 | 122 | start_latitude, start_longitude, end_latitude, end_longitude = varlist 123 | 124 | geod = Geod(a=rmajor, b=rminor) 125 | azimuth, back_azimuth, distance = geod.inv(start_longitude, start_latitude, end_longitude, end_latitude) 126 | 127 | if isinstance(back_azimuth, (list, np.ndarray)): 128 | back_azimuth = np.array([i % 360 for i in back_azimuth]) 129 | else: 130 | back_azimuth = back_azimuth % 360 131 | 132 | if final_mask is not None: 133 | distance_d = np.ma.masked_all(final_mask.size, dtype=np.float64) 134 | azimuth_d = np.ma.masked_all(final_mask.size, dtype=np.float64) 135 | back_azimuth_d = np.ma.masked_all(final_mask.size, dtype=np.float64) 136 | 137 | distance_d[final_mask] = distance 138 | azimuth_d[final_mask] = azimuth 139 | back_azimuth_d[final_mask] = back_azimuth 140 | 141 | return { 142 | 'distance': distance_d, 143 | 'azimuth': azimuth_d, 144 | 'reverse_azimuth': back_azimuth_d 145 | } 146 | 147 | else: 148 | return { 149 | 'distance': np.array(distance) if isinstance(distance, (list, np.ndarray)) else distance, 150 | 'azimuth': np.array(azimuth) if isinstance(azimuth, (list, np.ndarray)) else azimuth, 151 | 'reverse_azimuth': back_azimuth if isinstance(back_azimuth, (list, np.ndarray)) else back_azimuth 152 | } 153 | -------------------------------------------------------------------------------- /pygc/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiom-data-science/pygc/f784902bcd0c30746f5297116a658fcbdde445cb/pygc/tests/__init__.py -------------------------------------------------------------------------------- /pygc/tests/test_gc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pygc import great_circle 4 | 5 | 6 | def test_great_circle_scalars(): 7 | # One decimal degree is 111000m 8 | latitude = 40.0 9 | longitude = -76.0 10 | 11 | azimuth = 90 12 | new_gc = great_circle( 13 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 14 | ) 15 | # We should have gone to the right 16 | assert new_gc["longitude"] > longitude + 0.9 17 | 18 | azimuth = 270 19 | new_gc = great_circle( 20 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 21 | ) 22 | # We should have gone to the left 23 | assert new_gc["longitude"] < longitude - 0.9 24 | 25 | azimuth = 180 26 | new_gc = great_circle( 27 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 28 | ) 29 | # We should have gone down 30 | assert new_gc["latitude"] < latitude - 0.9 31 | 32 | azimuth = 0 33 | new_gc = great_circle( 34 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 35 | ) 36 | # We should have gone up 37 | assert new_gc["latitude"] > latitude + 0.9 38 | 39 | azimuth = 315 40 | new_gc = great_circle( 41 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 42 | ) 43 | # We should have gone up and to the left 44 | assert new_gc["latitude"] > latitude + 0.45 45 | assert new_gc["longitude"] < longitude - 0.45 46 | 47 | 48 | def test_great_circle_numpy(): 49 | # One decimal degree is 111000m 50 | latitude = np.asarray([40.0, 50.0, 60.0]) 51 | longitude = np.asarray([-76.0, -86.0, -96.0]) 52 | 53 | azimuth = 90 54 | new_gc = great_circle( 55 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 56 | ) 57 | # We should have gone to the right 58 | assert (new_gc["longitude"] > longitude + 0.9).all() 59 | 60 | azimuth = 270 61 | new_gc = great_circle( 62 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 63 | ) 64 | # We should have gone to the left 65 | assert (new_gc["longitude"] < longitude - 0.9).all() 66 | 67 | azimuth = 180 68 | new_gc = great_circle( 69 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 70 | ) 71 | # We should have gone down 72 | assert (new_gc["latitude"] < latitude - 0.9).all() 73 | 74 | azimuth = 0 75 | new_gc = great_circle( 76 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 77 | ) 78 | # We should have gone up 79 | assert (new_gc["latitude"] > latitude + 0.9).all() 80 | 81 | azimuth = 315 82 | new_gc = great_circle( 83 | distance=111000, azimuth=azimuth, latitude=latitude, longitude=longitude 84 | ) 85 | # We should have gone up and to the left 86 | assert (new_gc["latitude"] > latitude + 0.45).all() 87 | assert (new_gc["longitude"] < longitude - 0.45).all() 88 | -------------------------------------------------------------------------------- /pygc/tests/test_gd.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from pygc import great_distance 7 | 8 | test_path = Path(__file__).parent.absolute() 9 | 10 | 11 | def test_great_distance_scalars(): 12 | # One decimal degree at the equator is about 111.32km 13 | latitude_start = 0.0 14 | latitude_end = 0.0 15 | longitude_start = 50.0 16 | longitude_end = 52.0 17 | gd = great_distance( 18 | start_latitude=latitude_start, 19 | start_longitude=longitude_start, 20 | end_latitude=latitude_end, 21 | end_longitude=longitude_end, 22 | ) 23 | assert np.round(gd["distance"] / 1000, 2) == 111.32 * 2 24 | 25 | # One decimal degree is 111000m 26 | latitude_start = 0.0 27 | latitude_end = 0.0 28 | longitude_start = [49.0, 75.0] 29 | longitude_end = [50.0, 76.0] 30 | gd = great_distance( 31 | start_latitude=latitude_start, 32 | start_longitude=longitude_start, 33 | end_latitude=latitude_end, 34 | end_longitude=longitude_end, 35 | ) 36 | assert np.allclose(gd["distance"] / 1000, 111.32) 37 | 38 | latitude_start = np.nan 39 | latitude_end = np.nan 40 | longitude_start = np.nan 41 | longitude_end = np.nan 42 | gd = great_distance( 43 | start_latitude=latitude_start, 44 | start_longitude=longitude_start, 45 | end_latitude=latitude_end, 46 | end_longitude=longitude_end, 47 | ) 48 | assert np.isnan(gd["distance"]) 49 | 50 | 51 | def test_great_distance_numpy(): 52 | latitude_start = np.asarray([0.0]) 53 | latitude_end = np.asarray([0.0]) 54 | longitude_start = np.asarray([50.0]) 55 | longitude_end = np.asarray([52.0]) 56 | gd = great_distance( 57 | start_latitude=latitude_start, 58 | start_longitude=longitude_start, 59 | end_latitude=latitude_end, 60 | end_longitude=longitude_end, 61 | ) 62 | assert np.round(gd["distance"] / 1000, 2) == 111.32 * 2 63 | 64 | latitude_start = np.asarray([0.0]) 65 | latitude_end = np.asarray([0.0]) 66 | longitude_start = 50.0 67 | longitude_end = 52.0 68 | gd = great_distance( 69 | start_latitude=latitude_start, 70 | start_longitude=longitude_start, 71 | end_latitude=latitude_end, 72 | end_longitude=longitude_end, 73 | ) 74 | assert np.round(gd["distance"] / 1000, 2) == 111.32 * 2 75 | 76 | 77 | def test_great_distance_masked_numpy(): 78 | with pytest.raises(ValueError): 79 | latitude_start = np.ma.asarray([0.0]) 80 | latitude_end = 0.0 81 | longitude_start = 50.0 82 | longitude_end = 52.0 83 | great_distance( 84 | start_latitude=latitude_start, 85 | start_longitude=longitude_start, 86 | end_latitude=latitude_end, 87 | end_longitude=longitude_end, 88 | ) 89 | 90 | latitude_start = np.ma.asarray([0.0]) 91 | latitude_end = np.ma.asarray([0.0]) 92 | longitude_start = np.ma.asarray([50.0]) 93 | longitude_end = np.ma.asarray([52.0]) 94 | gd = great_distance( 95 | start_latitude=latitude_start, 96 | start_longitude=longitude_start, 97 | end_latitude=latitude_end, 98 | end_longitude=longitude_end, 99 | ) 100 | assert np.round(gd["distance"] / 1000, 2) == 111.32 * 2 101 | 102 | xmask = np.load(test_path.joinpath("xmask.npy")) 103 | ymask = np.load(test_path.joinpath("ymask.npy")) 104 | xdata = np.load(test_path.joinpath("x.npy")) 105 | x = np.ma.fix_invalid(xdata, mask=xmask) 106 | ydata = np.load(test_path.joinpath("y.npy")) 107 | y = np.ma.fix_invalid(ydata, mask=ymask) 108 | gd = great_distance( 109 | start_latitude=y[0:-1], 110 | start_longitude=x[0:-1], 111 | end_latitude=y[1:], 112 | end_longitude=x[1:], 113 | ) 114 | 115 | latitude_start = np.ma.MaskedArray(np.ma.asarray([0.0]), mask=[1]) 116 | latitude_end = np.ma.MaskedArray(np.ma.asarray([0.0]), mask=[1]) 117 | longitude_start = np.ma.MaskedArray(np.ma.asarray([50.0]), mask=[1]) 118 | longitude_end = np.ma.MaskedArray(np.ma.asarray([52.0]), mask=[1]) 119 | gd = great_distance( 120 | start_latitude=latitude_start, 121 | start_longitude=longitude_start, 122 | end_latitude=latitude_end, 123 | end_longitude=longitude_end, 124 | ) 125 | assert np.all(gd["distance"].mask == True) # noqa 126 | assert np.all(gd["azimuth"].mask == True) # noqa 127 | assert np.all(gd["reverse_azimuth"].mask == True) # noqa 128 | 129 | latitude_start = np.ma.MaskedArray(np.ma.asarray([0.0, 1.0]), mask=[1, 0]) 130 | latitude_end = np.ma.MaskedArray(np.ma.asarray([0.0, 1.0]), mask=[1, 0]) 131 | longitude_start = np.ma.MaskedArray(np.ma.asarray([49.0, 75.0]), mask=[1, 0]) 132 | longitude_end = np.ma.MaskedArray(np.ma.asarray([50.0, 76.0]), mask=[1, 0]) 133 | gd = great_distance( 134 | start_latitude=latitude_start, 135 | start_longitude=longitude_start, 136 | end_latitude=latitude_end, 137 | end_longitude=longitude_end, 138 | ) 139 | assert gd["distance"].mask.tolist() == [True, False] 140 | assert gd["azimuth"].mask.tolist() == [True, False] 141 | assert gd["reverse_azimuth"].mask.tolist() == [True, False] 142 | 143 | latitude_start = np.ma.MaskedArray(np.ma.asarray([0.0, 1.0]), mask=[1, 1]) 144 | latitude_end = np.ma.MaskedArray(np.ma.asarray([0.0, 1.0]), mask=[1, 1]) 145 | longitude_start = np.ma.MaskedArray(np.ma.asarray([49.0, 75.0]), mask=[1, 1]) 146 | longitude_end = np.ma.MaskedArray(np.ma.asarray([50.0, 76.0]), mask=[1, 1]) 147 | gd = great_distance( 148 | start_latitude=latitude_start, 149 | start_longitude=longitude_start, 150 | end_latitude=latitude_end, 151 | end_longitude=longitude_end, 152 | ) 153 | assert gd["distance"].mask.tolist() == [True, True] 154 | assert gd["azimuth"].mask.tolist() == [True, True] 155 | assert gd["reverse_azimuth"].mask.tolist() == [True, True] 156 | 157 | 158 | def test_great_distance_empty_numpy(): 159 | latitude_start = np.ma.asarray([]) 160 | latitude_end = np.ma.asarray([]) 161 | longitude_start = np.ma.asarray([]) 162 | longitude_end = np.ma.asarray([]) 163 | gd = great_distance( 164 | start_latitude=latitude_start, 165 | start_longitude=longitude_start, 166 | end_latitude=latitude_end, 167 | end_longitude=longitude_end, 168 | ) 169 | assert np.all(gd["distance"].mask == True) # noqa 170 | assert np.all(gd["azimuth"].mask == True) # noqa 171 | assert np.all(gd["reverse_azimuth"].mask == True) # noqa 172 | 173 | 174 | def test_great_distance_infinite_loop(): 175 | gd = great_distance( 176 | start_latitude=52.11, 177 | start_longitude=312.44, 178 | end_latitude=-52.31, 179 | end_longitude=132.54, 180 | ) 181 | 182 | expected = 19973984.51165855 183 | np.testing.assert_allclose(gd["distance"], expected, rtol=4e-4) 184 | -------------------------------------------------------------------------------- /pygc/tests/x.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiom-data-science/pygc/f784902bcd0c30746f5297116a658fcbdde445cb/pygc/tests/x.npy -------------------------------------------------------------------------------- /pygc/tests/xmask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiom-data-science/pygc/f784902bcd0c30746f5297116a658fcbdde445cb/pygc/tests/xmask.npy -------------------------------------------------------------------------------- /pygc/tests/y.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiom-data-science/pygc/f784902bcd0c30746f5297116a658fcbdde445cb/pygc/tests/y.npy -------------------------------------------------------------------------------- /pygc/tests/ymask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiom-data-science/pygc/f784902bcd0c30746f5297116a658fcbdde445cb/pygc/tests/ymask.npy -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=41.2", "setuptools_scm", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | check-manifest 2 | pre-commit 3 | pytest 4 | setuptools_scm 5 | twine 6 | wheel 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pyproj -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pygc 3 | description = "Great Circle calculations in Python using Vincenty's formulae" 4 | author = Kyle Wilcox 5 | author_email = kyle@axiomdatascience.com 6 | url = https://github.com/axiom-data-science/pygc 7 | long_description_content_type = text/markdown 8 | long_description = file: README.md 9 | license = MIT 10 | license_file = LICENSE 11 | classifiers = 12 | Development Status :: 5 - Production/Stable 13 | Intended Audience :: Developers 14 | Intended Audience :: Science/Research 15 | License :: OSI Approved :: MIT License 16 | Operating System :: OS Independent 17 | Programming Language :: Python 18 | Programming Language :: Python :: 3 19 | Programming Language :: Python :: 3.7 20 | Programming Language :: Python :: 3.8 21 | Programming Language :: Python :: 3.9 22 | Programming Language :: Python :: 3.10 23 | Topic :: Scientific/Engineering 24 | 25 | [options] 26 | zip_safe = True 27 | include_package_data = True 28 | install_requires = 29 | numpy 30 | pyproj 31 | python_requires = >=3.6 32 | packages = find: 33 | 34 | [sdist] 35 | formats = gztar 36 | 37 | [check-manifest] 38 | ignore = 39 | *.yml 40 | 41 | [tool:pytest] 42 | addopts = -s -rxs -v 43 | flake8-max-line-length = 100 44 | flake8-ignore = 45 | *.py E265 E501 E221 E203 E201 E124 E202 E241 E251 W293 W291 W504 46 | 47 | [flake8] 48 | max-line-length = 100 49 | per-file-ignores = 50 | *.py: E265 E501 E221 E203 E201 E124 E202 E241 E251 W293 W291 W504 51 | 52 | [tool:isort] 53 | line_length=100 54 | indent=' ' 55 | balanced_wrapping=1 56 | multi_line_output=3 57 | default_section=FIRSTPARTY 58 | use_parentheses=1 59 | reverse_relative=1 60 | length_sort=1 61 | combine_star=1 62 | order_by_type=0 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="pygc", 5 | use_scm_version={ 6 | "write_to": "pygc/_version.py", 7 | "write_to_template": '__version__ = "{version}"', 8 | "tag_regex": r"^(?Pv)?(?P[^\+]+)(?P.*)?$", 9 | }, 10 | ) 11 | --------------------------------------------------------------------------------