Repository: GIScience/openrouteservice-py

Issue:
Add skip_segments and alternative_routes parameters

Comments:

Patch:
diff --git a/docs/source/openrouteservice.rst b/docs/source/openrouteservice.rst
index 0139f7b..fff8892 100644
--- a/docs/source/openrouteservice.rst
+++ b/docs/source/openrouteservice.rst
@@ -76,14 +76,6 @@ openrouteservice\.optimization module
     :undoc-members:
     :show-inheritance:
 
-openrouteservice\.validator module
-------------------------------------
-
-.. automodule:: openrouteservice.validator
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
 openrouteservice\.exceptions module
 -----------------------------------
 
diff --git a/openrouteservice/directions.py b/openrouteservice/directions.py
index 1c28b06..39f0475 100644
--- a/openrouteservice/directions.py
+++ b/openrouteservice/directions.py
@@ -37,11 +37,13 @@ def directions(client,
                geometry_simplify=None,
                instructions=None,
                instructions_format=None,
+               alternative_routes=None,
                roundabout_exits=None,
                attributes=None,
                maneuvers=None,
                radiuses=None,
                bearings=None,
+               skip_segments=None,
                continue_straight=None,
                elevation=None,
                extra_info=None,
@@ -103,6 +105,12 @@ def directions(client,
         One of ["text", "html"]. Default "text".
     :type instructions_format: string
 
+    :param alternative_routes: Specifies whether alternative routes are computed,
+        and parameters for the algorithm determining suitable alternatives. Expects
+        3 keys: share_factor (float), target_count (int), weight_factor (float).
+        More on https://openrouteservice.org/dev/#/api-docs/v2/directions/{profile}/geojson/post.
+    :type alternative_routes: dict[int|float]
+
     :param roundabout_exits: Provides bearings of the entrance and all passed
         roundabout exits. Adds the 'exit_bearings' array to the 'step' object
         in the response. Default False.
@@ -137,6 +145,11 @@ def directions(client,
         waypoint may be reached.
     :type bearings: list or tuple or lists or tuples
 
+    :param skip_segments: Specifies the segments that should be skipped in the route calculation.
+        A segment is the connection between two given coordinates and the counting starts with 1
+        for the connection between the first and second coordinate.
+    :type skip_segments: list[int]
+
     :param continue_straight: Forces the route to keep going straight at waypoints
         restricting u-turns even if u-turns would be faster. This setting
         will work for all profiles except for driving-*. In this case you will
@@ -159,7 +172,7 @@ def directions(client,
     :param optimized: If set False, forces to not use Contraction Hierarchies.
     :type optimized: bool
 
-    :param options: Refer to https://go.openrouteservice.org/documentation for
+    :param options: Refer to https://openrouteservice.org/dev/#/api-docs/v2/directions/{profile}/geojson/post for
         detailed documentation. Construct your own dict() following the example
         of the minified options object. Will be converted to json automatically.
     :type options: dict
@@ -222,6 +235,9 @@ def directions(client,
     if instructions_format:
         params["instructions_format"] = instructions_format
 
+    if alternative_routes:
+        params["alternative_routes"] = alternative_routes
+
     if roundabout_exits is not None:
         params["roundabout_exits"] = roundabout_exits
 
@@ -237,6 +253,9 @@ def directions(client,
     if bearings:
         params["bearings"] = bearings
 
+    if skip_segments:
+        params["skip_segments"] = skip_segments
+
     if continue_straight is not None:
         params["continue_straight"] = continue_straight
 
diff --git a/openrouteservice/optimization.py b/openrouteservice/optimization.py
index 2f4b733..0c05c8c 100644
--- a/openrouteservice/optimization.py
+++ b/openrouteservice/optimization.py
@@ -19,8 +19,9 @@
 
 
 def optimization(client,
-                 jobs,
-                 vehicles,
+                 jobs=None,
+                 vehicles=None,
+                 shipments=None,
                  matrix=None,
                  geometry=None,
                  dry_run=None):
@@ -40,10 +41,13 @@ def optimization(client,
         >>> result = api.optimization(jobs=jobs, vehicles=vehicles)
 
     :param jobs: The Job objects to fulfill.
-    :type jobs: list of :class:`openrouteservice.optimization.Job`
+    :type jobs: list of Job
 
     :param vehicles: The vehicles to fulfill the :class:`openrouteservice.optimization.Job`'s.
-    :type vehicles: list of :class:`openrouteservice.optimization.Vehicle`
+    :type vehicles: list of Vehicle
+
+    :param shipments: The Shipment objects to fulfill.
+    :type shipments: list of Shipment
 
     :param matrix: Specify a custom cost matrix. If not specified, it will be calculated with
         the :meth:`openrouteservice.matrix.matrix` endpoint.
@@ -59,11 +63,30 @@ def optimization(client,
     :rtype: dict
     """
 
-    assert all([isinstance(x, Job) for x in jobs])
     assert all([isinstance(x, Vehicle) for x in vehicles])
 
-    params = {"jobs": [job.__dict__ for job in jobs],
-              "vehicles": [vehicle.__dict__ for vehicle in vehicles]}
+    params = {"vehicles": [vehicle.__dict__ for vehicle in vehicles]}
+
+    if jobs:
+        assert all([isinstance(x, Job) for x in jobs])
+        params['jobs'] = [job.__dict__ for job in jobs]
+    if shipments:
+        assert all([isinstance(x, Shipment) for x in shipments])
+        params['shipments'] = list()
+
+        for shipment in shipments:
+            shipment_dict = dict()
+            if getattr(shipment, 'pickup'):
+                assert isinstance(shipment.pickup, ShipmentStep)
+                shipment_dict['pickup'] = shipment.pickup.__dict__
+            if getattr(shipment, 'delivery'):
+                assert isinstance(shipment.delivery, ShipmentStep)
+                shipment_dict['delivery'] = shipment.delivery.__dict__
+            shipment_dict['amount'] = shipment.amount
+            shipment_dict['skills'] = shipment.skills
+            shipment_dict['priority'] = shipment.priority
+
+            params['shipments'].append(shipment_dict)
 
     if geometry is not None:
         params.update({"options": {"g": geometry}})
@@ -87,6 +110,7 @@ class Job(object):
                  service=None,
                  amount=None,
                  skills=None,
+                 priority=None,
                  time_windows=None
                  ):
         """
@@ -111,6 +135,9 @@ class Job(object):
         :param skills: An array of integers defining mandatory skills for this job.
         :type skills: list of int or tuple of int
 
+        :param priority: An integer in the [0, 10] range describing priority level (defaults to 0).
+        :type priority: int
+
         :param time_windows: An array of time_window objects describing valid slots for job service start.
         :type time_windows: list of lists of int
         """
@@ -132,10 +159,109 @@ class Job(object):
         if skills is not None:
             self.skills = skills
 
+        if priority is not None:
+            self.priority = priority
+
+        if time_windows is not None:
+            self.time_windows = time_windows
+
+
+class ShipmentStep(object):
+    """
+    Class to create a Shipment object for optimization endpoint.
+
+    Full documentation at https://github.com/VROOM-Project/vroom/blob/master/docs/API.md#shipments.
+    """
+    def __init__(self,
+                 id=None,
+                 location=None,
+                 location_index=None,
+                 service=None,
+                 time_windows=None
+                 ):
+        """
+        Create a shipment step object for the optimization endpoint.
+
+        :param id: Integer used as unique identifier.
+        :type id: int
+
+        :param location: Location of the job, as [lon, lat]. Optional if custom matrix is provided.
+        :type location: tuple of float or list of float
+
+        :param location_index: Index of relevant row and column in custom matrix. Mandatory if custom
+            matrix is provided. Irrelevant when no custom matrix is provided.
+        :type location_index: int
+
+        :param service: Optional job service duration in seconds
+        :type service: int
+
+        :param time_windows: An array of time_window objects describing valid slots for job service start.
+        :type time_windows: list of lists of int
+        """
+
+        self.id = id
+
+        if location is not None:
+            self.location = location
+
+        if location_index is not None:
+            self.location_index = location_index
+
+        if service is not None:
+            self.service = service
+
         if time_windows is not None:
             self.time_windows = time_windows
 
 
+class Shipment(object):
+    """
+    Class to create a Shipment object for optimization endpoint.
+
+    Full documentation at https://github.com/VROOM-Project/vroom/blob/master/docs/API.md#shipments.
+    """
+    def __init__(self,
+                 pickup=None,
+                 delivery=None,
+                 amount=None,
+                 skills=None,
+                 priority=None
+                 ):
+        """
+        Create a shipment object for the optimization endpoint.
+
+        :param pickup: a ShipmentStep object describing pickup
+        :type pickup: ShipmentStep
+
+        :param delivery: a ShipmentStep object describing delivery
+        :type delivery: ShipmentStep
+
+        :param amount: An array of integers describing multidimensional quantities.
+        :type amount: list of int or tuple of int
+
+        :param skills: An array of integers defining mandatory skills.
+        :type skills: list of int or tuple of int
+
+        :param priority: An integer in the [0, 10] range describing priority level (defaults to 0).
+        :type priority: int
+        """
+
+        if pickup is not None:
+            self.pickup = pickup
+
+        if delivery is not None:
+            self.delivery = delivery
+
+        if amount is not None:
+            self.amount = amount
+
+        if skills is not None:
+            self.skills = skills
+
+        if priority is not None:
+            self.priority = priority
+
+
 class Vehicle(object):
     """
     Class to create a Vehicle object for optimization endpoint.


Test patch:
diff --git a/test/test_helper.py b/test/test_helper.py
index 08afc2c..916002e 100644
--- a/test/test_helper.py
+++ b/test/test_helper.py
@@ -27,10 +27,16 @@ ENDPOINT_DICT = {
         'suppress_warnings': False,
         'instructions': 'false',
         'instructions_format': 'html',
+        'alternative_routes': {
+            'share_factor': 0.6,
+            'target_count': 2,
+            'weight_factor': 1.4
+        },
         'roundabout_exits': 'true',
         'attributes': ['avgspeed'],
         'radiuses': PARAM_LIST_ONE,
         'bearings': PARAM_LIST_TWO,
+        'skip_segments': [0, 1],
         'elevation': 'true',
         'extra_info': ['roadaccessrestrictions'],
         'optimized': 'false',
@@ -129,6 +135,88 @@ ENDPOINT_DICT = {
         'sortby': 'distance',
     },
     'optimization': {
+        "shipments": [
+          {
+            "pickup": {
+              "id": 0,
+              "location": [
+                8.688641,
+                49.420577
+              ],
+              "location_index": 0,
+              "service": 500,
+              "time_windows": [
+                [
+                  50,
+                  50
+                ]
+              ]
+            },
+            "delivery": {
+              "id": 0,
+              "location": [
+                8.688641,
+                49.420577
+              ],
+              "location_index": 0,
+              "service": 500,
+              "time_windows": [
+                [
+                  50,
+                  50
+                ]
+              ]
+            },
+            "amount": [
+              50
+            ],
+            "skills": [
+              50,
+              50
+            ],
+            "priority": 50
+          },
+          {
+            "pickup": {
+              "id": 1,
+              "location": [
+                8.680916,
+                49.415776
+              ],
+              "location_index": 1,
+              "service": 500,
+              "time_windows": [
+                [
+                  50,
+                  50
+                ]
+              ]
+            },
+            "delivery": {
+              "id": 1,
+              "location": [
+                8.680916,
+                49.415776
+              ],
+              "location_index": 1,
+              "service": 500,
+              "time_windows": [
+                [
+                  50,
+                  50
+                ]
+              ]
+            },
+            "amount": [
+              50
+            ],
+            "skills": [
+              50,
+              50
+            ],
+            "priority": 50
+          }
+        ],
         "jobs": [
           {
             "id": 0,
@@ -137,6 +225,7 @@ ENDPOINT_DICT = {
             "service": PARAM_INT_BIG,
             "amount": [PARAM_INT_SMALL],
             "skills": PARAM_LIST_ONE,
+            "priority": PARAM_INT_SMALL,
             "time_windows": [PARAM_LIST_ONE]
           },
           {
@@ -146,6 +235,7 @@ ENDPOINT_DICT = {
             "service": PARAM_INT_BIG,
             "amount": [PARAM_INT_SMALL],
             "skills": PARAM_LIST_ONE,
+            "priority": PARAM_INT_SMALL,
             "time_windows": [PARAM_LIST_ONE]
           }
         ],
diff --git a/test/test_optimization.py b/test/test_optimization.py
index f9e4cca..c2ced67 100644
--- a/test/test_optimization.py
+++ b/test/test_optimization.py
@@ -24,13 +24,13 @@ from copy import deepcopy
 import json
 
 from test.test_helper import *
-from openrouteservice.optimization import Job, Vehicle
+from openrouteservice.optimization import Job, Vehicle, ShipmentStep, Shipment
 
 
 class OptimizationTest(_test.TestCase):
 
     def _get_params(self):
-        jobs, vehicles = list(), list()
+        jobs, vehicles, shipments = list(), list(), list()
 
         for idx, coord in enumerate(PARAM_LINE):
             jobs.append(Job(idx, location=coord,
@@ -38,6 +38,7 @@ class OptimizationTest(_test.TestCase):
                              location_index=idx,
                              amount=[PARAM_INT_SMALL],
                              skills=PARAM_LIST_ONE,
+                             priority=PARAM_INT_SMALL,
                              time_windows=[PARAM_LIST_ONE]
                              ))
 
@@ -49,11 +50,32 @@ class OptimizationTest(_test.TestCase):
                                      capacity=[PARAM_INT_SMALL],
                                      skills=PARAM_LIST_ONE,
                                      time_window=PARAM_LIST_ONE))
-        return jobs, vehicles
+
+            shipments.append(Shipment(
+                pickup=ShipmentStep(
+                    idx,
+                    location=coord,
+                    location_index=idx,
+                    service=PARAM_INT_BIG,
+                    time_windows=[PARAM_LIST_ONE]
+                ),
+                delivery=ShipmentStep(
+                    idx,
+                    location=coord,
+                    location_index=idx,
+                    service=PARAM_INT_BIG,
+                    time_windows=[PARAM_LIST_ONE]
+                ),
+                amount=[PARAM_INT_SMALL],
+                skills=PARAM_LIST_ONE,
+                priority=PARAM_INT_SMALL
+            ))
+
+        return jobs, vehicles, shipments
 
     def test_jobs_vehicles_classes(self):
 
-        jobs, vehicles = self._get_params()
+        jobs, vehicles, shipments = self._get_params()
 
         self.assertEqual(ENDPOINT_DICT['optimization']['jobs'], [j.__dict__ for j in jobs])
         self.assertEqual(ENDPOINT_DICT['optimization']['vehicles'], [v.__dict__ for v in vehicles])
@@ -62,7 +84,7 @@ class OptimizationTest(_test.TestCase):
     def test_full_optimization(self):
         query = deepcopy(ENDPOINT_DICT['optimization'])
 
-        jobs, vehicles = self._get_params()
+        jobs, vehicles, shipments = self._get_params()
 
         responses.add(responses.POST,
                       'https://api.openrouteservice.org/optimization',
@@ -70,6 +92,6 @@ class OptimizationTest(_test.TestCase):
                       status=200,
                       content_type='application/json')
 
-        self.client.optimization(jobs, vehicles, geometry=False, matrix=PARAM_LIST_TWO)
+        self.client.optimization(jobs, vehicles, shipments, geometry=False, matrix=PARAM_LIST_TWO)
 
         self.assertEqual(query, json.loads(responses.calls[0].request.body.decode('utf-8')))