ZENRIN Maps API と OpenStreetMap の最適巡回ルートを比較してみた

## 要約
本記事では、**ZENRIN Maps API** と**OpenStreetMap**を使用した経路探索と最適巡回ルートの実装を比較しています。
ZENRIN Maps APIは日本地図に強く、専用のエンドポイントを提供しており、実装が容易です。
一方、OpenStreetMapは世界中で使用されるオープンな地図データを利用し、Leaflet.jsとOSRMを組み合わせて実装しています。
両者とも、出発地から目的地までの最適ルートを計算し、地図上に表示できますが、それぞれに長所と課題があります。


## はじめに

経路探索は地図アプリケーションにおける重要な機能の一つで物流や観光アプリなどで需要の高い機能です。
また、効率化が求められる業界では特に重要視されています。
この記事では、日本地図に強い ZENRIN Maps API と、世界中で使われるオープンな地図データである OpenStreetMap を用いて、最適巡回ルート検索の実装と性能を比較します。

### 対象読者
- Webアプリで地図機能を実装しようとしている開発者
- 日本国内向けアプリで地図APIの選定をしている方
- 経路探索・最適巡回ルートのアルゴリズムに興味がある方
- OpenStreetMap とZENRIN Maps API の特徴・違いを知りたい方


## ZENRIN Maps APIを使用したルート検索

### 実装概要

ZENRIN Maps API には drive_tsp エンドポイントが用意されています。
これにより、複雑なアルゴリズムを実装する必要がなく、複数の経由地を最適な順番で巡るルートを取得可能です。
- 地図の初期化
- 出発地・経由地・目的地の設定
- ZENRIN Maps API の API による 最適巡回ルートの取得
- 地図上への描画とルート情報の表示
ZENRIN Maps APIを使用するためには検証用IDとPW取得が必要です。
お試しIDは下記から簡単に発行できました。
[ZENRIN Maps API　無料お試しID　お申込みフォーム（2か月無料でお試しできます）](https://www.zenrin-datacom.net/solution/zenrin-maps-api/trial?fm_cp=6757baf3203e3a00bb118509&fm_mu=67639226bdef2d04981aaa12&utm_campaign=6757baf3203e3a00bb118509&utm_medium=else&utm_source=Qiita10)

詳細手順は[こちら](https://qiita.com/goldie9/items/1983bf8c4ad796290cbf)を参照しました。

**主な機能：**
- 経由地の順序を自動で最適化
- 所要時間・総移動距離の取得
- JavaScript SDK による容易な地図表示の実装

#### 実装コード

以下は、ZENRIN Maps API を使って「新宿駅 → 経由地3つ → 東京タワー」のルートを描画し、距離・所要時間を表示するコードです。

```html
<script>
    // マップオブジェクトと中心座標の設定
    var map;
    let mapCenter = { lat: 35.68989861111111, lng: 139.75450958333334 };

    // 経由地の座標をカンマ区切りの文字列で定義
    let waypointString = '139.7966564581166,35.71480344840882,139.81071112543816,35.71017592344799,139.7729444161881,35.716797251823365';

    // ルート情報（所要時間と距離）を表示する関数
    function showRouteInfo(rawDuration, rawDistance) {
        convertTime(rawDuration);
        convertDtn(rawDistance);
    }

    // 経由地にマーカーを表示する関数
    function showMarker(waypoints) {
        waypoints.forEach((location, index) => {
            const marker = new ZDC.Marker(
                new ZDC.LatLng(location.lat, location.lng),
                {
                    styleId: ZDC.MARKER_COLOR_ID_RED_L,
                    contentStyleId: ZDC[`MARKER_NUMBER_ID_${index + 1}_L`],
                }
            );
            map.addWidget(marker);
        });
    }

    // 経由地の文字列を解析し、最適化された順序で配列を返す関数
    function parseWaypoints(waypointString, origin, destination, routeorder) {
        const coordinates = waypointString.split(",").map(Number);
        const waypoints = [origin];

        for (let i = 0; i < coordinates.length; i += 2) {
            const lng = coordinates[i];
            const lat = coordinates[i + 1];
            waypoints.push(new ZDC.LatLng(lat, lng));
        }
        optWaypts = waypointOpt(waypoints, routeorder);
        optWaypts.push(destination);
        return optWaypts;
    }

    // 経由地の順序を最適化する関数
    function waypointOpt(waypoints, routeorder) {
        let stringArray = routeorder.split(',');
        let integerArray = stringArray.map(num => parseInt(num, 10));
        integerArray = [0, ...integerArray];
        const optWaypts = [];
        for (let i = 0; i < integerArray.length; i++) {
            optWaypts.push(waypoints[integerArray[i]]);
        }
        return optWaypts
    }

    // 所要時間を適切な形式で表示する関数
    function convertTime(rawDuration) {
        const timeInfoArea = document.getElementById('time');
        if (timeInfoArea) {
            const hours = Math.floor(rawDuration / 60);
            const minutes = (rawDuration % 60).toFixed(0);

            if (hours === 0 && minutes > 0) {
                timeInfoArea.textContent = `${minutes}分`;
            } else if (hours > 0 && minutes === 0) {
                timeInfoArea.textContent = `${hours}時間`;
            } else if (hours > 0 && minutes > 0) {
                timeInfoArea.textContent = `${hours}時間${minutes}分`;
            } else {
                timeInfoArea.textContent = 'すぐに到着します';
            }
        } else {
            console.error("所要時間の表示エリアが見つかりません。");
        }
    }

    // 距離をキロメートル単位で表示する機能
    function convertDtn(rawDistance) {
        const distInfoArea = document.getElementById('dist');
        if (rawDistance && distInfoArea) {
            const distanceInKm = (rawDistance / 1000).toFixed(1);
            distInfoArea.textContent = `${distanceInKm} km`;
        } else {
            console.error("距離の表示エリアが見つかりません。");
        }
    }

    // ルート検索を実行する関数
    function performRouteSearch(origin, destination, waypointString) {
        const startPoint = `${origin.lng},${origin.lat}`;
        const goalPoint = `${destination.lng},${destination.lat}`;
        const api = "/route/route_mbn/drive_tsp";
        const params = {
            search_type: 1,
            from: startPoint,
            to: goalPoint,
            waypoint: waypointString,
        };

        try {
            map.requestAPI(api, params, function (response) {
                if (response.ret && response.ret.status === 'OK') {
                    const route = response.ret.message.result.item[0].route;
                    const coordinates = route.link.flatMap(link =>
                        link.line.coordinates.map(coord => new ZDC.LatLng(coord[1], coord[0]))
                    );

                    const bounds = calculatePolylineBounds(coordinates);
                    if (bounds) {
                        const adjustZoom = map.getAdjustZoom(coordinates, { fix: false });
                        map.setCenter(adjustZoom.center);
                        map.setZoom(adjustZoom.zoom - 0.5);
                    }

                    const routeorder = route.waypoint_order;
                    const rawDuration = route.time;
                    const rawDistance = route.distance;
                    const waypoints = parseWaypoints(waypointString, origin, destination, routeorder);
                    showMarker(waypoints);
                    showRouteInfo(rawDuration, rawDistance);

                    const polyline = new ZDC.Polyline(coordinates, {
                        color: '#1a73e8',
                        width: 5,
                        pattern: 'solid',
                        opacity: 0.8
                    });
                    map.addWidget(polyline);
                } else {
                    console.error("ルート検索に失敗しました。");
                }
            });
        } catch (error) {
            console.error("ルート検索中にエラーが発生しました:", error);
        }
    }

    // ポリラインの範囲を計算する関数
    function calculatePolylineBounds(polylineCoordinates) {
        if (!polylineCoordinates || polylineCoordinates.length === 0) {
            console.log('ポリラインの座標が無効です');
        }

        let minLat = Number.POSITIVE_INFINITY;
        let maxLat = Number.NEGATIVE_INFINITY;
        let minLng = Number.POSITIVE_INFINITY;
        let maxLng = Number.NEGATIVE_INFINITY;

        polylineCoordinates.forEach(point => {
            if (point.lat < minLat) minLat = point.lat;
            if (point.lat > maxLat) maxLat = point.lat;
            if (point.lng < minLng) minLng = point.lng;
            if (point.lng > maxLng) maxLng = point.lng;
        });

        const southWest = new ZDC.LatLng(minLat, minLng);
        const northEast = new ZDC.LatLng(maxLat, maxLng);
        const bounds = new ZDC.LatLngBounds(southWest, northEast);

        return bounds;
    }

    // 地図の読み込み処理
    document.addEventListener('DOMContentLoaded', function() {
        // ZENRIN Maps API Loaderの初期化
        ZENRIN Maps API Loader.setOnLoad(function (mapOptions, error) {
            if (error) {
                console.error(error);
                return;
            }
            mapOptions.center = new ZDC.LatLng(mapCenter.lat, mapCenter.lng);
            mapOptions.zoom = 13;
            mapOptions.centerZoom = false; // ★地図の中心点を中心に拡大縮小する指定
            mapOptions.mouseWheelReverseZoom = true;
            mapOptions.minZoom = 4.5;

            map = new ZDC.Map(
                document.getElementById('ZENRIN Maps API'),
                mapOptions,
                function () {
                    const origin = new ZDC.LatLng(35.690881942542795, 139.6996382651929); // 新宿駅
                    const destination = new ZDC.LatLng(35.658711231010265, 139.74543289660156); // 東京タワー
                    performRouteSearch(origin, destination, waypointString);

                    map.addControl(new ZDC.ZoomButton('bottom-left'));
                    map.addControl(new ZDC.Compass('top-right'));
                    map.addControl(new ZDC.ScaleBar('bottom-left'));
                },
                function () {
                    console.log("APIエラー");
                }
            );
        });
    });
</script>

```
### 実装結果と課題

- **結果**：ZENRIN Maps API の drive_tsp エンドポイントを利用することで、出発地（新宿駅）から東京タワーまで、3つの経由地を最適な順序で巡るルートを地図上に表示できました。
また、総移動距離と所要時間を自動で取得・計算し、画面上にわかりやすく表示することにも成功しました。
経由地には番号付きのマーカーが表示され、ユーザーにも視覚的にルートが把握しやすい構成になっています。
- **課題**：ZENRIN Maps API の仕様上、リクエスト制限や認証の取り扱いに注意が必要で、実運用にあたってはAPIキーの保護やリクエスト回数制限の設計も考慮する必要があります。

## OpenStreetMap を使用したルート検索

### 実装概要
OpenStreetMap （OpenStreetMap ）をベースにしたルート検索システムを実装しました。
以下の構成で経路探索および巡回順の最適化を行っています。
- 地図描画：Leaflet.js による地図表示
- 経路探索：OSRM（Open Source Routing Machine）を使用

以下は主な機能です

- 指定した出発地・目的地・経由地に対して、最適な巡回ルートを計算
- OSRM API による詳細なルート取得（ポリラインで表示）
- 総距離と所要時間の計算・表示
- Leaflet上へのルート線描画とマーカー表示

#### 実装コード

以下は実装したコードです。

```html
<script>
    // マップの初期化
    const map = L.map('map').setView([35.68989861111111, 139.75450958333334], 13);

    // OpenStreetMap のタイルレイヤーを追加
    L.tileLayer('https://{s}.tile.OpenStreetMap .org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.OpenStreetMap .org/copyright">OpenStreetMap </a> contributors',
        maxZoom: 19
    }).addTo(map);

    // 出発点と目的地の座標
    const origin = L.latLng(35.690881942542795, 139.6996382651929); // 新宿駅
    const destination = L.latLng(35.658711231010265, 139.74543289660156); // 東京タワー

    // 経由地の座標を解析
    const waypointString = '139.7966564581166,35.71480344840882,139.81071112543816,35.71017592344799,139.7729444161881,35.716797251823365';
    const coordinates = waypointString.split(",").map(Number);
    const waypoints = [];

    for (let i = 0; i < coordinates.length; i += 2) {
        const lng = coordinates[i];
        const lat = coordinates[i + 1];
        waypoints.push(L.latLng(lat, lng));
    }

    // TSPソルバーを使用して最適な経路を計算する関数
    async function solveTSP(start, end, intermediatePoints) {
        // 全ての点を配列に格納（始点、経由地、終点の順）
        const allPoints = [start, ...intermediatePoints, end];

        // 距離行列を生成するための関数
        function calculateDistance(point1, point2) {
            return point1.distanceTo(point2);
        }

        // 距離行列を生成
        const distanceMatrix = [];
        for (let i = 0; i < allPoints.length; i++) {
            const row = [];
            for (let j = 0; j < allPoints.length; j++) {
row.push(calculateDistance(allPoints[i], allPoints[j]));
            }
            distanceMatrix.push(row);
        }

        // 最近傍法を使用して経路を計算
        const visited = new Array(allPoints.length).fill(false);
        const path = [0]; // 始点から開始
        visited[0] = true;

        // 始点から探索を開始し未訪問の地点の中で最も近い地点を選択
        for (let i = 0; i < allPoints.length - 2; i++) { // 終点は別途処理するため -2
            const current = path[path.length - 1];
            let minDistance = Infinity;
            let nearest = -1;

            // 未訪問の点の中で最も近い点を見つける
            for (let j = 1; j < allPoints.length - 1; j++) { // 始点と終点は除く
if (!visited[j] && distanceMatrix[current][j] < minDistance) {
    minDistance = distanceMatrix[current][j];
    nearest = j;
}
            }

            if (nearest !== -1) {
path.push(nearest);
visited[nearest] = true;
            }
        }

        // 最後に終点を追加
        path.push(allPoints.length - 1);

        // 経路の順序を返す
        return {
            path: path,
            waypoints: path.map(index => allPoints[index])
        };
    }

    // マーカーを表示する関数
    function addMarkers(points) {
        // マーカーアイコンの設定
        const redIcon = L.icon({
            iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
            iconSize: [25, 41],
            iconAnchor: [12, 41],
            popupAnchor: [1, -34]
        });

        // マーカーを追加
        points.forEach((point, index) => {
            const marker = L.marker(point, { icon: redIcon }).addTo(map);
            marker.bindPopup(`Point ${index + 1}`);
        });
    }

    // 所要時間を表示する関数
    function displayTime(minutes) {
        const timeInfoArea = document.getElementById('time');
        if (timeInfoArea) {
            const hours = Math.floor(minutes / 60);
            const mins = Math.round(minutes % 60);

            if (hours === 0 && mins > 0) {
timeInfoArea.textContent = `${mins}分`;
            } else if (hours > 0 && mins === 0) {
timeInfoArea.textContent = `${hours}時間`;
            } else if (hours > 0 && mins > 0) {
timeInfoArea.textContent = `${hours}時間${mins}分`;
            } else {
timeInfoArea.textContent = 'すぐに到着します';
            }
        }
    }

    // 距離を表示する関数
    function displayDistance(meters) {
        const distInfoArea = document.getElementById('dist');
        if (distInfoArea) {
            const km = (meters / 1000).toFixed(1);
            distInfoArea.textContent = `${km} km`;
        }
    }

    // OSRM APIを使用してルートを計算する関数
    async function calculateRoutes(orderedPoints) {
        // OSRM APIのエンドポイント
        const osrmBaseUrl = 'https://router.project-osrm.org/route/v1/driving/';

        // 全ての点を結合して一つのルートとする
        const coordsString = orderedPoints.map(p => `${p.lng},${p.lat}`).join(';');
        const url = `${osrmBaseUrl}${coordsString}?overview=full&geometries=polyline`;

        try {
            const response = await fetch(url);
            const data = await response.json();

            if (data.code === 'Ok' && data.routes && data.routes.length > 0) {
const route = data.routes[0];
const routePolyline = polyline.decode(route.geometry);
const routeCoordinates = routePolyline.map(coords => L.latLng(coords[0], coords[1]));

// ルートを地図に表示
L.polyline(routeCoordinates, {
    color: '#1a73e8',
    weight: 5,
    opacity: 0.8
}).addTo(map);

// 所要時間と距離を表示
const durationInMinutes = route.duration / 60;
const distanceInMeters = route.distance;

displayTime(durationInMinutes);
displayDistance(distanceInMeters);

// 地図の表示範囲を調整
map.fitBounds(routeCoordinates);

return {
    duration: durationInMinutes,
    distance: distanceInMeters
};
            }
        } catch (error) {
            console.error('ルート計算中にエラーが発生しました:', error);
        }

        return null;
    }

    // 最適ルートを計算して表示する関数
    async function displayOptimalRoute() {
        try {
            // 最適な経路を計算
            const result = await solveTSP(origin, destination, waypoints);

            // マーカーを表示
            addMarkers(result.waypoints);

            // ルートを表示
            const routeInfo = await calculateRoutes(result.waypoints);

            if (!routeInfo) {
throw new Error('ルート計算に失敗しました');
            }

            console.log('Route calculated:', routeInfo);
        } catch (error) {
            console.error('最適ルート計算中にエラーが発生しました:', error);
            document.getElementById('time').textContent = 'エラー';
            document.getElementById('dist').textContent = 'エラー';
        }
    }

    // ページ読み込み時に最適ルートを表示
    document.addEventListener('DOMContentLoaded', displayOptimalRoute);
</script>
```
### 実装結果と課題

- **結果**：Leaflet 上に出発地から目的地までのルートを、複数の経由地を含めて正しく描画できました。
経路の取得にはOSRM（Open Source Routing Machine）を用い、走行距離および所要時間の表示も行えています。
また、経由地の巡回順を最適化しました。
- **課題**：現在採用しているアルゴリズムは最近傍法（Nearest Neighbor）であり、厳密な最適解ではなく近似解が得られます。
そのため、経由地が多くなるにつれて精度が下がり、処理時間も増加する傾向があります。

## 地図表示
- **ZENRIN Maps API**
![ZMA_tps.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3979666/c3fb511e-a309-4e97-8abe-0fcf699c0e38.png)

- **OpenStreetMap**
![OSM_tps.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3979666/e67b1cdd-255e-4d27-aa7f-ab910b7f3dde.png)


## API比較
### 実装の容易さ
ZENRIN Maps API は公式ドキュメントが充実しており、エンドポイントの使い方が直感的であるため、実装が比較的簡単です。一方、OpenStreetMap はオープンソースであり、カスタマイズの自由度が高いですが、その分設定や利用にはやや手間がかかる場合があります。
### 日本の道路精度
ZENRIN Maps API は日本国内向けのデータに特化しており、精度が高いとされています。特に都市部では非常に正確な道路情報が提供されるため、日本の地図精度に関しては優れています。OpenStreetMap も十分高精度ですが、更新頻度や地域によって差が出ることがあります。（特に地方部ではデータの精度にばらつきが見られる場合があります。）
### カスタマイズ性
OpenStreetMap はオープンソースであるため、地図データのカスタマイズや拡張が可能です。ユーザーが独自にデータを追加したり（例えば特定の店舗情報や観光地データなど）、特定の条件に基づくカスタマイズがしやすい点で優れています。ZENRIN Maps API はカスタマイズ性ではOpenStreetMap に及ばないものの、特定の日本のニーズに応じたデータ提供が整っているため、標準的な利用には十分です。
### コスト
ZENRIN Maps API は商用サービスであり、使用量や機能に応じてコストがかかります。OpenStreetMap は基本的に無料で利用できるものの、商用利用や高負荷時の利用には有料のホスティングサービスを利用する必要があり、運用コストが発生する場合があります。
### 2つのスクリプト間で所要時間に大きな差が生じる理由
- **APIと地図データの差異：** ZENRIN Maps API は日本特化型の商用APIで、詳細な道路情報と交通状況を反映しています。OpenStreetMap はボランティアによるオープンデータで、交通量や日本の特有の道路状況に関する情報が限定的です。
- **経路最適化アルゴリズムの違い：** OpenStreetMap のルーティングエンジン（OSRM）は単純な距離計算に基づいており、交差点での待ち時間や信号、渋滞などを考慮していません。一方、ZENRIN Maps API は実際の交通量や時間帯による変動を反映した経路最適化を行います。
- **交通状況とルート特性の考慮：** ZENRIN Maps API は時間帯別の渋滞や信号の待機時間、道路種別に基づく速度推定を行い、実際の運転時間を反映しています。OpenStreetMap はこれらの詳細を考慮していないため、単純化された結果となり、所要時間が短くなる傾向があります。

## まとめ
日本国内向けで高精度なナビゲーションを求める場合、ZENRIN Maps API が優れた選択肢です。
一方で、世界展開を視野に入れたい場合や自由度・コストを重視する場合は、OpenStreetMap + OSRM の組み合わせも有力です。
実装のしやすさでは ZENRIN Maps API に軍配が上がりますが、OpenStreetMap はカスタマイズ性や拡張性が高く、独自の要件に応じた柔軟な対応が可能です。
両者にはそれぞれ明確な強みがあるため、目的やプロジェクトの要件に応じた選定が重要になります。