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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/cb76975f-ca15-f6f2-bb29-44663acd5e71.png)

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

https://www.here.com/jp/platform/location-based-services/fleet-telematics

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

https://developer.here.com/documentation/sdk-python-v2/dev_guide/index.html

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

# シミュレーションするシナリオ
物流の最終拠点である営業所からエンドユーザに荷物を配送するラストワンマイルのシナリオとします。
## 場所
エリアは**京都**とし、配送する場所についても、イメージし易い有名観光スポットとしました。さらに、配送デポについては京都駅としました。こうすることで、配送順序の妥当性についてイメージがし易いかと思います。（京都に修学旅行に行かれた方は、そのルートを思い出してみてください。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/25afe475-af42-2a30-b619-97b3866ac4e0.png)

## 配送リスト
以下のような配送リストをCSVフォーマットで作成しました。（これから配達する荷物の宛先リストになります。）Time Restrictionというフィールドはいわゆる配達の時間指定になります。Service Timeというのは配送先毎にかかるサービス時間（分）となります。この配送リストの時点では、<font color="blue">***配送順序、配送ルートそして作業終了時間***</font>は確定していません。これらをDXします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/9f022d94-825b-c155-64d6-84ce961d433b.png)

## シナリオ

1. 配送リスト(配達する荷物の宛先リスト)をもとに配送順序を決定します。<font color="red">**(DX部分)**</font>
1. 決定した配送順序をもとに配送ルートを決定します。 <font color="red">**(DX部分)**</font>
1. 決定した配送順序及び配送ルートをもとに配送作業を行います。
1. ３地点(清水寺、銀閣寺、南禅寺）で不在のため、未配達というシナリオとします。
1. 以上より、再配達リストを作成します。<font color="red">**(DX部分)**</font>
1. 再配達リストより配送順序を決定します。<font color="red">**(DX部分)**</font>
1. 決定した配送順序をもとに現在地からの配送ルートを決定します。<font color="red">**(DX部分)**</font>
1. 決定した配送順序及び配送ルートをもとに配送作業を行います。

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

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

https://qiita.com/innovation1005/items/2f433d6d859f075033a7

https://qiita.com/reraise/items/41353e0f098ad063a3bd

## HEREのアカウント取得 
以下の記事が大変参考になります。


https://qiita.com/kekomat/items/d2e29bac84a27c1ec521

https://qiita.com/Akira-Isegawa/items/a31be5893e362c6f10c3

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

```bash: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
```
```bash: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に応じたファイルパスに保管します。

```bash:Mac OS/Linux
$HOME/.here/credentials.properties
```
```bash: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
上記のリンクにも説明がありますが、以下のリンクが参考になります。

https://here-map-widget-for-jupyter.readthedocs.io/en/latest/

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

https://here-map-widget-for-jupyter.readthedocs.io/en/latest/installation.html

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

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

## 配送リストCSVファイル
冒頭で説明しました配送リストです。

```text:京都.csv
ID,Address,Time Restriction,Service Time,Job Type
京都駅,京都府京都市下京区東塩小路釜殿町,,,
東寺,京都府京都市南区九条町１,,5,配送
清水寺,京都府京都市東山区清水１丁目２９４,18-20,10,配送
金閣寺,京都府京都市北区金閣寺町１,8-12,5,配送
銀閣寺,京都府京都市左京区銀閣寺町２,,5,配送
渡月橋,京都府京都市右京区嵯峨天龍寺芒ノ馬場町１−５,8-12,10,配送
映画村,京都府京都市右京区太秦東蜂岡町１０,8-12,10,配送
京都御所,京都府京都市上京区京都御苑３,18-20,10,配送
伏見稲荷,京都府京都市伏見区深草藪之内町６８,18-20,5,配送
南禅寺,京都府京都市左京区南禅寺福地町,8-12,5,配送
二条城,京都府京都市中京区二条城町５４１,,10,配送
八坂神社,京都府京都市東山区祇園町北側６２５,14-16,5,配送
京都駅(戻り）,京都府京都市下京区東塩小路釜殿町,,,
```
## 配送リストCSVファイルの読み込み
上記のCSVファイルを読み込みpandasのdataframeとして表示します。

```python:jupyter
import pandas as pd
file = "京都.csv"
df = pd.read_csv(file,sep=",",encoding="cp932")
df
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/b3630148-1349-925c-0e36-6e75a950a150.png)


## HERE Geocoding & Search APIを使用した住所から緯度経度情報への変換
配送リストの住所情報を緯度経度情報に変換します。その前に、下記のコードスニペットを実行するために以下のモジュールが必要になります。`tqdm`は経過情報を表示するためのツールになります。
```bash:tqdm
pip install tqdm
```
一方、以下のモジュールはHEREのロケーションサービスを簡易的に使用するために作られたモジュールです。全く同じ様な機能がHERE Data SDK for Pythonにも準備されておりますが、HERE Geocoding & Search APIを実装する上ではこちらの方が簡単であったため、あえてこちらを使用しております。

```bash:here-location-services
pip install here-location-services
```
また、HERE Geocoding & Search APIの詳細については以下のリンクをご参照ください。

https://developer.here.com/documentation/geocoding-search-api/dev_guide/index.html

以下のコードスニペットで住所を緯度経度に変換しています。`<APIKEY>`と記載されている部分は取得されましたAPIKEYに変換してください。
```python: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を作成します。
```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/4be7673f-b08c-cba5-441b-dfb14db417eb.png)
## HERE Waypoint Sequence APIによる配送順序の決定
HEREのラストワンマイル向けAPIとして以下の２つのAPIが存在しています。

https://developer.here.com/documentation/tour-planning/3.16.0/dev_guide/index.html

https://developer.here.com/documentation/routing-waypoints/dev_guide/index.html

今回は、トラック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のインプットパラメータを生成します。
```python:jupyter
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
departureTime = "2023-03-04T08:00:00+09:00"
```
```python: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)
```
出力結果は以下のとおりです。
```json:output
{'start': '京都駅:京都府京都市下京区東塩小路釜殿町;34.98515,135.75709', 
'destination1': '東寺:京都府京都市南区九条町１;34.98054,135.74664;;st:300', 
'destination2': '清水寺:京都府京都市東山区清水１丁目２９４;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600', 
'destination3': '金閣寺:京都府京都市北区金閣寺町１;35.03909,135.72928;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300', 
'destination4': '銀閣寺:京都府京都市左京区銀閣寺町２;35.02686,135.79827;;st:300', 
'destination5': '渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町１－５;35.01374,135.67753;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600', 
'destination6': '映画村:京都府京都市右京区太秦東蜂岡町１０;35.01553,135.70837;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600', 
'destination7': '京都御所:京都府京都市上京区京都御苑３;35.02658,135.75989;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600', 
'destination8': '伏見稲荷:京都府京都市伏見区深草藪之内町６８;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': '二条城:京都府京都市中京区二条城町５４１;35.01482,135.74658;;st:600', 
'destination11': '八坂神社:京都府京都市東山区祇園町北側６２５;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をコールしています。詳しくは以下のリンクをご参照ください。

https://developer.here.com/documentation/sdk-python-v2/dev_guide/topics/usage/here-platform-services.html


```python: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()))

```
以下はその出力結果になります。
```json:output
{
    "results": [
        {
            "waypoints": [
                {
                    "id": "京都駅:京都府京都市下京区東塩小路釜殿町",
                    "lat": 34.98515,
                    "lng": 135.75709,
                    "sequence": 0,
                    "estimatedArrival": null,
                    "estimatedDeparture": "2023-03-04T08:00:00+09:00",
                    "fulfilledConstraints": []
                },
                {
                    "id": "東寺:京都府京都市南区九条町１",
                    "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": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町１－５",
                    "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": "映画村:京都府京都市右京区太秦東蜂岡町１０",
                    "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": "金閣寺:京都府京都市北区金閣寺町１",
                    "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": "二条城:京都府京都市中京区二条城町５４１",
                    "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": "銀閣寺:京都府京都市左京区銀閣寺町２",
                    "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": "八坂神社:京都府京都市東山区祇園町北側６２５",
                    "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": "京都御所:京都府京都市上京区京都御苑３",
                    "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": "清水寺:京都府京都市東山区清水１丁目２９４",
                    "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": "伏見稲荷:京都府京都市伏見区深草藪之内町６８",
                    "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": "東寺:京都府京都市南区九条町１",
                    "distance": 1546.0,
                    "time": 318.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "東寺:京都府京都市南区九条町１",
                    "toWaypoint": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町１－５",
                    "distance": 9806.0,
                    "time": 1560.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町１－５",
                    "toWaypoint": "映画村:京都府京都市右京区太秦東蜂岡町１０",
                    "distance": 3100.0,
                    "time": 703.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "映画村:京都府京都市右京区太秦東蜂岡町１０",
                    "toWaypoint": "金閣寺:京都府京都市北区金閣寺町１",
                    "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": "金閣寺:京都府京都市北区金閣寺町１",
                    "toWaypoint": "二条城:京都府京都市中京区二条城町５４１",
                    "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": "二条城:京都府京都市中京区二条城町５４１",
                    "toWaypoint": "銀閣寺:京都府京都市左京区銀閣寺町２",
                    "distance": 6026.0,
                    "time": 1104.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "銀閣寺:京都府京都市左京区銀閣寺町２",
                    "toWaypoint": "南禅寺:京都府京都市左京区南禅寺福地町",
                    "distance": 2431.0,
                    "time": 670.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "南禅寺:京都府京都市左京区南禅寺福地町",
                    "toWaypoint": "八坂神社:京都府京都市東山区祇園町北側６２５",
                    "distance": 2568.0,
                    "time": 596.0,
                    "rest": 0.0,
                    "waiting": 11920.0
                },
                {
                    "fromWaypoint": "八坂神社:京都府京都市東山区祇園町北側６２５",
                    "toWaypoint": "京都御所:京都府京都市上京区京都御苑３",
                    "distance": 4585.0,
                    "time": 853.0,
                    "rest": 0.0,
                    "waiting": 13247.0
                },
                {
                    "fromWaypoint": "京都御所:京都府京都市上京区京都御苑３",
                    "toWaypoint": "清水寺:京都府京都市東山区清水１丁目２９４",
                    "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": "清水寺:京都府京都市東山区清水１丁目２９４",
                    "toWaypoint": "伏見稲荷:京都府京都市伏見区深草藪之内町６８",
                    "distance": 4366.0,
                    "time": 878.0,
                    "rest": 0.0,
                    "waiting": 0.0
                },
                {
                    "fromWaypoint": "伏見稲荷:京都府京都市伏見区深草藪之内町６８",
                    "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とします。
```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/b1681b25-e1c8-e773-56dd-ec4e27e168d1.png)


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

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

https://developer.here.com/documentation/routing-api/dev_guide/index.html

https://qiita.com/kekishida/items/5325abcf5fa897cac2b7

先ほど作成した配送順序のdataframeを使用して再起的にRouting APIをコールさせます。
```python: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モジュールのインストールが必要になります。
```bash:pip install
pip install flexpolyline
```
その上で、以下のコードを実行し緯度経度情報のリストに変換します。
```python: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を使用して地図上に表示します。

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

https://here-map-widget-for-jupyter.readthedocs.io/en/latest/api_reference/basemap/japan.html



`<APIKEY>`と記載されている部分は取得されましたAPIKEYに変換してください。
```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/92c9abd0-1150-4edb-b988-237a07c05d27.png)

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


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


```python: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
```
上記の情報をベースにして、新たに再配送リストを作成します。
```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/e589d87e-05e0-363e-3b72-221050a62270.png)

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


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


```python: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のインプットパラメータを生成します。
```python:jupyter
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
```
```python: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)
```

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

```JSON:output
{'start': '伏見稲荷:京都府京都市伏見区深草藪之内町６８;34.96781,135.77273', 
'destination1': '清水寺:京都府京都市東山区清水１丁目２９４;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600', 
'destination2': '銀閣寺:京都府京都市左京区銀閣寺町２;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をコールしてみましょう。

```python: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にします。
```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/3f005d24-f8dd-d8b4-447b-3f6fd3dc2157.png)
この結果を見て、南禅寺には配達指定時間の制約があるため、当日中の配達ができないことがわかります。
## 再配達順序の決定（最終版）
ベースの配送リストの不在列を”翌日”と更新します。
```python:jupyter
orders_df.loc[orders_df[orders_df["ID"] == "南禅寺"].index, '不在'] = '翌日'
orders_df
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/ea3b0269-93f8-2aa8-5e2e-808d9db8f768.png)

先ほどと同じ要領で、再配送リスト（最終版）を作成します。
```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/9b2e6d23-3cf7-63fa-cc37-2a939e4c3b51.png)


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


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


```python: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のインプットパラメータを生成します。
```python:jupyter
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
```
```python: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)
```

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

```JSON:output
{'start': '伏見稲荷:京都府京都市伏見区深草藪之内町６８;34.96781,135.77273', 
'destination1': '清水寺:京都府京都市東山区清水１丁目２９４;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600', 
'destination2': '銀閣寺:京都府京都市左京区銀閣寺町２;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をコールしてみましょう。

```python: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にします。
```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/15a9cc29-ef96-a251-aa27-6bc46399281c.png)

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

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

```python: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に変換してください。

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/72c90769-23fe-af0a-45ee-622e33ad2600.png)
上記のように、伏見稲荷を18:53に出発し、最終的に20:08に配達作業が終了しました。お疲れ様でした！
# おまけ (トラックと普通車指定の比較）
今回使用しましたHERE Waypoint Sequence APIとRouting APIはトラック規制などの道路情報も考慮に入れて配送順序及びルートを決定します。以下は先ほどの再配達ルートをそれぞれトラックまたは普通車指定とした場合のルートになります。（トラック指定の場合、より幹線道路を選択している傾向を読み取れます。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/36eaf4c1-59b1-5ab0-de36-a28705627c02.png)
また、単純なトラック指定以外に車幅、車高、全長、重量などの規制情報にも対応していることも付け加えておきます。

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

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