Skip to content

Commit

Permalink
docs: add Distance class doc and Distance.destination method (#473)
Browse files Browse the repository at this point in the history
  • Loading branch information
KostyaEsmukov committed Apr 17, 2021
1 parent 6afb62d commit 9c7bf7b
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 38 deletions.
9 changes: 6 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,18 @@ Calculating Distance
~~~~~~~~~~~~~~~~~~~~

.. automodule:: geopy.distance
:members: __doc__
:members: __doc__

.. autofunction:: geopy.distance.lonlat

.. autoclass:: geopy.distance.Distance
:members: __init__, destination

.. autoclass:: geopy.distance.geodesic
:members: __init__
:show-inheritance:

.. autoclass:: geopy.distance.great_circle
:members: __init__
:show-inheritance:

Data
~~~~
Expand Down
156 changes: 127 additions & 29 deletions geopy/distance.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
1.359986705262199
An attempt to calculate distances between points with different altitudes
would result in a ``ValueError`` exception.
would result in a :class:`ValueError` exception.
"""
from math import asin, atan2, cos, sin, sqrt
Expand Down Expand Up @@ -188,8 +188,83 @@ def _ensure_same_altitude(a, b):


class Distance:
"""
Base class for other distance algorithms. Represents a distance.
Can be used for units conversion::
>>> from geopy.distance import Distance
>>> Distance(miles=10).km
16.09344
Distance instances have all *distance* properties from :mod:`geopy.units`,
e.g.: ``km``, ``m``, ``meters``, ``miles`` and so on.
Distance instances are immutable.
They support comparison::
>>> from geopy.distance import Distance
>>> Distance(kilometers=2) == Distance(meters=2000)
True
>>> Distance(kilometers=2) > Distance(miles=1)
True
String representation::
>>> from geopy.distance import Distance
>>> repr(Distance(kilometers=2))
'Distance(2.0)'
>>> str(Distance(kilometers=2))
'2.0 km'
>>> repr(Distance(miles=2))
'Distance(3.218688)'
>>> str(Distance(miles=2))
'3.218688 km'
Arithmetics::
>>> from geopy.distance import Distance
>>> -Distance(miles=2)
Distance(-3.218688)
>>> Distance(miles=2) + Distance(kilometers=1)
Distance(4.218688)
>>> Distance(miles=2) - Distance(kilometers=1)
Distance(2.218688)
>>> Distance(kilometers=6) * 5
Distance(30.0)
>>> Distance(kilometers=6) / 5
Distance(1.2)
"""

def __init__(self, *args, **kwargs):
"""
There are 3 ways to create a distance:
- From kilometers::
>>> from geopy.distance import Distance
>>> Distance(1.42)
Distance(1.42)
- From units::
>>> from geopy.distance import Distance
>>> Distance(kilometers=1.42)
Distance(1.42)
>>> Distance(miles=1)
Distance(1.609344)
- From points (for non-abstract distances only),
calculated as a sum of distances between all points::
>>> from geopy.distance import geodesic
>>> geodesic((40, 160), (40.1, 160.1))
Distance(14.003702498106215)
>>> geodesic((40, 160), (40.1, 160.1), (40.2, 160.2))
Distance(27.999954644813478)
"""

kilometers = kwargs.pop('kilometers', 0)
if len(args) == 1:
# if we only get one argument we assume
Expand Down Expand Up @@ -237,6 +312,40 @@ def __nonzero__(self):
__bool__ = __nonzero__

def measure(self, a, b):
# Intentionally not documented, because this method is not supposed
# to be used directly.
raise NotImplementedError("Distance is an abstract class")

def destination(self, point, bearing, distance=None):
"""
Calculate destination point using a starting point, bearing
and a distance. This method works for non-abstract distances only.
Example: a point 10 miles east from ``(34, 148)``::
>>> import geopy.distance
>>> geopy.distance.distance(miles=10).destination((34, 148), bearing=90)
Point(33.99987666492774, 148.17419994321995, 0.0)
:param point: Starting point.
:type point: :class:`geopy.point.Point`, list or tuple of ``(latitude,
longitude)``, or string as ``"%(latitude)s, %(longitude)s"``.
:param float bearing: Bearing in degrees: 0 -- North, 90 -- East,
180 -- South, 270 or -90 -- West.
:param distance: Distance, can be used to override
this instance::
>>> from geopy.distance import distance, Distance
>>> distance(miles=10).destination((34, 148), bearing=90, \
distance=Distance(100))
Point(33.995238229104764, 149.08238904409637, 0.0)
:type distance: :class:`.Distance`
:rtype: :class:`geopy.point.Point`
"""
raise NotImplementedError("Distance is an abstract class")

def __repr__(self): # pragma: no cover
Expand Down Expand Up @@ -269,6 +378,14 @@ def __ge__(self, other):
def __le__(self, other):
return self.__cmp__(other) <= 0

@property
def feet(self):
return units.feet(kilometers=self.kilometers)

@property
def ft(self):
return self.feet

@property
def kilometers(self):
return self.__kilometers
Expand All @@ -277,29 +394,21 @@ def kilometers(self):
def km(self):
return self.kilometers

@property
def meters(self):
return units.meters(kilometers=self.kilometers)

@property
def m(self):
return self.meters

@property
def miles(self):
return units.miles(kilometers=self.kilometers)
def meters(self):
return units.meters(kilometers=self.kilometers)

@property
def mi(self):
return self.miles

@property
def feet(self):
return units.feet(kilometers=self.kilometers)

@property
def ft(self):
return self.feet
def miles(self):
return units.miles(kilometers=self.kilometers)

@property
def nautical(self):
Expand All @@ -312,7 +421,7 @@ def nm(self):

class great_circle(Distance):
"""
Use spherical geometry to calculate the surface distance between two
Use spherical geometry to calculate the surface distance between
points.
Set which radius of the earth to use by specifying a ``radius`` keyword
Expand Down Expand Up @@ -354,9 +463,6 @@ def measure(self, a, b):
return self.RADIUS * d

def destination(self, point, bearing, distance=None):
"""
TODO docs.
"""
point = Point(point)
lat1 = units.radians(degrees=point.latitude)
lng1 = units.radians(degrees=point.longitude)
Expand Down Expand Up @@ -387,7 +493,7 @@ def destination(self, point, bearing, distance=None):

class geodesic(Distance):
"""
Calculate the geodesic distance between two points.
Calculate the geodesic distance between points.
Set which ellipsoidal model of the earth to use by specifying an
``ellipsoid`` keyword argument. The default is 'WGS-84', which is the
Expand All @@ -407,19 +513,15 @@ class geodesic(Distance):
"""

ellipsoid_key = None
ELLIPSOID = None
geod = None

def __init__(self, *args, **kwargs):
self.ellipsoid_key = None
self.ELLIPSOID = None
self.geod = None
self.set_ellipsoid(kwargs.pop('ellipsoid', 'WGS-84'))
major, minor, f = self.ELLIPSOID
super().__init__(*args, **kwargs)

def set_ellipsoid(self, ellipsoid):
"""
Change the ellipsoid used in the calculation.
"""
if isinstance(ellipsoid, str):
try:
self.ELLIPSOID = ELLIPSOIDS[ellipsoid]
Expand All @@ -432,7 +534,6 @@ def set_ellipsoid(self, ellipsoid):
self.ELLIPSOID = ellipsoid
self.ellipsoid_key = None

# Call geographiclib routines for measure and destination
def measure(self, a, b):
a, b = Point(a), Point(b)
_ensure_same_altitude(a, b)
Expand All @@ -450,9 +551,6 @@ def measure(self, a, b):
return s12

def destination(self, point, bearing, distance=None):
"""
TODO docs.
"""
point = Point(point)
lat1 = point.latitude
lon1 = point.longitude
Expand Down
17 changes: 11 additions & 6 deletions test/test_distance.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ def test_should_compute_destination_for_trip_between_poles(self):
self.assertAlmostEqual(destination.latitude, -90, 0)
self.assertAlmostEqual(destination.longitude, 0)

def test_destination_bearing_east(self):
distance = self.cls(kilometers=100)
for EAST in (90, 90 + 360, -360+90):
p = distance.destination(Point(0, 160), bearing=EAST)
self.assertAlmostEqual(p.latitude, 0)
self.assertAlmostEqual(p.longitude, 160.8993, delta=1e-3)

p = distance.destination(Point(60, 160), bearing=EAST)
self.assertAlmostEqual(p.latitude, 59.9878, delta=1e-3)
self.assertAlmostEqual(p.longitude, 161.79, delta=1e-2)

def test_should_recognize_equivalence_of_pos_and_neg_180_longitude(self):
distance1 = self.cls((0, -180), (0, 180)).kilometers
distance2 = self.cls((0, 180), (0, -180)).kilometers
Expand Down Expand Up @@ -302,12 +313,6 @@ class TestWhenComputingGeodesicDistance(CommonDistanceCases,

cls = GeodesicDistance

def setUp(self):
self.original_ellipsoid = self.cls.ELLIPSOID

def tearDown(self):
self.cls.ELLIPSOID = self.original_ellipsoid

def test_different_altitudes_error(self):
with self.assertRaises(ValueError):
# Different altitudes raise an exception:
Expand Down

0 comments on commit 9c7bf7b

Please sign in to comment.