Repository: abdullahselek/HerePy

Issue:
Combine routing api and geocoder to allow routing to/from names
Add a new function to the routing api that takes names for the origin and destination.
Call the geocoder api to resolve these to coordinates.

I would be happy to create a PR for this if you would welcome this feature.

Discussion:
eifinger: @abdullahselek Codecov upload fails
codecov-io: # [Codecov](https://codecov.io/gh/abdullahselek/HerePy/pull/25?src=pr&el=h1) Report
> Merging [#25](https://codecov.io/gh/abdullahselek/HerePy/pull/25?src=pr&el=desc) into [master](https://codecov.io/gh/abdullahselek/HerePy/commit/9cb8692fb01df0fd94b180942a30dbdbc0c32245?src=pr&el=desc) will **increase** coverage by `0.22%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/abdullahselek/HerePy/pull/25/graphs/tree.svg?width=650&token=D8ShyWQoQO&height=150&src=pr)](https://codecov.io/gh/abdullahselek/HerePy/pull/25?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #25      +/-   ##
==========================================
+ Coverage   95.83%   96.06%   +0.22%     
==========================================
  Files          14       14              
  Lines         745      762      +17     
==========================================
+ Hits          714      732      +18     
+ Misses         31       30       -1
```

| Flag | Coverage Δ | |
|---|---|---|
| #unittests | `?` | |

| [Impacted Files](https://codecov.io/gh/abdullahselek/HerePy/pull/25?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [herepy/routing\_api.py](https://codecov.io/gh/abdullahselek/HerePy/pull/25/diff?src=pr&el=tree#diff-aGVyZXB5L3JvdXRpbmdfYXBpLnB5) | `94.97% <100%> (+0.52%)` | :arrow_up: |
| [herepy/error.py](https://codecov.io/gh/abdullahselek/HerePy/pull/25/diff?src=pr&el=tree#diff-aGVyZXB5L2Vycm9yLnB5) | `100% <0%> (+25%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abdullahselek/HerePy/pull/25?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abdullahselek/HerePy/pull/25?src=pr&el=footer). Last update [9cb8692...f815ea0](https://codecov.io/gh/abdullahselek/HerePy/pull/25?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

abdullahselek: > @abdullahselek Codecov upload fails

I realized I did something wrong when I setup codecov on workflows. Could you please rebase from `master` into your branch?
eifinger: Should I remove both `.` or only the ones after "order"?
abdullahselek: > Should I remove both `.` or only the ones after "order"?

Only the ones after "order" please.

Patch:
diff --git a/README.rst b/README.rst
index b6bd1b8..78d2e19 100644
--- a/README.rst
+++ b/README.rst
@@ -224,6 +224,14 @@ Calculate an MxN cost matrix for M start points and N destinations
         departure='2013-07-04T17:00:00+02',
         modes=[herepy.RouteMode.fastest, herepy.RouteMode.car])
 
+Calculate route providing names instead of coordinates
+
+.. code:: python
+
+    response = routingApi.car_route([11.0, 12.0],
+                                    '200 S Mathilda Sunnyvale CA',
+                                    [herepy.RouteMode.car, herepy.RouteMode.fastest])
+
 
 GeocoderAutoCompleteApi
 -----------------------
diff --git a/herepy/routing_api.py b/herepy/routing_api.py
index d188aaf..9a9fd52 100644
--- a/herepy/routing_api.py
+++ b/herepy/routing_api.py
@@ -5,12 +5,13 @@ import sys
 import json
 import requests
 
+from herepy.geocoder_api import GeocoderApi
 from herepy.here_api import HEREApi
 from herepy.utils import Utils
 from herepy.error import HEREError
 from herepy.models import RoutingResponse, RoutingMatrixResponse
 from herepy.here_enum import RouteMode, MatrixSummaryAttribute
-from typing import List
+from typing import List, Union
 
 class RoutingApi(HEREApi):
     """A python interface into the HERE Routing API"""
@@ -53,6 +54,10 @@ class RoutingApi(HEREApi):
         return str.format('geo!{0},{1}', waypoint_a[0], waypoint_a[1])
 
     def _route(self, waypoint_a, waypoint_b, modes=None, departure=None, arrival=None):
+        if isinstance(waypoint_a, str):
+            waypoint_a = self._get_coordinates_for_location_name(waypoint_a)
+        if isinstance(waypoint_b, str):
+            waypoint_b = self._get_coordinates_for_location_name(waypoint_b)
         data = {'waypoint0': self.__array_to_waypoint(waypoint_a),
                 'waypoint1': self.__array_to_waypoint(waypoint_b),
                 'mode': self.__prepare_mode_values(modes),
@@ -84,16 +89,18 @@ class RoutingApi(HEREApi):
         return response
 
     def bicycle_route(self,
-                      waypoint_a: List[float],
-                      waypoint_b: List[float],
+                      waypoint_a: Union[List[float], str],
+                      waypoint_b: Union[List[float], str],
                       modes: List[RouteMode]=None,
                       departure: str='now'):
         """Request a bicycle route between two points
         Args:
-          waypoint_a (array):
-            array including latitude and longitude in order.
-          waypoint_b (array):
-            array including latitude and longitude in order.
+          waypoint_a:
+            array including latitude and longitude in order
+            or string with the location name
+          waypoint_b:
+            array including latitude and longitude in order
+            or string with the location name.
           modes (array):
             array including RouteMode enums.
           departure (str):
@@ -108,16 +115,18 @@ class RoutingApi(HEREApi):
         return self._route(waypoint_a, waypoint_b, modes, departure)
 
     def car_route(self,
-                  waypoint_a: List[float],
-                  waypoint_b: List[float],
+                  waypoint_a: Union[List[float], str],
+                  waypoint_b: Union[List[float], str],
                   modes: List[RouteMode]=None,
                   departure: str='now'):
         """Request a driving route between two points
         Args:
           waypoint_a (array):
-            array including latitude and longitude in order.
+            array including latitude and longitude in order
+            or string with the location name.
           waypoint_b (array):
-            array including latitude and longitude in order.
+            array including latitude and longitude in order
+            or string with the location name.
           modes (array):
             array including RouteMode enums.
           departure (str):
@@ -132,16 +141,18 @@ class RoutingApi(HEREApi):
         return self._route(waypoint_a, waypoint_b, modes, departure)
 
     def pedastrian_route(self,
-                         waypoint_a: List[float],
-                         waypoint_b: List[float],
+                         waypoint_a: Union[List[float], str],
+                         waypoint_b: Union[List[float], str],
                          modes: List[RouteMode]=None,
                          departure: str='now'):
         """Request a pedastrian route between two points
         Args:
           waypoint_a (array):
-            array including latitude and longitude in order.
+            array including latitude and longitude in order
+            or string with the location name.
           waypoint_b (array):
-            array including latitude and longitude in order.
+            array including latitude and longitude in order
+            or string with the location name.
           modes (array):
             array including RouteMode enums.
           departure (str):
@@ -156,19 +167,22 @@ class RoutingApi(HEREApi):
         return self._route(waypoint_a, waypoint_b, modes, departure)
 
     def intermediate_route(self,
-                           waypoint_a: List[float],
-                           waypoint_b: List[float],
-                           waypoint_c: List[float],
+                           waypoint_a: Union[List[float], str],
+                           waypoint_b: Union[List[float], str],
+                           waypoint_c: Union[List[float], str],
                            modes: List[RouteMode]=None,
                            departure: str='now'):
         """Request a intermediate route from three points
         Args:
           waypoint_a (array):
-            Starting array including latitude and longitude in order.
+            Starting array including latitude and longitude in order
+            or string with the location name.
           waypoint_b (array):
-            Intermediate array including latitude and longitude in order.
+            Intermediate array including latitude and longitude in order
+            or string with the location name.
           waypoint_c (array):
-            Last array including latitude and longitude in order.
+            Last array including latitude and longitude in order
+            or string with the location name.
           modes (array):
             array including RouteMode enums.
           departure (str):
@@ -183,17 +197,19 @@ class RoutingApi(HEREApi):
         return self._route(waypoint_a, waypoint_b, modes, departure)
 
     def public_transport(self,
-                         waypoint_a: List[float],
-                         waypoint_b: List[float],
+                         waypoint_a: Union[List[float], str],
+                         waypoint_b: Union[List[float], str],
                          combine_change: bool,
                          modes: List[RouteMode]=None,
                          departure='now'):
         """Request a public transport route between two points
         Args:
           waypoint_a (array):
-            Starting array including latitude and longitude in order.
+            Starting array including latitude and longitude in order
+            or string with the location name.
           waypoint_b (array):
-            Intermediate array including latitude and longitude in order.
+            Intermediate array including latitude and longitude in order
+            or string with the location name.
           combine_change (bool):
             Enables the change manuever in the route response, which
             indicates a public transit line change.
@@ -211,8 +227,8 @@ class RoutingApi(HEREApi):
         return self._route(waypoint_a, waypoint_b, modes, departure)
 
     def public_transport_timetable(self,
-                                   waypoint_a: List[float],
-                                   waypoint_b: List[float],
+                                   waypoint_a: Union[List[float], str],
+                                   waypoint_b: Union[List[float], str],
                                    combine_change: bool,
                                    modes: List[RouteMode]=None,
                                    departure: str=None,
@@ -220,9 +236,11 @@ class RoutingApi(HEREApi):
         """Request a public transport route between two points based on timetables
         Args:
           waypoint_a (array):
-            Starting array including latitude and longitude in order.
+            Starting array including latitude and longitude in order
+            or string with the location name.
           waypoint_b (array):
-            Intermediate array including latitude and longitude in order.
+            Intermediate array including latitude and longitude in order
+            or string with the location name.
           combine_change (bool):
             Enables the change manuever in the route response, which
             indicates a public transit line change.
@@ -242,16 +260,18 @@ class RoutingApi(HEREApi):
         return self._route(waypoint_a, waypoint_b, modes, departure, arrival)
 
     def location_near_motorway(self,
-                               waypoint_a: List[float],
-                               waypoint_b: List[float],
+                               waypoint_a: Union[List[float], str],
+                               waypoint_b: Union[List[float], str],
                                modes: List[RouteMode]=None,
                                departure: str='now'):
         """Calculates the fastest car route between two location
         Args:
           waypoint_a (array):
-            array including latitude and longitude in order.
+            array including latitude and longitude in order
+            or string with the location name.
           waypoint_b (array):
-            array including latitude and longitude in order.
+            array including latitude and longitude in order
+            or string with the location name.
           modes (array):
             array including RouteMode enums.
           departure (str):
@@ -266,16 +286,18 @@ class RoutingApi(HEREApi):
         return self._route(waypoint_a, waypoint_b, modes, departure)
 
     def truck_route(self,
-                    waypoint_a: List[float],
-                    waypoint_b: List[float],
+                    waypoint_a: Union[List[float], str],
+                    waypoint_b: Union[List[float], str],
                     modes: List[RouteMode]=None,
                     departure: str='now'):
         """Calculates the fastest truck route between two location
         Args:
           waypoint_a (array):
-            array including latitude and longitude in order.
+            array including latitude and longitude in order
+            or string with the location name.
           waypoint_b (array):
-            array including latitude and longitude in order.
+            array including latitude and longitude in order
+            or string with the location name.
           modes (array):
             array including RouteMode enums.
           departure (str):
@@ -290,8 +312,8 @@ class RoutingApi(HEREApi):
         return self._route(waypoint_a, waypoint_b, modes, departure)
 
     def matrix(self,
-               start_waypoints: List[float],
-               destination_waypoints: List[float],
+               start_waypoints: Union[List[float], str],
+               destination_waypoints: Union[List[float], str],
                departure: str='now',
                modes: List[RouteMode]=[],
                summary_attributes: List[MatrixSummaryAttribute]=[]):
@@ -299,8 +321,10 @@ class RoutingApi(HEREApi):
         Args:
           start_waypoints (array):
             array of arrays of coordinates [lat,long] of start waypoints.
+            or array of string with the location names.
           destination_waypoints (array):
             array of arrays of coordinates [lat,long] of destination waypoints.
+            or array of string with the location names.
           departure (str):
             time when travel is expected to start, e.g.: '2013-07-04T17:00:00+02'
           modes (array):
@@ -320,12 +344,27 @@ class RoutingApi(HEREApi):
             'summaryAttributes': ','.join([attribute.__str__() for attribute in summary_attributes])
         }
         for i, start_waypoint in enumerate(start_waypoints):
+            if isinstance(start_waypoint, str):
+                start_waypoint = self._get_coordinates_for_location_name(start_waypoint)
             data['start' + str(i)] = self.__array_to_waypoint(start_waypoint)
         for i, destination_waypoint in enumerate(destination_waypoints):
+            if isinstance(destination_waypoint, str):
+                destination_waypoint = self._get_coordinates_for_location_name(destination_waypoint)
             data['destination' + str(i)] = self.__array_to_waypoint(destination_waypoint)
         response = self.__get(self.URL_CALCULATE_MATRIX, data, RoutingMatrixResponse)
         return response
 
+    def _get_coordinates_for_location_name(self, location_name: str) -> List[float]:
+        """Use the Geocoder API to resolve a location name to a set of coordinates."""
+
+        geocoder_api = GeocoderApi(self._api_key)
+        try:
+            geocoder_response = geocoder_api.free_form(location_name)
+            coordinates = geocoder_response.Response["View"][0]["Result"][0]["Location"]["NavigationPosition"][0]
+            return [coordinates["Latitude"], coordinates["Longitude"]]
+        except (HEREError) as here_error:
+            raise WaypointNotFoundError(here_error.message)
+
     @staticmethod
     def _convert_datetime_to_isoformat(datetime_object):
         """Convert a datetime.datetime object to an ISO8601 string."""


Test patch:
diff --git a/tests/test_routing_api.py b/tests/test_routing_api.py
index db150bd..9239be6 100644
--- a/tests/test_routing_api.py
+++ b/tests/test_routing_api.py
@@ -455,6 +455,22 @@ class RoutingApiTest(unittest.TestCase):
         self.assertTrue(response)
         self.assertIsInstance(response, herepy.RoutingMatrixResponse)
 
+    @responses.activate
+    def test_matrix_multiple_start_names(self):
+        with codecs.open('testdata/models/routing_matrix_multiple_starts.json', mode='r', encoding='utf-8') as f:
+            server_response = f.read()
+        responses.add(responses.GET, 'https://matrix.route.ls.hereapi.com/routing/7.2/calculatematrix.json',
+                      server_response, status=200)
+        with open('testdata/models/geocoder.json', 'r') as f:
+            expectedGeocoderResponse = f.read()
+        responses.add(responses.GET, 'https://geocoder.ls.hereapi.com/6.2/geocode.json',
+                  expectedGeocoderResponse, status=200)
+        response = self._api.matrix(
+            start_waypoints=['Seattle', 'Kentucky'],
+            destination_waypoints=[[9.934574, -84.065544]])
+        self.assertTrue(response)
+        self.assertIsInstance(response, herepy.RoutingMatrixResponse)
+
     @responses.activate
     def test_matrix_multiple_destinations(self):
         with codecs.open('testdata/models/routing_matrix_multiple_destinations.json', mode='r', encoding='utf-8') as f:
@@ -467,6 +483,22 @@ class RoutingApiTest(unittest.TestCase):
         self.assertTrue(response)
         self.assertIsInstance(response, herepy.RoutingMatrixResponse)
 
+    @responses.activate
+    def test_matrix_multiple_destinations(self):
+        with codecs.open('testdata/models/routing_matrix_multiple_destinations.json', mode='r', encoding='utf-8') as f:
+            server_response = f.read()
+        responses.add(responses.GET, 'https://matrix.route.ls.hereapi.com/routing/7.2/calculatematrix.json',
+                      server_response, status=200)
+        with open('testdata/models/geocoder.json', 'r') as f:
+            expectedGeocoderResponse = f.read()
+        responses.add(responses.GET, 'https://geocoder.ls.hereapi.com/6.2/geocode.json',
+                  expectedGeocoderResponse, status=200)
+        response = self._api.matrix(
+            start_waypoints=[[9.933231, -84.076831]],
+            destination_waypoints=['Seattle', 'Kentucky'])
+        self.assertTrue(response)
+        self.assertIsInstance(response, herepy.RoutingMatrixResponse)
+
     @responses.activate
     def test_matrix_bad_request(self):
         with codecs.open('testdata/models/routing_matrix_bad_request.json', mode='r', encoding='utf-8') as f:
@@ -500,4 +532,30 @@ class RoutingApiTest(unittest.TestCase):
         response = self._api.truck_route([11.0, 12.0],
                                          [22.0, 23.0],
                                          departure=date)
-        
+
+    @responses.activate
+    def test_location_by_name(self):
+        with codecs.open('testdata/models/routing_truck_route_short.json', mode='r', encoding='utf-8') as f:
+            expectedRoutingResponse = f.read()
+        responses.add(responses.GET, 'https://route.ls.hereapi.com/routing/7.2/calculateroute.json',
+                  expectedRoutingResponse, status=200)
+        with open('testdata/models/geocoder.json', 'r') as f:
+            expectedGeocoderResponse = f.read()
+        responses.add(responses.GET, 'https://geocoder.ls.hereapi.com/6.2/geocode.json',
+                  expectedGeocoderResponse, status=200)
+        response = self._api.truck_route('200 S Mathilda Sunnyvale CA',
+                                         '200 S Mathilda Sunnyvale CA')
+                                         
+    @responses.activate
+    def test_location_by_name_throws_WaypointNotFoundError(self):
+        with codecs.open('testdata/models/routing_truck_route_short.json', mode='r', encoding='utf-8') as f:
+            expectedRoutingResponse = f.read()
+        responses.add(responses.GET, 'https://route.ls.hereapi.com/routing/7.2/calculateroute.json',
+                  expectedRoutingResponse, status=200)
+        with open('testdata/models/geocoder_error.json', 'r') as f:
+            expectedGeocoderResponse = f.read()
+        responses.add(responses.GET, 'https://geocoder.ls.hereapi.com/6.2/geocode.json',
+                  expectedGeocoderResponse, status=200)
+        with self.assertRaises(herepy.WaypointNotFoundError):
+            response = self._api.truck_route('200 S Mathilda Sunnyvale CA',
+                                            '200 S Mathilda Sunnyvale CA')