ランニングコースAPI比較：Google vs. ZENRIN、開発者が選ぶべきはどっち？

## 要約
本記事では、GoogleMaps APIとZENRINMaps APIを使用してランニングコースを検索し、その結果を比較しました。両APIにはランニング専用モードは存在しませんが、歩行者ルートをカスタマイズすることで実用的なランニングルートを作成できることがわかりました。GoogleMaps APIではwaypointsの活用が重要であり、ZENRINMaps APIでは各種パラメータのカスタマイズが可能です。実装方法、結果の違い、それぞれのAPIの長所と短所を解説し、開発者がプロジェクトに最適なAPIを選択する際の参考となる情報を提供しています。

## はじめに
ランニングコース検索は、健康志向のランナーや効率的なトレーニングを求めるアスリートなど、様々なニーズを持つ人々にとって非常に重要です。

本記事では、ZENRIN Maps APIとGoogle Maps APIを用いて、このランニングコース検索を実装し、それぞれのAPIの特性と実装方法を比較検討します。

### 対象読者
- ランニングアプリの開発に興味がある方
- 経路探索APIの選定に悩んでいる方
- ZENRIN Maps APIとGoogle Maps APIの違いを詳しく知りたい方


## ZENRINMaps APIでのランニングコース実装
### 実装概要
ZENRINMaps 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=6763964e1fd14c054ecd0db7&utm_campaign=6757baf3203e3a00bb118509&utm_medium=else&utm_source=Qiita24)

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

## 実装コード

```JavaScript
var map;
// ルート情報（所要時間、距離、消費カロリー）を表示する関数
function showRouteInfo(rawDuration, rawDistance, rawCalorie) {
    convertTime(rawDuration);
    convertDtn(rawDistance);
    convertCalorie(rawCalorie);
}

// 所要時間を適切な形式で表示する関数
function convertTime(rawDuration) {
    const timeInfoArea = document.getElementById('time');
    if (timeInfoArea) {
        const minutes = Math.round(rawDuration / 60); // 秒 → 分に変換
        timeInfoArea.textContent = `${minutes} 分`;
    } 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 convertCalorie(rawCalorie) {
    const calorieInfoArea = document.getElementById('calorie');
    if (rawCalorie && calorieInfoArea) {
        calorieInfoArea.textContent = `${rawCalorie.toFixed(1)} kcal`;
    } else {
        console.error("消費カロリーの表示エリアが見つかりません。");
    }
}

// ルート検索を実行する関数
function performRouteSearch(origin, destination) {
    const startPoint = `${origin.lng},${origin.lat}`;
    const goalPoint = `${destination.lng},${destination.lat}`;

    const api = "/route/route_mbn/walk";
    const params = {
        search_type: 5,
        from: startPoint,
        to: goalPoint,
        llunit: 'dec',
        datum: 'JGD',
        eta: true,
        speed: 200,
        weight: 70,
        stride: 100,
        step_count: true,
        calorie: true,
        avoid: '1,2'
    };

    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.section.flatMap(section =>
                    section.link.flatMap(link =>
                        link.line.coordinates.map(coord => new ZDC.LatLng(coord[1], coord[0]))
                    )
                );

                // 座標データが存在するか確認
                if (coordinates.length === 0) {
                    console.warn("座標データが存在しません。");
                    return;
                }

                // 地図の範囲を調整
                if (coordinates.length > 0) {
                    // 最初の座標を中心にする
                    const firstCoord = coordinates[0];
                    map.setCenter(new ZDC.LatLng(firstCoord.lat, firstCoord.lng));
                    map.setZoom(15); // 適切なズームレベルを設定
                }

                // ルート情報を取得して表示
                const rawDuration = route.time; // 所要時間
                const rawDistance = route.distance; // 距離
                const rawCalorie = route.calorie; // 消費カロリー
                showRouteInfo(rawDuration, rawDistance, rawCalorie);

                // ポリラインでルートを地図上に描画
                try {
                    const polyline = new ZDC.Polyline(coordinates, {
                        color: '#008dcb',
                        width: 5,
                        opacity: 0.7,
                    });
                    map.addWidget(polyline);
                } catch (error) {
                    console.error("ZDC.Polylineの作成に失敗しました:", error);
                }
            } else {
                console.error("ルート検索に失敗しました。");
            }
        });
    } catch (error) {
        console.error("ルート検索中にエラーが発生しました:", error);
    }
}

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

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

    try {
        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;
        });
    } catch (error) {
        console.error("calculatePolylineBounds関数でエラーが発生しました:", error);
        return null;
    }

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

// ZMALoaderの初期化
ZMALoader.setOnLoad(function (mapOptions, error) {
    if (error) {
        console.error(error);
        return;
    }

    mapOptions.zoom = 17;
    mapOptions.centerZoom = false;

    map = new ZDC.Map(
        document.getElementById('ZMap'),
        mapOptions,
        function () {
            // スタート地点と終点を設定
            const origin = new ZDC.LatLng(35.669, 139.700);
            const destination = new ZDC.LatLng(35.671, 139.714);

            console.log("map:", map);

            performRouteSearch(origin, destination);

            // 地図コントロールを追加
            map.addControl(new ZDC.ZoomButton('bottom-left'));
            map.addControl(new ZDC.Compass('top-right'));
            map.addControl(new ZDC.ScaleBar('bottom-left'));
        },
        function () {
            console.error("地図の生成に失敗しました");
        }
    );
});


```

## 実装結果と課題
- **結果**: 結果: ZENRINMaps APIを使用することで、ランニングに適したルートを地図上に表示できました。速度、体重、歩幅などのパラメータを設定することで、消費カロリーの計算や階段・急な坂を避けるルートの生成が可能となりました。
ZENRINMaps APIで、以下のパラメータ指定によって、ランニングの条件を追加しています

```JavaScript
const params = {
    // 既存のパラメータに加えて
    eta: true,        // 到着予定時刻の計算を有効にする
    speed: 200,       // 移動速度を200m/分（12km/h）に設定（ジョギング程度の速度）
    weight: 70,       // ランナーの体重を70kgと仮定
    stride: 100,      // 歩幅を100cmと設定
    step_count: true, // 歩数（ステップ数）の計算を有効にする
    calorie: true,    // 消費カロリーの計算を有効にする
    avoid: '1,2'      // 1: 階段、2: 急な坂を避ける
};
```
上記のようにパラメータを追加することでランニングコース検索用にカスタマイズしています。
- ランニングに適した速度でのルート計算が可能になります
- ランナーの体重と歩幅を考慮した、より正確な消費カロリーと歩数の計算ができます
- 階段や急な坂を避けることで、ランニングに適したルートを優先的に選択できます
- 到着予定時刻の計算により、ランニングの所要時間を正確に把握できます

- **課題**: ランニング専用モードがないため、歩行者モードをベースにしたカスタマイズが必要です。また、パラメータの最適な設定値には個人差があることが考えられます。


## GoogleMaps APIでのランニングコース実装
### 実装概要
GoogleMaps APIを使用して、出発地点から目的地点までのランニングコースを検索し、地図上に表示する機能を実装しました。WALKINGモードを使用し、waypointsを活用して公園内を通るルートを緯度経度で指定しています。
## 実装コード
```html
    <script>
        function initMap() {
            const map = new google.maps.Map(document.getElementById("map"), {
                center: { lat: 35.465833, lng: 139.622778 },
                zoom: 15,
            });

            const directionsService = new google.maps.DirectionsService();
            const directionsRenderer = new google.maps.DirectionsRenderer();
            directionsRenderer.setMap(map);

            const request = {
                origin: new google.maps.LatLng(35.669, 139.700), // 渋谷駅付近
                destination: new google.maps.LatLng(35.671, 139.714), // 代々木公園付近
                travelMode: google.maps.TravelMode.WALKING,
                waypoints: [
                    { location: new google.maps.LatLng(35.6705, 139.7055), stopover: false }, // 公園入口
                    { location: new google.maps.LatLng(35.6712, 139.7100), stopover: false }, // 公園中央
                    { location: new google.maps.LatLng(35.6700, 139.7120), stopover: false }  // ランニングコース途中
                ],
                optimizeWaypoints: true, // 最適なルートを計算
            };

            directionsService.route(request, (result, status) => {
                if (status === 'OK') {
                    directionsRenderer.setDirections(result);
                    const route = result.routes[0].legs[0];

                    document.getElementById('distance').textContent = '総距離: ' + route.distance.text;
                    document.getElementById('duration').textContent = '所要時間: ' + route.duration.text;

                    const steps = route.steps;
                    const routeSteps = document.getElementById('route-steps');
                    routeSteps.innerHTML = "";
                    steps.forEach(step => {
                        const li = document.createElement('li');
                        li.innerHTML = step.instructions + ' (' + step.distance.text + ')';
                        routeSteps.appendChild(li);
                    });
                } else {
                    console.error("ルート検索に失敗しました: " + status);
                }
            });
        }
    </script>
```

## 実装結果と課題
- **結果**：GoogleMaps APIを使用して、基本的な歩行者ルートを地図上に表示し、距離や所要時間などの情報を提供できました。waypointsを活用することで、公園内を通るランニングコースを作成することができました。

- **課題**：GoogleMaps APIはランニングに特化したオプションを提供していないため、ランナーが求める細かい条件を完全に満たすルート作成は難しい場合があります。


## 地図表示
- **ZENRIN Maps API**
![zma_Running.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3979666/1666241c-d4e1-40df-97e5-802268f6bb32.png)


- **Google Maps API**
![gma_Running.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3979666/7a3ed242-5d36-4383-9ca2-c233e6a77d72.png)




## API比較
- **ランニング対応**: ZENRINMaps APIは、パラメータのカスタマイズによりランニングに適したルート生成が可能です。GoogleMaps APIは、waypointsを活用することで公園内のルートなどを指定できますが、ランニング特有の条件設定は限定的です。

- **情報提供**: ZENRINMaps APIは消費カロリーなどのランナー向け情報を直接提供できます。GoogleMaps APIは基本的な距離と時間情報を提供しますが、ランナー特有の情報は別途計算が必要です。

- **カスタマイズ性**: GoogleMaps APIは豊富なドキュメントと広範なコミュニティサポートがあり、カスタマイズの幅が広いです。ZENRINMaps APIは日本国内に特化した詳細な地図情報を提供しており、日本のランナー向けに最適化しやすい可能性があります。


- **使いやすさ**: GoogleMaps APIは世界中で広く使われており、導入しやすいです。ZENRINMaps APIは日本国内での利用に特化しており、日本語のサポートが充実しています。


## まとめ
本記事では、Google Maps APIとZENRIN Maps APIを使用してランニングコースを検索し、その結果を比較しました。両APIともランニング専用モードは存在しませんが、それぞれ異なるアプローチでランニングに適したルートを作成できることがわかりました。Google Maps APIではwaypointsによるルートのカスタマイズが、ZENRIN Maps APIでは詳細なパラメータ設定によるカスタマイズが可能です。それぞれのAPIの特徴を理解し、プロジェクトの要件や利用シーンに応じて適切なAPIを選択することで、より魅力的なランニングアプリを開発できるでしょう。