mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Vendorize vincenty requirement (#2176)
This commit is contained in:
parent
8494ac7cef
commit
4b0df51b40
@ -4,12 +4,24 @@ Module with location helpers.
|
|||||||
detect_location_info and elevation are mocked by default during tests.
|
detect_location_info and elevation are mocked by default during tests.
|
||||||
"""
|
"""
|
||||||
import collections
|
import collections
|
||||||
|
import math
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from vincenty import vincenty
|
|
||||||
|
|
||||||
ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json'
|
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 = collections.namedtuple(
|
||||||
"LocationInfo",
|
"LocationInfo",
|
||||||
@ -17,8 +29,6 @@ LocationInfo = collections.namedtuple(
|
|||||||
'city', 'zip_code', 'time_zone', 'latitude', 'longitude',
|
'city', 'zip_code', 'time_zone', 'latitude', 'longitude',
|
||||||
'use_fahrenheit'])
|
'use_fahrenheit'])
|
||||||
|
|
||||||
DATA_SOURCE = ['https://freegeoip.io/json/', 'http://ip-api.com/json']
|
|
||||||
|
|
||||||
|
|
||||||
def detect_location_info():
|
def detect_location_info():
|
||||||
"""Detect location information."""
|
"""Detect location information."""
|
||||||
@ -76,3 +86,74 @@ def elevation(latitude, longitude):
|
|||||||
return int(float(req.json()['results'][0]['elevation']))
|
return int(float(req.json()['results'][0]['elevation']))
|
||||||
except (ValueError, KeyError):
|
except (ValueError, KeyError):
|
||||||
return 0
|
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)
|
||||||
|
@ -3,7 +3,6 @@ requests>=2,<3
|
|||||||
pyyaml>=3.11,<4
|
pyyaml>=3.11,<4
|
||||||
pytz>=2016.4
|
pytz>=2016.4
|
||||||
pip>=7.0.0
|
pip>=7.0.0
|
||||||
vincenty==0.1.4
|
|
||||||
jinja2>=2.8
|
jinja2>=2.8
|
||||||
voluptuous==0.8.9
|
voluptuous==0.8.9
|
||||||
webcolors==1.5
|
webcolors==1.5
|
||||||
|
1
setup.py
1
setup.py
@ -15,7 +15,6 @@ REQUIRES = [
|
|||||||
'pyyaml>=3.11,<4',
|
'pyyaml>=3.11,<4',
|
||||||
'pytz>=2016.4',
|
'pytz>=2016.4',
|
||||||
'pip>=7.0.0',
|
'pip>=7.0.0',
|
||||||
'vincenty==0.1.4',
|
|
||||||
'jinja2>=2.8',
|
'jinja2>=2.8',
|
||||||
'voluptuous==0.8.9',
|
'voluptuous==0.8.9',
|
||||||
'webcolors==1.5',
|
'webcolors==1.5',
|
||||||
|
45
tests/util/test_location.py
Normal file
45
tests/util/test_location.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user