├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── geo ├── __init__.py ├── _ellipsoid.pyx ├── _helpers.pxd ├── _sphere.pyx ├── constants.py ├── ellipsoid.py └── sphere.py ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | *.c 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Juno Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include LICENSE 3 | graft geo 4 | include README.rst 5 | include test.py 6 | global-exclude __pycache__ 7 | global-exclude *.so 8 | global-exclude *.pyd 9 | global-exclude *.py[co] -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-geo 2 | ========== 3 | 4 | Set of algorithms and structures related to geodesy. 5 | 6 | API 7 | --- 8 | 9 | geo.sphere 10 | ~~~~~~~~~~~~ 11 | 12 | Functions onto sphere 13 | 14 | geo.sphere.approximate_distance 15 | _________________________________ 16 | 17 | .. code-block:: python 18 | 19 | def approximate_distance(point1, point2): 20 | 21 | Approximate calculation distance 22 | (expanding the trigonometric functions around the midpoint) 23 | 24 | geo.sphere.haversine_distance 25 | _______________________________ 26 | 27 | .. code-block:: python 28 | 29 | def _haversine_distance(point1, point2): 30 | 31 | Calculating haversine distance between two points (see https://en.wikipedia.org/wiki/Haversine_formula, https://www.math.ksu.edu/~dbski/writings/haversine.pdf) 32 | 33 | Is numerically better-conditioned for small distances 34 | 35 | geo.sphere.distance 36 | _____________________ 37 | 38 | .. code-block:: python 39 | 40 | def distance(point1, point2): 41 | 42 | Calculating great-circle distance (see https://en.wikipedia.org/wiki/Great-circle_distance) 43 | 44 | geo.sphere.bearing 45 | __________________ 46 | 47 | .. code-block:: python 48 | 49 | def bearing(point1, point2): 50 | 51 | Calculating initial bearing between two points 52 | (see http://www.movable-type.co.uk/scripts/latlong.html) 53 | 54 | geo.sphere.final_bearing 55 | ________________________ 56 | 57 | .. code-block:: python 58 | 59 | def final_bearing(point1, point2): 60 | 61 | Calculating finatl bearing (initial bering + 180) between two points 62 | 63 | geo.sphere.destination 64 | ______________________ 65 | 66 | .. code-block:: python 67 | 68 | def destination(point, distance, bearing): 69 | 70 | Given a start point, initial bearing, and distance, this will 71 | calculate the destina­tion point and final bearing travelling 72 | along a (shortest distance) great circle arc. (see http://www.movable-type.co.uk/scripts/latlong.htm) 73 | 74 | geo.sphere.approximate_destination 75 | __________________________________ 76 | 77 | .. code-block:: python 78 | 79 | def approximate_destination(point, distance, theta): 80 | 81 | geo.sphere.from4326_to3857 82 | __________________________ 83 | 84 | .. code-block:: python 85 | 86 | def from4326_to3857(point): 87 | 88 | Reproject point from EPSG:4326 (https://epsg.io/4326) to EPSG:3857 (https://epsg.io/3857) (see http://wiki.openstreetmap.org/wiki/Mercator) 89 | 90 | Spherical Mercator: 91 | E = R*(λ - λo) 92 | N = R*ln(tan(π/4+φ/2)) 93 | 94 | geo.sphere.from3857_to4326 95 | __________________________ 96 | 97 | .. code-block:: python 98 | 99 | def from4326_to3857(point): 100 | 101 | Reproject point from EPSG:3857 (https://epsg.io/3857) to EPSG:4326 (https://epsg.io/4326) (see http://wiki.openstreetmap.org/wiki/Mercator) 102 | 103 | Reverse Spherical Mercator: 104 | λ = E/R + λo 105 | φ = π/2 - 2*arctan(exp(-N/R)) 106 | 107 | geo.ellipsoid 108 | ~~~~~~~~~~~~~ 109 | 110 | Functions onto ellipsoid 111 | 112 | geo.ellipsoid.distance 113 | ______________________ 114 | 115 | .. code-block:: python 116 | 117 | def distance(point1, point2, ellipsoid=WGS84): 118 | 119 | Calculating distance with using vincenty's formula 120 | (see https://en.wikipedia.org/wiki/Vincenty's_formulae) 121 | 122 | geo.ellipsoid.from4326_to3395 123 | _____________________________ 124 | 125 | .. code-block:: python 126 | 127 | def from4326_to3395(point, ellipsoid=WGS84): 128 | 129 | Reproject point from EPSG:4326 (https://epsg.io/4326) to EPSG:3395 (https://epsg.io/3395) (see https://en.wikipedia.org/wiki/Mercator_projection#Generalization_to_the_ellipsoid) 130 | 131 | Ellipsoidal Mercator: 132 | E = a*(λ - λo) 133 | N = a*ln(tan(π/4+φ/2)*((1-e*sin(φ))/(1+e*sin(φ)))**e/2) 134 | 135 | geo.ellipsoid.from3395_to4326 136 | _____________________________ 137 | 138 | .. code-block:: python 139 | 140 | def from3395_to4326(point, ellipsoid=WGS84): 141 | 142 | Reproject point from EPSG:3395 (https://epsg.io/3395) to EPSG:4326 (https://epsg.io/4326) (see https://en.wikipedia.org/wiki/Mercator_projection#Generalization_to_the_ellipsoid) 143 | 144 | Reverse Ellipsoidal Mercator: 145 | λ = E/a + λo 146 | φ = π/2 + 2*arctan(exp(-N/a)*((1-e*sin(φ))/(1+e*sin(φ))**e/2)) 147 | -------------------------------------------------------------------------------- /geo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuno/geo-py/17eb60a0143db573e27dc2e5555f33ed03275439/geo/__init__.py -------------------------------------------------------------------------------- /geo/_ellipsoid.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | cimport cpython 3 | from libc.math cimport fabs, sqrt, cos, sin, tan, atan, asin, atan2, log, exp, M_PI 4 | from _helpers cimport unpack_point, to_degrees 5 | 6 | from geo.constants import WGS84 7 | 8 | cdef double CONVERGENCE_THRESHOLD = 1e-12 9 | cdef int MAX_ITERATIONS = 10 10 | cdef double HALF_PI = M_PI / 2.0 11 | cdef double QUARTER_PI = M_PI / 4.0 12 | 13 | 14 | def _distance(point1, point2, ellipsoid=WGS84): 15 | cdef double lon1, lat1, lon2, lat2 16 | cdef ellipsoid_a = ellipsoid.a 17 | cdef ellipsoid_b = ellipsoid.b 18 | cdef ellipsoid_f = ellipsoid.f 19 | 20 | unpack_point(point1, &lon1, &lat1) 21 | unpack_point(point2, &lon2, &lat2) 22 | 23 | cdef double U1 = atan((1 - ellipsoid_f) * tan(lat1)) 24 | cdef double U2 = atan((1 - ellipsoid_f) * tan(lat2)) 25 | cdef double L = lon2 - lon1 26 | 27 | cdef double sinU1 = sin(U1) 28 | cdef double cosU1 = cos(U1) 29 | cdef double sinU2 = sin(U2) 30 | cdef double cosU2 = cos(U2) 31 | 32 | cdef double sinLambda, sinSigma, sigma, sinAlpha, cosSqAlpha 33 | cdef double C, Lambda, LambdaPrev 34 | 35 | Lambda = L 36 | 37 | for _ in range(MAX_ITERATIONS): 38 | sinLambda = sin(Lambda) 39 | cosLambda = cos(Lambda) 40 | sinSigma = sqrt( 41 | (cosU2 * sinLambda) ** 2 + 42 | (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2) 43 | # coincident points 44 | if sinSigma == 0: 45 | return 0.0 46 | 47 | cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda 48 | sigma = atan2(sinSigma, cosSigma) 49 | sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma 50 | cosSqAlpha = 1 - sinAlpha ** 2 51 | try: 52 | cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha 53 | except ZeroDivisionError: 54 | cos2SigmaM = 0 55 | 56 | C = (ellipsoid_f / 16) * cosSqAlpha * ( 57 | 4 + ellipsoid_f * (4 - 3 * cosSqAlpha) 58 | ) 59 | LambdaPrev = Lambda 60 | Lambda = ( 61 | L + (1 - C) * ellipsoid.f * sinAlpha * ( 62 | sigma + C * sinSigma * ( 63 | cos2SigmaM + C * cosSigma * ( 64 | -1 + 2 * cos2SigmaM ** 2 65 | ) 66 | ) 67 | ) 68 | ) 69 | 70 | if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD: 71 | break 72 | else: 73 | # failure to converge 74 | return None 75 | 76 | cdef double uSq, A, B, deltaSigma, s 77 | 78 | uSq = cosSqAlpha * (ellipsoid_a ** 2 - ellipsoid_b ** 2) / (ellipsoid_b ** 2) 79 | A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) 80 | B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) 81 | deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * 82 | (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * 83 | (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2))) 84 | s = ellipsoid_b * A * (sigma - deltaSigma) 85 | return s 86 | 87 | 88 | def _from4326_to3395(point, ellipsoid=WGS84): 89 | cdef double lon, lat 90 | cdef double ellipsoid_a = ellipsoid.a 91 | cdef double ellipsoid_e = ellipsoid.e 92 | 93 | unpack_point(point, &lon, &lat) 94 | 95 | cdef double e_sin_lat = ellipsoid_e*sin(lat) 96 | cdef double multiplier1 = tan(QUARTER_PI + lat / 2.0) 97 | cdef double multiplier2 = pow((1-e_sin_lat)/(1+e_sin_lat), ellipsoid_e/2) 98 | cdef double E = ellipsoid_a * lon 99 | cdef double N = ellipsoid_a * log(multiplier1*multiplier2) 100 | return (E, N) 101 | 102 | 103 | def _from3395_to4326(point, ellipsoid=WGS84): 104 | cdef double E, N 105 | E, N = point 106 | cdef double a = ellipsoid.a 107 | cdef double e = ellipsoid.e 108 | cdef double half_e = e * 0.5 109 | 110 | cdef double m1 = exp(-N / a) 111 | cdef double m2 112 | cdef double new_phi 113 | cdef double phi = HALF_PI - 2.0 * atan(m1) 114 | cdef double e_sin_phi 115 | 116 | for _ in range(MAX_ITERATIONS): 117 | e_sin_phi = e*sin(phi) 118 | m2 = ((1 - e_sin_phi) / (1 + e_sin_phi))**half_e 119 | new_phi = HALF_PI - 2.0 * atan(m1 * m2) 120 | if abs(new_phi - phi) <= CONVERGENCE_THRESHOLD: 121 | phi = new_phi 122 | break 123 | phi = new_phi 124 | 125 | lon = to_degrees(E / a) 126 | lat = to_degrees(phi) 127 | return (lon, lat) 128 | -------------------------------------------------------------------------------- /geo/_helpers.pxd: -------------------------------------------------------------------------------- 1 | cimport cython 2 | cimport cpython 3 | from libc.math cimport M_PI 4 | 5 | 6 | @cython.cdivision(True) 7 | cdef inline double to_degrees(double radians): 8 | return radians * (180.0 / M_PI) 9 | 10 | 11 | @cython.cdivision(True) 12 | cdef inline double to_radians(double degrees): 13 | return (degrees * M_PI) / 180.0 14 | 15 | 16 | cdef inline unpack_point(point, double *lon, double *lat): 17 | if (not cpython.PySequence_Check(point)): 18 | raise TypeError("point must be an iterable of double") 19 | lon[0] = to_radians(point[0]) 20 | lat[0] = to_radians(point[1]) 21 | -------------------------------------------------------------------------------- /geo/_sphere.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | cimport cpython 3 | from libc.math cimport log, exp, fabs, sqrt, cos, sin, tan, asin, atan, atan2, M_PI 4 | 5 | from _helpers cimport unpack_point, to_radians, to_degrees 6 | 7 | from geo.constants import ( 8 | EARTH_MEAN_RADIUS as py_EARTH_MEAN_RADIUS, 9 | EARTH_MEAN_DIAMETER as py_EARTH_MEAN_DIAMETER, 10 | EARTH_EQUATORIAL_RADIUS as py_EARTH_EQUATORIAL_RADIUS, 11 | EARTH_EQUATORIAL_METERS_PER_DEGREE as py_EARTH_EQUATORIAL_METERS_PER_DEGREE, 12 | I_EARTH_EQUATORIAL_METERS_PER_DEGREE as py_I_EARTH_EQUATORIAL_METERS_PER_DEGREE 13 | ) 14 | 15 | cdef double EARTH_MEAN_RADIUS = py_EARTH_MEAN_RADIUS 16 | cdef double EARTH_MEAN_DIAMETER = py_EARTH_MEAN_DIAMETER 17 | cdef double EARTH_EQUATORIAL_RADIUS = py_EARTH_EQUATORIAL_RADIUS 18 | cdef double EARTH_EQUATORIAL_METERS_PER_DEGREE = py_EARTH_EQUATORIAL_METERS_PER_DEGREE 19 | cdef double I_EARTH_EQUATORIAL_METERS_PER_DEGREE = py_I_EARTH_EQUATORIAL_METERS_PER_DEGREE 20 | cdef HALF_PI = M_PI / 2.0 21 | 22 | def _approximate_distance(point1, point2): 23 | cdef double lon1, lat1, lon2, lat2 24 | 25 | unpack_point(point1, &lon1, &lat1) 26 | unpack_point(point2, &lon2, &lat2) 27 | 28 | cdef double cos_lat 29 | cos_lat = cos((lat1+lat2)/2.0) 30 | 31 | cdef double dx = (lat2 - lat1) 32 | cdef double dy = (cos_lat*(lon2 - lon1)) 33 | 34 | return EARTH_MEAN_RADIUS*sqrt(dx**2 + dy**2) 35 | 36 | 37 | def _haversine_distance(point1, point2): 38 | cdef double lon1, lat1, lon2, lat2 39 | 40 | unpack_point(point1, &lon1, &lat1) 41 | unpack_point(point2, &lon2, &lat2) 42 | 43 | cdef double dlat = (lat2 - lat1) 44 | cdef double dlon = (lon2 - lon1) 45 | cdef double a = ( 46 | sin(dlat * 0.5)**2 + 47 | cos(lat1) * cos(lat2) * sin(dlon * 0.5)**2 48 | ) 49 | 50 | return EARTH_MEAN_DIAMETER * asin(sqrt(a)) 51 | 52 | 53 | def _distance(point1, point2): 54 | cdef double lon1, lat1, lon2, lat2 55 | 56 | unpack_point(point1, &lon1, &lat1) 57 | unpack_point(point2, &lon2, &lat2) 58 | 59 | cdef double dlon = fabs(lon1 - lon2) 60 | cdef double dlat = fabs(lat1 - lat2) 61 | 62 | cdef double numerator = sqrt( 63 | (cos(lat2)*sin(dlon))**2 + 64 | ((cos(lat1)*sin(lat2)) - (sin(lat1)*cos(lat2)*cos(dlon)))**2) 65 | 66 | cdef double denominator = ( 67 | (sin(lat1)*sin(lat2)) + 68 | (cos(lat1)*cos(lat2)*cos(dlon))) 69 | 70 | cdef double c = atan2(numerator, denominator) 71 | return EARTH_MEAN_RADIUS*c 72 | 73 | def _from4326_to3857(point): 74 | cdef double lon = point[0] 75 | cdef double lat = point[1] 76 | 77 | cdef double xtile = lon * EARTH_EQUATORIAL_METERS_PER_DEGREE 78 | cdef double ytile = log(tan(to_radians(45 + lat / 2.0))) * EARTH_EQUATORIAL_RADIUS 79 | return (xtile, ytile) 80 | 81 | 82 | def _from3857_to4326(point): 83 | cdef double x = point[0] 84 | cdef double y = point[1] 85 | cdef double lon = x / EARTH_EQUATORIAL_METERS_PER_DEGREE 86 | cdef double lat = to_degrees( 87 | 2.0 * atan(exp(y/EARTH_EQUATORIAL_RADIUS)) - HALF_PI) 88 | return (lon, lat) 89 | -------------------------------------------------------------------------------- /geo/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from math import pi 3 | 4 | # https://en.wikipedia.org/wiki/Earth_radius#Mean_radius 5 | EARTH_MEAN_RADIUS = 6371008.8 6 | EARTH_MEAN_DIAMETER = 2 * EARTH_MEAN_RADIUS 7 | 8 | # https://en.wikipedia.org/wiki/Earth_radius#Equatorial_radius 9 | EARTH_EQUATORIAL_RADIUS = 6378137.0 10 | EARTH_EQUATORIAL_METERS_PER_DEGREE = pi * EARTH_EQUATORIAL_RADIUS / 180 # 111319.49079327358 11 | I_EARTH_EQUATORIAL_METERS_PER_DEGREE = 1 / EARTH_EQUATORIAL_METERS_PER_DEGREE 12 | 13 | HALF_PI = pi / 2.0 14 | QUARTER_PI = pi / 4.0 15 | 16 | 17 | class Datum: 18 | ''' 19 | https://en.wikipedia.org/wiki/Geodetic_datum 20 | ''' 21 | 22 | __slots__ = ('a', 'b', 'e', 'f', 'w') 23 | 24 | def __init__(self, a, b, e, f, w): 25 | self.a = a 26 | self.b = b 27 | self.e = e 28 | self.f = f 29 | self.w = w 30 | 31 | 32 | # https://epsg.io/7030-ellipsoid 33 | WGS84 = Datum( 34 | # equatorial radius (semi-major axis) 35 | a=6378137.0, 36 | # polar radius (semi-minor axis) 37 | b=6356752.314245179, # b = a * (1 - f) 38 | # derived ellipsoid parameters 39 | # eccentricity 40 | e=0.08181919084262149, # e = (2*f - f**2)**0.5 41 | # flattening 42 | f=0.0033528106647474805, # 1/298.257223563 43 | # rotation speed 44 | w=7292115e-11, # rad/sec; 45 | ) 46 | -------------------------------------------------------------------------------- /geo/ellipsoid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from math import ( 4 | degrees, radians, 5 | sin, cos, asin, tan, atan, atan2, pi, 6 | sqrt, exp, log, fabs 7 | ) 8 | from geo.constants import WGS84, QUARTER_PI, HALF_PI 9 | 10 | 11 | CONVERGENCE_THRESHOLD = 1e-12 12 | MAX_ITERATIONS = 15 13 | 14 | 15 | def _py_distance(point1, point2, ellipsoid=WGS84): 16 | ''' 17 | Calculating distance with using vincenty's formula 18 | https://en.wikipedia.org/wiki/Vincenty's_formulae 19 | ''' 20 | lon1, lat1 = (radians(coord) for coord in point1) 21 | lon2, lat2 = (radians(coord) for coord in point2) 22 | 23 | U1 = atan((1 - ellipsoid.f) * tan(lat1)) 24 | U2 = atan((1 - ellipsoid.f) * tan(lat2)) 25 | L = lon2 - lon1 26 | Lambda = L 27 | 28 | sinU1 = sin(U1) 29 | cosU1 = cos(U1) 30 | sinU2 = sin(U2) 31 | cosU2 = cos(U2) 32 | 33 | for _ in range(MAX_ITERATIONS): 34 | sinLambda = sin(Lambda) 35 | cosLambda = cos(Lambda) 36 | sinSigma = sqrt( 37 | (cosU2 * sinLambda) ** 2 + 38 | (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2) 39 | # coincident points 40 | if sinSigma == 0: 41 | return 0.0 42 | 43 | cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda 44 | sigma = atan2(sinSigma, cosSigma) 45 | sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma 46 | cosSqAlpha = 1 - sinAlpha ** 2 47 | try: 48 | cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha 49 | except ZeroDivisionError: 50 | cos2SigmaM = 0 51 | 52 | C = (ellipsoid.f / 16) * cosSqAlpha * ( 53 | 4 + ellipsoid.f * (4 - 3 * cosSqAlpha) 54 | ) 55 | LambdaPrev = Lambda 56 | Lambda = ( 57 | L + (1 - C) * ellipsoid.f * sinAlpha * ( 58 | sigma + C * sinSigma * ( 59 | cos2SigmaM + C * cosSigma * ( 60 | -1 + 2 * cos2SigmaM ** 2 61 | ) 62 | ) 63 | ) 64 | ) 65 | 66 | if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD: 67 | break 68 | else: 69 | # failure to converge 70 | return None 71 | 72 | uSq = cosSqAlpha * (ellipsoid.a ** 2 - ellipsoid.b ** 2) / (ellipsoid.b ** 2) 73 | A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) 74 | B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) 75 | deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * 76 | (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * 77 | (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2))) 78 | s = ellipsoid.b * A * (sigma - deltaSigma) 79 | return s 80 | 81 | 82 | def _py_from4326_to3395(point, ellipsoid=WGS84): 83 | ''' 84 | https://en.wikipedia.org/wiki/Mercator_projection#Generalization_to_the_ellipsoid 85 | http://epsg.io/3395 86 | 87 | Ellipsoidal Mercator: 88 | E = a*(λ - λo) 89 | N = a*ln(tan(π/4+φ/2)*((1-e*sin(φ))/(1+e*sin(φ)))**e/2) 90 | ''' 91 | lon, lat = (radians(coord) for coord in point) 92 | e = ellipsoid.e 93 | a = ellipsoid.a 94 | e_sin_lat = e*sin(lat) 95 | multiplier1 = tan(QUARTER_PI + lat / 2.0) 96 | multiplier2 = pow((1-e_sin_lat)/(1+e_sin_lat),e/2) 97 | E = a * lon 98 | N = a*log(multiplier1*multiplier2) 99 | return (E, N) 100 | 101 | 102 | def _py_from3395_to4326(point, ellipsoid=WGS84): 103 | ''' 104 | Reverse Ellipsoidal Mercator: 105 | λ = E/a + λo 106 | φ = π/2 + 2*arctan(exp(-N/a)*((1-e*sin(φ))/(1+e*sin(φ))**e/2)) 107 | ''' 108 | E, N = point 109 | e = ellipsoid.e 110 | a = ellipsoid.a 111 | half_e = e * 0.5 112 | 113 | m1 = exp(-N / a) 114 | old_phi = HALF_PI - 2.0 * atan(m1) 115 | 116 | for _ in range(MAX_ITERATIONS): 117 | e_sin_phi = e*sin(old_phi) 118 | m2 = ((1 - e_sin_phi) / (1 + e_sin_phi))**half_e 119 | phi = HALF_PI - 2.0 * atan(m1 * m2) 120 | if abs(old_phi - phi) <= CONVERGENCE_THRESHOLD: 121 | break 122 | old_phi = phi 123 | 124 | lon = degrees(E / a) 125 | lat = degrees(phi) 126 | return (lon, lat) 127 | 128 | if '__pypy__' in sys.builtin_module_names: 129 | distance = _py_distance 130 | from4326_to3395 = _py_from4326_to3395 131 | from3395_to4326 = _py_from3395_to4326 132 | else: 133 | try: 134 | from ._ellipsoid import ( 135 | _distance, 136 | _from4326_to3395, 137 | _from3395_to4326 138 | ) 139 | distance = _distance 140 | from4326_to3395 = _from4326_to3395 141 | from3395_to4326 = _from3395_to4326 142 | except ImportError: # pragma: no cover 143 | distance = _py_distance 144 | from4326_to3395 = _py_from4326_to3395 145 | from3395_to4326 = _py_from3395_to4326 146 | -------------------------------------------------------------------------------- /geo/sphere.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from math import ( 4 | degrees, radians, 5 | sin, cos, asin, tan, atan, atan2, pi, 6 | sqrt, exp, log, fabs 7 | ) 8 | 9 | from geo.constants import ( 10 | EARTH_MEAN_RADIUS, 11 | EARTH_MEAN_DIAMETER, 12 | EARTH_EQUATORIAL_RADIUS, 13 | EARTH_EQUATORIAL_METERS_PER_DEGREE, 14 | I_EARTH_EQUATORIAL_METERS_PER_DEGREE, 15 | HALF_PI, 16 | QUARTER_PI, 17 | ) 18 | 19 | def _py_approximate_distance(point1, point2): 20 | ''' 21 | Approximate calculation distance 22 | (expanding the trigonometric functions around the midpoint) 23 | ''' 24 | 25 | lon1, lat1 = (radians(coord) for coord in point1) 26 | lon2, lat2 = (radians(coord) for coord in point2) 27 | cos_lat = cos((lat1+lat2)/2.0) 28 | dx = (lat2 - lat1) 29 | dy = (cos_lat*(lon2 - lon1)) 30 | return EARTH_MEAN_RADIUS*sqrt(dx**2 + dy**2) 31 | 32 | 33 | def _py_haversine_distance(point1, point2): 34 | ''' 35 | Calculating haversine distance between two points 36 | (see https://en.wikipedia.org/wiki/Haversine_formula, 37 | https://www.math.ksu.edu/~dbski/writings/haversine.pdf) 38 | 39 | Is numerically better-conditioned for small distances 40 | ''' 41 | lon1, lat1 = (radians(coord) for coord in point1[:2]) 42 | lon2, lat2 = (radians(coord) for coord in point2[:2]) 43 | dlat = (lat2 - lat1) 44 | dlon = (lon2 - lon1) 45 | a = ( 46 | sin(dlat * 0.5)**2 + 47 | cos(lat1) * cos(lat2) * sin(dlon * 0.5)**2 48 | ) 49 | 50 | return EARTH_MEAN_DIAMETER * asin(sqrt(a)) 51 | 52 | 53 | def _py_distance(point1, point2): 54 | ''' 55 | Calculating great-circle distance 56 | (see https://en.wikipedia.org/wiki/Great-circle_distance) 57 | ''' 58 | lon1, lat1 = (radians(coord) for coord in point1) 59 | lon2, lat2 = (radians(coord) for coord in point2) 60 | 61 | dlon = fabs(lon1 - lon2) 62 | dlat = fabs(lat1 - lat2) 63 | 64 | numerator = sqrt( 65 | (cos(lat2)*sin(dlon))**2 + 66 | ((cos(lat1)*sin(lat2)) - (sin(lat1)*cos(lat2)*cos(dlon)))**2) 67 | 68 | denominator = ( 69 | (sin(lat1)*sin(lat2)) + 70 | (cos(lat1)*cos(lat2)*cos(dlon))) 71 | 72 | c = atan2(numerator, denominator) 73 | return EARTH_MEAN_RADIUS*c 74 | 75 | 76 | def bearing(point1, point2): 77 | ''' 78 | Calculating initial bearing between two points 79 | (see http://www.movable-type.co.uk/scripts/latlong.html) 80 | ''' 81 | lon1, lat1 = (radians(coord) for coord in point1) 82 | lon2, lat2 = (radians(coord) for coord in point2) 83 | 84 | dlat = (lat2 - lat1) 85 | dlon = (lon2 - lon1) 86 | numerator = sin(dlon) * cos(lat2) 87 | denominator = ( 88 | cos(lat1) * sin(lat2) - 89 | (sin(lat1) * cos(lat2) * cos(dlon)) 90 | ) 91 | 92 | theta = atan2(numerator, denominator) 93 | theta_deg = (degrees(theta) + 360) % 360 94 | return theta_deg 95 | 96 | 97 | def final_bearing(point1, point2): 98 | return (bearing(point2, point1) + 180) % 360 99 | 100 | 101 | def destination(point, distance, bearing): 102 | ''' 103 | Given a start point, initial bearing, and distance, this will 104 | calculate the destina­tion point and final bearing travelling 105 | along a (shortest distance) great circle arc. 106 | 107 | (see http://www.movable-type.co.uk/scripts/latlong.htm) 108 | ''' 109 | 110 | lon1, lat1 = (radians(coord) for coord in point) 111 | radians_bearing = radians(bearing) 112 | 113 | delta = distance / EARTH_MEAN_RADIUS 114 | 115 | lat2 = asin( 116 | sin(lat1)*cos(delta) + 117 | cos(lat1)*sin(delta)*cos(radians_bearing) 118 | ) 119 | numerator = sin(radians_bearing) * sin(delta) * cos(lat1) 120 | denominator = cos(delta) - sin(lat1) * sin(lat2) 121 | 122 | lon2 = lon1 + atan2(numerator, denominator) 123 | 124 | lon2_deg = (degrees(lon2) + 540) % 360 - 180 125 | lat2_deg = degrees(lat2) 126 | 127 | return (lon2_deg, lat2_deg) 128 | 129 | 130 | def approximate_destination(point, distance, theta): 131 | # http://stackoverflow.com/questions/2187657/calculate-second-point-knowing-the-starting-point-and-distance 132 | lon, lat = point 133 | radians_theta = radians(theta) 134 | dx = distance*cos(radians_theta) 135 | dy = distance*sin(radians_theta) 136 | 137 | dlon = dx / (EARTH_EQUATORIAL_METERS_PER_DEGREE*cos(radians(lat))) 138 | dlat = dy / EARTH_EQUATORIAL_METERS_PER_DEGREE 139 | 140 | return (lon+dlon, lat+dlat) 141 | 142 | 143 | def _py_from4326_to3857(point): 144 | ''' 145 | Reproject point from EPSG:4326 to EPSG:3857 146 | (see 147 | http://wiki.openstreetmap.org/wiki/Mercator, 148 | https://epsg.io/4326, 149 | https://epsg.io/3857) 150 | ''' 151 | lon, lat = point 152 | xtile = lon * EARTH_EQUATORIAL_METERS_PER_DEGREE 153 | ytile = log(tan(radians(45 + lat / 2.0))) * EARTH_EQUATORIAL_RADIUS 154 | return (xtile, ytile) 155 | 156 | 157 | def _py_from3857_to4326(point): 158 | ''' 159 | Reproject point from EPSG:3857 to EPSG:4326 160 | (see http://wiki.openstreetmap.org/wiki/Mercator) 161 | 162 | Reverse Spherical Mercator: 163 | λ = E/R + λo 164 | φ = π/2 - 2*arctan(exp(-N/R)) 165 | ''' 166 | x, y = point 167 | lon = x / EARTH_EQUATORIAL_METERS_PER_DEGREE 168 | lat = degrees(2.0 * atan(exp(y/EARTH_EQUATORIAL_RADIUS)) - HALF_PI) 169 | return (lon, lat) 170 | 171 | if '__pypy__' in sys.builtin_module_names: 172 | approximate_distance = _py_approximate_distance 173 | haversine_distance = _py_haversine_distance 174 | distance = _py_distance 175 | from4326_to3857 = _py_from4326_to3857 176 | from3857_to4326 = _py_from3857_to4326 177 | else: 178 | try: 179 | from ._sphere import ( 180 | _approximate_distance, 181 | _haversine_distance, 182 | _distance, 183 | _from4326_to3857, 184 | _from3857_to4326, 185 | ) 186 | approximate_distance = _approximate_distance 187 | haversine_distance = _haversine_distance 188 | distance = _distance 189 | from4326_to3857 = _from4326_to3857 190 | from3857_to4326 = _from3857_to4326 191 | except ImportError: # pragma: no cover 192 | approximate_distance = _py_approximate_distance 193 | haversine_distance = _py_haversine_distance 194 | distance = _py_distance 195 | from4326_to3857 = _py_from4326_to3857 196 | from3857_to4326 = _py_from3857_to4326 197 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, Extension 4 | 5 | try: 6 | from Cython.Build import cythonize 7 | ext = '.pyx' 8 | except ImportError: 9 | def cythonize(extensions): return extensions 10 | ext = '.c' 11 | 12 | MIN_PYTHON = (2, 7) 13 | if sys.version_info < MIN_PYTHON: 14 | sys.stderr.write("Python {}.{} or later is required\n".format(*MIN_PYTHON)) 15 | sys.exit(1) 16 | 17 | def read(fname): 18 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 19 | 20 | extensions = cythonize([ 21 | Extension('geo._sphere', ['geo/_sphere' + ext]), 22 | Extension('geo._ellipsoid', ['geo/_ellipsoid' + ext]), 23 | ]) 24 | 25 | 26 | setup( 27 | name='geo-py', 28 | version='0.5', 29 | author='Alexander Verbitsky', 30 | author_email='habibutsu@gmail.com', 31 | maintainer='Alexander Verbitsky', 32 | maintainer_email='habibutsu@gmail.com', 33 | description='Set of algorithms and structures related to geodesy and geospatial data', 34 | long_description=read('README.rst'), 35 | keywords='geodesy, haversine distance, great circle distance, vincenty\'s formula', 36 | url='https://github.com/gojuno/geo-py', 37 | packages=['geo'], 38 | ext_modules = extensions, 39 | test_suite='test', 40 | license='BSD', 41 | classifiers=[ 42 | 'Development Status :: 5 - Production/Stable', 43 | 'Topic :: Utilities', 44 | 'Programming Language :: Python', 45 | 'License :: OSI Approved :: BSD License', 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | import logging 4 | import math 5 | 6 | import geo.sphere as sphere 7 | import geo._sphere as _sphere 8 | import geo.ellipsoid as ellipsoid 9 | import geo._ellipsoid as _ellipsoid 10 | import geo._sphere as csphere 11 | 12 | logging.basicConfig(level=logging.DEBUG) 13 | logger = logging.getLogger() 14 | 15 | p_minsk = (27.561831, 53.902257) 16 | p_moscow = (37.620393, 55.75396) 17 | 18 | 19 | def isclose(a, b, rel_tol=1e-09, abs_tol=0): 20 | """ 21 | Python 2 implementation of Python 3.5 math.isclose() 22 | https://hg.python.org/cpython/file/v3.5.2/Modules/mathmodule.c#l1993 23 | """ 24 | # sanity check on the inputs 25 | if rel_tol < 0 or abs_tol < 0: 26 | raise ValueError("tolerances must be non-negative") 27 | # short circuit exact equality -- needed to catch two infinities of 28 | # the same sign. And perhaps speeds things up a bit sometimes. 29 | if a == b: 30 | return True 31 | # This catches the case of two infinities of opposite sign, or 32 | # one infinity and one finite number. Two infinities of opposite 33 | # sign would otherwise have an infinite relative tolerance. 34 | # Two infinities of the same sign are caught by the equality check 35 | # above. 36 | if math.isinf(a) or math.isinf(b): 37 | return False 38 | # Cast to float to allow decimal.Decimal arguments 39 | if not isinstance(a, float): 40 | a = float(a) 41 | if not isinstance(b, float): 42 | b = float(b) 43 | # now do the regular computation 44 | # this is essentially the "weak" test from the Boost library 45 | diff = math.fabs(b - a) 46 | result = ((diff <= math.fabs(rel_tol * a)) or 47 | (diff <= math.fabs(rel_tol * b)) or 48 | (diff <= abs_tol)) 49 | return result 50 | 51 | 52 | if not hasattr(math, 'isclose'): 53 | math.isclose = isclose 54 | 55 | 56 | class TestSphere(unittest.TestCase): 57 | 58 | def test_distance(self): 59 | assert math.isclose( 60 | sphere._py_approximate_distance(p_minsk, p_moscow), 61 | 676371.322420, 62 | rel_tol=1e-06) 63 | assert math.isclose( 64 | _sphere._approximate_distance(p_minsk, p_moscow), 65 | 676371.322420, 66 | rel_tol=1e-06) 67 | 68 | assert math.isclose( 69 | sphere._py_haversine_distance(p_minsk, p_moscow), 70 | 675656.299481, 71 | rel_tol=1e-06) 72 | assert math.isclose( 73 | _sphere._haversine_distance(p_minsk, p_moscow), 74 | 675656.299481, 75 | rel_tol=1e-06) 76 | 77 | assert math.isclose( 78 | sphere._py_distance(p_minsk, p_moscow), 79 | 675656.299481, 80 | rel_tol=1e-06) 81 | assert math.isclose( 82 | _sphere._distance(p_minsk, p_moscow), 83 | 675656.299481, 84 | rel_tol=1e-06) 85 | 86 | def test_projection(self): 87 | x, y = sphere._py_from4326_to3857(p_minsk) 88 | assert math.isclose(x, 3068168.9922502628, rel_tol=1e-06) 89 | assert math.isclose(y, 7151666.629430503, rel_tol=1e-06) 90 | x, y = _sphere._from4326_to3857(p_minsk) 91 | assert math.isclose(x, 3068168.9922502628, rel_tol=1e-06) 92 | assert math.isclose(y, 7151666.629430503, rel_tol=1e-06) 93 | 94 | lon, lat = sphere._py_from3857_to4326( 95 | sphere._py_from4326_to3857(p_minsk)) 96 | assert math.isclose(lon, p_minsk[0], rel_tol=1e-06) 97 | assert math.isclose(lat, p_minsk[1], rel_tol=1e-06) 98 | 99 | lon, lat = _sphere._from3857_to4326( 100 | _sphere._from4326_to3857(p_minsk)) 101 | assert math.isclose(lon, p_minsk[0], rel_tol=1e-06) 102 | assert math.isclose(lat, p_minsk[1], rel_tol=1e-06) 103 | 104 | 105 | class TestEllipsoid(unittest.TestCase): 106 | 107 | def test_distance(self): 108 | assert math.isclose( 109 | ellipsoid._py_distance(p_minsk, p_moscow), 110 | 677789.531233, 111 | rel_tol=1e-06) 112 | assert math.isclose( 113 | _ellipsoid._distance(p_minsk, p_moscow), 114 | 677789.531233, 115 | rel_tol=1e-06) 116 | 117 | def test_projection(self): 118 | assert ( 119 | ellipsoid._py_from4326_to3395(p_minsk) == 120 | (3068168.9922502623, 7117115.955611216) 121 | ) 122 | rp_minsk = ellipsoid._py_from3395_to4326( 123 | ellipsoid._py_from4326_to3395(p_minsk)) 124 | 125 | assert math.isclose(rp_minsk[0], p_minsk[0], rel_tol=1e-06) 126 | assert math.isclose(rp_minsk[1], p_minsk[1], rel_tol=1e-06) 127 | 128 | assert ( 129 | _ellipsoid._from4326_to3395(p_minsk) == 130 | (3068168.9922502623, 7117115.955611216) 131 | ) 132 | 133 | rp_minsk = _ellipsoid._from3395_to4326( 134 | _ellipsoid._from4326_to3395(p_minsk)) 135 | 136 | assert math.isclose(rp_minsk[0], p_minsk[0], rel_tol=1e-06) 137 | assert math.isclose(rp_minsk[1], p_minsk[1], rel_tol=1e-06) 138 | --------------------------------------------------------------------------------