OSMnxで挑戦！ゼンリンの地図APIの大型車ルート検索をOpenStreetMapで実装してみた

## はじめに

この記事では、PythonライブラリのOSMnxとJavaScriptベースのZENRIN Maps APIを使用して経路探索を行う方法を比較します。
<strong>本記事の目的</strong>
この記事の目的は、OpenStreetMapデータを利用するOSMnxと商用APIであるZENRIN Maps APIを用いた経路探索の実装方法を比較し、それぞれの特性を理解することです。
<strong>対象読者</strong>
この記事は以下のような読者を対象としています。
・Pythonで地理空間情報を扱いたい方
・JavaScriptで地図アプリケーションを開発したい方
・経路探索に興味がある方
・OpenStreetMapと商用地図APIの違いを知りたい方

## 両サービスの概要
<strong>OpenStreetMap(OSMnx)</strong>
OpenStreetMapは、誰でも自由に利用・編集できるオープンソースの地図データです。
OSMnxは、OpenStreetMapデータへのアクセス、操作、分析を行うためのPythonライブラリです。
オフラインでの利用やカスタマイズ性に優れています。
<strong>ZENRIN Maps API</strong>
ZENRIN Maps APIは、株式会社ゼンリンが提供する地図APIです。
詳細な地図データと高度な経路探索機能を提供します。
商用サービスであるため、利用には契約が必要です。

## APIキーの取得
<strong>ZENRIN Maps API</strong>
１．検証用IDとパスワード（PW）取得
ZENRIN Maps APIを使用するためには、検証用IDとPWを取得します。

１－１．検証用IDとPWの発行を依頼
[ZENRIN Maps API　無料お試しID　お申込みフォーム（２か月無料）](https://www.zenrin-datacom.net/solution/zenrin-maps-api/trial?fm_cp=6757baf3203e3a00bb118509&fm_mu=67639184bdef2d03f01aa7d9&utm_campaign=6757baf3203e3a00bb118509&utm_medium=else&utm_source=Qiita6)から必要事項を入力して送信します。
![ZMA_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965389/bd6879b2-4ccd-e78b-7cf5-e48d47b7ac73.png)

１－２．検証用IDとPWの確認
フォーム送信から3営業日以内にメールにて検証用IDとPWが発行されます。

２．コンソールにログイン
以下のURLでメールにて送られてきたIDとPWを入力し、[ログイン](https://test-console.zmaps-api.com/)をします。
![ZMA_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965389/3e184372-64c3-1a2d-774c-b049f73f3a4c.png)

３．ユーザーの設定
ユーザー設定にて、「コンソール管理者メールアドレス」と「情報配信用メールアドレス」を設定します。
特にコンソール管理者メールアドレスはZENRIN Maps API コンソールのパスワードを忘れて再設定が必要となった場合に必要になります。
![ZMA_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965389/a9948b72-b0c8-e751-2531-5d1153c1446b.png)

４．チャネルの認証方式の設定、チャネル名変更
チャネルの設定変更は左メニューの「チャネル設定」から行うことができます。
「チャネル設定」押下後表示される画面にはチャネルの一覧が表示されています。
設定を行うチャネルの「編集」ボタンを押下してください。
![ZMA_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965389/6e53e2f0-61f3-e67b-48f4-4454411c2fd4.png)

認証方式は3種類あり、設定したいタブを押下するとそれぞれの認証方式の設定項目が表示されます。
認証方式を有効にするには、「無効」を「有効」に変更してください。
有効にしたい認証方式が複数ある場合は、それぞれの認証方式のタブで「有効」状態に変更してください。
それぞれの設定項目を入力後「変更」ボタンを押下すると有効状態と設定項目が反映されます。
![ZMA_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965389/19bfbee6-b973-3e4b-adde-7e6319ea2b05.png)

５．APIキーの取得・API利用開始
「チャネル一覧」のグレーアウトされている箇所にマウスオーバーすると表示される「APIキー」を使い、
[APIリファレンス](https://developers.zmaps-api.com/v20/reference/)に則りAPIの利用を開始してください。

これで準備は完了です。
![ZMA_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965389/0ff5f1b3-46dd-efe6-55ce-6bdab4a2005f.png)

 ## サンプルコード
<strong>OpenStreetMap</strong>
```route_search.py
import osmnx as ox
import folium

# 経路探索対象地域を設定
area = 'Setagaya,Tokyo,Japan'

# 車道のみでグラフを作成
G = ox.graph_from_place(area, network_type='drive')

# 出発点と到着点の座標を設定（経度、緯度の順）
departure = (139.6707547792902, 35.637085839175484)
destination = (139.66985552394794, 35.63448502797939)

# 2地点の近似ノードを取得
dep_node = ox.nearest_nodes(G, departure[0], departure[1])
des_node = ox.nearest_nodes(G, destination[0], destination[1])

# 最短経路探索
shortest_route = ox.shortest_path(G, dep_node, des_node)

# foliumを使用してHTMLマップを作成
m = folium.Map(location=[35.637, 139.669], zoom_start=17)

# ルートを地図に追加
locations = [(G.nodes[node]['y'], G.nodes[node]['x']) for node in shortest_route]
folium.PolyLine(locations, color="red", weight=4, opacity=0.8).add_to(m)

# マーカーを追加
folium.Marker(departure[::-1], popup="出発", icon=folium.Icon(color='blue')).add_to(m)
folium.Marker(destination[::-1], popup="到着", icon=folium.Icon(color='green')).add_to(m)

# HTMLファイルとして保存
m.save("route_map.html")
```
このコードを実行すると、「route_map.html」というファイルが生成されます。
このファイルをウェブブラウザで開くと、インタラクティブな地図が表示されます。

*****

OSMnxで大型車経路探索を試みましたが、うまくいかなかったです。
参考までに試したコードです。

```route_search_large.py
import osmnx as ox
import folium
import networkx as nx

ox.settings.overpass_endpoint = "https://overpass.kumi.systems/api/interpreter"
ox.settings.timeout = 180  # タイムアウトを180秒に設定

# 経路探索対象地域を設定（範囲を拡大）
area = 'Setagaya, Tokyo, Japan'

# 大型車用のカスタムフィルタを定義
custom_filter = '["highway"~"motorway|trunk|primary|secondary|tertiary"]'

# カスタムフィルタを使用してグラフを作成
G = ox.graph_from_place(area, custom_filter=custom_filter, network_type='drive', simplify=True)


# 出発点と到着点の座標を設定（経度、緯度の順）
departure = (139.6707547792902, 35.637085839175484)  # 日本大学認定こども園
destination = (139.66985552394794, 35.63448502797939)  # 青葉学園野沢こども園 

# 2地点の近似ノードを取得
dep_node = ox.nearest_nodes(G, departure[0], departure[1])
des_node = ox.nearest_nodes(G, destination[0], destination[1])

# エッジの重みを調整
for u, v, k, data in G.edges(keys=True, data=True):
    # 道路の種類に基づいて重みを設定
    if 'highway' in data:
        if data['highway'] == 'motorway':
            data['weight'] = data['length'] * 0.8  # 高速道路を優先
        elif data['highway'] in ['trunk', 'primary']:
            data['weight'] = data['length'] * 0.9  # 幹線道路を次に優先
        else:
            data['weight'] = data['length']  # その他の道路は通常の長さ
    else:
        data['weight'] = data['length']

# 大型車向け経路探索（重み付きの最短経路）
route = nx.shortest_path(G, dep_node, des_node, weight='weight')

# foliumを使用してHTMLマップを作成
m = folium.Map(location=[35.637, 139.669], zoom_start=15)

# ルートを地図に追加
locations = [(G.nodes[node]['y'], G.nodes[node]['x']) for node in route]
folium.PolyLine(locations, color="blue", weight=4, opacity=0.8).add_to(m)

# マーカーを追加
folium.Marker(departure[::-1], popup="出発: 日本大学認定こども園", icon=folium.Icon(color='green')).add_to(m)
folium.Marker(destination[::-1], popup="到着: 青葉学園野沢こども園", icon=folium.Icon(color='red')).add_to(m)

# タイトルを追加
title_html = '''
             <h3 align="center" style="font-size:16px"><b>日本大学認定こども園から青葉学園野沢こども園への大型車ルート</b></h3>
             '''
m.get_root().html.add_child(folium.Element(title_html))

# HTMLファイルとして保存
m.save("large_vehicle_route_map.html")

```

<strong>ZENRIN Maps API</strong>
```zma_route.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZENRIN Maps API Route Display</title>
<style>
    body { margin: 0; padding: 0; }
    #ZMap { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
<script src="https://test-js.zmaps-api.com/zma_loader.js?key=[APIキー]&auth=referer"></script>
<script src="js/zma_route.js"></script>
</head>
<body>
<div id="ZMap"></div>
</body>
</html>
```
```zma_route.js
ZMALoader.setOnLoad(function (mapOptions, error) {
    if (error) {
        console.error(error);
        return;
    }

    const mapElement = document.getElementById('ZMap');

    // 地図の初期設定
    mapOptions.center = new ZDC.LatLng(35.637085839175484, 139.6707547792902); //日本大学認定こども園
    mapOptions.zoom = 17;
    mapOptions.mouseWheelReverseZoom = true; // ★マウスホイールのズーム方向の反転を指定


    const map = new ZDC.Map(mapElement, mapOptions, function() {
        // 地図生成成功時の処理

        // コントロールを追加
        map.addControl(new ZDC.ZoomButton('top-left'));
        map.addControl(new ZDC.Compass('top-right'));
        map.addControl(new ZDC.ScaleBar('bottom-left'));
        
        const start = new ZDC.LatLng(35.637085839175484, 139.6707547792902); //日本大学認定こども園
        const end = new ZDC.LatLng(35.63448502797939, 139.66985552394794); //青葉学園野沢こども園

        const startMarker = new ZDC.Marker(start);
        const endMarker = new ZDC.Marker(end);
        map.addWidget(startMarker);
        map.addWidget(endMarker);

        const routeUrl = `https://test-web.zmaps-api.com/route/route_mbn/drive_ptp?search_type=1&from=${start.lng},${start.lat}&to=${end.lng},${end.lat}&regulation_type=121200&toll_type=large`;

        fetch(routeUrl, {
            method: 'GET',
            headers: {
                'x-api-key': '[APIキー]',
                'Authorization': 'referer'
            },
        })
        .then(response => response.json())
        .then(data => {
            if (data.status === 'OK' && data.result?.item?.length > 0) {
                const routeItem = data.result.item[0];
                const links = routeItem.route?.link;

                if (Array.isArray(links) && links.length > 0) {
                    const decodedPath = [];
                    links.forEach(link => {
                        if (Array.isArray(link.line.coordinates)) {
                            link.line.coordinates.forEach(coord => {
                                if (Array.isArray(coord) && coord.length === 2) {
                                    decodedPath.push(new ZDC.LatLng(coord[1], coord[0]));
                                }
                            });
                        }
                    });

                    console.log("デコードされた座標:", decodedPath);

                    if (decodedPath.length > 0) {
                        const routeLine = new ZDC.Polyline(
                            decodedPath,
                            {
                                color: '#008dcb',
                                width: 5,
                                opacity: 0.7
                            }
                        );
                        map.addWidget(routeLine);
                    } else {
                        console.error("デコードされたルートデータが空です。");
                    }
                } else {
                    console.error("リンクデータが存在しません。");
                }
            } else {
                console.error("ルート検索に失敗しました。レスポンス:", data);
            }
        })
        .catch(error => console.error('ルート検索エラー:', error));
    }, function() {
        console.error('地図の生成に失敗しました');
    });
});

```


 ## 地図表示
<strong>OpenStreetMap</strong>
![OSMルート_野沢.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965389/6b01d8ba-ffd5-2d0d-c5f6-a04e385e893a.png))
<strong>ZENRIN Maps API</strong>
[ZENRINMapsAPIルート表示](https://goldiek24.github.io/qiita-zma_largeroute_noge/zma_route.html)
![ZMA大型車_野沢.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965389/5ba1cad0-b7de-e9da-ce4f-dd13c1600777.png)

## 解説
### OpenStreetMap
#### １．ライブラリのインポートと初期設定
この部分では、必要なライブラリをインポートし、経路探索の対象地域を設定しています。
OSMnxライブラリを使用して、指定した地域の道路ネットワークグラフを作成します。
```route_search.py
import osmnx as ox
import folium

# 経路探索対象地域を設定（範囲を拡大）
area = 'Setagaya,Tokyo,Japan'

# 車道のみでグラフを作成
G = ox.graph_from_place(area, network_type='drive')
</code></pre>
```

#### ２．出発点と到着点の設定
ここでは、出発点と到着点の座標を設定し、それぞれの座標に最も近い道路ネットワーク上のノードを取得しています。
```route_search.py
# 出発点と到着点の座標を設定（経度、緯度の順）
departure = (139.6707547792902, 35.637085839175484)  # 日本大学認定こども園
destination = (139.66985552394794, 35.63448502797939)  # 青葉学園野沢こども園 

# 2地点の近似ノードを取得
dep_node = ox.nearest_nodes(G, departure[0], departure[1])
des_node = ox.nearest_nodes(G, destination[0], destination[1])
```

#### ３．最短経路の探索
OSMnxライブラリを使用して、出発点と到着点の間の最短経路を計算します。
```route_search.py
# 最短経路探索
shortest_route = ox.shortest_path(G, dep_node, des_node)
```

#### ４．地図の作成とルートの追加
foliumライブラリを使用して地図を作成し、計算された最短経路を赤い線で地図上に描画します。
```route_search.py
# foliumを使用してHTMLマップを作成
m = folium.Map(location=[35.637, 139.669], zoom_start=17)

# ルートを地図に追加
locations = [(G.nodes[node]['y'], G.nodes[node]['x']) for node in shortest_route]
folium.PolyLine(locations, color="red", weight=4, opacity=0.8).add_to(m)
```

#### ５．マーカーとタイトルの追加
出発点と到着点にマーカーを追加し、地図にタイトルを付けています。
```route_search.py
# マーカーを追加
folium.Marker(departure[::-1], popup="出発: 日本大学認定こども園", icon=folium.Icon(color='blue')).add_to(m)
folium.Marker(destination[::-1], popup="到着: 青葉学園野沢こども園", icon=folium.Icon(color='green')).add_to(m)

# タイトルを追加
title_html = '''
             <h3 align="center" style="font-size:16px"><b>日本大学認定こども園から青葉学園野沢こども園へのルート</b></h3>
             '''
m.get_root().html.add_child(folium.Element(title_html))
```

#### ６．HTMLファイルの保存
最後に、作成した地図をHTMLファイルとして保存します。
```route_search.py
# HTMLファイルとして保存
m.save("route_map.html")
```


### ZENRIN Maps API
#### １．地図の初期化
ZENRINMapsAPIの初期化と地図の生成を行っています。ZMALoader.setOnLoad関数を使用してAPIのロードを待ち、地図のオプションを設定し、ZDC.Mapオブジェクトを作成しています。
 ```zma_route.js
 ZMALoader.setOnLoad(function (mapOptions, error) {
    if (error) {
        console.error(error);
        return;
    }

    const mapElement = document.getElementById('ZMap');

    // 地図の初期設定
    mapOptions.center = new ZDC.LatLng(35.681406, 139.767132); // 東京駅
    mapOptions.zoom = 13;

    const map = new ZDC.Map(mapElement, mapOptions, function() {
        // 地図生成成功時の処理
    }, function() {
        console.error('地図の生成に失敗しました');
    });
});
```

#### ２．地図コントロールの追加
ここでは、地図上にズームボタン、コンパス、スケールバーなどのコントロールを追加しています。これらのコントロールにより、ユーザーは地図の操作や情報の確認が容易になります。
 ```zma_route.js
 // コントロールを追加
map.addControl(new ZDC.ZoomButton('top-left'));
map.addControl(new ZDC.Compass('top-right'));
map.addControl(new ZDC.ScaleBar('bottom-left'));
 ```

#### ３．マーカーの設定
ルートの始点（日本大学認定こども園）と終点（青葉学園野沢こども園）にマーカーを設置しています。ZDC.Markerオブジェクトを作成し、map.addWidgetメソッドで地図上に追加しています。
 ```zma_route.js
        const start = new ZDC.LatLng(35.637085839175484, 139.6707547792902); //日本大学認定こども園
        const end = new ZDC.LatLng(35.63448502797939, 139.66985552394794); //青葉学園野沢こども園
const startMarker = new ZDC.Marker(start);
const endMarker = new ZDC.Marker(end);
map.addWidget(startMarker);
map.addWidget(endMarker);
 ```

#### ４．ルート検索APIリクエスト
ZENRINMapsAPIのルート検索エンドポイントにリクエストを送信しています。fetch関数を使用してHTTPリクエストを行い、APIキーと認証情報をヘッダーに含めています。
リクエストパラメータに「ルート探索用車種(regulation_type=121200)」を設定しています。
 ```zma_route.js
const routeUrl = `https://test-web.zmaps-api.com/route/route_mbn/drive_ptp?search_type=1&from=${start.lng},${start.lat}&to=${end.lng},${end.lat}&regulation_type=121200`;

fetch(routeUrl, {
    method: 'GET',
    headers: {
        'x-api-key': '[APIキー]',
        'Authorization': 'referer'
    },
})
.then(response => response.json())
.then(data => {
    // ルートデータの処理
})
.catch(error => console.error('ルート検索エラー:', error));
```

#### ５．ルートデータの処理とポリライン描画
APIから返されたルートデータを処理し、ポリラインとして地図上に描画しています。ルートの座標データをZDC.LatLngオブジェクトに変換し、ZDC.Polylineを使用してルートを描画しています。
```zma_route.js
if (data.status === 'OK' && data.result?.item?.length > 0) {
    const routeItem = data.result.item[0];
    const links = routeItem.route?.link;

    if (Array.isArray(links) && links.length > 0) {
        const decodedPath = [];
        links.forEach(link => {
            if (Array.isArray(link.line.coordinates)) {
                link.line.coordinates.forEach(coord => {
                    if (Array.isArray(coord) && coord.length === 2) {
                        decodedPath.push(new ZDC.LatLng(coord[1], coord[0]));
                    }
                });
            }
        });

        if (decodedPath.length > 0) {
            const routeLine = new ZDC.Polyline(
                decodedPath,
                {
                    color: '#008dcb',
                    width: 5,
                    opacity: 0.7
                }
            );
            map.addWidget(routeLine);
        }
    }
}
```

## API比較
### 1. データの精度と更新頻度
**OpenStreetMap**
・オープンソースの地図データを使用
・ボランティアによる更新が主
・更新頻度は場所によって異なる
**ZENRIN Maps API**
・専門の調査員による高精度なデータ収集
・年12回の更新で、鮮度の高い情報を提供

### 2. ルート検索の柔軟性
**OpenStreetMap**
・カスタマイズ性が高い
・様々な条件（道路タイプ、速度制限など）を考慮したルート検索が可能
**ZENRIN Maps API**
・標準的なルート検索に加え、車種による規制や時間帯規制なども考慮可能
・複数の経由地点を設定し、最適な訪問順を算出する機能あり

### 3. 実装の容易さ
**OpenStreetMap**
・Pythonライブラリ（osmnx）を使用するため、比較的簡単に実装可能・基本的なルート検索パラメータのみ設定可能です。
・データの取得から可視化まで一貫して行える
**ZENRIN Maps API**
・JavaScript APIを使用するため、Webアプリケーションへの統合が容易

## まとめ
OpenStreetMapとZENRIN Maps APIは、それぞれ異なる特徴を持っています。
OpenStreetMapはオープンソースで柔軟性が高く、研究や個人プロジェクトに適しています。
一方、ZENRIN Maps APIは高精度なデータと豊富な機能を提供し、商用利用に適しています