From 4b0df51b4088c64f1a647f838697d8656b43cf32 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 29 May 2016 20:55:16 +0200 Subject: [PATCH] Vendorize vincenty requirement (#2176) --- homeassistant/util/location.py | 91 ++++++++++++++++++++++++++++++++-- requirements_all.txt | 1 - setup.py | 1 - tests/util/test_location.py | 45 +++++++++++++++++ 4 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 tests/util/test_location.py diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 55f8a834308..a596d9bc476 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -4,12 +4,24 @@ Module with location helpers. detect_location_info and elevation are mocked by default during tests. """ import collections - +import math import requests -from vincenty import vincenty - ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' +DATA_SOURCE = ['https://freegeoip.io/json/', 'http://ip-api.com/json'] + +# Constants from https://github.com/maurycyp/vincenty +# Earth ellipsoid according to WGS 84 +# Axis a of the ellipsoid (Radius of the earth in meters) +AXIS_A = 6378137 +# Flattening f = (a-b) / a +FLATTENING = 1 / 298.257223563 +# Axis b of the ellipsoid in meters. +AXIS_B = 6356752.314245 + +MILES_PER_KILOMETER = 0.621371 +MAX_ITERATIONS = 200 +CONVERGENCE_THRESHOLD = 1e-12 LocationInfo = collections.namedtuple( "LocationInfo", @@ -17,8 +29,6 @@ LocationInfo = collections.namedtuple( 'city', 'zip_code', 'time_zone', 'latitude', 'longitude', 'use_fahrenheit']) -DATA_SOURCE = ['https://freegeoip.io/json/', 'http://ip-api.com/json'] - def detect_location_info(): """Detect location information.""" @@ -76,3 +86,74 @@ def elevation(latitude, longitude): return int(float(req.json()['results'][0]['elevation'])) except (ValueError, KeyError): return 0 + + +# Author: https://github.com/maurycyp +# Source: https://github.com/maurycyp/vincenty +# License: https://github.com/maurycyp/vincenty/blob/master/LICENSE +# pylint: disable=too-many-locals, invalid-name, unused-variable +def vincenty(point1, point2, miles=False): + """ + Vincenty formula (inverse method) to calculate the distance. + + Result in kilometers or miles between two points on the surface of a + spheroid. + """ + # short-circuit coincident points + if point1[0] == point2[0] and point1[1] == point2[1]: + return 0.0 + + U1 = math.atan((1 - FLATTENING) * math.tan(math.radians(point1[0]))) + U2 = math.atan((1 - FLATTENING) * math.tan(math.radians(point2[0]))) + L = math.radians(point2[1] - point1[1]) + Lambda = L + + sinU1 = math.sin(U1) + cosU1 = math.cos(U1) + sinU2 = math.sin(U2) + cosU2 = math.cos(U2) + + for iteration in range(MAX_ITERATIONS): + sinLambda = math.sin(Lambda) + cosLambda = math.cos(Lambda) + sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 + + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2) + if sinSigma == 0: + return 0.0 # coincident points + cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda + sigma = math.atan2(sinSigma, cosSigma) + sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma + cosSqAlpha = 1 - sinAlpha ** 2 + try: + cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha + except ZeroDivisionError: + cos2SigmaM = 0 + C = FLATTENING / 16 * cosSqAlpha * (4 + FLATTENING * (4 - 3 * + cosSqAlpha)) + LambdaPrev = Lambda + Lambda = L + (1 - C) * FLATTENING * sinAlpha * (sigma + C * sinSigma * + (cos2SigmaM + C * + cosSigma * + (-1 + 2 * + cos2SigmaM ** 2))) + if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD: + break # successful convergence + else: + return None # failure to converge + + uSq = cosSqAlpha * (AXIS_A ** 2 - AXIS_B ** 2) / (AXIS_B ** 2) + A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) + B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) + deltaSigma = B * sinSigma * (cos2SigmaM + + B / 4 * (cosSigma * (-1 + 2 * + cos2SigmaM ** 2) - + B / 6 * cos2SigmaM * + (-3 + 4 * sinSigma ** 2) * + (-3 + 4 * cos2SigmaM ** 2))) + s = AXIS_B * A * (sigma - deltaSigma) + + s /= 1000 # Converion of meters to kilometers + if miles: + s *= MILES_PER_KILOMETER # kilometers to miles + + return round(s, 6) diff --git a/requirements_all.txt b/requirements_all.txt index 614e4d85d3b..a4b7df30835 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3,7 +3,6 @@ requests>=2,<3 pyyaml>=3.11,<4 pytz>=2016.4 pip>=7.0.0 -vincenty==0.1.4 jinja2>=2.8 voluptuous==0.8.9 webcolors==1.5 diff --git a/setup.py b/setup.py index d315ae7d386..c531281c75b 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ REQUIRES = [ 'pyyaml>=3.11,<4', 'pytz>=2016.4', 'pip>=7.0.0', - 'vincenty==0.1.4', 'jinja2>=2.8', 'voluptuous==0.8.9', 'webcolors==1.5', diff --git a/tests/util/test_location.py b/tests/util/test_location.py new file mode 100644 index 00000000000..7d0052fe62c --- /dev/null +++ b/tests/util/test_location.py @@ -0,0 +1,45 @@ +"""Test Home Assistant location util methods.""" +# pylint: disable=too-many-public-methods +import unittest + +import homeassistant.util.location as location_util + +# Paris +COORDINATES_PARIS = (48.864716, 2.349014) +# New York +COORDINATES_NEW_YORK = (40.730610, -73.935242) + +# Results for the assertion (vincenty algorithm): +# Distance [km] Distance [miles] +# [0] 5846.39 3632.78 +# [1] 5851 3635 +# +# [0]: http://boulter.com/gps/distance/ +# [1]: https://www.wolframalpha.com/input/?i=from+paris+to+new+york +DISTANCE_KM = 5846.39 +DISTANCE_MILES = 3632.78 + + +class TestLocationUtil(unittest.TestCase): + """Test util location methods.""" + + def test_get_distance(self): + """Test getting the distance.""" + meters = location_util.distance(COORDINATES_PARIS[0], + COORDINATES_PARIS[1], + COORDINATES_NEW_YORK[0], + COORDINATES_NEW_YORK[1]) + self.assertAlmostEqual(meters / 1000, DISTANCE_KM, places=2) + + def test_get_kilometers(self): + """Test getting the distance between given coordinates in km.""" + kilometers = location_util.vincenty(COORDINATES_PARIS, + COORDINATES_NEW_YORK) + self.assertEqual(round(kilometers, 2), DISTANCE_KM) + + def test_get_miles(self): + """Test getting the distance between given coordinates in miles.""" + miles = location_util.vincenty(COORDINATES_PARIS, + COORDINATES_NEW_YORK, + miles=True) + self.assertEqual(round(miles, 2), DISTANCE_MILES)