Skip to content

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
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}")