HERE Location ServiceとWhat3Wordsを組み合わせてみた

HEREが提供するLocation Serviceは情報も豊富で色々やりたいことがHEREのサービスだけで完結できるほど。
サービスの紹介などは以下の記事で分かりやすく説明されている。

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

「HEREの＊＊使ってみた」だと面白味もなく、HEREマニアやHEREの中の人達が色々やってるだろうから、折角なので「HERE以外と組み合わせてみた」にしようと思う。今回選んだのは「What3Words」。

# What3Words って？

https://what3words.com/ja/about

世界の位置情報を伝える手段として座標やら住所やら今まであったけど、場所によって正確性が約束されないかもしれない、ということでWhat3Wordsさんが世界を3メートル四方に切り分けて、その一つ一つに3つの単語を繋げた言葉を割り当てている。

例えばスクランブル交差点のど真ん中の座標をWhat3Wordsに変換すると「うわばき。はがゆい。つらら」という3つのワードになる。
What3Wordsを知ってる間同士で待ち合わせ場所を決める際に役立つと思う。

Aさん「待合せはスクランブル交差点な」
Bさん「どんだけ広いと思ってんねん！もっと具体的に教えてよ」
Aさん「しゃーないな。”うわばき。はがゆい。つらら”やったら分かるやろ」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619978/15410623-91ef-3979-e4cc-da4760b561f8.png)

Bさん「交差点のど真ん中やないか！」

みたいな。

# 便利だけども・・・

HEREにはルート案内やPOI(Point Of Interest)検索なんかもできるから、カーナビとかナビアプリとかに使ってる人も多そう。ただ、そのユーザがWhat3Wordsだけをもらっても、スマホで座標や地図の位置を確認するくらいはできるだろうけど、それをナビに入れようと思うと、手入力の必要があって面倒＆間違いそう。

ってことで、What3WordsからHEREのRoutingまでの一連の流れを自動化してみようと思う。

# ざっくりの流れ

```mermaid
%% W3W → HERE Routing
sequenceDiagram
    autonumber
    actor ユーザ
    participant 本アプリ
    participant What3Words
    participant HERE
    ユーザ->>本アプリ: What3Words入力
　　本アプリ->>What3Words: What3Wordsワード
　　What3Words->>本アプリ: 座標
    本アプリ->>HERE: Routing API
    HERE->>本アプリ: ルート
    Note over 本アプリ: 地図とルート表示
```
こんな感じで進めようと思う。

# 動作環境

|使用ツール|バージョン|
|:--:|:--|
|OS|Windows 11|
|ブラウザ|Chrome Version 119.0.6045.124 (Official Build) (64-bit)|
|Python|3.10.11|
|Routing|HERE Routing API v8|
|What3Words|API v3|

# 事前準備

今回はPythonを使うことにする。

なぜか？Pythonしか使い方を知らないから。

VSCodeでもPythonは書けるけど、やっぱり地図やルートを可視化したい。
ということでJupyter Notebookを使う。

### Jupyter Notebookの準備

Pythonが既に使える状態という前提だと、Jupyter Notebookをインストールするだけ。

https://www.javadrive.jp/python/jupyter-notebook/index1.html

ここの手順に従えばインストールできるはず。

### HERE for Pythonの準備

PythonでHERE地図を表示するために必要なインストールと、Routingを使うために必要なインストールがある。

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

上記リンクにHEREサービスをPythonで使うためのインストール方法や、そもそもHEREサービスを使う上で必要なAPIKEY取得方法などが書かれている。

https://here-location-services-python.readthedocs.io/en/latest/install.html

HEREのPython Widgetの簡単な使い方は上記リンクに書いてあるが、当然全て書かれていない。
引数で渡すのはどんな型？Attributeは他にどんなのあるの？など知りたいことだらけ。

https://github.com/heremaps/here-map-widget-for-jupyter/tree/master

なら ↑ のGithubからソースコードを見た方が分かりやすい場合もある。

### What3Words API利用の準備

HERE同様にAPIKEY取得が必要。

https://accounts.what3words.com/select-plan?referrer=/public-api/docs

100回/月までなら無料で利用できるプランもある。

### ipywidgetsの準備

別になくてもいいけど、折角なら最初の入力であるWhat3Wordsをユーザに入力してもらうのにGUIを作りたい。

https://ebi-works.com/jupyternb-gui/#outline__2_3

ここに基本的な使い方があるので参考にさせてもらう。
因みにインストール自体は
```
pip install ipywidgets
```
でOK。

準備が整ったところで、早速書いてみよう。

# ステップ 1~3 : What3Words入力 ～ 座標取得まで

「インタラクティブなGUIを作る方法」の記事で紹介されている”テキストボックス”のコードをそのまま動かしてみる。

```
from ipywidgets import interact
import datetime
```
```
def func(year,month,day):
    dt = datetime.datetime(int(year), int(month), int(day))
    w_list = ['月', '火', '水', '木', '金', '土', '日']
    ans = w_list[dt.weekday()]
    return ans
```
```
# テキストボックス
interact(func, year="2021",month="5",day="15");
```
:::note info
Jupyter Notebookはエクセルみたく「セル」に分かれている。
別に一つのセルに全部入力してもいいけど、これから色々増やしていくから
「Import」「関数定義」「実行」に分ける。
:::

コードを実行すると3つのテキストボックスが表示され、入力した年月日に応じて曜日が表示されるアプリ。
試しに2023年11月15日を入力してみると、'水' という結果が表示された。
問題なく動作している。
ここから必要に応じて修正していく。

```Python:Import(変更後)
from ipywidgets import interact
import requests

# ****************************************************************
# 変数／定数　設定
# ****************************************************************
key_W3W = "***"

# ****************************************************************
# 地図設定
# ****************************************************************
m = Map(api_key=key_HERE, center=[52.51375, 13.42462], zoom=14)
```
datetimeは使う予定がないので削除。
代わりにPythonでRestAPIを使うのでrequestsをimport。
後、ここでAPIKEYも設定しておく。まずはW3WのAPIKEY。

また、m=なんちゃら　もここで設定しておく。
C言語的に言うところのグローバル変数。
自作関数内で継ぎ足しもするので、ここで定義しておく。

```Python:関数定義(変更後)
def func(word):
    ans = w3w_ConverToCoordinate(word)
    return ans

def w3w_ConverToCoordinate(word):
    # (1) 配列初期化
    Coord = []
    
    # (2) 変数定義
    apikey = key_W3W
    BaseUrl = 'https://api.what3words.com/v3/convert-to-coordinates?'
    
    # (3) RestAPI設定
    RequestUrl = BaseUrl + 'key=' + apikey + '&' + 'words=' + word
    
    # (4) APIコール
    res = requests.get(RequestUrl, verify=False)
    result = res.json()
    
    # (5) 用意していた配列にLatとLonをそれぞれ入れる
    Coord.append(result['coordinates']['lat'])
    Coord.append(result['coordinates']['lng'])
    
    # (6) Return
    return Coord
```

結構変わった。
まず、サンプルからとってきた`func()`という関数。
(１) 引数を一つに変更
(２) What3WordのAPIをコールする別の関数`w3w_ConverToCoordinate()`をコール

次に新規追加した関数。別に作る必要はなかったけど、なんとなく。
(１) 戻り値用の配列初期化
(２) 変数の設定。
(３) RestAPIをコールするためのURLを作り上げる
(４) 実際にコールして、レスポンスを格納
(５) 戻り値用配列に座標の`lat` `lon`を分けて格納
(６) 座標を配列ごとReturn

```Python:実行(変更後)
# テキストボックス
interact(func, word="うわばき。はがゆい。つらら");
```
テキストボックスを一つに変更。

### 動かしてみる

実行すると以下の様な結果。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619978/8e8a4124-d186-d85e-9d3d-a93fe7c7d76e.png)
成功。

知り合いに適当な場所のWhat3Word、「こんやく。てざわり。ろせん」を教えてもらったので、そのままテキストボックスに入れてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619978/3fe48c5c-691b-35c2-e6b5-a99c1c9091b9.png)
座標が変わったので知り合いに確認してもらったら、正解とのこと。
上手く動いているみたい。

# ステップ4 : HERE Routing APIのコール

これからHERE RoutingのAPIを使ってルート表示までするのだが、まずはシナリオを選定。
山梨県の「身延駅」から静岡県の「浜名湖佐久米駅」までの車のルートを引くことにする。
（これらの場所に特に理由はないし、行ったこともない。某漫画／アニメに出てきたからという理由では決してない）

細かい調整などはさておき、一旦HEREのサンプルコードで動くかやってみる。

https://here-location-services-python.readthedocs.io/en/latest/car_route.html

```Python:Import(変更後)
from ipywidgets import interact
import datetime
import requests

import os

from here_location_services import LS
from here_location_services.config.routing_config import ROUTING_RETURN
from here_map_widget import Map, Marker, GeoJSON

# ****************************************************************
# 変数／定数　設定
# ****************************************************************
key_HERE = "***"
key_W3W = '***'

# ****************************************************************
# 地図設定
# ****************************************************************
m = Map(api_key=key_HERE, center=[52.51375, 13.42462], zoom=14)
```
ここは言われるがまま。
あと、HERE用のAPIKEYも設定しておく。
```Python:関数(新規追加)
def HERE_Routing(Lat, Lon):
    LS_API_KEY = key_HERE
    ls = LS(api_key=LS_API_KEY)

    result = ls.car_route(
        origin=[52.51375, 13.42462],
        destination=[52.52332, 13.42800],
        return_results=[
            ROUTING_RETURN.polyline,
            ROUTING_RETURN.elevation,
            ROUTING_RETURN.instructions,
            ROUTING_RETURN.actions,
        ],
    )
    geo_json = result.to_geojson()
    data = geo_json
    geo_layer = GeoJSON(data=data, style={"lineWidth": 5})

    origin_marker = Marker(lat=52.51375, lng=13.42462)
    dest_marker = Marker(lat=52.52332, lng=13.42800)
    m.add_layer(geo_layer)
    m.add_object(origin_marker)
    m.add_object(dest_marker)
```
サンプルコードからの変更点は3つ
- 自前関数`HERE_Routing()`というものを作り、その中にサンプルコード挿入。コードを見る限り、座標のLatとLonを別々に食わす必要があるので、自前関数の引数も分ける。
- `LS_API_KEY` に代入するAPIKEYを自分のものに変更
- サンプルコード最終行にあった`m`を削除。どうやら`m`というのは、それまで設定してきたもの全てを実行するための1行っぽいが、自前関数内で`m`を実行しても何も起きなかった。

:::note
恐らく、ネストされた中にあると`m`がうまく動かないと予想
:::

```Python:実行(変更後)
# テキストボックス
interact(func, word="うわばき。はがゆい。つらら");

m
```
自前関数にあった`m`を移動。

そして実行。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619978/ed758c82-440f-0996-8418-f14e67a118b5.png)

どこかの場所で何かしらのルートが引かれた。
あとは場所を日本にして、What3Wordから返ってきた座標を目的地として入力すれば完成するはず。

# ステップ 4.5 : 色々と日本対応

変更箇所は
1. 先頭の地図設定の前に日本地図用の設定を追加
1. 自前関数 `HERE_Routing()` の座標変更

のみ。

### ① 日本地図用の設定を追加

```diff_python
from here_location_services import LS
from here_location_services.config.routing_config import ROUTING_RETURN
from here_map_widget import Map, Marker, GeoJSON
+ from here_map_widget import ServiceNames, OMVUrl, Platform, Style, OMV, TileLayer

# ****************************************************************
# 変数／定数　設定
# ****************************************************************
key_HERE = "***"
key_W3W = '***'

+ # ****************************************************************
+ # 日本用設定
+ # ****************************************************************
+ services_config = {
+     ServiceNames.omv: {
+         OMVUrl.scheme: "https",
+         OMVUrl.host: "vector.hereapi.com",
+         OMVUrl.path: "/v2/vectortiles/core/mc",
+     }
+ }
+ 
+ platform = Platform(api_key=key_HERE, services_config=services_config)
+ 
+ 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/",
+ )
+ 
+ omv_provider = OMV(path="v2/vectortiles/core/mc", platform=platform, style=style)
+ 
+ omv_layer = TileLayer(provider=omv_provider, style={"max": 22})
# ****************************************************************
# 地図設定
# ****************************************************************
- m = Map(api_key=key_HERE, center=[52.51375, 13.42462], zoom=14)
+ m = Map(api_key=key_HERE, center=[35.3614, 138.4530], zoom=14, basemap=omv_layer)
```
HEREの地図で日本を表示させる場合のやり方が以下に書いてある。

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

基本的にコピーし、APIKEYを自身のものに変更し、センター座標を身延駅の座標に変更。

この状態で実行してみると、きちんと日本の地図が表示されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619978/576abe28-27ec-c0e2-4aea-b5183a776cd6.png)


### ② `HERE_Routing()` の座標変更

```diff_python
def HERE_Routing(Lat, Lon):
    LS_API_KEY = key_HERE
    ls = LS(api_key=LS_API_KEY)

    result = ls.car_route(
-       origin=[52.51375, 13.42462],
+       origin=[35.3614, 138.4530],
-       destination=[52.52332, 13.42800],
+       destination=[Lat, Lon],
        return_results=[
            ROUTING_RETURN.polyline,
            ROUTING_RETURN.elevation,
            ROUTING_RETURN.instructions,
            ROUTING_RETURN.actions,
        ],
    )
    geo_json = result.to_geojson()
    data = geo_json

    # 新しいレイヤーを設定
    geo_layer = GeoJSON(data=data, style={"lineWidth": 5})

    # 新しいマーカーを設定
-   origin_marker = Marker(lat=52.51375, lng=13.42462)
+   origin_marker = Marker(lat=35.3614, lng=138.4530)
-   dest_marker = Marker(lat=52.52332, lng=13.42800)
+   dest_marker = Marker(lat=Lat, lng=Lon)
```
変更箇所は4点。
1. `origin` を身延駅の座標に変更。ここを開始点とする。
1. `destination` は関数の引数で渡されるLatとLonに変更。
1. `origin_marker` の座標をOriginと同じ身延駅に変更。
1. `dest_marker` の座標を引数で渡されるLatとLonに変更。

最後に、目的地を変更。
浜名湖佐久米駅の座標を調べる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619978/5e224166-237f-c69e-99a8-f59f3fdccad6.png)


```diff_python
# テキストボックス
- interact(func, word="うわばき。はがゆい。つらら");
+ interact(func, word="しょぞく。すみれ。なのか");

m
```

### Let's 実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619978/7b3404e3-86ac-9058-a7e6-e7d42ec345b2.png)


ちゃんとルートを引けてるっぽい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619978/4de35acd-f7c4-40a1-f542-e6ac091917fd.png)


ちゃんとW3Wで探した場所までルートが引けてることが確認できた。

# プラスα

今のままだとテキストボックスに別のW3Wを入れると、前のルートやマーカーが残ったまま。
既存の自前関数 `HERE_Routing` の流れは以下：

```mermaid
graph TD
    A(START)-->B[1__HERE Routing APIをコール]
    B-->C[2__APIの結果からルートの線をレイヤーとして設定]
    C-->D[3__開始点と終点のマーカーを設定]
    D-->E[4__レイヤーとマーカーを地図に追加]
    E-->F(END)
```

追加すべきはステップ2とステップ3の前にそれぞれレイヤーとマーカーを削除する処理を入れる。

### レイヤーの削除

```diff_python
    data = geo_json
    
+   # 既存のレイヤーがあれば削除
+   if len(m._layer_ids) > 0:
+       m.remove_layer(geo_layer)
        
    # 新しいレイヤーを設定
    geo_layer = GeoJSON(data=data, style={"lineWidth": 5})
```
プログラムを走らせた初回はこの段階では地図にレイヤーが一つも追加されていない状態。
`if len(m._layer_ids) > 0` でレイヤーの数を調べ、1以上の場合のみ削除する。
`remove_layer` はHEREが用意するWidgetのメソッドで、指定したレイヤーを削除する。

これだけの変更だと、`geo_layer` に代入する前に参照することになり、エラーとなる。
コードの美しさは置いといて、簡単なのは`geo_layer`をグローバルにして、前段で代入してやる。

```diff_python
key_HERE = "***"
key_W3W = '***'

+ geo_layer = 1
# ****************************************************************
# 日本用設定
# ****************************************************************
services_config = {
```
`Import`とかしてる先頭のところで適当な値を入れる。

```diff_python
def HERE_Routing(Lat, Lon):    
+   global geo_layer
    
    LS_API_KEY = key_HERE
```
'HERE_Routing()`内先頭でglobal宣言しておくと、移行の代入／参照はグローバル変数として扱われる。
これをしないと、ローカル変数と認識されるので、レイヤーが削除されなくなる。

### マーカーの削除

基本的な流れはレイヤーと同じ。

```diff_python
key_HERE = "***"
key_W3W = '***'

geo_layer = 1
+ dest_marker = Marker(lat=34.789147, lng=137.599406)
# ****************************************************************
# 日本用設定
# ****************************************************************
services_config = {
```
先頭でグローバル宣言して適当な値を入れ、

```diff_python
def HERE_Routing(Lat, Lon):    
    global geo_layer    
+   global dest_marker
    
    LS_API_KEY = key_HERE
```
`HERE_Routing()`の先頭でグローバル宣言して、

```diff_python
    # 新しいレイヤーを設定
    geo_layer = GeoJSON(data=data, style={"lineWidth": 5})

+   # 既存のマーカーがあれば削除
+   if len(m._object_ids) > 0:
+       # 今回のユースケースは、開始地点は変更しないので消すのは終点のみ
+       m.remove_object(dest_marker)
    
    # 新しいマーカーを設定
    origin_marker = Marker(lat=35.3614, lng=138.4530)
    dest_marker = Marker(lat=Lat, lng=Lon)
```
同じく`HERE_Routing()`のマーカーを設定する手前で、
オブジェクト数が1以上のみ `remove_object`を使ってマーカーを消す。
今回のユースケースは目的地だけを変えるので、開始地点は無視。
もしかすると描画はされてて、重なってるから目に見えないだけかもだけど、面倒だから放置。

# 最後に

記事やコードを書きながら思ったこと。

- Jupyter Notebookをセルで分けると、カーネル再起動後に全部実行しないといけんから面倒
- HEREドキュメントは必要な事は書いてるけど、網羅性に欠けるので応用する時は少しクセ強
- 場所の連絡するのにW3W使ってる人・・・いるんだろうか？
- 「みのぶまんじゅう」食べたい