แก้ไข

แชร์ผ่าน


Tutorial: Route electric vehicles by using Azure Notebooks (Python)

Azure Maps is a portfolio of geospatial service APIs integrated into Azure, enabling developers to create location-aware applications for various scenarios like IoT, mobility, and asset tracking.

Azure Maps REST APIs support languages like Python and R for geospatial data analysis and machine learning, offering robust routing APIs for calculating routes based on conditions such as vehicle type or reachable area.

This tutorial guides users through routing electric vehicles using Azure Maps APIs along with Azure Notebooks and Python to find the closest charging station when the battery is low.

In this tutorial, you will:

  • Create and run a Jupyter Notebook file on Azure Notebooks in the cloud.
  • Call Azure Maps REST APIs in Python.
  • Search for a reachable range based on the electric vehicle's consumption model.
  • Search for electric vehicle charging stations within the reachable range, or isochrone.
  • Render the reachable range boundary and charging stations on a map.
  • Find and visualize a route to the closest electric vehicle charging station based on drive time.

Prerequisites

Note

For more information on authentication in Azure Maps, see manage authentication in Azure Maps.

Create an Azure Notebooks project

To proceed with this tutorial, it's necessary to create an Azure Notebooks project and download and execute the Jupyter Notebook file. This file contains Python code that demonstrates the scenario presented in this tutorial.

Follow these steps to create an Azure Notebooks project and upload the Jupyter Notebook document:

  1. Go to Azure Notebooks and sign in.

  2. At the top of your public profile page, select My Projects.

    The My Projects button

  3. On the My Projects page, select New Project.

    The New Project button

  4. In the Create New Project pane, enter a project name and project ID.

    The Create New Project pane

  5. Select Create.

  6. After your project is created, download this Jupyter Notebook document file from the Azure Maps Jupyter Notebook repository.

  7. In the projects list on the My Projects page, select your project, and then select Upload to upload the Jupyter Notebook document file.

    upload Jupyter Notebook

  8. Upload the file from your computer, and then select Done.

  9. Once uploaded successfully, your file is displayed on your project page. Double-click on the file to open it as a Jupyter Notebook.

Familiarize yourself with the functionality implemented in the Jupyter Notebook file. Execute the code within the Jupyter Notebook one cell at a time by selecting the Run button located at the top of the Jupyter Notebook application.

The Run button

Install project level packages

To run the code in Jupyter Notebook, install packages at the project level by following these steps:

  1. Download the requirements.txt file from the Azure Maps Jupyter Notebook repository, and then upload it to your project.

  2. On the project dashboard, select Project Settings.

  3. In the Project Settings pane, select the Environment tab, and then select Add.

  4. Under Environment Setup Steps, do the following: a. In the first drop-down list, select Requirements.txt.
    b. In the second drop-down list, select your requirements.txt file.
    c. In the third drop-down list, select the version of Python. Version 3.11 was used when creating this tutorial.

  5. Select Save.

    Install packages

Load the required modules and frameworks

Run the following script to load all the required modules and frameworks.

import time
import aiohttp
import urllib.parse
from IPython.display import Image, display

Request the reachable range boundary

A package delivery company operates a fleet that includes some electric vehicles. These vehicles need to be recharged during the day without returning to the warehouse. When the remaining charge drops below an hour, a search is conducted to find charging stations within a reachable range. The boundary information for the range of these charging stations is then obtained.

The requested routeType is eco to balance economy and speed. The following script calls the Get Route Range API of the Azure Maps routing service, using parameters related to the vehicle's consumption model. The script then parses the response to create a polygon object in GeoJSON format, representing the car's maximum reachable range.

subscriptionKey = "Your Azure Maps key"
currentLocation = [34.028115,-118.5184279]
session = aiohttp.ClientSession()

# Parameters for the vehicle consumption model 
travelMode = "car"
vehicleEngineType = "electric"
currentChargeInkWh=45
maxChargeInkWh=80
timeBudgetInSec=550
routeType="eco"
constantSpeedConsumptionInkWhPerHundredkm="50,8.2:130,21.3"

# Get boundaries for the electric vehicle's reachable range.
routeRangeResponse = await (await session.get("https://atlas.microsoft.com/route/range/json?subscription-key={}&api-version=1.0&query={}&travelMode={}&vehicleEngineType={}&currentChargeInkWh={}&maxChargeInkWh={}&timeBudgetInSec={}&routeType={}&constantSpeedConsumptionInkWhPerHundredkm={}"
                                              .format(subscriptionKey,str(currentLocation[0])+","+str(currentLocation[1]),travelMode, vehicleEngineType, currentChargeInkWh, maxChargeInkWh, timeBudgetInSec, routeType, constantSpeedConsumptionInkWhPerHundredkm))).json()

polyBounds = routeRangeResponse["reachableRange"]["boundary"]

for i in range(len(polyBounds)):
    coordList = list(polyBounds[i].values())
    coordList[0], coordList[1] = coordList[1], coordList[0]
    polyBounds[i] = coordList

polyBounds.pop()
polyBounds.append(polyBounds[0])

boundsData = {
               "geometry": {
                 "type": "Polygon",
                 "coordinates": 
                   [
                      polyBounds
                   ]
                }
             }

Search for electric vehicle charging stations within the reachable range

After determining the electric vehicle's reachable range (isochrone), you can search for charging stations within that area.

The following script uses the Azure Maps Post Search Inside Geometry API to find charging stations within the vehicle’s maximum reachable range. It then parses the response into an array of reachable locations.

# Search for electric vehicle stations within reachable range.
searchPolyResponse = await (await session.post(url = "https://atlas.microsoft.com/search/geometry/json?subscription-key={}&api-version=1.0&query=electric vehicle station&idxSet=POI&limit=50".format(subscriptionKey), json = boundsData)).json() 

reachableLocations = []
for loc in range(len(searchPolyResponse["results"])):
                location = list(searchPolyResponse["results"][loc]["position"].values())
                location[0], location[1] = location[1], location[0]
                reachableLocations.append(location)

Render the charging stations and reachable range on a map

Call the Azure Maps Get Map Image service to render the charging points and maximum reachable boundary on the static map image by running the following script:

# Get boundaries for the bounding box.
def getBounds(polyBounds):
    maxLon = max(map(lambda x: x[0], polyBounds))
    minLon = min(map(lambda x: x[0], polyBounds))

    maxLat = max(map(lambda x: x[1], polyBounds))
    minLat = min(map(lambda x: x[1], polyBounds))
    
    # Buffer the bounding box by 10 percent to account for the pixel size of pins at the ends of the route.
    lonBuffer = (maxLon-minLon)*0.1
    minLon -= lonBuffer
    maxLon += lonBuffer

    latBuffer = (maxLat-minLat)*0.1
    minLat -= latBuffer
    maxLat += latBuffer
    
    return [minLon, maxLon, minLat, maxLat]

minLon, maxLon, minLat, maxLat = getBounds(polyBounds)
polyBoundsFormatted = ('|'.join(map(str, polyBounds))).replace('[','').replace(']','').replace(',','')
reachableLocationsFormatted = ('|'.join(map(str, reachableLocations))).replace('[','').replace(']','').replace(',','')

path = "lcff3333|lw3|la0.80|fa0.35||{}".format(polyBoundsFormatted)
pins = "custom|an15 53||{}||https://raw.githubusercontent.com/Azure-Samples/AzureMapsCodeSamples/e3a684e7423075129a0857c63011e7cfdda213b7/Static/images/icons/ev_pin.png".format(reachableLocationsFormatted)

encodedPins = urllib.parse.quote(pins, safe='')

# Render the range and electric vehicle charging points on the map.
staticMapResponse =  await session.get("https://atlas.microsoft.com/map/static/png?api-version=2022-08-01&subscription-key={}&pins={}&path={}&bbox={}&zoom=12".format(subscriptionKey,encodedPins,path,str(minLon)+", "+str(minLat)+", "+str(maxLon)+", "+str(maxLat)))

poiRangeMap = await staticMapResponse.content.read()

display(Image(poiRangeMap))

A map showing the location range

Find the optimal charging station

First, identify all the potential charging stations within the vehicle’s reachable range. Next, determine which of these stations can be accessed in the shortest possible time.

The following script calls the Azure Maps Matrix Routing API. It returns the vehicle's location, travel time, and distance to each charging station. The subsequent script parses this response to identify the closest charging station that can be reached in the least amount of time.

locationData = {
            "origins": {
              "type": "MultiPoint",
              "coordinates": [[currentLocation[1],currentLocation[0]]]
            },
            "destinations": {
              "type": "MultiPoint",
              "coordinates": reachableLocations
            }
         }

# Get the travel time and distance to each specified charging station.
searchPolyRes = await (await session.post(url = "https://atlas.microsoft.com/route/matrix/json?subscription-key={}&api-version=1.0&routeType=shortest&waitForResults=true".format(subscriptionKey), json = locationData)).json()

distances = []
for dist in range(len(reachableLocations)):
    distances.append(searchPolyRes["matrix"][0][dist]["response"]["routeSummary"]["travelTimeInSeconds"])

minDistLoc = []
minDistIndex = distances.index(min(distances))
minDistLoc.extend([reachableLocations[minDistIndex][1], reachableLocations[minDistIndex][0]])
closestChargeLoc = ",".join(str(i) for i in minDistLoc)

Calculate the route to the closest charging station

After locating the nearest charging station, use the Get Route Directions API to obtain detailed directions from the vehicles current location. Run the script in the next cell to generate and parse a GeoJSON object representing the route.

# Get the route from the electric vehicle's current location to the closest charging station. 
routeResponse = await (await session.get("https://atlas.microsoft.com/route/directions/json?subscription-key={}&api-version=1.0&query={}:{}".format(subscriptionKey, str(currentLocation[0])+","+str(currentLocation[1]), closestChargeLoc))).json()

route = []
for loc in range(len(routeResponse["routes"][0]["legs"][0]["points"])):
                location = list(routeResponse["routes"][0]["legs"][0]["points"][loc].values())
                location[0], location[1] = location[1], location[0]
                route.append(location)

routeData = {
         "type": "LineString",
         "coordinates": route
     }

Visualize the route

To visualize the route, use the Get Map Image API to render it on the map.

destination = route[-1]

#destination[1], destination[0] = destination[0], destination[1]

routeFormatted = ('|'.join(map(str, route))).replace('[','').replace(']','').replace(',','')
path = "lc0f6dd9|lw6||{}".format(routeFormatted)
pins = "default|codb1818||{} {}|{} {}".format(str(currentLocation[1]),str(currentLocation[0]),destination[0],destination[1])


# Get boundaries for the bounding box.
minLon, maxLon = (float(destination[0]),currentLocation[1]) if float(destination[0])<currentLocation[1] else (currentLocation[1], float(destination[0]))
minLat, maxLat = (float(destination[1]),currentLocation[0]) if float(destination[1])<currentLocation[0] else (currentLocation[0], float(destination[1]))

# Buffer the bounding box by 10 percent to account for the pixel size of pins at the ends of the route.
lonBuffer = (maxLon-minLon)*0.1
minLon -= lonBuffer
maxLon += lonBuffer

latBuffer = (maxLat-minLat)*0.1
minLat -= latBuffer
maxLat += latBuffer

# Render the route on the map.
staticMapResponse = await session.get("https://atlas.microsoft.com/map/static/png?api-version=2022-08-01&subscription-key={}&&path={}&pins={}&bbox={}&zoom=16".format(subscriptionKey,path,pins,str(minLon)+", "+str(minLat)+", "+str(maxLon)+", "+str(maxLat)))

staticMapImage = await staticMapResponse.content.read()

await session.close()
display(Image(staticMapImage))

A map showing the route

In this tutorial, you learned how to call Azure Maps REST APIs directly and visualize Azure Maps data by using Python.

To explore the Azure Maps APIs that are used in this tutorial, see:

Next steps

To learn more about Azure Notebooks, see