How Do I Optimize a Route in Python?

To optimize a route in Python, you can use the Maptitude GISDK. This example can be run outside of Maptitude, even with the Maptitude application being closed. It can be run from within Visual Studio Code, for example.

Table of Contents
1. Download and Install Software
2. Create a Python file
3. Run the Python Script
4. Python Script Execution

Step 1: Download and Install Software

Step 2: Create a Python File

  • Open Visual Studio Code.
  • Create a Python file and copy the code below into the file.
  • Save the file.
import sys
import os
import traceback
import caliperpy
import pandas as pd
from tkinter import Tk
from tkinter.filedialog import askopenfilename

# Function to prompt user for an Excel file and return the file path
def get_excel_file():
    Tk().withdraw()
    return askopenfilename(title="Select the Excel file containing addresses", filetypes=[("Excel files", "*.xlsx *.xls")])

# Function to read addresses from the Excel file into a pandas DataFrame
def read_addresses_from_excel(file_path):
    df = pd.read_excel(file_path)
    print("Data from Excel file:\n", df)

    # List of possible column name variations
    address_variants = ["Street Address", "Address", "road", "location", "Street"]
    city_variants = ["City", "town", "municipality", "location"]
    zip_variants = ["ZIP Code", "postal code", "zip", "postcode", "Post Code", "Postal District"]

    # Function to detect the correct column name from possible variants
    def detect_columns(df, variants):
        for variant in variants:
            matching_cols = [col for col in df.columns if variant.lower() in col.lower()]
            if matching_cols:
                return matching_cols[0]
        return None

    # Detecting the columns
    address_col = detect_columns(df, address_variants)
    city_col = detect_columns(df, city_variants)
    zip_col = detect_columns(df, zip_variants)

    if not (address_col and city_col and zip_col):
        raise ValueError("The Excel file must contain columns for address, city, and ZIP code.")

    return df[[address_col, city_col, zip_col]].to_dict('records'), address_col, city_col, zip_col

# Function to open the default map using MapWizard
def open_default_map(dk):
    dataset = dk.CreateGisdkObject(None, "MapWizard.DataSet", None)
    dataset.LoadRegion(True)
    if dataset.GetRegionFile() is None:
        print("No data installed for the selected region.")
        return None

    basemap = dk.CreateGisdkObject(None, "MapWizard.BaseMap", dataset)
    ccmap = basemap.SelectMapForStartup()

    if ccmap.Path is None:
        print("Could not find a default or initial map for the current region.")
        return None

    if basemap.Open():
        mapname = basemap.GetName()
        title = dk.GetMapTitle(mapname)
        dk.SetMapFile(mapname, "Initial Map")
        dk.SetMapTitle(mapname, title)
        basemap.Draw()
        return mapname
    return None

# Main function to manage the entire workflow
def main():
    print("Python interpreter:", sys.executable)
    dk = None
    try:
        # Get the Excel file path from the user
        excel_file = get_excel_file()
        if not excel_file:
            print("No Excel file selected. Exiting.")
            return

        # Read addresses from the Excel file and get the detected column names
        addresses, address_col, city_col, zip_col = read_addresses_from_excel(excel_file)
        script_path = caliperpy.GetDirectory(sys.argv[0])
        app_name = "Maptitude"
        log_file = None if caliperpy.IsApplicationRunning(app_name) else os.path.join(script_path, f"{app_name}.log")

        if log_file:
            try:
                os.remove(log_file)
            except OSError:
                pass

        # Connect to Maptitude
        dk = caliperpy.Maptitude.connect(log_file=log_file)
        if not dk.IsConnected():
            print("Cannot connect to", app_name)
            return

        print(dk.Dump(dk.GetProgram()))

        # Open the default map
        map_name = open_default_map(dk)
        if map_name is None:
            print("Failed to open the default map.")
            dk.Close()
            return

        # Use the extracted addresses as stops for routing
        stop_list = []
        for address in addresses:
            if pd.notna(address.get(address_col)) and pd.notna(address.get(city_col)):
                stop = {
                    "address": address.get(address_col),
                    "city": address.get(city_col),
                    "postal_code": str(address.get(zip_col)),
                    "silent": 1,
                    "Coordinate": None,
                    "StopName": "",
                    "StopDuration": 30
                }

                # Attempt to find coordinates for the address
                finder = dk.CreateGisdkObject(None, "Data.Finder", None).SetRegion()
                result = finder.Find("ADDRESS", stop)
                if result is not None and "Coord" in dict(result):
                    stop["Coordinate"] = dict(result)["Coord"]
                    stop_list.append(stop)
                    print(f"Address found: {stop['address']}, {stop['city']}, {stop['postal_code']}")
                else:
                    print(f"Cannot find address: {stop['address']}, {stop['city']}, {stop['postal_code']}")

                # Break if three valid stops have been found
                if len(stop_list) == 3:
                    break

        # Ensure three valid addresses are found
        if len(stop_list) < 3:
            print("Could not find three valid addresses for routing. Exiting.")
            dk.Close()
            return

        # Routing with the stops
        router = dk.CreateGisdkObject(None, "Routing.Router", None)
        router.Minimize = "Time"
        router.StartTime = dk.CreateTime(9, 0, 0)  # 9 AM
        router.EndTime = dk.CreateTime(17, 0, 0)   # 5 PM
        router.IncludeRestStops = True
        router.TimeBetweenRests = 240  # minutes
        router.RestStopDuration = 20   # minutes
        router.FuelPrice = 2.38        # dollars

        # Find the shortest path visiting the stops optimized with fixed first stop
        path = router.Calculate(stop_list, {"Fix": "First"})
        if path is None:
            print(router.ErrorMessage)
            dk.Close()
            return

        # Create direction reports in Excel, PDF
        excel_report_path = os.path.join(script_path, "Shortest Path Report Time.xlsx")
        pdf_report_path = os.path.join(script_path, "Shortest Path Report Time.pdf")

        router.CreateReport({"PathObj": path, "OpenReport": 0, "FileName": excel_report_path})
        router.CreateReport({"PathObj": path, "OpenReport": 0, "FileName": pdf_report_path})

        # Disconnect from Maptitude
        caliperpy.Maptitude.disconnect()
        if caliperpy.IsApplicationRunning(app_name):
            print(app_name, "continues to run.")
        else:
            print(app_name, "has stopped running.")

    except Exception as e:
        traceback.print_exc()
        if dk:
            dk.Close()
    finally:
        if dk and dk.IsConnected():
            dk.Close()
        caliperpy.Maptitude.disconnect()

if __name__ == "__main__":
    main()

Step 3: Run the Python Script

  • In Visual Studio Code, run the script. You can press F5 to do this, for example.
  • When prompted, choose an Excel file with addresses for the country that you installed with Maptitude, e.g.,
    • “C:\Users\[USER]\Documents\Caliper\Maptitude 2024\Tutorial\Sample Customers United States.xlsx”
    • “C:\Users\[USER]\Documents\Caliper\Maptitude 2024\Tutorial\Sample Customers United Kingdom.xlsx”

Step 4: Python Script Execution

  • The code prompts the user to select an Excel file containing address information using a file dialog. 
  • It reads the selected Excel file into a Pandas DataFrame. 
  • The code attempts to detect columns for street address, city, and postal code by matching various common column names. 
  • It raises an error if the required columns for address, city, and postal code are not found. 
  • The code connects to the Maptitude application using `caliperpy` and attempts to open the default map for the selected region. 
  • It searches for coordinates of up to three valid addresses using the Maptitude geocoding functionality. 
  • If fewer than three valid addresses are found, the code stops execution. 
  • The code creates a routing object, sets parameters like start/end times, rest stops, and fuel price, and calculates the shortest route. 
  • It generates routing reports in Excel and PDF formats, saving them to the script’s directory. 
  • The code includes exception handling to manage errors and ensures the Maptitude connection is properly closed after execution. 
  • It manages Maptitude log files in the script’s directory, deleting any old log file before connecting to the application. 
Scroll to Top
Maptitude Support Chat