LoginSignup
45
57

Pythonを使用して物流DXのプロトタイピングをしてみる

Last updated at Posted at 2023-05-17

はじめに

HEREでエンジニアをしておりますkekishidaと申します。本記事では、PythonとHEREのAPI群を利用して、以下の箇条書きに示した事項を実現するべく、物流向けの配送オペレーションについてJupyter上でシミュレーションしてみたいと思います。

  • 配送計画の最適化
  • 配送時間の短縮化
  • 計画策定作業の効率化
  • トラック規制考慮によるドライバーの安全確保

image.png

HEREでは物流関連ソリューション向けAPIにも注力しています。具体的には、複数の配送地点の配送ルートを検索するAPI、運搬荷物のアセットを追跡管理するAPIなどなどを取り揃えています。

一方で、HEREではData SDK for Pythonというロケーションデータを分析するプラットフォームを提供しており、こちらの機能を利用することでPythonユーザが、データ分析のみならず、HEREのAPI群を利用したアプリケーションの開発も可能にします。(本記事では、本SDKの機能の一部のみを使用しております。)

今回シミュレーションするシナリオは物流向けに特化していますが、インタラクティブに対話することが可能なJupyterエコシステムを利用して、いかに目的のシミュレーションができるのか?さらに、トラックドライバーの配送ルートをインタラクティブに疑似体験しながら、物流DXのプロトタイピングをしていきたいと思います。

シミュレーションするシナリオ

物流の最終拠点である営業所からエンドユーザに荷物を配送するラストワンマイルのシナリオとします。

場所

エリアは京都とし、配送する場所についても、イメージし易い有名観光スポットとしました。さらに、配送デポについては京都駅としました。こうすることで、配送順序の妥当性についてイメージがし易いかと思います。(京都に修学旅行に行かれた方は、そのルートを思い出してみてください。)
image.png

配送リスト

以下のような配送リストをCSVフォーマットで作成しました。(これから配達する荷物の宛先リストになります。)Time Restrictionというフィールドはいわゆる配達の時間指定になります。Service Timeというのは配送先毎にかかるサービス時間(分)となります。この配送リストの時点では、配送順序、配送ルートそして作業終了時間は確定していません。これらをDXします。
image.png

シナリオ

  1. 配送リスト(配達する荷物の宛先リスト)をもとに配送順序を決定します。(DX部分)
  2. 決定した配送順序をもとに配送ルートを決定します。 (DX部分)
  3. 決定した配送順序及び配送ルートをもとに配送作業を行います。
  4. 3地点(清水寺、銀閣寺、南禅寺)で不在のため、未配達というシナリオとします。
  5. 以上より、再配達リストを作成します。(DX部分)
  6. 再配達リストより配送順序を決定します。(DX部分)
  7. 決定した配送順序をもとに現在地からの配送ルートを決定します。(DX部分)
  8. 決定した配送順序及び配送ルートをもとに配送作業を行います。

プロトタイピングに必要なもの

それではプロトタイピングを開始したいと思いますが、その前に必要なものがいくつか存在します。

Jupyter notebookまたはJupyter lab

インストール方法についてはQiita内にいくつか有用な記事が存在します。

HEREのアカウント取得

以下の記事が大変参考になります。

HERE Data SDK for Pythonのインストール

インストール方法の詳細は下記のリンクに記載されておりますが、念の為、記事掲載時(2023年5月)の最新版のインストールコマンドconda install/pip install両方について掲載しておきます。
https://developer.here.com/documentation/sdk-python-v2/dev_guide/topics/install.html

conda install
conda install -c conda-forge -c https://repo.platform.here.com/artifactory/api/conda/olp_analytics/analytics_sdk here-platform=2.20.0 here-geotiles=2.20.0 here-geopandas-adapter=2.20.0 here-content=2.20.0 here-inspector=2.20.0
pip install
pip install --extra-index-url https://repo.platform.here.com/artifactory/api/pypi/analytics-pypi/simple/ here-platform==2.20.0 here-geotiles==2.20.0 here-geopandas-adapter==2.20.0 here-content==2.20.0 here-inspector==2.20.0

HERE Credentialの設定

HERE Data SDK for Pythonを使用するためには、Credentialの設定が必要になります。
https://developer.here.com/documentation/sdk-python-v2/dev_guide/topics/credentials.html
詳細は上記リンクに記載されておりますが、HEREアカウントを取得し、OAuthキーを作成する際に以下のフォーマットのcredentials.propertiesファイルを入手することができます。

here.user.id = <example_here>
here.client.id = <example_here>
here.access.key.id = <example_here>
here.access.key.secret = <example_here>
here.token.endpoint.url = <example_here>

こちらのファイルをOSに応じたファイルパスに保管します。

Mac OS/Linux
$HOME/.here/credentials.properties
Windows
%USERPROFILE%\.here\credentials.properties

HERE Map Widget for Jupyterのインストール

HERE Data SDK for Pythonを使用して地図を描画するには、いくつかの方法が存在します。ここではHERE Map Widget for Jupyterを使用します。
https://developer.here.com/documentation/sdk-python-v2/dev_guide/topics/usage/here-inspector.html
上記のリンクにも説明がありますが、以下のリンクが参考になります。

インストール手順は以下のリンクに記載されておりますが、念の為、記事掲載時(2023年5月)の最新版のインストールコマンドconda install/pip install両方について掲載しておきます。

conda install
conda install -c conda-forge here-map-widget-for-jupyter
pip install
pip install here-map-widget-for-jupyter

以上で最低限必要なものの準備は完了です。残りはJupyterエコシステムの考え方に乗っ取り、対話的に、その都度必要なモジュールをインストールする形で進めていきたいと思います。

配送シミュレーション by Jupyter

以下にjupyterと注釈してありますコードスニペットをそのままJupyter notebook/labのセルにペーストするだけでシミュレーションの実行が可能なように配慮をしておりますが、コードが冗長であること、個別環境に依存する部分がありますことについてはご了承ください。(あくまでもプロトタイピングという体です。)

配送リストCSVファイル

冒頭で説明しました配送リストです。

京都.csv
ID,Address,Time Restriction,Service Time,Job Type
京都駅,京都府京都市下京区東塩小路釜殿町,,,
東寺,京都府京都市南区九条町1,,5,配送
清水寺,京都府京都市東山区清水1丁目294,18-20,10,配送
金閣寺,京都府京都市北区金閣寺町1,8-12,5,配送
銀閣寺,京都府京都市左京区銀閣寺町2,,5,配送
渡月橋,京都府京都市右京区嵯峨天龍寺芒ノ馬場町1−5,8-12,10,配送
映画村,京都府京都市右京区太秦東蜂岡町10,8-12,10,配送
京都御所,京都府京都市上京区京都御苑3,18-20,10,配送
伏見稲荷,京都府京都市伏見区深草藪之内町68,18-20,5,配送
南禅寺,京都府京都市左京区南禅寺福地町,8-12,5,配送
二条城,京都府京都市中京区二条城町541,,10,配送
八坂神社,京都府京都市東山区祇園町北側625,14-16,5,配送
京都駅(戻り),京都府京都市下京区東塩小路釜殿町,,,

配送リストCSVファイルの読み込み

上記のCSVファイルを読み込みpandasのdataframeとして表示します。

jupyter
import pandas as pd
file = "京都.csv"
df = pd.read_csv(file,sep=",",encoding="cp932")
df

image.png

HERE Geocoding & Search APIを使用した住所から緯度経度情報への変換

配送リストの住所情報を緯度経度情報に変換します。その前に、下記のコードスニペットを実行するために以下のモジュールが必要になります。tqdmは経過情報を表示するためのツールになります。

tqdm
pip install tqdm

一方、以下のモジュールはHEREのロケーションサービスを簡易的に使用するために作られたモジュールです。全く同じ様な機能がHERE Data SDK for Pythonにも準備されておりますが、HERE Geocoding & Search APIを実装する上ではこちらの方が簡単であったため、あえてこちらを使用しております。

here-location-services
pip install here-location-services

また、HERE Geocoding & Search APIの詳細については以下のリンクをご参照ください。

以下のコードスニペットで住所を緯度経度に変換しています。<APIKEY>と記載されている部分は取得されましたAPIKEYに変換してください。

jupyter
from tqdm import tqdm
from here_location_services import LS

APIKEY = <APIKEY>
ls = LS(api_key=APIKEY)
addrList = df['Address'].tolist()

latlngList = []
for m in tqdm(addrList):
    gc_response = ls.geocode(query=m)
    data = gc_response.to_geojson()
    if len(data["features"]) != 0:
        latlngList.append(data["features"][0]["properties"]["position"])
    else:
        latlngList.append({'lat': 0.0, 'lng': 0.0})

配送リストと緯度経度情報のマージ

取得した緯度経度情報を配送リストにマージし、新しいdataframeを作成します。

jupyter
latList = []
lngList = []
for o in latlngList:
    latList.append(o['lat'])
    lngList.append(o['lng'])
idList = []
for p in range(len(addrList)):
    p = p + 1
    idList.append(str(p))

orders_df = df.copy()
orders_df['Latitude'] = latList
orders_df['Longitude'] = lngList
orders_df

image.png

HERE Waypoint Sequence APIによる配送順序の決定

HEREのラストワンマイル向けAPIとして以下の2つのAPIが存在しています。

今回は、トラック1台あたりの配送計画をシミュレーションするため、よりシンプルなHERE Waypoint Sequence APIを使用します。(複数台の配送計画にはTour Planning APIが最適になります。)

シンプルと申し上げましたが、実際は多くのインプットパラメータが存在しており、HEREが管理するロケーションデータを活用して様々な機能を実現することが可能です。(トラック規制、渋滞情報、配達時間の考慮など)
https://developer.here.com/documentation/routing-waypoints/api-reference.html

今回は、以下の基本条件でWaypoint Sequence APIをコールします。

  • 時間優先(fastest)
  • 車種(truck)
  • 渋滞などの交通状況考慮(enabled)
  • 出発時間の指定(2023-03-04 08:00)
  • 配達指定時間の考慮(各配送リスト内の情報)
  • サービス時間の考慮(各配送リスト内の情報)

以下のコードスニペットによりWaypoint Seaquence APIのインプットパラメータを生成します。

jupyter
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
departureTime = "2023-03-04T08:00:00+09:00"
jupyter
import numpy as np
import time

list = orders_df.to_numpy().tolist()
dayOfWeek = ""
temp = pd.Timestamp(departureTime)
if temp.dayofweek == 0:
    dayOfWeek = "mo"
elif temp.dayofweek == 1:
    dayOfWeek = "tu"
elif temp.dayofweek == 2:
    dayOfWeek = "we"
elif temp.dayofweek == 3:
    dayOfWeek = "th"
elif temp.dayofweek == 4:
    dayOfWeek = "fr"
elif temp.dayofweek == 5:
    dayOfWeek = "sa"
elif temp.dayofweek == 6:
    dayOfWeek = "su"
convertLists = []
params = {}
number = len(list)
count = 1
for m in list:
    key = ""
    acc = ""
    convertList = ""
    st = m[3]*60
    if count == 1:
        key = "start"
    elif count == number:
        key = "end"
    else:
        key += "destination"
        key += str(count-1)
    convertList += m[0]+":"+m[1]
    convertList += ";"
    convertList += str(m[5])
    convertList += ","
    convertList += str(m[6])
    if count != 1:
        if count != number:
            if m[2]:
                if m[2] == "8-12":
                    acc = "acc:"+dayOfWeek+"08:00:00+09:00|"+dayOfWeek+"12:00:00+09:00"
                elif m[2] == "12-14":
                    acc = "acc:"+dayOfWeek+"12:00:00+09:00|"+dayOfWeek+"14:00:00+09:00"
                elif m[2] == "14-16":
                    acc = "acc:"+dayOfWeek+"14:00:00+09:00|"+dayOfWeek+"16:00:00+09:00"
                elif m[2] == "18-20":
                    acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"20:00:00+09:00"
                elif m[2] == "20-22":
                    acc = "acc:"+dayOfWeek+"20:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
                elif m[2] == "18-22":
                    acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
                convertList += ";"
                convertList += acc
            convertList += ";st:"
            if np.isnan(st):
                convertList += ""
            else:
                convertList += str(int(st))
    params[key] = convertList
    if count == number:
        params["departure"] = departureTime
        params["improveFor"] = "distance"
        params["mode"] = routingMode+";"+transportMode+";traffic:"+trafficMode+";dirtRoad:-2"
    count = count + 1
print(params)

出力結果は以下のとおりです。

output
{'start': '京都駅:京都府京都市下京区東塩小路釜殿町;34.98515,135.75709', 
'destination1': '東寺:京都府京都市南区九条町1;34.98054,135.74664;;st:300', 
'destination2': '清水寺:京都府京都市東山区清水1丁目294;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600', 
'destination3': '金閣寺:京都府京都市北区金閣寺町1;35.03909,135.72928;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300', 
'destination4': '銀閣寺:京都府京都市左京区銀閣寺町2;35.02686,135.79827;;st:300', 
'destination5': '渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町1-5;35.01374,135.67753;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600', 
'destination6': '映画村:京都府京都市右京区太秦東蜂岡町10;35.01553,135.70837;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600', 
'destination7': '京都御所:京都府京都市上京区京都御苑3;35.02658,135.75989;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600', 
'destination8': '伏見稲荷:京都府京都市伏見区深草藪之内町68;34.96781,135.77273;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:300', 
'destination9': '南禅寺:京都府京都市左京区南禅寺福地町;35.01015,135.79144;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300', 
'destination10': '二条城:京都府京都市中京区二条城町541;35.01482,135.74658;;st:600', 
'destination11': '八坂神社:京都府京都市東山区祇園町北側625;35.00369,135.77861;acc:sa14:00:00+09:00|sa16:00:00+09:00;st:300', 
'end': '京都駅(戻り):京都府京都市下京区東塩小路釜殿町;34.98515,135.75709', 
'departure': '2023-03-04T08:00:00+09:00', 
'improveFor': 'time', 
'mode': 'fastest;truck;traffic:enabled;dirtRoad:-2'}

つまり、こちらのJSONフォーマットのデータがHERE Waypoint Sequence APIに与えられる実際のインプットパラメータになります。それでは、HERE Waypoint Sequence APIをコールしてみましょう。ここではHERE Data SDK for Pythonの作法に沿ってAPIをコールしています。詳しくは以下のリンクをご参照ください。

jupyter
import json
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter
from here.platform import Platform

platform = Platform()
wps = platform.get_service("hrn:here:service::olp-here:waypoints-sequence-8")
result = wps.get("/findsequence2", params=params)
print(highlight(json.dumps(result, indent=4, ensure_ascii=False), JsonLexer(), TerminalFormatter()))

以下はその出力結果になります。

output
{
    "results": [
        {
            "waypoints": [
                {
                    "id": "京都駅:京都府京都市下京区東塩小路釜殿町",
                    "lat": 34.98515,
                    "lng": 135.75709,
                    "sequence": 0,
                    "estimatedArrival": null,
                    "estimatedDeparture": "2023-03-04T08:00:00+09:00",
                    "fulfilledConstraints": []
                },
                {
                    "id": "東寺:京都府京都市南区九条町1",
                    "lat": 34.98054,
                    "lng": 135.74664,
                    "sequence": 1,
                    "estimatedArrival": "2023-03-04T08:05:18+09:00",
                    "estimatedDeparture": "2023-03-04T08:10:18+09:00",
                    "fulfilledConstraints": [
                        "st:300"
                    ]
                },
                {
                    "id": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町1-5",
                    "lat": 35.01374,
                    "lng": 135.67753,
                    "sequence": 2,
                    "estimatedArrival": "2023-03-04T08:36:18+09:00",
                    "estimatedDeparture": "2023-03-04T08:46:18+09:00",
                    "fulfilledConstraints": [
                        "acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600"
                    ]
                },
                {
                    "id": "映画村:京都府京都市右京区太秦東蜂岡町10",
                    "lat": 35.01553,
                    "lng": 135.70837,
                    "sequence": 3,
                    "estimatedArrival": "2023-03-04T08:58:01+09:00",
                    "estimatedDeparture": "2023-03-04T09:08:01+09:00",
                    "fulfilledConstraints": [
                        "acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600"
                    ]
                },
                {
                    "id": "金閣寺:京都府京都市北区金閣寺町1",
                    "lat": 35.03909,
                    "lng": 135.72928,
                    "sequence": 4,
                    "estimatedArrival": "2023-03-04T09:24:19+09:00",
                    "estimatedDeparture": "2023-03-04T09:29:19+09:00",
                    "fulfilledConstraints": [
                        "acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300"
                    ]
                },
                {
                    "id": "二条城:京都府京都市中京区二条城町541",
                    "lat": 35.01482,
                    "lng": 135.74658,
                    "sequence": 5,
                    "estimatedArrival": "2023-03-04T09:41:50+09:00",
                    "estimatedDeparture": "2023-03-04T09:51:50+09:00",
                    "fulfilledConstraints": [
                        "st:600"
                    ]
                },
                {
                    "id": "銀閣寺:京都府京都市左京区銀閣寺町2",
                    "lat": 35.02686,
                    "lng": 135.79827,
                    "sequence": 6,
                    "estimatedArrival": "2023-03-04T10:10:14+09:00",
                    "estimatedDeparture": "2023-03-04T10:15:14+09:00",
                    "fulfilledConstraints": [
                        "st:300"
                    ]
                },
                {
                    "id": "南禅寺:京都府京都市左京区南禅寺福地町",
                    "lat": 35.01015,
                    "lng": 135.79144,
                    "sequence": 7,
                    "estimatedArrival": "2023-03-04T10:26:24+09:00",
                    "estimatedDeparture": "2023-03-04T10:31:24+09:00",
                    "fulfilledConstraints": [
                        "acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300"
                    ]
                },
                {
                    "id": "八坂神社:京都府京都市東山区祇園町北側625",
                    "lat": 35.00369,
                    "lng": 135.77861,
                    "sequence": 8,
                    "estimatedArrival": "2023-03-04T10:41:20+09:00",
                    "estimatedDeparture": "2023-03-04T14:05:00+09:00",
                    "fulfilledConstraints": [
                        "acc:sa14:00:00+09:00|sa16:00:00+09:00;st:300"
                    ]
                },
                {
                    "id": "京都御所:京都府京都市上京区京都御苑3",
                    "lat": 35.02658,
                    "lng": 135.75989,
                    "sequence": 9,
                    "estimatedArrival": "2023-03-04T14:19:13+09:00",
                    "estimatedDeparture": "2023-03-04T18:10:00+09:00",
                    "fulfilledConstraints": [
                        "acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600"
                    ]
                },
                {
                    "id": "清水寺:京都府京都市東山区清水1丁目294",
                    "lat": 34.99587,
                    "lng": 135.78305,
                    "sequence": 10,
                    "estimatedArrival": "2023-03-04T18:23:46+09:00",
                    "estimatedDeparture": "2023-03-04T18:33:46+09:00",
                    "fulfilledConstraints": [
                        "acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600"
                    ]
                },
                {
                    "id": "伏見稲荷:京都府京都市伏見区深草藪之内町68",
                    "lat": 34.96781,
                    "lng": 135.77273,
                    "sequence": 11,
                    "estimatedArrival": "2023-03-04T18:48:24+09:00",
                    "estimatedDeparture": "2023-03-04T18:53:24+09:00",
                    "fulfilledConstraints": [
                        "acc:sa18:00:00+09:00|sa20:00:00+09:00;st:300"
                    ]
                },
                {
                    "id": "京都駅(戻り):京都府京都市下京区東塩小路釜殿町",
                    "lat": 34.98515,
                    "lng": 135.75709,
                    "sequence": 12,
                    "estimatedArrival": "2023-03-04T19:06:32+09:00",
                    "estimatedDeparture": null,
                    "fulfilledConstraints": []
                }
            ],
            "distance": "52265",
            "time": "39992",
            "interconnections": [
                {
                    "fromWaypoint": "京都駅:京都府京都市下京区東塩小路釜殿町",
                    "toWaypoint": "東寺:京都府京都市南区九条町1",
                    "distance": 1546.0,
                    "time": 318.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "東寺:京都府京都市南区九条町1",
                    "toWaypoint": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町1-5",
                    "distance": 9806.0,
                    "time": 1560.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町1-5",
                    "toWaypoint": "映画村:京都府京都市右京区太秦東蜂岡町10",
                    "distance": 3100.0,
                    "time": 703.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "映画村:京都府京都市右京区太秦東蜂岡町10",
                    "toWaypoint": "金閣寺:京都府京都市北区金閣寺町1",
                    "distance": 4509.0,
                    "time": 978.0,
                    "rest": 0.0,
                    "waiting": 0.0,
                    "warnings": [
                        {
                            "message": "Route violates requested 'avoid' options or legal/physical restrictions",
                            "code": 3
                        }
                    ]
                },
                {
                    "fromWaypoint": "金閣寺:京都府京都市北区金閣寺町1",
                    "toWaypoint": "二条城:京都府京都市中京区二条城町541",
                    "distance": 4396.0,
                    "time": 751.0,
                    "rest": 0.0,
                    "waiting": 0.0,
                    "warnings": [
                        {
                            "message": "Route violates requested 'avoid' options or legal/physical restrictions",
                            "code": 3
                        }
                    ]
                },
                {
                    "fromWaypoint": "二条城:京都府京都市中京区二条城町541",
                    "toWaypoint": "銀閣寺:京都府京都市左京区銀閣寺町2",
                    "distance": 6026.0,
                    "time": 1104.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "銀閣寺:京都府京都市左京区銀閣寺町2",
                    "toWaypoint": "南禅寺:京都府京都市左京区南禅寺福地町",
                    "distance": 2431.0,
                    "time": 670.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "南禅寺:京都府京都市左京区南禅寺福地町",
                    "toWaypoint": "八坂神社:京都府京都市東山区祇園町北側625",
                    "distance": 2568.0,
                    "time": 596.0,
                    "rest": 0.0,
                    "waiting": 11920.0
                },
                {
                    "fromWaypoint": "八坂神社:京都府京都市東山区祇園町北側625",
                    "toWaypoint": "京都御所:京都府京都市上京区京都御苑3",
                    "distance": 4585.0,
                    "time": 853.0,
                    "rest": 0.0,
                    "waiting": 13247.0
                },
                {
                    "fromWaypoint": "京都御所:京都府京都市上京区京都御苑3",
                    "toWaypoint": "清水寺:京都府京都市東山区清水1丁目294",
                    "distance": 5571.0,
                    "time": 826.0,
                    "rest": 0.0,
                    "waiting": 0.0,
                    "warnings": [
                        {
                            "message": "Route violates requested 'avoid' options or legal/physical restrictions",
                            "code": 3
                        }
                    ]
                },
                {
                    "fromWaypoint": "清水寺:京都府京都市東山区清水1丁目294",
                    "toWaypoint": "伏見稲荷:京都府京都市伏見区深草藪之内町68",
                    "distance": 4366.0,
                    "time": 878.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "伏見稲荷:京都府京都市伏見区深草藪之内町68",
                    "toWaypoint": "京都駅(戻り):京都府京都市下京区東塩小路釜殿町",
                    "distance": 3361.0,
                    "time": 788.0,
                    "rest": 0.0,
                    "waiting": 0.0
                }
            ],
            "description": "Targeted best time; with , improvement for traffic",
            "timeBreakdown": {
                "driving": 10025,
                "service": 4800,
                "rest": 0,
                "waiting": 25167
            }
        }
    ],
    "errors": [],
    "processingTimeDesc": "1183ms",
    "responseCode": "200",
    "warnings": null,
    "requestId": null
}

配送順序を表として表示

このHERE Waypoint Sequence APIのレスポンスを人間にとって理解し易いフォーマットとするために、pandasのdataframeとします。

jupyter
wayPointList = []
for m in result["results"][0]["waypoints"]:
    wayPointList.append(m)
pd.set_option('display.max_colwidth', 150)
waypointDf=pd.DataFrame(wayPointList)
waypointDf

image.png

以上から、配送順序と大まかな配送計画を俯瞰することができます。8:00に京都駅(デポ)を出発し、19:06に京都駅(デポ)に戻ります。また、最短時間という条件で順序を導きましたが、時間指定配達の制約条件により、八坂神社と京都御所の地点でそれぞれ4時間の空きが出来てしてしまうことが分かります。

HERE Routing APIによる配送ルートの決定

HERE Routing APIは、詳細は以下のリンクで記載しておりますが、基本的には出発地と目的地の最適な経路を探索するAPIになります。

先ほど作成した配送順序のdataframeを使用して再起的にRouting APIをコールさせます。

jupyter
route = platform.get_service("hrn:here:service::olp-here:routing-8")

if routingMode == "fastest":
    routingMode = "fast"
elif routingMode == "shortest":
    routingMode = "short"

polyLineList = []
departureList = []
arrivalList = []
summaryList = []
waypointLocationList = []
transitDeparture = ""
count = 0
for n in wayPointList:
    if count == 0:
        estimatedDeparture = str(wayPointList[count-1]['estimatedDeparture'])
        waypointLocation = {"order": count, "name": wayPointList[count]['id'], "lat": wayPointList[count]['lat'], "lng": wayPointList[count]['lng'], "estimatedDeparture": estimatedDeparture}
        waypointLocationList.append(waypointLocation)
        count = count + 1
        continue
    lat1 = str(wayPointList[count-1]['lat'])
    lng1 = str(wayPointList[count-1]['lng'])
    lat2 = str(wayPointList[count]['lat'])
    lng2 = str(wayPointList[count]['lng'])
    name = str(wayPointList[count]['id'])
    estimatedDeparture = str(wayPointList[count-1]['estimatedDeparture'])
    transitDeparture = str(wayPointList[count-1]['estimatedDeparture'])
    waypointLocation = {"order": count, "name": name, "lat": wayPointList[count]['lat'], "lng": wayPointList[count]['lng'], "estimatedDeparture": estimatedDeparture}
    waypointLocationList.append(waypointLocation)
    params = {
        "origin": f'{lat1},{lng1}',
        "destination": f'{lat2},{lng2}',
        "routingMode": f'{routingMode}',
        "transportMode": f'{transportMode}',
        "departureTime": f'{transitDeparture}',
        "return": "polyline,actions,instructions,summary,passthrough,routeHandle"
    }
    result = route.get("/routes", params=params)
    polyLineList.append(result["routes"][0]['sections'][0]["polyline"])
    departureList.append(result["routes"][0]['sections'][0]["departure"])
    arrivalList.append(result["routes"][0]['sections'][0]["arrival"])
    summaryList.append(result["routes"][0]['sections'][0]["summary"])
    count = count + 1

配送ルートを地図上に表示

それでは、配送ルートを地図上に表示しますが、まずは、HERE Routing APIを地図上に表示するために取得したPolylineを緯度経度情報にデコードする必要があります。そのためにflexpolylineモジュールのインストールが必要になります。

pip install
pip install flexpolyline

その上で、以下のコードを実行し緯度経度情報のリストに変換します。

jupyter
import flexpolyline as fp
gpsLists = []
for o in polyLineList:
    pl = fp.decode(o)
    gpsList = []
    for p in pl:
        gpsList.append(p[0])
        gpsList.append(p[1])
        gpsList.append(0)
    gpsLists.append(gpsList)

さあ、最後の仕上げです。HERE Map widget for Jupyterを使用して地図上に表示します。

こちらの使用方法については以下のリンクをご参考ください。

<APIKEY>と記載されている部分は取得されましたAPIKEYに変換してください。

jupyter
from here_map_widget import MultiLineString, LineString, Polyline, Point, Circle, WKT, Marker, Icon, FullscreenControl, InfoBubble
from here_map_widget import Map, OMV, Platform, Style, TileLayer, MapSettingsControl
from here_map_widget import ServiceNames, OMVUrl
from here_map_widget import LineString, Polyline

df = pd.DataFrame(waypointLocationList)
avg_lat = df['lat'].mean()
avg_lng = df['lng'].mean()

zoom = 13
apiKey = <APIKEY>

services_config = {
    ServiceNames.omv: {
        OMVUrl.scheme: "https",
        OMVUrl.host: "vector.hereapi.com",
        OMVUrl.path: "/v2/vectortiles/core/mc",
    }
}
style = Style(
    config="https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/normal.day.yaml",
    base_url="https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/",
)
platform = Platform(api_key=apiKey,services_config=services_config)
omv_provider = OMV(path="v2/vectortiles/core/mc", platform=platform, style=style)

omv_layer = TileLayer(provider=omv_provider, style={"max": 22})
center = [avg_lat,avg_lng]
m = Map(api_key=apiKey,center=center, zoom=zoom, basemap=omv_layer)
for count in range(len(waypointLocationList)):
    name = waypointLocationList[count]['name']
    if count == 0:
        info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
                                         content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Departure: '+str(departureList[count]['time'])+'</div>')
    elif 0 < count < len(waypointLocationList)-1:
        info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
                                         content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Departure: '+str(departureList[count]['time'])+'</div><div>Arrival: '+str(arrivalList[count-1]['time'])+'</div>')
    else:
        info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
                                         content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Arrival: '+str(arrivalList[count-1]['time'])+'</div>')

    marker = Marker(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng'], info=info, evt_type="tap", show_bubble=True)
    m.add_object(marker)
    count = count + 1

for q in gpsLists:
    style = {"lineWidth": 5}
    l = q
    ls = LineString(points=l)
    pl = Polyline(object=ls, style=style)
    m.add_object(pl)

    setttings = MapSettingsControl(
    layers=[
        {"label": "Original", "layer": omv_layer},
    ],
    basemaps=["raster.satellite.map","vector.normal.map"],
)
m.add_control(setttings)
fs = FullscreenControl()
m.add_control(fs)
m

結果をフルスクリーンモードで確認しましょう。緑のマーカーをクリックすると各配送地点の情報を確認することができます。
image.png

未配達の処理

さて、最終配送地点の伏見稲荷に到着した時点で、3地点で不在のため未配達となりました。
- 清水寺
- 銀閣寺
- 南禅寺
まずは元の配送リストに不在という列を追加し、更新します。

プロトタイピングであるため、このような処理を行なっていますが、本番環境ではトラックドライバー様専用のスマートフォンアプリなどにより逐一更新するといったアプローチが一般的だと思います。

jupyter
orders_df["不在"]=""
orders_df.loc[orders_df[orders_df["ID"] == "清水寺"].index, '不在'] = '不在'
orders_df.loc[orders_df[orders_df["ID"] == "銀閣寺"].index, '不在'] = '不在'
orders_df.loc[orders_df[orders_df["ID"] == "南禅寺"].index, '不在'] = '不在'
orders_df

上記の情報をベースにして、新たに再配送リストを作成します。

jupyter
not_deliver_df = orders_df[orders_df.不在 == "不在"]
restart_df = orders_df[orders_df.ID == "伏見稲荷"]
end_df = orders_df[orders_df.ID == "京都駅(戻り)"]
not_deliver_df = pd.concat([restart_df, not_deliver_df, end_df])
not_deliver_df

image.png

再配達時間をシミュレーションするため、最初の配送計画の最終配送地点の出発時刻を取得します。(今回の場合は伏見稲荷です。)

プロトタイピングであるため、このような処理を行なっていますが、本番環境では実際に再配達を開始する時間を指定します。

jupyter
waypointDf_restart = waypointDf.loc[[waypointDf.shape[0] - 2],['estimatedDeparture']]
list_restart = waypointDf_restart.to_numpy().tolist()
departureTime = list_restart[0][0]
departureTime

それでは改めて、以下のコードによりWaypoint Seaquence APIのインプットパラメータを生成します。

jupyter
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
jupyter
import numpy as np
import time

list = orders_df.to_numpy().tolist()
dayOfWeek = ""
temp = pd.Timestamp(departureTime)
if temp.dayofweek == 0:
    dayOfWeek = "mo"
elif temp.dayofweek == 1:
    dayOfWeek = "tu"
elif temp.dayofweek == 2:
    dayOfWeek = "we"
elif temp.dayofweek == 3:
    dayOfWeek = "th"
elif temp.dayofweek == 4:
    dayOfWeek = "fr"
elif temp.dayofweek == 5:
    dayOfWeek = "sa"
elif temp.dayofweek == 6:
    dayOfWeek = "su"
convertLists = []
params = {}
number = len(list)
count = 1
for m in list:
    key = ""
    acc = ""
    convertList = ""
    st = m[3]*60
    if count == 1:
        key = "start"
    elif count == number:
        key = "end"
    else:
        key += "destination"
        key += str(count-1)
    convertList += m[0]+":"+m[1]
    convertList += ";"
    convertList += str(m[5])
    convertList += ","
    convertList += str(m[6])
    if count != 1:
        if count != number:
            if m[2]:
                if m[2] == "8-12":
                    acc = "acc:"+dayOfWeek+"08:00:00+09:00|"+dayOfWeek+"12:00:00+09:00"
                elif m[2] == "12-14":
                    acc = "acc:"+dayOfWeek+"12:00:00+09:00|"+dayOfWeek+"14:00:00+09:00"
                elif m[2] == "14-16":
                    acc = "acc:"+dayOfWeek+"14:00:00+09:00|"+dayOfWeek+"16:00:00+09:00"
                elif m[2] == "18-20":
                    acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"20:00:00+09:00"
                elif m[2] == "20-22":
                    acc = "acc:"+dayOfWeek+"20:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
                elif m[2] == "18-22":
                    acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
                convertList += ";"
                convertList += acc
            convertList += ";st:"
            if np.isnan(st):
                convertList += ""
            else:
                convertList += str(int(st))
    params[key] = convertList
    if count == number:
        params["departure"] = departureTime
        params["improveFor"] = "distance"
        params["mode"] = routingMode+";"+transportMode+";traffic:"+trafficMode+";dirtRoad:-2"
    count = count + 1
print(params)

出力結果は以下のとおりです。

output
{'start': '伏見稲荷:京都府京都市伏見区深草藪之内町68;34.96781,135.77273', 
'destination1': '清水寺:京都府京都市東山区清水1丁目294;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600', 
'destination2': '銀閣寺:京都府京都市左京区銀閣寺町2;35.02686,135.79827;;st:300', 
'destination3': '南禅寺:京都府京都市左京区南禅寺福地町;35.01015,135.79144;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300', 
'end': '京都駅(戻り):京都府京都市下京区東塩小路釜殿町;34.98515,135.75709', 
'departure': '2023-03-04T18:53:24+09:00', 
'improveFor': 'time', 
'mode': 'fastest;truck;traffic:enabled;dirtRoad:-2'}

このインプットパラメータを使用して、HERE Waypoint Sequence APIをコールしてみましょう。

jupyter
import json
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter
from here.platform import Platform

platform = Platform()
wps = platform.get_service("hrn:here:service::olp-here:waypoints-sequence-8")
result = wps.get("/findsequence2", params=params)
print(highlight(json.dumps(result, indent=4, ensure_ascii=False), JsonLexer(), TerminalFormatter()))

先ほどと同じくその結果を再配送の順序としてdataframeにします。

jupyter
wayPointList = []
for m in result["results"][0]["waypoints"]:
    wayPointList.append(m)
pd.set_option('display.max_colwidth', 150)
waypointDf=pd.DataFrame(wayPointList)
waypointDf

image.png
この結果を見て、南禅寺には配達指定時間の制約があるため、当日中の配達ができないことがわかります。

再配達順序の決定(最終版)

ベースの配送リストの不在列を”翌日”と更新します。

jupyter
orders_df.loc[orders_df[orders_df["ID"] == "南禅寺"].index, '不在'] = '翌日'
orders_df

image.png

先ほどと同じ要領で、再配送リスト(最終版)を作成します。

jupyter
not_deliver_df = orders_df[orders_df.不在 == "不在"]
restart_df = orders_df[orders_df.ID == "伏見稲荷"]
end_df = orders_df[orders_df.ID == "京都駅(戻り)"]
not_deliver_df = pd.concat([restart_df, not_deliver_df, end_df])
not_deliver_df

image.png

先ほどと同様ですが、再配達時間をシミュレーションするため、最初の配送計画の最終配送地点の出発時刻を取得します。(今回の場合は伏見稲荷です。)

プロトタイピングであるため、このような処理を行なっていますが、本番環境では実際に再配達を開始する時間を指定します。

jupyter
waypointDf_restart = waypointDf.loc[[waypointDf.shape[0] - 2],['estimatedDeparture']]
list_restart = waypointDf_restart.to_numpy().tolist()
departureTime = list_restart[0][0]
departureTime

それでは改めて(これが最後です)、以下のコードによりWaypoint Seaquence APIのインプットパラメータを生成します。

jupyter
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
jupyter
import numpy as np
import time

list = orders_df.to_numpy().tolist()
dayOfWeek = ""
temp = pd.Timestamp(departureTime)
if temp.dayofweek == 0:
    dayOfWeek = "mo"
elif temp.dayofweek == 1:
    dayOfWeek = "tu"
elif temp.dayofweek == 2:
    dayOfWeek = "we"
elif temp.dayofweek == 3:
    dayOfWeek = "th"
elif temp.dayofweek == 4:
    dayOfWeek = "fr"
elif temp.dayofweek == 5:
    dayOfWeek = "sa"
elif temp.dayofweek == 6:
    dayOfWeek = "su"
convertLists = []
params = {}
number = len(list)
count = 1
for m in list:
    key = ""
    acc = ""
    convertList = ""
    st = m[3]*60
    if count == 1:
        key = "start"
    elif count == number:
        key = "end"
    else:
        key += "destination"
        key += str(count-1)
    convertList += m[0]+":"+m[1]
    convertList += ";"
    convertList += str(m[5])
    convertList += ","
    convertList += str(m[6])
    if count != 1:
        if count != number:
            if m[2]:
                if m[2] == "8-12":
                    acc = "acc:"+dayOfWeek+"08:00:00+09:00|"+dayOfWeek+"12:00:00+09:00"
                elif m[2] == "12-14":
                    acc = "acc:"+dayOfWeek+"12:00:00+09:00|"+dayOfWeek+"14:00:00+09:00"
                elif m[2] == "14-16":
                    acc = "acc:"+dayOfWeek+"14:00:00+09:00|"+dayOfWeek+"16:00:00+09:00"
                elif m[2] == "18-20":
                    acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"20:00:00+09:00"
                elif m[2] == "20-22":
                    acc = "acc:"+dayOfWeek+"20:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
                elif m[2] == "18-22":
                    acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
                convertList += ";"
                convertList += acc
            convertList += ";st:"
            if np.isnan(st):
                convertList += ""
            else:
                convertList += str(int(st))
    params[key] = convertList
    if count == number:
        params["departure"] = departureTime
        params["improveFor"] = "distance"
        params["mode"] = routingMode+";"+transportMode+";traffic:"+trafficMode+";dirtRoad:-2"
    count = count + 1
print(params)

出力結果は以下のとおりです。

output
{'start': '伏見稲荷:京都府京都市伏見区深草藪之内町68;34.96781,135.77273', 
'destination1': '清水寺:京都府京都市東山区清水1丁目294;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600', 
'destination2': '銀閣寺:京都府京都市左京区銀閣寺町2;35.02686,135.79827;;st:300', 
'end': '京都駅(戻り):京都府京都市下京区東塩小路釜殿町;34.98515,135.75709', 
'departure': '2023-03-04T18:53:24+09:00', 
'improveFor': 'time', 
'mode': 'fastest;truck;traffic:enabled;dirtRoad:-2'}

このインプットパラメータを使用して、HERE Waypoint Sequence APIをコールしてみましょう。

jupyter
import json
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter
from here.platform import Platform

platform = Platform()
wps = platform.get_service("hrn:here:service::olp-here:waypoints-sequence-8")
result = wps.get("/findsequence2", params=params)
print(highlight(json.dumps(result, indent=4, ensure_ascii=False), JsonLexer(), TerminalFormatter()))

先ほどと同じくその結果を再配達の順序としてdataframeにします。

jupyter
wayPointList = []
for m in result["results"][0]["waypoints"]:
    wayPointList.append(m)
pd.set_option('display.max_colwidth', 150)
waypointDf=pd.DataFrame(wayPointList)
waypointDf

image.png

京都駅(デポ)に20:08に到着するという再配達順序が作成されました。

再配達ルートの決定

先ほどと同じコードを繰り返します。
作成した再配達順序のリストをベースに再帰的にHERE Routing APIをコールし、それぞれのルートを算出します。

jupyter
route = platform.get_service("hrn:here:service::olp-here:routing-8")

if routingMode == "fastest":
    routingMode = "fast"
elif routingMode == "shortest":
    routingMode = "short"

polyLineList = []
departureList = []
arrivalList = []
summaryList = []
waypointLocationList = []
transitDeparture = ""
count = 0
for n in wayPointList:
    if count == 0:
        estimatedDeparture = str(wayPointList[count-1]['estimatedDeparture'])
        waypointLocation = {"order": count, "name": wayPointList[count]['id'], "lat": wayPointList[count]['lat'], "lng": wayPointList[count]['lng'], "estimatedDeparture": estimatedDeparture}
        waypointLocationList.append(waypointLocation)
        count = count + 1
        continue
    lat1 = str(wayPointList[count-1]['lat'])
    lng1 = str(wayPointList[count-1]['lng'])
    lat2 = str(wayPointList[count]['lat'])
    lng2 = str(wayPointList[count]['lng'])
    name = str(wayPointList[count]['id'])
    estimatedDeparture = str(wayPointList[count-1]['estimatedDeparture'])
    transitDeparture = str(wayPointList[count-1]['estimatedDeparture'])
    waypointLocation = {"order": count, "name": name, "lat": wayPointList[count]['lat'], "lng": wayPointList[count]['lng'], "estimatedDeparture": estimatedDeparture}
    waypointLocationList.append(waypointLocation)
    params = {
        "origin": f'{lat1},{lng1}',
        "destination": f'{lat2},{lng2}',
        "routingMode": f'{routingMode}',
        "transportMode": f'{transportMode}',
        "departureTime": f'{transitDeparture}',
        "return": "polyline,actions,instructions,summary,passthrough,routeHandle"
    }
    result = route.get("/routes", params=params)
    polyLineList.append(result["routes"][0]['sections'][0]["polyline"])
    departureList.append(result["routes"][0]['sections'][0]["departure"])
    arrivalList.append(result["routes"][0]['sections'][0]["arrival"])
    summaryList.append(result["routes"][0]['sections'][0]["summary"])
    count = count + 1

再配達ルートを地図上に表示

先ほどと同様にHERE Map Widget for Jupyterを使用して地図上に描画します。<APIKEY>と記載されている部分は取得されましたAPIKEYに変換してください。

jupyter
from here_map_widget import MultiLineString, LineString, Polyline, Point, Circle, WKT, Marker, Icon, FullscreenControl, InfoBubble
from here_map_widget import Map, OMV, Platform, Style, TileLayer, MapSettingsControl
from here_map_widget import ServiceNames, OMVUrl
from here_map_widget import LineString, Polyline

df = pd.DataFrame(waypointLocationList)
avg_lat = df['lat'].mean()
avg_lng = df['lng'].mean()

zoom = 13
apiKey = <APIKEY>

services_config = {
    ServiceNames.omv: {
        OMVUrl.scheme: "https",
        OMVUrl.host: "vector.hereapi.com",
        OMVUrl.path: "/v2/vectortiles/core/mc",
    }
}
style = Style(
    config="https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/normal.day.yaml",
    base_url="https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/",
)
platform = Platform(api_key=apiKey,services_config=services_config)
omv_provider = OMV(path="v2/vectortiles/core/mc", platform=platform, style=style)

omv_layer = TileLayer(provider=omv_provider, style={"max": 22})
center = [avg_lat,avg_lng]
m = Map(api_key=apiKey,center=center, zoom=zoom, basemap=omv_layer)
for count in range(len(waypointLocationList)):
    name = waypointLocationList[count]['name']
    if count == 0:
        info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
                                         content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Departure: '+str(departureList[count]['time'])+'</div>')
    elif 0 < count < len(waypointLocationList)-1:
        info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
                                         content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Departure: '+str(departureList[count]['time'])+'</div><div>Arrival: '+str(arrivalList[count-1]['time'])+'</div>')
    else:
        info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
                                         content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div>')

    marker = Marker(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng'], info=info, evt_type="tap", show_bubble=True)
    m.add_object(marker)
    count = count + 1

for q in gpsLists:
    style = {"lineWidth": 5}
    l = q
    ls = LineString(points=l)
    pl = Polyline(object=ls, style=style)
    m.add_object(pl)

    setttings = MapSettingsControl(
    layers=[
        {"label": "Original", "layer": omv_layer},
    ],
    basemaps=["raster.satellite.map","vector.normal.map"],
)
m.add_control(setttings)
fs = FullscreenControl()
m.add_control(fs)
m

結果をフルスクリーンモードで確認します。
image.png
上記のように、伏見稲荷を18:53に出発し、最終的に20:08に配達作業が終了しました。お疲れ様でした!

おまけ (トラックと普通車指定の比較)

今回使用しましたHERE Waypoint Sequence APIとRouting APIはトラック規制などの道路情報も考慮に入れて配送順序及びルートを決定します。以下は先ほどの再配達ルートをそれぞれトラックまたは普通車指定とした場合のルートになります。(トラック指定の場合、より幹線道路を選択している傾向を読み取れます。)
image.png
また、単純なトラック指定以外に車幅、車高、全長、重量などの規制情報にも対応していることも付け加えておきます。

おわりに

いかがでしたでしょうか?
このように、Jupyterを使用してHERE API群を駆使することで、物流DXのプロトタイピングが容易にできます。慣れてくれば1時間程度で今回紹介したシナリオのプロトタイピングは可能です。今回紹介したアプローチはJupyterエコシステムを使ってのRPA実現(プロトタイピング)のみとなります。一方で日々の配送データをビッグデータ化していき、Jupyterエコシステムを使用して、機械学習というアプローチを加えることで、より精度の高い物流DXを実現することも可能なのではないかと思います。
最後に、日々配送業務に携わるトラックドライバー様に頭が下がる思いですが、これらのHEREのソリューションを使用して是非とも物流DXに貢献できれば幸いと思います。

ここまで読んでいただきましてありがとうございました!

45
57
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
57