Skip to content
This repository was archived by the owner on Sep 5, 2023. It is now read-only.

Commit 11ad5ef

Browse files
add tour planning api (#24)
* add fleet * add async tour planning solving * add documentation * fix * fixes
1 parent 1788335 commit 11ad5ef

File tree

7 files changed

+720
-0
lines changed

7 files changed

+720
-0
lines changed
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# Copyright (C) 2019-2021 HERE Europe B.V.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""This module defines all the configs which will be required as inputs to Tour planning API."""
5+
from datetime import datetime
6+
from typing import Any, Dict, List, Optional, Tuple
7+
8+
from .base_config import Bunch, Truck
9+
10+
11+
class TourPlanningAvoidFeatures(Bunch):
12+
"""A class to define values for features to avoid features during tour planning calculation."""
13+
14+
15+
#: Use this config for avoid of Tour planning API.
16+
#: Example: for ``tollRoad`` avoids use ``TOUR_PLANNING_AVOID_FEATURES.tollRoad``.
17+
TOUR_PLANNING_AVOID_FEATURES = TourPlanningAvoidFeatures(
18+
**{
19+
"tollRoad": "tollRoad",
20+
"motorway": "motorway",
21+
"ferry": "ferry",
22+
"tunnel": "tunnel",
23+
"dirtRoad": "dirtRoad",
24+
}
25+
)
26+
27+
28+
class VehicleMode(Bunch):
29+
"""A class to define values vehicle mode in vehicle profile."""
30+
31+
32+
#: Use this config for vehicle_mode of Tour planning API.
33+
#: Example: for ``scooter`` avoids use ``VEHICLE_MODE.scooter``.
34+
VEHICLE_MODE = VehicleMode(
35+
**{
36+
"scooter": "scooter",
37+
"bicycle": "bicycle",
38+
"pedestrian": "pedestrian",
39+
"car": "car",
40+
"truck": "truck",
41+
}
42+
)
43+
44+
45+
class VehicleType(object):
46+
"""A class to define ``VehicleType``
47+
48+
Type of vehicle in a fleet
49+
"""
50+
51+
def __init__(
52+
self,
53+
id: str,
54+
profile_name: str,
55+
amount: int,
56+
capacity: List[int],
57+
shift_start: Dict,
58+
shift_end: Optional[Dict] = None,
59+
shift_breaks: Optional[List] = None,
60+
costs_fixed: float = 0,
61+
costs_distance: float = 0,
62+
costs_time: float = 0,
63+
skills: Optional[List[str]] = None,
64+
limits: Optional[Dict] = None,
65+
):
66+
"""
67+
:param id: Specifies id of the vehicle type. Avoid assigning real-life identifiers,
68+
such as vehicle license plate as the id of a vehicle
69+
:param profile_name: characters ^[a-zA-Z0-9_-]+$ Specifies the name of the profile.
70+
Avoid assigning real-life identifiers, such as a vehicle license plate Id or
71+
personal name as the profileName of the routing profile.
72+
:param amount: Amount of vehicles available.
73+
:param capacity: Unit of measure, e.g. volume, mass, size, etc.
74+
:param shift_start: Represents a depot: a place where a vehicle starts
75+
:param costs_fixed: A fixed cost to start using vehicle of this type. It is
76+
optional with a default value of zero
77+
:param shift_end: Represents a depot: a place where a vehicle ends
78+
:param shift_breaks: Represents a depot: a place where a vehicle takes breaks
79+
:param costs_distance: A cost per meter. It is optional with a default value of zero.
80+
:param costs_time: A cost per second. It is optional with a default value of zero.
81+
In case time and distance costs are zero then a small time cost 0.00000000001
82+
will be used instead
83+
:param skills: A list of skills for a vehicle or a job.
84+
:param limits: Contains constraints applied to a vehicle type.
85+
86+
"""
87+
self.id = id
88+
self.profile = profile_name
89+
self.amount = amount
90+
self.capacity = capacity
91+
self.costs = {"fixed": costs_fixed, "distance": costs_distance, "time": costs_time}
92+
shifts: Dict[Any, Any] = {"start": shift_start}
93+
if shift_end:
94+
shifts["end"] = shift_end
95+
if shift_breaks:
96+
l_breaks = []
97+
for b in shift_breaks:
98+
l_breaks.append(b)
99+
shifts["breaks"] = l_breaks
100+
self.shifts = [shifts]
101+
self.skills = skills
102+
self.limits = limits
103+
104+
105+
class VehicleProfile(object):
106+
"""A class to define ``VehicleProfile``
107+
108+
Profile of vehicle in a fleet
109+
110+
:param name: Specifies the name of the profile. Avoid assigning real-life identifiers,
111+
such as a vehicle license plate Id or personal name as the profileName of
112+
the routing profile.
113+
:param vehicle_mode: Contains constraints applied to a vehicle type.
114+
:param departure_time: Represents time of departure.
115+
:param avoid: Avoid routes that violate these properties.
116+
:param truck_options: Specifies truck profile options.
117+
:param allow_highway_for_scooter: Specifies whether routing calculation should take
118+
highways into account. When this parameter isn't provided, then by default
119+
highways would be avoided. If the avoid feature motorway is provided, then
120+
highways would be avoided, even if this is set to true.
121+
:raises ValueError: If ``truck_options`` are provided without setting `vehicle_mode` to
122+
``VEHICLE_MODE.truck``
123+
:raises ValueError: If ``allow_highway_for_scooter`` is provided without setting
124+
`vehicle_mode` to ``VEHICLE_MODE.scooter``
125+
"""
126+
127+
def __init__(
128+
self,
129+
name: str,
130+
vehicle_mode: str,
131+
departure_time: Optional[datetime] = None,
132+
avoid: Optional[List[TourPlanningAvoidFeatures]] = None,
133+
truck_options: Optional[Truck] = None,
134+
allow_highway_for_scooter: Optional[bool] = None,
135+
):
136+
if vehicle_mode != "truck" and truck_options:
137+
raise ValueError(
138+
"`truck_options` can only be provided when `vehicle_mode` is `VEHICLE_MODE.truck`"
139+
)
140+
if vehicle_mode != "scooter" and allow_highway_for_scooter:
141+
raise ValueError(
142+
"`allow_highway_for_scooter` can only be provided when "
143+
+ "`vehicle_mode` is `VEHICLE_MODE.scooter`"
144+
)
145+
self.name = name
146+
self.type = vehicle_mode
147+
if departure_time:
148+
self.departureTime = departure_time.isoformat()
149+
if avoid:
150+
self.avoid = {"features": avoid}
151+
if truck_options:
152+
self.options = vars(truck_options)
153+
if allow_highway_for_scooter:
154+
self.options = {"allowHighway": allow_highway_for_scooter}
155+
156+
157+
class Fleet(object):
158+
"""A class to define ``Fleet``
159+
160+
A fleet represented by various vehicle types for serving jobs.
161+
162+
:param vehicle_types: A list of vehicle types. The upper limit for the number of vehicle
163+
types is 35 for the synchronous problems endpoint and 150 for the asynchronous
164+
problems endpoint.
165+
:param vehicle_profiles: Specifies the profile of the vehicle.
166+
167+
"""
168+
169+
def __init__(self, vehicle_types: List[VehicleType], vehicle_profiles: List):
170+
l_types = []
171+
for t in vehicle_types:
172+
l_types.append(vars(t))
173+
self.types = l_types
174+
175+
l_profiles = []
176+
for pro in vehicle_profiles:
177+
l_profiles.append(vars(pro))
178+
self.profiles = l_profiles
179+
180+
181+
class JobPlaces(object):
182+
"""A class to define ``JobPlaces``
183+
184+
:param duration: Represents duration in seconds.
185+
:param demand: Unit of measure, e.g. volume, mass, size, etc.
186+
:param location: Represents geospatial location defined by latitude and longitude.
187+
:param tag: A free text associated with the job place. Avoid referencing any confidential
188+
or personal information as part of the JobTag.
189+
:param times: Represents multiple time windows.
190+
191+
"""
192+
193+
def __init__(
194+
self,
195+
duration: int,
196+
demand: List[int],
197+
location: Tuple,
198+
tag: Optional[str] = None,
199+
times: Optional[List[List[str]]] = None,
200+
):
201+
self.duration = duration
202+
self.demand = demand
203+
self.location = {"lat": location[0], "lng": location[1]}
204+
if tag:
205+
self.tag = tag
206+
if times:
207+
self.times = times
208+
209+
210+
class Job(object):
211+
"""A class to define ``Job``
212+
213+
A fleet represented by various vehicle types for serving jobs.
214+
215+
:param id: Specifies id of the job. Avoid referencing any sensitive or personal information,
216+
such as names, addresses, information about a delivery or service, as part of the jobId.
217+
:param skills: A list of skills for a vehicle or a job.
218+
:param priority: Specifies the priority of the job with 1 for high priority jobs and
219+
2 for normal jobs.
220+
:param pickups: Places where sub jobs to be performed. All pickups are done before any
221+
other delivery.
222+
:param deliveries: Places where sub jobs to be performed. All pickups are done before any
223+
other delivery.
224+
:raises ValueError: If no subjob is specified either as pickup or delivery.
225+
226+
"""
227+
228+
def __init__(
229+
self,
230+
id: str,
231+
skills: Optional[List] = None,
232+
priority: Optional[int] = None,
233+
pickups: Optional[List[JobPlaces]] = None,
234+
deliveries: Optional[List[JobPlaces]] = None,
235+
):
236+
if pickups is None and deliveries is None:
237+
raise ValueError("At least one subjob must be specified as pickup or delivery")
238+
self.id = id
239+
if skills:
240+
self.skills = skills
241+
if priority:
242+
self.priority = priority
243+
self.places = {}
244+
if pickups:
245+
l_pickups = []
246+
for p in pickups:
247+
l_pickups.append(vars(p))
248+
self.places["pickups"] = l_pickups
249+
if deliveries:
250+
l_deliveries = []
251+
for d in deliveries:
252+
l_deliveries.append(vars(d))
253+
self.places["deliveries"] = l_deliveries
254+
255+
256+
class Relation(object):
257+
"""A class to define ``Relation``
258+
259+
Represents a list of preferred relations between jobs, vehicles.
260+
261+
:param type: "sequence" "tour" "flexible"
262+
Defines a relation between jobs and a specific vehicle
263+
:param jobs: Ids of jobs or reserved activities. There are three reserved activity ids:
264+
- departure: specifies departure activity. Should be first in the list.
265+
- break: specifies vehicle break activity
266+
- arrival: specifies arrival activity. Should be last in the list.
267+
:param vehicle_id: A unique identifier of an entity. Avoid referencing any confidential or
268+
personal information as part of the Id.
269+
270+
"""
271+
272+
def __init__(self, type: str, jobs: List, vehicle_id: str):
273+
self.type = type
274+
self.jobs = jobs
275+
self.vehicleId = vehicle_id
276+
277+
278+
class Plan(object):
279+
"""A class to define ``Plan``
280+
281+
Represents the list of jobs to be served.
282+
"""
283+
284+
def __init__(self, jobs: List[Job], relations: Optional[List[Relation]] = None):
285+
l_jobs = []
286+
for p in jobs:
287+
l_jobs.append(vars(p))
288+
self.jobs = l_jobs
289+
290+
if relations:
291+
l_relations = []
292+
for r in relations:
293+
l_relations.append(vars(r))
294+
self.relations = l_relations

here_location_services/ls.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from geojson import LineString, Point
1414

1515
from here_location_services.config.routing_config import Scooter, Via
16+
from here_location_services.config.tour_planning_config import Fleet, Plan
1617
from here_location_services.platform.apis.aaa_oauth2_api import AAAOauth2Api
1718
from here_location_services.platform.auth import Auth
1819
from here_location_services.platform.credentials import PlatformCredentials
@@ -44,9 +45,11 @@
4445
MatrixRoutingResponse,
4546
ReverseGeocoderResponse,
4647
RoutingResponse,
48+
TourPlanningResponse,
4749
WeatherAlertsResponse,
4850
)
4951
from .routing_api import RoutingApi
52+
from .tour_planning_api import TourPlanningApi
5053

5154

5255
class LS:
@@ -105,6 +108,12 @@ def __init__(
105108
proxies=proxies,
106109
country=country,
107110
)
111+
self.tour_planning_api = TourPlanningApi(
112+
api_key=api_key,
113+
auth=self.auth,
114+
proxies=proxies,
115+
country=country,
116+
)
108117

109118
def geocode(self, query: str, limit: int = 20, lang: str = "en-US") -> GeocoderResponse:
110119
"""Calculate coordinates as result of geocoding for the given ``query``.
@@ -421,6 +430,67 @@ def get_weather_alerts(
421430
response = WeatherAlertsResponse.new(resp.json())
422431
return response
423432

433+
def solve_tour_planning(
434+
self,
435+
fleet: Fleet,
436+
plan: Plan,
437+
id: Optional[str] = None,
438+
optimization_traffic: Optional[str] = None,
439+
optimization_waiting_time: Optional[Dict] = None,
440+
is_async: Optional[bool] = False,
441+
) -> TourPlanningResponse:
442+
"""Requests profile-aware routing data, creates a Vehicle Routing Problem and solves it.
443+
444+
:param fleet: A fleet represented by various vehicle types for serving jobs.
445+
:param plan: Represents the list of jobs to be served.
446+
:param id: A unique identifier of an entity. Avoid referencing any confidential or
447+
personal information as part of the Id.
448+
:param optimization_traffic: "liveOrHistorical" "historicalOnly" "automatic"
449+
Specifies what kind of traffic information should be considered for routing
450+
:param optimization_waiting_time: Configures departure time optimization which tries to
451+
adapt the starting time of the tour in order to reduce waiting time as a consequence
452+
of a vehicle arriving at a stop before the starting time of the time window defined
453+
for serving the job.
454+
:param is_async: Solves the problem Asynchronously
455+
:raises ApiError: If
456+
:return: :class:`TourPlanningResponse` object.
457+
"""
458+
459+
if is_async is True:
460+
resp = self.tour_planning_api.solve_tour_planning(
461+
fleet=fleet,
462+
plan=plan,
463+
id=id,
464+
optimization_traffic=optimization_traffic,
465+
optimization_waiting_time=optimization_waiting_time,
466+
is_async=is_async,
467+
)
468+
status_url = resp.json()["href"]
469+
while True:
470+
resp_status = self.tour_planning_api.get_async_tour_planning_status(status_url)
471+
if resp_status.status_code == 200 and resp_status.json().get("error"):
472+
raise ApiError(resp_status)
473+
elif resp_status.status_code == 200 and resp_status.json()["status"] == "success":
474+
result_url = resp_status.json()["resource"]["href"]
475+
break
476+
elif resp_status.status_code in (401, 403, 404, 500):
477+
raise ApiError(resp_status)
478+
sleep(2)
479+
result = self.matrix_routing_api.get_async_matrix_route_results(result_url)
480+
response = TourPlanningResponse.new(result)
481+
return response
482+
else:
483+
resp = self.tour_planning_api.solve_tour_planning(
484+
fleet=fleet,
485+
plan=plan,
486+
id=id,
487+
optimization_traffic=optimization_traffic,
488+
optimization_waiting_time=optimization_waiting_time,
489+
is_async=is_async,
490+
)
491+
response = TourPlanningResponse.new(resp.json())
492+
return response
493+
424494
def discover(
425495
self,
426496
query: str,

0 commit comments

Comments
 (0)