Google Maps API: たくさんの経由地点を含めてルート検索する

Google Maps APIにはDirectionsServiceというルート検索用のオブジェクトが用意されていて、出発地と目的地を指定してルート検索を行うことができます。

この文書では、途中の経由地点(中継地点)を指定してルート検索する簡単な方法と、たくさんの経由地点が必要な場合の方法を説明します。


## 簡単な方法 (経由地点の数が8以下の場合)

DirectionsServiceのルート検索では、途中の経由地点(中継地点)を「waypoints」として指定することができます。

(下記では出発地(origin), 目的地(destination), 経由地点(waypoints)とも緯度経度(LatLng)で指定していますが、「東京駅」などの文字列でも指定できます)

```html
<!DOCTYPE html>
<html>
<head><style>html, body, #map { width: 100%; height: 100%; }</style></head>
<body>
<div id="map"></div>

<script>
function initMap() {
    // ルート検索の条件
    var request = {
        origin: new google.maps.LatLng(35.681382,139.766084), // 出発地
        destination: new google.maps.LatLng(34.73348,135.500109), // 目的地
        waypoints: [ // 経由地点(指定なしでも可)
            { location: new google.maps.LatLng(35.630152,139.74044) },
            { location: new google.maps.LatLng(35.507456,139.617585) },
            { location: new google.maps.LatLng(35.25642,139.154904) },
            { location: new google.maps.LatLng(35.103217,139.07776) },
            { location: new google.maps.LatLng(35.127152,138.910627) },
            { location: new google.maps.LatLng(35.142365,138.663199) },
            { location: new google.maps.LatLng(34.97171,138.38884) },
            { location: new google.maps.LatLng(34.769758,138.014928) },
        ],
        travelMode: google.maps.DirectionsTravelMode.WALKING, // 交通手段(歩行。DRIVINGの場合は車)
    };

    // マップの生成
    var map = new google.maps.Map(document.getElementById("map"), {
        center: new google.maps.LatLng(35.681382,139.766084), // マップの中心
        zoom: 7 // ズームレベル
    });

    var d = new google.maps.DirectionsService(); // ルート検索オブジェクト
    var r = new google.maps.DirectionsRenderer({ // ルート描画オブジェクト
        map: map, // 描画先の地図
        preserveViewport: true, // 描画後に中心点をずらさない
    });
    // ルート検索
    d.route(request, function(result, status){
        // OKの場合ルート描画
        if (status == google.maps.DirectionsStatus.OK) {
            r.setDirections(result);
        }
    });
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap" async defer></script>
</body>
</html>
```

※ YOUR_API_KEY の部分は GoogleのAPIキーを取得して設定してください。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/782/9e7ae03d-648c-5b94-9a86-11c1f1115e16.png)


## 経由地点の数が8より多い場合

上記の方法は簡単ですが、「ウォーキングマップを作りたい」などと思ったときに経由地点が8地点より多くなる場合だと上記の方法では「Too many waypoints in the request (地点数). The maximum allowed waypoints for this request is 8, plus the origin, and destination.」(検索条件に経由地点が多すぎます。検索条件に許可される経由地点の最大数は8つ + 出発地と目的地です)というエラーになってしまいます。

経由地点が8つより多い場合は、「地点を出発地 + 経由地点8つ + 出発地の最大10で区切って検索してから最後にまとめる」ことで回避することができます。

```html
<!DOCTYPE html>
<html>
<head><style>html, body, #map { width: 100%; height: 100%; }</style></head>
<body>
<div id="map"></div>

<script>
function initMap() {
    // たくさんの地点...
    var points = [
        new google.maps.LatLng(35.681382,139.766084),
        new google.maps.LatLng(35.630152,139.74044),
        new google.maps.LatLng(35.507456,139.617585),
        new google.maps.LatLng(35.25642,139.154904),
        new google.maps.LatLng(35.103217,139.07776),
        new google.maps.LatLng(35.127152,138.910627),
        new google.maps.LatLng(35.142365,138.663199),
        new google.maps.LatLng(34.97171,138.38884),
        new google.maps.LatLng(34.769758,138.014928),
        new google.maps.LatLng(34.703741,137.734442),
        new google.maps.LatLng(34.762811,137.381651),
        new google.maps.LatLng(34.96897,137.060662),
        new google.maps.LatLng(35.170694,136.881637),
        new google.maps.LatLng(35.315705,136.685593),
        new google.maps.LatLng(35.314188,136.290488),
        new google.maps.LatLng(34.985458,135.757755),
        new google.maps.LatLng(34.73348,135.500109),
    ];

    // マップの生成
    var map = new google.maps.Map(document.getElementById("map"), {
        center: new google.maps.LatLng(35.681298, 139.766247), // マップの中心
        zoom: 7 // ズームレベル
    });

    // 地点を分割してルート検索を行います。

    var d = new google.maps.DirectionsService(); // ルート検索オブジェクト
    var origin = null, waypoints = [], dest = null; // 出発地、経由地、目的地
    var resultMap = {}; // 分割してルート検索した結果データ
    var requestIndex = 0; // 検索番号
    var done = 0; // ルート検索が完了した数
    for (var i = 0, len = points.length; i < len; i++) {
        // 最初の場合、originに値をセット
        if (origin == null) {
            origin = points[i];
        }
        // 経由地が8つたまったか最後の地点の場合、ルート検索
        else if (waypoints.length == 8 || i == len - 1) {
            dest = points[i];

            (function(index){
                // ルート検索の条件
                var request = {
                    origin: origin, // 出発地
                    destination: dest, // 目的地
                    waypoints: waypoints, // 経由地
                    travelMode: google.maps.DirectionsTravelMode.WALKING, // 交通手段(歩行。DRIVINGの場合は車)
                };
                console.log(request);
                // ルート検索
                d.route(request, function(result, status){
                    // OKの場合ルートデータ保持
                    if (status == google.maps.DirectionsStatus.OK) {
                        resultMap[index] = result; // 並行でリクエストするので配列だとリクエスト順とずれる場合があります
                        done++;
                    }
                    else {
                        console.log(status); // デバッグ
                    }
                });
            })(requestIndex);

            requestIndex++;
            origin = points[i]; // 今回の目的地を次回の出発地にします。
            waypoints = [];
        }
        // 上記以外、waypointsに地点を追加
        else {
            waypoints.push({ location: points[i], stopover: true });
        }
    }

    // マーカーを表示する場合の準備
    var infoWindow = new google.maps.InfoWindow();
    var mark = function(position, content) {
        var marker = new google.maps.Marker({
            map: map, // 描画先の地図
            position: position // 座標
        });
        // クリック時吹き出しを表示
        marker.addListener("click", function(){
            infoWindow.setContent(content);
            infoWindow.open(map, marker);
        });
    };

    var sid = setInterval(function(){
        // 分割したすべての検索が完了するまで待ちます。
        if (requestIndex > done) return;
        clearInterval(sid);

        // すべての結果のルート座標を順番に取得して平坦な配列にします。
        var path = [];
        var result;
        for (var i = 0, len = requestIndex; i < len; i++) {
            result = resultMap[i]; // 検索結果
            var legs = result.routes[0].legs; // Array<DirectionsLeg>
            for (var li = 0, llen = legs.length; li < llen; li++) {
                var leg = legs[li]; // DirectionLeg
                var steps = leg.steps; // Array<DirectionsStep>
                // DirectionsStepが持っているpathを取得して平坦(2次元配列→1次元配列)にします。
                var _path = steps.map(function(step){ return step.path })
                    .reduce(function(all, paths){ return all.concat(paths) });
                path = path.concat(_path);

                // マーカーが必要ならマーカーを表示します。
                mark(leg.start_location, leg.start_address);
            }
        }
        // マーカーが必要ならマーカーを表示します。(最終)
        var endLeg = result.routes[0].legs[result.routes[0].legs.length-1];
        mark(endLeg.end_location, endLeg.end_address);

        // パスを描画します。
        var line = new google.maps.Polyline({
            map: map, // 描画先の地図
            strokeColor: "#2196f3", // 線の色
            strokeOpacity: 0.8, // 線の不透明度
            strokeWeight: 6, // 先の太さ
            path: path // 描画するパスデータ
        });
    }, 1000);
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap" async defer></script>
</body>
</html>
```

![2.png](https://qiita-image-store.s3.amazonaws.com/0/782/4bd33ace-1005-305e-8fb5-46abc45b6c0f.png)

※ YOUR_API_KEY の部分は GoogleのAPIキーを取得して設定してください。

※ この方法でも無制限に検索できるわけではなく、Google Maps APIのDirectionsServiceでは一気に検索できるのが10回までに制限されているため、検索可能な最大地点数は出発地と目的地を含めて91地点までになります。(10地点分検索した後、最後の10地点目を次の検索の出発地にする必要があるので 10 + 9 × 9 = 91になる)

※ この方法ではDirectionsRendererが使えなくなるので、自力でパス描画やマーカーの処理を行っています。(1つの結果しかDirectionsRendererにセットできないため。ただ、DirectionsRendererはカスタマイズできる部分があまり多くないので、独自で処理を作成したほうが融通がききやすいです)

## 出発地、経由地点、目的地の合計が91より多い場合

「91では足りない」というケースはあまり無い気がしますが、そのようなケースになったときは「「経由地点の数が8より多い場合」のコード内に、DirectionsServiceが返してきた結果をJSON形式で保存するコードをデバッグで挟んでおいて、保存できたらそれ以降は保存したデータをキャッシュとして使う」という形がとれます。(「とれます」といっても「とれなくもない」というレベルで、だいぶ無理矢理な部類です)

サンプルコードが書きづらいのですが、DirectionsServiceのroute()で返ってきた結果を「[How to save the output of a console.log(object) to a file? - StackOverflow](http://stackoverflow.com/questions/11849562/how-to-save-the-output-of-a-console-logobject-to-a-file)」の回答(console.save())ような形でJSONで保存して、保存した結果データをAjaxで取得してルート描画するようなイメージです。

※ 結果 (DirectionsResult) のデータは巨大なので、pathだけ抜き出すなど工夫したほうがよいです。

これだと最終的に「[Google Maps API無償版のポリシー変更](http://internet.watch.impress.co.jp/docs/special/1029218.html)」 のような無償版アカウントのDirections APIリクエスト回数上限(1日2500回)も少し回避できますが、ここまでやるのであれば素直に有償にするか代替サービスを検討したほうがよい気がします。