Demo: Tour Import with Python Client
This example is intended for MapTrip Maps users who want to regularly import
addresses from their system into Maps to create routes.
To do this, it reads a CSV file containing addresses and geocodes them. Each
address is then imported as a location and tagged with the label imported.
After the addresses are imported, a tour optimization is performed to
determine the optimal sequence of stops. Once the optimization is complete,
the route is also imported and tagged with the label.
The demo's code is based on an API client generated using
OpenAPI Generator.
Generating the API Client
OpenAPI Generator is a tool that generates source code in various languages
from an OpenAPI specification. In this example, we use it to create a Python
client.
Follow the installation instructions
to install OpenAPI Generator on your machine. Once you are done, create a new
Python project in your favorite IDE, switch to the project directory and
use this command to create the API client:
npx @openapitools/openapi-generator-cli generate \
-i https://api.maptrip.de/specification/yaml \
-g python -o .
This command reads the OpenAPI specification of the MapTrip Server API and
uses the generator python to create a client in the current working directory.
See client generators
for a list of all available programming languages.
Initializing the API Client
The MapTrip Server API provides a set of APIs for various use cases. In this example,
we will use the Geocoder API to find coordinates for addresses,
the Tour Optimization API to optimize the order of tour stops,
and the Storage API to import labels, locations and tours to MapTrip Maps.
These APIs are based on an API client, which must be initialized with a token for
your user account. Use this code to create an instance of APIClient, use the
AuthenticateApi to fetch a token for the configured API key, and initialize the client
with this token.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | def init_api_client() -> ApiClient:
configuration = Configuration(
host=BASE_URL
)
client = ApiClient(configuration)
authenticate_api = AuthenticateApi(client)
api_key = APIKey(
apikey=API_KEY,
duration=600
)
try:
api_response = authenticate_api.authenticate_by_api_key(api_key, _request_timeout=REQUEST_TIMEOUT_SECONDS)
client.configuration.access_token = api_response.token
return client
except UnauthorizedException as e:
raise RuntimeError("Failed to authenticate with API key")
|
You can use the returned client to initialize any of the provided APIs:
geocoder_api = GeocoderApi(api_client)
Using Labels
Labels can be used to categorize vehicles, locations or tours. In this example, we will
check if there already is a label called imported. If that's the case, we'll use it;
otherwise, we'll create a new one with that name.
Working with labels requires an instance of StorageLabelApi:
label_api = StorageLabelApi(api_client)
This can be used to fetch the existing or create a new label:
| def get_label(label_api, label_name) -> Label:
labels = label_api.get_all_labels()
for label in labels:
if label.name == label_name:
print(f"Using existing label {label}")
return label
# no label found, so create it
label = label_api.create_label(Label(name=label_name, color=LABEL_COLOR))
print(f"Created new label {label}")
return label
|
Import the Locations
In this example, we use a CSV file containing names and addresses of customers:
| name;street;houseNumber;postalCode;city;countryCode
infoware GmbH;Weiherstr.;38;53111;Bonn;DEU
infoware altes Büro;Riemenschneiderstr.;11;53175;Bonn;DEU
Mr and Mrs Humus;Kaiser-Karl-Ring;27;53111;Bonn;DEU
Strandhaus;Georgstraße;28;53111;Bonn;DEU
Esskalation;Clemens-August-Straße;7A;53115;Bonn;DEU
|
You should be able to easily adapt this to your data or APIs.
This function...
- iterates over the lines of the CSV file
- geocodes the addresses that have been read
- creates an instance of
LocationRequest (LocationRequest, Location and Address are based on AddressBase and share the same fields)
- imports the location using the label which has been fetched or created before
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | def import_locations(geocoder_api, location_api, label_id) -> list:
locations = []
with open(CSV_FILE, encoding="utf-8") as csvfile:
reader = csv.reader(csvfile, delimiter=";")
for row in reader:
if len(row) < 6:
continue # skip incomplete lines
if row[0] == "name":
continue
address = geocode(
geocoder_api,
street=row[1],
housenumber=row[2],
postalcode=row[3],
city=row[4],
country=row[5])
if address is not None:
# The geocoder api returns an instance of Address, but we have to create a Location.
# As the class Location is based on Address, we can use the fields from the geocoder response.
location = LocationRequest.from_json(address.to_json())
location.name = row[0] # use the name from the CSV file
location.labels = [label_id] # use the label which has been queried or created
result = location_api.create_location(location)
locations.append(result)
return locations
|
You have to provide instances of GeocoderApi and StorageLocationApi to use it:
| geocoder_api = GeocoderApi(api_client)
label_api = StorageLabelApi(api_client)
location_api = StorageLocationApi(api_client)
label = get_label(label_api, LABEL_NAME)
imported_locations = import_locations(geocoder_api, location_api, label.id)
|
Fetch a Vehicle
An instance of StorageVehicleApi can be used to query the vehicles you have created in MapTrip Maps,
as well as all the default vehicles like Truck 40t, Truck_7.5t, Sprinter or Car.
This method retrieves one of the default vehicles by its name:
1
2
3
4
5
6
7
8
9
10
11
12 | def get_default_vehicle(vehicle_api, vehicle_name) -> Vehicle:
vehicles = vehicle_api.get_all_default_vehicles()
for vehicle in vehicles:
if vehicle.name == vehicle_name:
print(f"Using default vehicle {vehicle_name}")
return vehicle
raise RuntimeError(f"Default vehicle '{vehicle_name}' was not found")
VEHICLE_NAME = "Sprinter"
vehicle_api = StorageVehicleApi(api_client)
vehicle = get_default_vehicle(vehicle_api, VEHICLE_NAME)
|
Optimize the Tour
The code from section reads the stops in the order of
The code from the section Import the Locations reads the locations
in the order they appear in the CSV file. For the route, the stops should be rearranged
to optimize driving time. The OptimizeApi can be used for this purpose.
This code creates a Stop for each location. In this example, the first stop is used as start
and the last one as destination of the tour (see highlighted lines).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | def create_stops_for_each_location(locations) -> list:
stops = []
for index, location in enumerate(locations):
stops.append(Stop(
name=str(index),
coordinate=Coordinate(
lat=location.coordinate.lat,
lon=location.coordinate.lon
),
start=index == 0,
destination=index == len(locations) - 1
))
return stops
|
The optimization runs asynchronously:
You have to call optimize_stops to start the calculation, and call
get_optimized_stops to retrieve the result with the UUID (see the highlighted lines).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | def optimize_order(optimize_api, locations) -> list:
stops = create_stops_for_each_location(locations)
# starting the tour optimization returns a UUID which can be used to retrieve the result
uuid = optimize_api.optimize_stops(
provider=PROVIDER,
start_time=get_start_time_as_iso_8601(),
stop=stops,
_request_timeout=REQUEST_TIMEOUT_SECONDS
).id
# wait until the status is Done
while True:
time.sleep(1)
try:
optimization_result = optimize_api.get_optimized_stops(uuid)
except ServiceException as e:
raise RuntimeError(f"Optimization failed")
if optimization_result.status == "Done":
print("Optimization done")
# get a copy of the locations in the optimized order
return get_optimized_order(locations, optimization_result)
|
The result is an optimized order of the stops. You can use it to sort your locations:
| def get_optimized_order(locations, optimization_result) -> list:
optimized = []
for stop in optimization_result.stops:
index = int(stop.name)
optimized.append(locations[index])
return optimized
|
Import the Tour
This function shows how to create a tour from the imported and optimized locations and the retrieved vehicle:
def import_tour(tour_api: StorageTourApi, locations: list, label_id, vehicle_id) -> Tour:
stops = []
for location in locations:
stops.append(TourStopRequest(address=location.id))
tour = TourRequest(
name="My Tour",
labels=[label_id],
vehicle=vehicle_id,
stops=stops,
startTime=START_TIME
)
return tour_api.create_tour(tour)
Complete example
Here is the complete code of the previous sections:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247 | import csv
import time
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from openapi_client import (ApiClient, AuthenticateApi, StorageLocationApi, Configuration, APIKey,
GeocoderApi, OptimizeApi, Stop, StorageLabelApi, Label, StorageTourApi,
LocationRequest, Coordinate, TourRequest, TourStopRequest, StorageVehicleApi, Address,
Vehicle, Tour)
from openapi_client.exceptions import ServiceException, UnauthorizedException
BASE_URL = "https://api.maptrip.de/v1"
API_KEY = "<insert your API key here>"
CSV_FILE = "<insert path to CSV file here>"
START_TIME = "08:00:00"
TIMEZONE = ZoneInfo("Europe/Berlin")
LABEL_NAME = "imported"
LABEL_COLOR = "#ff0000"
VEHICLE_NAME = "Sprinter"
PROVIDER = "TOMTOM"
MIN_GEOCODING_PROBABILITY = 90
REQUEST_TIMEOUT_SECONDS = 2
def init_api_client() -> ApiClient:
configuration = Configuration(
host=BASE_URL
)
client = ApiClient(configuration)
authenticate_api = AuthenticateApi(client)
api_key = APIKey(
apikey=API_KEY,
duration=600
)
try:
api_response = authenticate_api.authenticate_by_api_key(api_key, _request_timeout=REQUEST_TIMEOUT_SECONDS)
client.configuration.access_token = api_response.token
return client
except UnauthorizedException as e:
raise RuntimeError("Failed to authenticate with API key")
def geocode(geocoder_api, street, housenumber, postalcode, city, country) -> Address:
result = geocoder_api.geocode_address(
provider=PROVIDER,
street=street,
housenumber=housenumber,
postalcode=postalcode,
city=city,
country=country)
if len(result) == 0:
print(f"Warning: Found no address for {street} {housenumber}, {postalcode} {city}, {country}")
elif result[0].probability < MIN_GEOCODING_PROBABILITY:
print(f"Warning: Ignoring address address {street} {housenumber}, {postalcode} {city}, {country}"
f" with low probability of {result[0].probability}%")
else:
return result[0].address
def get_label(label_api, label_name) -> Label:
labels = label_api.get_all_labels()
for label in labels:
if label.name == label_name:
print(f"Using existing label {label}")
return label
# no label found, so create it
label = label_api.create_label(Label(name=label_name, color=LABEL_COLOR))
print(f"Created new label {label}")
return label
def get_default_vehicle(vehicle_api, vehicle_name) -> Vehicle:
vehicles = vehicle_api.get_all_default_vehicles()
for vehicle in vehicles:
if vehicle.name == vehicle_name:
print(f"Using default vehicle {vehicle_name}")
return vehicle
raise RuntimeError(f"Default vehicle '{vehicle_name}' was not found")
def import_locations(geocoder_api, location_api, label_id) -> list:
locations = []
with open(CSV_FILE, encoding="utf-8") as csvfile:
reader = csv.reader(csvfile, delimiter=";")
for row in reader:
if len(row) < 6:
continue # skip incomplete lines
if row[0] == "name":
continue
address = geocode(
geocoder_api,
street=row[1],
housenumber=row[2],
postalcode=row[3],
city=row[4],
country=row[5])
if address is not None:
# The geocoder api returns an instance of Address, but we have to create a Location.
# As the class Location is based on Address, we can use the fields from the geocoder response.
location = LocationRequest.from_json(address.to_json())
location.name = row[0] # use the name from the CSV file
location.labels = [label_id] # use the label which has been queried or created
result = location_api.create_location(location)
locations.append(result)
return locations
def get_start_time_as_iso_8601() -> str:
local_time = datetime.strptime(
START_TIME,
"%H:%M:%S",
).time()
start_date = datetime.now(TIMEZONE).date() + timedelta(days=1)
start_datetime = datetime.combine(
start_date,
local_time,
tzinfo=TIMEZONE,
)
return start_datetime.isoformat()
def optimize_order(optimize_api, locations) -> list:
stops = create_stops_for_each_location(locations)
# starting the tour optimization returns a UUID which can be used to retrieve the result
uuid = optimize_api.optimize_stops(
provider=PROVIDER,
start_time=get_start_time_as_iso_8601(),
stop=stops,
_request_timeout=REQUEST_TIMEOUT_SECONDS
).id
# wait until the status is Done
while True:
time.sleep(1)
try:
optimization_result = optimize_api.get_optimized_stops(uuid)
except ServiceException as e:
raise RuntimeError(f"Optimization failed")
if optimization_result.status == "Done":
print("Optimization done")
# get a copy of the locations in the optimized order
return get_optimized_order(locations, optimization_result)
def create_stops_for_each_location(locations) -> list:
stops = []
for index, location in enumerate(locations):
stops.append(Stop(
name=str(index),
coordinate=Coordinate(
lat=location.coordinate.lat,
lon=location.coordinate.lon
),
start=index == 0,
destination=index == len(locations) - 1
))
return stops
def get_optimized_order(locations, optimization_result) -> list:
optimized = []
for stop in optimization_result.stops:
index = int(stop.name)
optimized.append(locations[index])
return optimized
def import_tour(tour_api: StorageTourApi, locations: list, label_id, vehicle_id) -> Tour:
stops = []
for location in locations:
stops.append(TourStopRequest(address=location.id))
tour = TourRequest(
name="My Tour",
labels=[label_id],
vehicle=vehicle_id,
stops=stops,
startTime=START_TIME
)
return tour_api.create_tour(tour)
def main() -> None:
# initialize the API
api_client = init_api_client()
geocoder_api = GeocoderApi(api_client)
label_api = StorageLabelApi(api_client)
location_api = StorageLocationApi(api_client)
vehicle_api = StorageVehicleApi(api_client)
tour_api = StorageTourApi(api_client)
optimize_api = OptimizeApi(api_client)
# fetch or create label "imported"
label = get_label(label_api, LABEL_NAME)
# get the id of a default vehicle
vehicle = get_default_vehicle(vehicle_api, VEHICLE_NAME)
# import locations from CSV
imported_locations = import_locations(geocoder_api, location_api, label.id)
# if there are at least 4 stops, optimize the order
if len(imported_locations) >= 4:
print("Optimizing tour")
imported_locations = optimize_order(optimize_api, imported_locations)
# if at least 2 stops were imported (i.e. it is a valid tour), create a tour
if len(imported_locations) >= 2:
result = import_tour(tour_api, imported_locations, label.id, vehicle.id)
print(f"Created tour with id {result.id}")
if __name__ == "__main__":
try:
main()
except (
FileNotFoundError,
RuntimeError,
TimeoutError,
ValueError,
ServiceException,
) as exception:
print(f"Import failed: {exception}")
|