LoginSignup
2
2

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

Last updated at Posted at 2023-12-06

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

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

What3Words って?

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

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

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

image.png

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

みたいな。

便利だけども・・・

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

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

ざっくりの流れ

こんな感じで進めようと思う。

動作環境

使用ツール バージョン
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をインストールするだけ。

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

HERE for Pythonの準備

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

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

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

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

What3Words API利用の準備

HERE同様にAPIKEY取得が必要。

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

ipywidgetsの準備

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

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

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");

Jupyter Notebookはエクセルみたく「セル」に分かれている。
別に一つのセルに全部入力してもいいけど、これから色々増やしていくから
「Import」「関数定義」「実行」に分ける。

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

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言語的に言うところのグローバル変数。
自作関数内で継ぎ足しもするので、ここで定義しておく。

関数定義(変更後)
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()という関数。
(1) 引数を一つに変更
(2) What3WordのAPIをコールする別の関数w3w_ConverToCoordinate()をコール

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

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

テキストボックスを一つに変更。

動かしてみる

実行すると以下の様な結果。
image.png
成功。

知り合いに適当な場所のWhat3Word、「こんやく。てざわり。ろせん」を教えてもらったので、そのままテキストボックスに入れてみる。
image.png
座標が変わったので知り合いに確認してもらったら、正解とのこと。
上手く動いているみたい。

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

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

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

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も設定しておく。

関数(新規追加)
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を実行しても何も起きなかった。

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

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

m

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

そして実行。

image.png

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

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

変更箇所は

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

のみ。

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

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の地図で日本を表示させる場合のやり方が以下に書いてある。

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

この状態で実行してみると、きちんと日本の地図が表示されている。
image.png

HERE_Routing() の座標変更

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 を身延駅の座標に変更。ここを開始点とする。
  2. destination は関数の引数で渡されるLatとLonに変更。
  3. origin_marker の座標をOriginと同じ身延駅に変更。
  4. dest_marker の座標を引数で渡されるLatとLonに変更。

最後に、目的地を変更。
浜名湖佐久米駅の座標を調べる。
image.png

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

m

Let's 実行

image.png

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

image.png

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

プラスα

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

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

レイヤーの削除

    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をグローバルにして、前段で代入してやる。

key_HERE = "***"
key_W3W = '***'

+ geo_layer = 1
# ****************************************************************
# 日本用設定
# ****************************************************************
services_config = {

Importとかしてる先頭のところで適当な値を入れる。

def HERE_Routing(Lat, Lon):    
+   global geo_layer
    
    LS_API_KEY = key_HERE

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

マーカーの削除

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

key_HERE = "***"
key_W3W = '***'

geo_layer = 1
+ dest_marker = Marker(lat=34.789147, lng=137.599406)
# ****************************************************************
# 日本用設定
# ****************************************************************
services_config = {

先頭でグローバル宣言して適当な値を入れ、

def HERE_Routing(Lat, Lon):    
    global geo_layer    
+   global dest_marker
    
    LS_API_KEY = key_HERE

HERE_Routing()の先頭でグローバル宣言して、

    # 新しいレイヤーを設定
    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使ってる人・・・いるんだろうか?
  • 「みのぶまんじゅう」食べたい
2
2
1

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
2
2