pax_global_header00006660000000000000000000000064145514744500014523gustar00rootroot0000000000000052 comment=6661b21fbdecbecd6cd7d94675a922d97668343f mapado-haversine-6661b21/000077500000000000000000000000001455147445000152175ustar00rootroot00000000000000mapado-haversine-6661b21/.bumpversion.cfg000066400000000000000000000001231455147445000203230ustar00rootroot00000000000000[bumpversion] files = ./setup.py commit = True tag = True current_version = 2.8.1 mapado-haversine-6661b21/.github/000077500000000000000000000000001455147445000165575ustar00rootroot00000000000000mapado-haversine-6661b21/.github/dependabot.yml000066400000000000000000000010011455147445000213770ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" mapado-haversine-6661b21/.github/workflows/000077500000000000000000000000001455147445000206145ustar00rootroot00000000000000mapado-haversine-6661b21/.github/workflows/codeql-analysis.yml000066400000000000000000000044361455147445000244360ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '39 12 * * 3' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 mapado-haversine-6661b21/.github/workflows/python-publish.yml000066400000000000000000000017731455147445000243340ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: workflow_dispatch: ~ create: tags: - v* permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.x' cache: 'pipenv' - name: Install pipenv run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python - name: Publish package run: TWINE_USERNAME=__token__ TWINE_PASSWORD=${{ secrets.PIPY_HAVERSINE_TOKEN }} make deploy mapado-haversine-6661b21/.github/workflows/python-tests.yml000066400000000000000000000023441455147445000240230ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Testing on: workflow_dispatch: ~ push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: python-version: - '3.7' - '3.8' - '3.9' - '3.10' - '3.11' experimental: [false] # include: # - python-version: '3.12-dev' # experimental: true name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: 'pipenv' - name: Install pipenv run: | python -m pip install --upgrade pip pip install pipenv - name: Install dependencies run: | pipenv install --dev - name: Test with pytest run: | pipenv run py.test mapado-haversine-6661b21/.gitignore000066400000000000000000000000521455147445000172040ustar00rootroot00000000000000*~ *.pyc dist/ *.egg-info/ .cache/ build/ mapado-haversine-6661b21/CHANGELOG.md000066400000000000000000000075651455147445000170450ustar00rootroot00000000000000# CHANGELOG ## 2.8.0 - Performance improvements, especially with the `haversine_vector` function [#65](https://github.com/mapado/haversine/pull/65) by [jobh](https://github.com/jobh) ## 2.7.0 Official support of python 3.10, 3.11 and 3.12 ## 2.6.0 - Check or normalize given lat/lon. [#49](https://github.com/mapado/haversine/issues/49) by [@uri-rodberg](https://github.com/uri-rodberg) and [@merschformann](https://github.com/merschformann) ## 2.5.1 - Reset type hinting for `inverse_haversine` ## 2.5.0 - [Minor break] Drop support for python 2.7 [#42](https://github.com/mapado/haversine/pull/42) ## 2.4.1 - Fix issue with python 2.7 compatibily. See [#41](https://github.com/mapado/haversine/issues/41) ## 2.4.0 - Added inver haversine functionality [#39](https://github.com/mapado/haversine/pull/39) by [@CrapsJeroen](https://github.com/CrapsJeroen) - Adds radians and degrees units [#40](https://github.com/mapado/haversine/pull/40) by [@merschformann](https://github.com/merschformann) ## 2.3.1 - Fix license in setup.py [#38](https://github.com/mapado/haversine/pull/38) by [@kraj](https://github.com/kraj) ## 2.3.0 ### Added - Added the `comb` parameter for `haversine_vector` (thanks to [Fd-3741](https://github.com/Fd-3741)) [#34](https://github.com/mapado/haversine/pull/34) ## 2.2.0 ### Added - Added the `haversine_vector` function (thanks to [ccforgy](https://github.com/ccforgy)) [#26](https://github.com/mapado/haversine/pull/26) ### Changed - Improve performance thanks to [adamchainz](https://github.com/adamchainz) [#27](https://github.com/mapado/haversine/pull/27) ## 2.1.2 - 2019-07-19 Fix typo in documentation ## 2.1.1 - 2019-05-07 Quick improvement left out at [#22](https://github.com/mapado/haversine/pull/22) Renamed Units to Unit along the way, to comply with conventions (Breaking if you were on 2.1.0) See more: [#23](https://github.com/mapado/haversine/pull/23) — Paolo Lammens ## 2.1.0 - 2019-05-07 General refactor: use Enum for available units, extract constants [#22](https://github.com/mapado/haversine/pull/22) — Paolo Lammens ## 2.0.0 - 2018-11-27 ### Changed - Add a `unit` parameter accepting different units (`miles`, `meter`, `feet`, etc.) - [BREAKING] The `miles` and `nautical_miles` parameters have been removed and replaced by the `unit` parameter. See [#20](https://github.com/mapado/haversine/pull/20) ### How to upgrade If you did not use the `miles` or `nautical_miles`, you are good to go, this is non-breaking for you ! If you did use `miles` or `nautical_miles`, you just need to do that: ```diff - haversine(lyon, paris, miles=True) + haversine(lyon, paris, unit='mi') ``` ```diff - haversine(lyon, paris, nautical_miles=True) + haversine(lyon, paris, unit='nmi') ``` Du to a small change in the formula, the precision for miles aud nautical miles has slighty changed. Example : distance between Lyon, France and Paris, France changed from `243.7125041070121 miles` to `243.71250609539814 miles`. Same for nautical miles. ## 1.0.2 - 2018-10-13 slightly better precision [#17](https://github.com/mapado/haversine/pull/17) ## 1.0.1 - 2018-10-10 fix wrong definition in setup.py ## 1.0.0 - 2018-10-10 No changes, haversine package has been stable and functional for years now. Time to make a 1.0 version :) (in fact there is one breaking changes but it concern the 0.5.0 version published just 10 minutes before 1.0 ;). In that case `nmiles` has been changed to `nautical_miles` for readability) ## 0.5.0 - 2018-10-10 Add nautical miles support ## 0.4.6 Fixed typos in README and docstring ## 0.4.5 Fixed issue with int instead of float [#6](https://github.com/mapado/haversine/pull/6/files) ## 0.4.3 - 0.4.4 - Remove useless code [#5](https://github.com/mapado/haversine/pull/5) ## 0.4.2 - Remove cPython usage: fail on Windows, no real perf gain [5d0ff179](https://github.com/mapado/haversine/commit/5d0ff179741b8417965d94dcb21f39ddbce674f8) mapado-haversine-6661b21/LICENSE000066400000000000000000000020611455147445000162230ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Mapado 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. mapado-haversine-6661b21/Makefile000066400000000000000000000010221455147445000166520ustar00rootroot00000000000000.DEFAULT_GOAL := help ## clean: Clean directory for build clean: rm -rf dist/* .PHONY: build ## build: Build package build: pipenv run python setup.py sdist bdist_wheel ## deploy: Deploy haversine to pypi deploy: clean build pipenv install twine pipenv run twine upload dist/* --verbose pipenv uninstall twine git checkout -- Pipfile.lock .PHONY: help ## help: Prints this help text. help: @echo '' @echo ' Usage:' @echo ' make ' @echo '' @echo ' Targets:' @sed -n 's/^## \?/ /p' $(MAKEFILE_LIST) mapado-haversine-6661b21/Pipfile000066400000000000000000000002201455147445000165240ustar00rootroot00000000000000[[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [dev-packages] pytest = {version = "~=7.1"} numpy = "*" [packages] mapado-haversine-6661b21/Pipfile.lock000066400000000000000000000122151455147445000174620ustar00rootroot00000000000000{ "_meta": { "hash": { "sha256": "d8c0d754e40d55ded3783dccd95744277f2a2d3e722215c958ef0eb753bf9d47" }, "pipfile-spec": 6, "requires": {}, "sources": [ { "name": "pypi", "url": "https://pypi.org/simple", "verify_ssl": true } ] }, "default": {}, "develop": { "attrs": { "hashes": [ "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==21.4.0" }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" ], "version": "==1.1.1" }, "numpy": { "hashes": [ "sha256:0cfe07133fd00b27edee5e6385e333e9eeb010607e8a46e1cd673f05f8596595", "sha256:11a1f3816ea82eed4178102c56281782690ab5993251fdfd75039aad4d20385f", "sha256:2762331de395739c91f1abb88041f94a080cb1143aeec791b3b223976228af3f", "sha256:283d9de87c0133ef98f93dfc09fad3fb382f2a15580de75c02b5bb36a5a159a5", "sha256:3d22662b4b10112c545c91a0741f2436f8ca979ab3d69d03d19322aa970f9695", "sha256:41388e32e40b41dd56eb37fcaa7488b2b47b0adf77c66154d6b89622c110dfe9", "sha256:42c16cec1c8cf2728f1d539bd55aaa9d6bb48a7de2f41eb944697293ef65a559", "sha256:47ee7a839f5885bc0c63a74aabb91f6f40d7d7b639253768c4199b37aede7982", "sha256:5a311ee4d983c487a0ab546708edbdd759393a3dc9cd30305170149fedd23c88", "sha256:5dc65644f75a4c2970f21394ad8bea1a844104f0fe01f278631be1c7eae27226", "sha256:6ed0d073a9c54ac40c41a9c2d53fcc3d4d4ed607670b9e7b0de1ba13b4cbfe6f", "sha256:76ba7c40e80f9dc815c5e896330700fd6e20814e69da9c1267d65a4d051080f1", "sha256:818b9be7900e8dc23e013a92779135623476f44a0de58b40c32a15368c01d471", "sha256:a024181d7aef0004d76fb3bce2a4c9f2e67a609a9e2a6ff2571d30e9976aa383", "sha256:a955e4128ac36797aaffd49ab44ec74a71c11d6938df83b1285492d277db5397", "sha256:a97a954a8c2f046d3817c2bce16e3c7e9a9c2afffaf0400f5c16df5172a67c9c", "sha256:a97e82c39d9856fe7d4f9b86d8a1e66eff99cf3a8b7ba48202f659703d27c46f", "sha256:b55b953a1bdb465f4dc181758570d321db4ac23005f90ffd2b434cc6609a63dd", "sha256:bb02929b0d6bfab4c48a79bd805bd7419114606947ec8284476167415171f55b", "sha256:bece0a4a49e60e472a6d1f70ac6cdea00f9ab80ff01132f96bd970cdd8a9e5a9", "sha256:e41e8951749c4b5c9a2dc5fdbc1a4eec6ab2a140fdae9b460b0f557eed870f4d", "sha256:f71d57cc8645f14816ae249407d309be250ad8de93ef61d9709b45a0ddf4050c" ], "index": "pypi", "version": "==1.22.0" }, "packaging": { "hashes": [ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], "markers": "python_version >= '3.6'", "version": "==21.3" }, "pluggy": { "hashes": [ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], "markers": "python_version >= '3.6'", "version": "==1.0.0" }, "py": { "hashes": [ "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.11.0" }, "pyparsing": { "hashes": [ "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" ], "markers": "python_full_version >= '3.6.8'", "version": "==3.0.9" }, "pytest": { "hashes": [ "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" ], "index": "pypi", "version": "==7.1.2" }, "tomli": { "hashes": [ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], "markers": "python_version >= '3.7'", "version": "==2.0.1" } } } mapado-haversine-6661b21/README.md000077500000000000000000000112471455147445000165060ustar00rootroot00000000000000# Haversine Calculate the distance (in various units) between two points on Earth using their latitude and longitude. ## Installation ```sh pip install haversine ``` ## Usage ### Calculate the distance between Lyon and Paris ```python from haversine import haversine, Unit lyon = (45.7597, 4.8422) # (lat, lon) paris = (48.8567, 2.3508) haversine(lyon, paris) >> 392.2172595594006 # in kilometers haversine(lyon, paris, unit=Unit.MILES) >> 243.71250609539814 # in miles # you can also use the string abbreviation for units: haversine(lyon, paris, unit='mi') >> 243.71250609539814 # in miles haversine(lyon, paris, unit=Unit.NAUTICAL_MILES) >> 211.78037755311516 # in nautical miles ``` The lat/lon values need to be provided in degrees of the ranges [-90,90] (lat) and [-180,180] (lon). If values are outside their ranges, an error will be raised. This can be avoided by automatic normalization via the `normalize` parameter. The `haversine.Unit` enum contains all supported units: ```python import haversine print(tuple(haversine.Unit)) ``` outputs ```text (, , , , , , , ) ``` #### Note for radians and degrees The radian and degrees returns the [great circle distance](https://en.wikipedia.org/wiki/Great-circle_distance) between two points on a sphere. Notes: - on a unit-sphere the angular distance in radians equals the distance between the two points on the sphere (definition of radians) - When using "degree", this angle is just converted from radians to degrees ### Inverse Haversine Formula Calculates a point from a given vector (distance and direction) and start point. Currently explicitly supports both cardinal (north, east, south, west) and intercardinal (northeast, southeast, southwest, northwest) directions. But also allows for explicit angles expressed in Radians. ## Example: Finding arbitary point from Paris ```python from haversine import inverse_haversine, Direction from math import pi paris = (48.8567, 2.3508) # (lat, lon) # Finding 32 km west of Paris inverse_haversine(paris, 32, Direction.WEST) # returns tuple (48.85587279023947, 1.9134085092836945) # Finding 32 km southwest of Paris inverse_haversine(paris, 32, pi * 1.25) # returns tuple (48.65279552300661, 2.0427666779658806) # Finding 50 miles north of Paris inverse_haversine(paris, 50, Direction.NORTH, unit=Unit.MILES) # returns tuple (49.58035791599536, 2.3508) # Finding 10 nautical miles south of Paris inverse_haversine(paris, 10, Direction.SOUTH, unit=Unit.NAUTICAL_MILES) # returns tuple (48.690145868497645, 2.3508) ``` ### Performance optimisation for distances between all points in two vectors You will need to install [numpy](https://pypi.org/project/numpy/) in order to gain performance with vectors. For optimal performance, you can turn off coordinate checking by adding `check=False` and install the optional packages [numba](https://pypi.org/project/numba/) and [icc_rt](https://pypi.org/project/icc_rt/). You can then do this: ```python from haversine import haversine_vector, Unit lyon = (45.7597, 4.8422) # (lat, lon) paris = (48.8567, 2.3508) new_york = (40.7033962, -74.2351462) haversine_vector([lyon, lyon], [paris, new_york], Unit.KILOMETERS) >> array([ 392.21725956, 6163.43638211]) ``` It is generally slower to use `haversine_vector` to get distance between two points, but can be really fast to compare distances between two vectors. ### Combine matrix You can generate a matrix of all combinations between coordinates in different vectors by setting `comb` parameter as True. ```python from haversine import haversine_vector, Unit lyon = (45.7597, 4.8422) # (lat, lon) london = (51.509865, -0.118092) paris = (48.8567, 2.3508) new_york = (40.7033962, -74.2351462) haversine_vector([lyon, london], [paris, new_york], Unit.KILOMETERS, comb=True) >> array([[ 392.21725956, 343.37455271], [6163.43638211, 5586.48447423]]) ``` The output array from the example above returns the following table: | | Paris | New York | | ------ | :---------------: | :------------------: | | Lyon | Lyon <\-> Paris | Lyon <\-> New York | | London | London <\-> Paris | London <\-> New York | By definition, if you have a vector _a_ with _n_ elements, and a vector _b_ with _m_ elements. The result matrix _M_ would be $n x m$ and a element M\[i,j\] from the matrix would be the distance between the ith coordinate from vector _a_ and jth coordinate with vector _b_. ## Contributing Clone the project. Install [pipenv](https://github.com/pypa/pipenv). Run `pipenv install --dev` Launch test with `pipenv run pytest` mapado-haversine-6661b21/haversine/000077500000000000000000000000001455147445000172035ustar00rootroot00000000000000mapado-haversine-6661b21/haversine/__init__.py000077500000000000000000000001611455147445000213150ustar00rootroot00000000000000from .haversine import Unit, haversine, haversine_vector, Direction, inverse_haversine, inverse_haversine_vector mapado-haversine-6661b21/haversine/haversine.py000077500000000000000000000252761455147445000215600ustar00rootroot00000000000000from enum import Enum from math import pi from typing import Union, Tuple import math # mean earth radius - https://en.wikipedia.org/wiki/Earth_radius#Mean_radius _AVG_EARTH_RADIUS_KM = 6371.0088 class Unit(str, Enum): """ Enumeration of supported units. The full list can be checked by iterating over the class; e.g. the expression `tuple(Unit)`. """ KILOMETERS = 'km' METERS = 'm' MILES = 'mi' NAUTICAL_MILES = 'nmi' FEET = 'ft' INCHES = 'in' RADIANS = 'rad' DEGREES = 'deg' class Direction(float, Enum): """ Enumeration of supported directions. The full list can be checked by iterating over the class; e.g. the expression `tuple(Direction)`. Angles expressed in radians. """ NORTH = 0.0 NORTHEAST = pi * 0.25 EAST = pi * 0.5 SOUTHEAST = pi * 0.75 SOUTH = pi SOUTHWEST = pi * 1.25 WEST = pi * 1.5 NORTHWEST = pi * 1.75 # Unit values taken from http://www.unitconversion.org/unit_converter/length.html _CONVERSIONS = { Unit.KILOMETERS: 1.0, Unit.METERS: 1000.0, Unit.MILES: 0.621371192, Unit.NAUTICAL_MILES: 0.539956803, Unit.FEET: 3280.839895013, Unit.INCHES: 39370.078740158, Unit.RADIANS: 1/_AVG_EARTH_RADIUS_KM, Unit.DEGREES: (1/_AVG_EARTH_RADIUS_KM)*(180.0/pi) } def get_avg_earth_radius(unit): return _AVG_EARTH_RADIUS_KM * _CONVERSIONS[unit] def _normalize(lat: float, lon: float) -> Tuple[float, float]: """ Normalize point to [-90, 90] latitude and [-180, 180] longitude. """ lat = (lat + 90) % 360 - 90 if lat > 90: lat = 180 - lat lon += 180 lon = (lon + 180) % 360 - 180 return lat, lon def _normalize_vector(lat: "numpy.ndarray", lon: "numpy.ndarray") -> Tuple["numpy.ndarray", "numpy.ndarray"]: """ Normalize points to [-90, 90] latitude and [-180, 180] longitude. """ lat = (lat + 90) % 360 - 90 lon = (lon + 180) % 360 - 180 wrap = lat > 90 if numpy.any(wrap): lat[wrap] = 180 - lat[wrap] lon[wrap] = lon[wrap] % 360 - 180 return lat, lon def _ensure_lat_lon(lat: float, lon: float): """ Ensure that the given latitude and longitude have proper values. An exception is raised if they are not. """ if lat < -90 or lat > 90: raise ValueError(f"Latitude {lat} is out of range [-90, 90]") if lon < -180 or lon > 180: raise ValueError(f"Longitude {lon} is out of range [-180, 180]") def _ensure_lat_lon_vector(lat: "numpy.ndarray", lon: "numpy.ndarray"): """ Ensure that the given latitude and longitude have proper values. An exception is raised if they are not. """ if numpy.abs(lat).max() > 90: raise ValueError("Latitude(s) out of range [-90, 90]") if numpy.abs(lon).max() > 180: raise ValueError("Longitude(s) out of range [-180, 180]") def _explode_args(f): return lambda ops: f(**ops.__dict__) @_explode_args def _create_haversine_kernel(*, asin=None, arcsin=None, cos, radians, sin, sqrt, **_): asin = asin or arcsin def _haversine_kernel(lat1, lng1, lat2, lng2): """ Compute the haversine distance on unit sphere. Inputs are in degrees, either scalars (with ops==math) or arrays (with ops==numpy). """ lat1 = radians(lat1) lng1 = radians(lng1) lat2 = radians(lat2) lng2 = radians(lng2) lat = lat2 - lat1 lng = lng2 - lng1 d = (sin(lat * 0.5) ** 2 + cos(lat1) * cos(lat2) * sin(lng * 0.5) ** 2) # Note: 2 * atan2(sqrt(d), sqrt(1-d)) is more accurate at # large distance (d is close to 1), but also slower. return 2 * asin(sqrt(d)) return _haversine_kernel @_explode_args def _create_inverse_haversine_kernel(*, asin=None, arcsin=None, atan2=None, arctan2=None, cos, degrees, radians, sin, sqrt, **_): asin = asin or arcsin atan2 = atan2 or arctan2 def _inverse_haversine_kernel(lat, lng, direction, d): """ Compute the inverse haversine on unit sphere. lat/lng are in degrees, direction in radians; all inputs are either scalars (with ops==math) or arrays (with ops==numpy). """ lat = radians(lat) lng = radians(lng) cos_d, sin_d = cos(d), sin(d) cos_lat, sin_lat = cos(lat), sin(lat) sin_d_cos_lat = sin_d * cos_lat return_lat = asin(cos_d * sin_lat + sin_d_cos_lat * cos(direction)) return_lng = lng + atan2(sin(direction) * sin_d_cos_lat, cos_d - sin_lat * sin(return_lat)) return degrees(return_lat), degrees(return_lng) return _inverse_haversine_kernel _haversine_kernel = _create_haversine_kernel(math) _inverse_haversine_kernel = _create_inverse_haversine_kernel(math) try: import numpy has_numpy = True _haversine_kernel_vector = _create_haversine_kernel(numpy) _inverse_haversine_kernel_vector = _create_inverse_haversine_kernel(numpy) except ModuleNotFoundError: # Import error will be reported in haversine_vector() / inverse_haversine_vector() has_numpy = False try: import numba # type: ignore if has_numpy: _haversine_kernel_vector = numba.vectorize(fastmath=True)(_haversine_kernel_vector) # Tuple output is not supported for numba.vectorize. Just jit the numpy version. _inverse_haversine_kernel_vector = numba.njit(fastmath=True)(_inverse_haversine_kernel_vector) _haversine_kernel = numba.njit(_haversine_kernel) _inverse_haversine_kernel = numba.njit(_inverse_haversine_kernel) except ModuleNotFoundError: pass def haversine(point1, point2, unit=Unit.KILOMETERS, normalize=False, check=True): """ Calculate the great-circle distance between two points on the Earth surface. Takes two 2-tuples, containing the latitude and longitude of each point in decimal degrees, and, optionally, a unit of length. :param point1: first point; tuple of (latitude, longitude) in decimal degrees :param point2: second point; tuple of (latitude, longitude) in decimal degrees :param unit: a member of haversine.Unit, or, equivalently, a string containing the initials of its corresponding unit of measurement (i.e. miles = mi) default 'km' (kilometers). :param normalize: if True, normalize the points to [-90, 90] latitude and [-180, 180] longitude. :param check: if True, check that points are normalized. Example: ``haversine((45.7597, 4.8422), (48.8567, 2.3508), unit=Unit.METERS)`` Precondition: ``unit`` is a supported unit (supported units are listed in the `Unit` enum) :return: the distance between the two points in the requested unit, as a float. The default returned unit is kilometers. The default unit can be changed by setting the unit parameter to a member of ``haversine.Unit`` (e.g. ``haversine.Unit.INCHES``), or, equivalently, to a string containing the corresponding abbreviation (e.g. 'in'). All available units can be found in the ``Unit`` enum. """ # unpack latitude/longitude lat1, lng1 = point1 lat2, lng2 = point2 # normalize points or ensure they are proper lat/lon, i.e., in [-90, 90] and [-180, 180] if normalize: lat1, lng1 = _normalize(lat1, lng1) lat2, lng2 = _normalize(lat2, lng2) elif check: _ensure_lat_lon(lat1, lng1) _ensure_lat_lon(lat2, lng2) return get_avg_earth_radius(unit) * _haversine_kernel(lat1, lng1, lat2, lng2) def haversine_vector(array1, array2, unit=Unit.KILOMETERS, comb=False, normalize=False, check=True): ''' The exact same function as "haversine", except that this version replaces math functions with numpy functions. This may make it slightly slower for computing the haversine distance between two points, but is much faster for computing the distance between two vectors of points due to vectorization. ''' if not has_numpy: raise RuntimeError('Error, unable to import Numpy, ' 'consider using haversine instead of haversine_vector.') # ensure arrays are numpy ndarrays if not isinstance(array1, numpy.ndarray): array1 = numpy.array(array1) if not isinstance(array2, numpy.ndarray): array2 = numpy.array(array2) # ensure will be able to iterate over rows by adding dimension if needed if array1.ndim == 1: array1 = numpy.expand_dims(array1, 0) if array2.ndim == 1: array2 = numpy.expand_dims(array2, 0) # Asserts that both arrays have same dimensions if not in combination mode if not comb: if array1.shape != array2.shape: raise IndexError( "When not in combination mode, arrays must be of same size. If mode is required, use comb=True as argument.") # unpack latitude/longitude lat1, lng1 = array1[:, 0], array1[:, 1] lat2, lng2 = array2[:, 0], array2[:, 1] # normalize points or ensure they are proper lat/lon, i.e., in [-90, 90] and [-180, 180] if normalize: lat1, lng1 = _normalize_vector(lat1, lng1) lat2, lng2 = _normalize_vector(lat2, lng2) elif check: _ensure_lat_lon_vector(lat1, lng1) _ensure_lat_lon_vector(lat2, lng2) # If in combination mode, turn coordinates of array1 into column vectors for broadcasting if comb: lat1 = numpy.expand_dims(lat1, axis=0) lng1 = numpy.expand_dims(lng1, axis=0) lat2 = numpy.expand_dims(lat2, axis=1) lng2 = numpy.expand_dims(lng2, axis=1) return get_avg_earth_radius(unit) * _haversine_kernel_vector(lat1, lng1, lat2, lng2) def inverse_haversine(point, distance, direction: Union[Direction, float], unit=Unit.KILOMETERS): lat, lng = point r = get_avg_earth_radius(unit) return _inverse_haversine_kernel(lat, lng, direction, distance/r) def inverse_haversine_vector(array, distance, direction, unit=Unit.KILOMETERS): if not has_numpy: raise RuntimeError('Error, unable to import Numpy, ' 'consider using inverse_haversine instead of inverse_haversine_vector.') # ensure arrays are numpy ndarrays array, distance, direction = map(numpy.asarray, (array, distance, direction)) # ensure will be able to iterate over rows by adding dimension if needed if array.ndim == 1: array = numpy.expand_dims(array, 0) # Asserts that arrays are correctly sized if array.ndim != 2 or array.shape[1] != 2 or array.shape[0] != len(distance) or array.shape[0] != len(direction): raise IndexError("Arrays must be of same size.") # unpack latitude/longitude lat, lng = array[:, 0], array[:, 1] r = get_avg_earth_radius(unit) return _inverse_haversine_kernel_vector(lat, lng, direction, distance/r) mapado-haversine-6661b21/setup.cfg000066400000000000000000000000341455147445000170350ustar00rootroot00000000000000[bdist_wheel] universal = 1 mapado-haversine-6661b21/setup.py000077500000000000000000000023221455147445000167330ustar00rootroot00000000000000from setuptools import setup setup( name='haversine', version='2.8.1', description='Calculate the distance between 2 points on Earth.', long_description=open('README.md').read(), long_description_content_type="text/markdown", include_package_data=True, python_requires='>=3.5', author='Balthazar Rouberol', maintainer='Julien Deniau', maintainer_email='julien.deniau@mapado.com', url='https://github.com/mapado/haversine', packages=['haversine'], license='MIT', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Topic :: Scientific/Engineering :: Mathematics', 'Topic :: Scientific/Engineering :: GIS' ], ) mapado-haversine-6661b21/tests/000077500000000000000000000000001455147445000163615ustar00rootroot00000000000000mapado-haversine-6661b21/tests/__init__.py000066400000000000000000000000001455147445000204600ustar00rootroot00000000000000mapado-haversine-6661b21/tests/geo_ressources.py000066400000000000000000000016661455147445000217730ustar00rootroot00000000000000from haversine import Unit LYON = (45.7597, 4.8422) PARIS = (48.8567, 2.3508) NEW_YORK = (40.7033962, -74.2351462) LONDON = (51.509865, -0.118092) EXPECTED_LYON_PARIS = {Unit.KILOMETERS: 392.2172595594006, Unit.METERS: 392217.2595594006, Unit.MILES: 243.71250609539814, Unit.NAUTICAL_MILES: 211.78037755311516, Unit.FEET: 1286802.0326751503, Unit.INCHES: 15441624.392102592, Unit.RADIANS: 0.061562818679421795, Unit.DEGREES: 3.5272896852600164} EXPECTED_LYON_NEW_YORK = {Unit.KILOMETERS: 6163.43638211, Unit.METERS: 61634363.8211} EXPECTED_PARIS_NEW_YORK = {Unit.KILOMETERS: 5853.32898662, Unit.METERS: 58533289.8662} EXPECTED_LONDON_PARIS = {Unit.KILOMETERS: 343.37455271} EXPECTED_LONDON_NEW_YORK = {Unit.KILOMETERS: 5586.48447423} mapado-haversine-6661b21/tests/haversine_baseline.py000066400000000000000000000256131455147445000225700ustar00rootroot00000000000000# # NOTE: This is a copy of haversine/haversine.py, 2023-02-28. It is intended # to create a baseline for performance regression tests. # # To establish a new baseline, replace this file with the current one. # from enum import Enum from math import pi from typing import Union, Tuple import math # mean earth radius - https://en.wikipedia.org/wiki/Earth_radius#Mean_radius _AVG_EARTH_RADIUS_KM = 6371.0088 class Unit(str, Enum): """ Enumeration of supported units. The full list can be checked by iterating over the class; e.g. the expression `tuple(Unit)`. """ KILOMETERS = 'km' METERS = 'm' MILES = 'mi' NAUTICAL_MILES = 'nmi' FEET = 'ft' INCHES = 'in' RADIANS = 'rad' DEGREES = 'deg' class Direction(float, Enum): """ Enumeration of supported directions. The full list can be checked by iterating over the class; e.g. the expression `tuple(Direction)`. Angles expressed in radians. """ NORTH = 0.0 NORTHEAST = pi * 0.25 EAST = pi * 0.5 SOUTHEAST = pi * 0.75 SOUTH = pi SOUTHWEST = pi * 1.25 WEST = pi * 1.5 NORTHWEST = pi * 1.75 # Unit values taken from http://www.unitconversion.org/unit_converter/length.html _CONVERSIONS = { Unit.KILOMETERS: 1.0, Unit.METERS: 1000.0, Unit.MILES: 0.621371192, Unit.NAUTICAL_MILES: 0.539956803, Unit.FEET: 3280.839895013, Unit.INCHES: 39370.078740158, Unit.RADIANS: 1/_AVG_EARTH_RADIUS_KM, Unit.DEGREES: (1/_AVG_EARTH_RADIUS_KM)*(180.0/pi) } def get_avg_earth_radius(unit): return _AVG_EARTH_RADIUS_KM * _CONVERSIONS[unit] def _normalize(lat: float, lon: float) -> Tuple[float, float]: """ Normalize point to [-90, 90] latitude and [-180, 180] longitude. """ lat = (lat + 90) % 360 - 90 if lat > 90: lat = 180 - lat lon += 180 lon = (lon + 180) % 360 - 180 return lat, lon def _normalize_vector(lat: "numpy.ndarray", lon: "numpy.ndarray") -> Tuple["numpy.ndarray", "numpy.ndarray"]: """ Normalize points to [-90, 90] latitude and [-180, 180] longitude. """ lat = (lat + 90) % 360 - 90 lon = (lon + 180) % 360 - 180 wrap = lat > 90 if numpy.any(wrap): lat[wrap] = 180 - lat[wrap] lon[wrap] = lon[wrap] % 360 - 180 return lat, lon def _ensure_lat_lon(lat: float, lon: float): """ Ensure that the given latitude and longitude have proper values. An exception is raised if they are not. """ if lat < -90 or lat > 90: raise ValueError(f"Latitude {lat} is out of range [-90, 90]") if lon < -180 or lon > 180: raise ValueError(f"Longitude {lon} is out of range [-180, 180]") def _ensure_lat_lon_vector(lat: "numpy.ndarray", lon: "numpy.ndarray"): """ Ensure that the given latitude and longitude have proper values. An exception is raised if they are not. """ if numpy.abs(lat).max() > 90: raise ValueError("Latitude(s) out of range [-90, 90]") if numpy.abs(lon).max() > 180: raise ValueError("Longitude(s) out of range [-180, 180]") def _explode_args(f): return lambda ops: f(**ops.__dict__) @_explode_args def _create_haversine_kernel(*, asin=None, arcsin=None, cos, radians, sin, sqrt, **_): asin = asin or arcsin def _haversine_kernel(lat1, lng1, lat2, lng2): """ Compute the haversine distance on unit sphere. Inputs are in degrees, either scalars (with ops==math) or arrays (with ops==numpy). """ lat1 = radians(lat1) lng1 = radians(lng1) lat2 = radians(lat2) lng2 = radians(lng2) lat = lat2 - lat1 lng = lng2 - lng1 d = (sin(lat * 0.5) ** 2 + cos(lat1) * cos(lat2) * sin(lng * 0.5) ** 2) # Note: 2 * atan2(sqrt(d), sqrt(1-d)) is more accurate at # large distance (d is close to 1), but also slower. return 2 * asin(sqrt(d)) return _haversine_kernel @_explode_args def _create_inverse_haversine_kernel(*, asin=None, arcsin=None, atan2=None, arctan2=None, cos, degrees, radians, sin, sqrt, **_): asin = asin or arcsin atan2 = atan2 or arctan2 def _inverse_haversine_kernel(lat, lng, direction, d): """ Compute the inverse haversine on unit sphere. lat/lng are in degrees, direction in radians; all inputs are either scalars (with ops==math) or arrays (with ops==numpy). """ lat = radians(lat) lng = radians(lng) cos_d, sin_d = cos(d), sin(d) cos_lat, sin_lat = cos(lat), sin(lat) sin_d_cos_lat = sin_d * cos_lat return_lat = asin(cos_d * sin_lat + sin_d_cos_lat * cos(direction)) return_lng = lng + atan2(sin(direction) * sin_d_cos_lat, cos_d - sin_lat * sin(return_lat)) return degrees(return_lat), degrees(return_lng) return _inverse_haversine_kernel _haversine_kernel = _create_haversine_kernel(math) _inverse_haversine_kernel = _create_inverse_haversine_kernel(math) try: import numpy has_numpy = True _haversine_kernel_vector = _create_haversine_kernel(numpy) _inverse_haversine_kernel_vector = _create_inverse_haversine_kernel(numpy) except ModuleNotFoundError: # Import error will be reported in haversine_vector() / inverse_haversine_vector() has_numpy = False try: import numba # type: ignore if has_numpy: _haversine_kernel_vector = numba.vectorize(fastmath=True)(_haversine_kernel) # Tuple output is not supported for numba.vectorize. Just jit the numpy version. _inverse_haversine_kernel_vector = numba.njit(fastmath=True)(_inverse_haversine_kernel_vector) _haversine_kernel = numba.njit(_haversine_kernel) _inverse_haversine_kernel = numba.njit(_inverse_haversine_kernel) except ModuleNotFoundError: pass def haversine(point1, point2, unit=Unit.KILOMETERS, normalize=False, check=True): """ Calculate the great-circle distance between two points on the Earth surface. Takes two 2-tuples, containing the latitude and longitude of each point in decimal degrees, and, optionally, a unit of length. :param point1: first point; tuple of (latitude, longitude) in decimal degrees :param point2: second point; tuple of (latitude, longitude) in decimal degrees :param unit: a member of haversine.Unit, or, equivalently, a string containing the initials of its corresponding unit of measurement (i.e. miles = mi) default 'km' (kilometers). :param normalize: if True, normalize the points to [-90, 90] latitude and [-180, 180] longitude. :param check: if True, check that points are normalized. Example: ``haversine((45.7597, 4.8422), (48.8567, 2.3508), unit=Unit.METERS)`` Precondition: ``unit`` is a supported unit (supported units are listed in the `Unit` enum) :return: the distance between the two points in the requested unit, as a float. The default returned unit is kilometers. The default unit can be changed by setting the unit parameter to a member of ``haversine.Unit`` (e.g. ``haversine.Unit.INCHES``), or, equivalently, to a string containing the corresponding abbreviation (e.g. 'in'). All available units can be found in the ``Unit`` enum. """ # unpack latitude/longitude lat1, lng1 = point1 lat2, lng2 = point2 # normalize points or ensure they are proper lat/lon, i.e., in [-90, 90] and [-180, 180] if normalize: lat1, lng1 = _normalize(lat1, lng1) lat2, lng2 = _normalize(lat2, lng2) elif check: _ensure_lat_lon(lat1, lng1) _ensure_lat_lon(lat2, lng2) return get_avg_earth_radius(unit) * _haversine_kernel(lat1, lng1, lat2, lng2) def haversine_vector(array1, array2, unit=Unit.KILOMETERS, comb=False, normalize=False, check=True): ''' The exact same function as "haversine", except that this version replaces math functions with numpy functions. This may make it slightly slower for computing the haversine distance between two points, but is much faster for computing the distance between two vectors of points due to vectorization. ''' if not has_numpy: raise RuntimeError('Error, unable to import Numpy, ' 'consider using haversine instead of haversine_vector.') # ensure arrays are numpy ndarrays if not isinstance(array1, numpy.ndarray): array1 = numpy.array(array1) if not isinstance(array2, numpy.ndarray): array2 = numpy.array(array2) # ensure will be able to iterate over rows by adding dimension if needed if array1.ndim == 1: array1 = numpy.expand_dims(array1, 0) if array2.ndim == 1: array2 = numpy.expand_dims(array2, 0) # Asserts that both arrays have same dimensions if not in combination mode if not comb: if array1.shape != array2.shape: raise IndexError( "When not in combination mode, arrays must be of same size. If mode is required, use comb=True as argument.") # unpack latitude/longitude lat1, lng1 = array1[:, 0], array1[:, 1] lat2, lng2 = array2[:, 0], array2[:, 1] # normalize points or ensure they are proper lat/lon, i.e., in [-90, 90] and [-180, 180] if normalize: lat1, lng1 = _normalize_vector(lat1, lng1) lat2, lng2 = _normalize_vector(lat2, lng2) elif check: _ensure_lat_lon_vector(lat1, lng1) _ensure_lat_lon_vector(lat2, lng2) # If in combination mode, turn coordinates of array1 into column vectors for broadcasting if comb: lat1 = numpy.expand_dims(lat1, axis=0) lng1 = numpy.expand_dims(lng1, axis=0) lat2 = numpy.expand_dims(lat2, axis=1) lng2 = numpy.expand_dims(lng2, axis=1) return get_avg_earth_radius(unit) * _haversine_kernel_vector(lat1, lng1, lat2, lng2) def inverse_haversine(point, distance, direction: Union[Direction, float], unit=Unit.KILOMETERS): lat, lng = point r = get_avg_earth_radius(unit) return _inverse_haversine_kernel(lat, lng, direction, distance/r) def inverse_haversine_vector(array, distance, direction, unit=Unit.KILOMETERS): if not has_numpy: raise RuntimeError('Error, unable to import Numpy, ' 'consider using inverse_haversine instead of inverse_haversine_vector.') # ensure arrays are numpy ndarrays array, distance, direction = map(numpy.asarray, (array, distance, direction)) # ensure will be able to iterate over rows by adding dimension if needed if array.ndim == 1: array = numpy.expand_dims(array, 0) # Asserts that arrays are correctly sized if array.ndim != 2 or array.shape[1] != 2 or array.shape[0] != len(distance) or array.shape[0] != len(direction): raise IndexError("Arrays must be of same size.") # unpack latitude/longitude lat, lng = array[:, 0], array[:, 1] r = get_avg_earth_radius(unit) return _inverse_haversine_kernel_vector(lat, lng, direction, distance/r) mapado-haversine-6661b21/tests/test_haversine.py000077500000000000000000000072731455147445000217720ustar00rootroot00000000000000from haversine import haversine, Unit from math import pi import pytest from tests.geo_ressources import LYON, PARIS, NEW_YORK, LONDON, EXPECTED_LYON_PARIS def haversine_test_factory(unit): def test(): expected = EXPECTED_LYON_PARIS[unit] assert haversine(LYON, PARIS, unit=unit) == expected assert isinstance(unit.value, str) assert haversine(LYON, PARIS, unit=unit.value) == expected return test test_kilometers = haversine_test_factory(Unit.KILOMETERS) test_meters = haversine_test_factory(Unit.METERS) test_miles = haversine_test_factory(Unit.MILES) test_nautical_miles = haversine_test_factory(Unit.NAUTICAL_MILES) test_feet = haversine_test_factory(Unit.FEET) test_inches = haversine_test_factory(Unit.INCHES) test_radians = haversine_test_factory(Unit.RADIANS) test_degrees = haversine_test_factory(Unit.DEGREES) def test_units_enum(): from haversine.haversine import _CONVERSIONS assert all(unit in _CONVERSIONS for unit in Unit) def test_haversine_deg_rad(): """ Test makes sure that one time around earth matches sphere circumference in degrees / radians. """ p1, p2 = (45, 0), (-45, 180) assert haversine(p1, p2, unit=Unit.RADIANS) == pi assert round(haversine(p1, p2, unit=Unit.DEGREES), 13) == 180.0 @pytest.mark.parametrize( "oob_from,oob_to,proper_from,proper_to", [ ((-90.0001, 0), (0, 0), (-89.9999, 180), (0, 0)), ((-90.0001, 30), (0, 0), (-89.9999, -150), (0, 0)), ((0, 0), (90.0001, 0), (0, 0), (89.9999, -180)), ((0, 0), (90.0001, 30), (0, 0), (89.9999, -150)), ((0, -180.0001), (0, 0), (0, 179.9999), (0, 0)), ((30, -180.0001), (0, 0), (30, 179.9999), (0, 0)), ((0, 0), (0, 180.0001), (0, 0), (0, -179.9999)), ((0, 0), (30, 180.0001), (0, 0), (30, -179.9999)), ] ) def test_normalization(oob_from, oob_to, proper_from, proper_to): """ Test makes sure that normalization works as expected by comparing distance of out of bounds points cases to equal cases where all points are within lat/lon ranges. The results are expected to be equal (within some tolerance to account for numerical issues). """ normalized_during, normalized_already = ( haversine(oob_from, oob_to, Unit.DEGREES, normalize=True), haversine(proper_from, proper_to, Unit.DEGREES, normalize=True), ) assert normalized_during == pytest.approx(normalized_already, abs=1e-10) @pytest.mark.parametrize( "oob_from,oob_to", [ ((-90.0001, 0), (0, 0)), ((0, 0), (90.0001, 0)), ((0, -180.0001), (0, 0)), ((0, 0), (0, 180.0001)), ] ) def test_out_of_bounds(oob_from, oob_to): """ Test makes sure that a ValueError is raised when latitude or longitude values are out of bounds. """ with pytest.raises(ValueError): haversine(oob_from, oob_to) with pytest.raises(ValueError): haversine(oob_from, oob_to, normalize=False) @pytest.mark.parametrize( "in_bounds_from,in_bounds_to", [ ((-90, 0), (0, 0)), ((0, 0), (90, 0)), ((0, -180), (0, 0)), ((0, 0), (0, 180)), ] ) def test_in_bounds(in_bounds_from, in_bounds_to): """ Test makes sure that a ValueError is NOT raised when latitude or longitude values are in bounds. """ assert haversine(in_bounds_from, in_bounds_to) > 0 def test_haversine_deg_rad_great_circle_distance(): """ Test makes sure the haversine functions returns the great circle distance (https://en.wikipedia.org/wiki/Great-circle_distance) between two points on a sphere. See https://github.com/mapado/haversine/issues/45 """ p1, p2 = (0, -45), (0, 45) assert haversine(p1, p2, Unit.DEGREES) == 89.99999999999997 mapado-haversine-6661b21/tests/test_haversine_vector.py000066400000000000000000000057621455147445000233520ustar00rootroot00000000000000from haversine import haversine_vector, Unit from numpy.testing import assert_allclose import pytest from tests.geo_ressources import EXPECTED_LONDON_PARIS, EXPECTED_LYON_NEW_YORK, EXPECTED_LYON_PARIS, EXPECTED_LONDON_NEW_YORK, LYON, PARIS, NEW_YORK, LONDON @pytest.mark.parametrize( 'unit', [Unit.KILOMETERS, Unit.METERS, Unit.INCHES] ) def test_pair(unit): def test_lyon_paris(unit): expected_lyon_paris = EXPECTED_LYON_PARIS[unit] assert haversine_vector(LYON, PARIS, unit=unit) == expected_lyon_paris assert isinstance(unit.value, str) assert haversine_vector( LYON, PARIS, unit=unit.value) == expected_lyon_paris return test_lyon_paris(unit) @pytest.mark.parametrize( "oob_from,oob_to,proper_from,proper_to", [ ((-90.0001, 0), (0, 0), (-89.9999, 180), (0, 0)), ((-90.0001, 30), (0, 0), (-89.9999, -150), (0, 0)), ((0, 0), (90.0001, 0), (0, 0), (89.9999, -180)), ((0, 0), (90.0001, 30), (0, 0), (89.9999, -150)), ((0, -180.0001), (0, 0), (0, 179.9999), (0, 0)), ((30, -180.0001), (0, 0), (30, 179.9999), (0, 0)), ((0, 0), (0, 180.0001), (0, 0), (0, -179.9999)), ((0, 0), (30, 180.0001), (0, 0), (30, -179.9999)), ] ) def test_normalization(oob_from, oob_to, proper_from, proper_to): """ Test makes sure that normalization works as expected by comparing distance of out of bounds points cases to equal cases where all points are within lat/lon ranges. The results are expected to be equal (within some tolerance to account for numerical issues). """ normalized_during, normalized_already = ( haversine_vector([oob_from], [oob_to], Unit.DEGREES, normalize=True), haversine_vector([proper_from], [proper_to], Unit.DEGREES, normalize=True), ) assert normalized_during == pytest.approx(normalized_already, abs=1e-10) @pytest.mark.parametrize( "oob_from,oob_to", [ ((-90.0001, 0), (0, 0)), ((0, 0), (90.0001, 0)), ((0, -180.0001), (0, 0)), ((0, 0), (0, 180.0001)), ] ) def test_out_of_bounds(oob_from, oob_to): """ Test makes sure that a ValueError is raised when latitude or longitude values are out of bounds. """ with pytest.raises(ValueError): haversine_vector([oob_from], [oob_to]) with pytest.raises(ValueError): haversine_vector([oob_from], [oob_to], normalize=False) def test_haversine_vector_comb(): unit = Unit.KILOMETERS expected = [ [EXPECTED_LYON_PARIS[unit], EXPECTED_LONDON_PARIS[unit]], [EXPECTED_LYON_NEW_YORK[unit], EXPECTED_LONDON_NEW_YORK[unit]] ] assert_allclose( # See https://numpy.org/doc/stable/reference/generated/numpy.testing.assert_allclose.html#numpy.testing.assert_allclose haversine_vector([LYON, LONDON], [PARIS, NEW_YORK], unit, comb=True), expected ) def test_units_enum(): from haversine.haversine import _CONVERSIONS assert all(unit in _CONVERSIONS for unit in Unit) mapado-haversine-6661b21/tests/test_inverse_haversine.py000077500000000000000000000032661455147445000235230ustar00rootroot00000000000000from haversine import inverse_haversine, haversine, Unit, Direction from numpy import isclose from math import pi import pytest from tests.geo_ressources import LYON, PARIS, NEW_YORK, LONDON @pytest.mark.parametrize( "point, dir, dist, result", [ (PARIS, Direction.NORTH, 32, (49.144444, 2.3508)), (PARIS, 0, 32, (49.144444, 2.3508)), (LONDON, Direction.WEST, 50, (51.507778, -0.840556)), (LONDON, pi * 1.5, 50, (51.507778, -0.840556)), (NEW_YORK, Direction.SOUTH, 15, (40.568611, -74.235278)), (NEW_YORK, Direction.NORTHWEST, 50, (41.020556, -74.656667)), (NEW_YORK, pi * 1.25, 50, (40.384722, -74.6525)), ], ) def test_inverse_kilometers(point, dir, dist, result): assert isclose(inverse_haversine(point, dist, dir), result, rtol=1e-5).all() @pytest.mark.parametrize( "point, direction, distance, unit", [ (PARIS, Direction.NORTH, 10, Unit.KILOMETERS), (LONDON, Direction.WEST, 32, Unit.MILES), (LYON, Direction.NORTHEAST, 45_000, Unit.METERS), (NEW_YORK, Direction.SOUTH, 15, Unit.NAUTICAL_MILES), ], ) def test_back_and_forth(point, direction, distance, unit): new_point = inverse_haversine(point, distance, direction, unit) assert isclose(haversine(new_point, point, unit), distance, rtol=1e-10) def test_inverse_miles(): assert isclose(inverse_haversine(PARIS, 50, Direction.NORTH, unit=Unit.MILES), (49.5803579218996, 2.3508), rtol=1e-5).all() def test_nautical_inverse_miles(): assert isclose(inverse_haversine(PARIS, 10, Direction.SOUTH, unit=Unit.NAUTICAL_MILES), (48.69014586638915, 2.3508), rtol=1e-5).all() mapado-haversine-6661b21/tests/test_inverse_haversine_vector.py000066400000000000000000000015431455147445000250760ustar00rootroot00000000000000from haversine import inverse_haversine_vector, Unit, Direction from numpy import isclose from math import pi import pytest from tests.geo_ressources import LYON, PARIS, NEW_YORK, LONDON @pytest.mark.parametrize( "point, dir, dist, result", [ (PARIS, Direction.NORTH, 32, (49.144444, 2.3508)), (PARIS, 0, 32, (49.144444, 2.3508)), (LONDON, Direction.WEST, 50, (51.507778, -0.840556)), (LONDON, pi * 1.5, 50, (51.507778, -0.840556)), (NEW_YORK, Direction.SOUTH, 15, (40.568611, -74.235278)), (NEW_YORK, Direction.NORTHWEST, 50, (41.020556, -74.656667)), (NEW_YORK, pi * 1.25, 50, (40.384722, -74.6525)), ], ) def test_inverse_kilometers(point, dir, dist, result): assert isclose(inverse_haversine_vector([point], [dist], [dir]), ([result[0]], [result[1]]), rtol=1e-5).all() mapado-haversine-6661b21/tests/test_performance.py000066400000000000000000000032611455147445000222750ustar00rootroot00000000000000from . import haversine_baseline as baseline import haversine as current import numpy as np import pytest from timeit import timeit def assert_performance(func, number): # Interleave measurements and compare fastest current to median baseline. # All in an attempt to avoid spurious errors caused by fluctuating load on # the runner. t_baseline, t_current = [], [] for repeat in range(5): t_baseline.append(timeit(lambda: func(baseline), number=number)) t_current.append(timeit(lambda: func(current), number=number)) perf_ratio = np.min(t_current) / np.median(t_baseline) assert perf_ratio <= 1.1 @pytest.mark.parametrize( "check", [False, True] ) @pytest.mark.parametrize( "normalize", [False, True] ) def test_haversine(check, normalize): if check == normalize == True: pytest.skip() assert_performance(lambda m: m.haversine((0,1), (2,3), check=check, normalize=normalize), number=100000) @pytest.mark.parametrize( "check", [False, True] ) @pytest.mark.parametrize( "normalize", [False, True] ) def test_haversine_vector(check, normalize): if check == normalize == True: pytest.skip() arr = np.random.uniform(size=(1000, 2)) assert_performance(lambda m: m.haversine_vector(arr, arr, check=check, normalize=normalize), number=1000) def test_inverse_haversine(): assert_performance(lambda m: m.inverse_haversine((0,1), 2, 3), number=100000) def test_inverse_haversine_vector(): arr = np.random.uniform(size=(1000, 2)) assert_performance(lambda m: m.inverse_haversine_vector(arr, *arr.T), number=1000)