GoogleMapを使ってサイクリングに出かけよう

タイトルからは、何の技術の話かわからないですね。
GoogleMapにナビゲーション機能ってありますよね、それをサイクリングでナビゲートしてもらおうというものです。
サイクリング中は、手が使えないので、音声ナビがあると便利です。
ですが、ポケストップを探すたびに、立ち止まってスマホを見るのは手間ですし、軽快ではありません。

そこで、(ポケストップに限りませんが)サイクリング前に、途中のチェックポイントを複数覚えておいて、チェックポイントまでの道のりをナビゲートしてもらって、到着したら、ポケストップのアイテムをもらったり、コンビニで休憩したのち、次のチェックポイントを目指す、というものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/6d8f8eec-26e7-3520-d0f0-e41e25bf1481.png)

# 原理の説明：GoogleMapの機能

GoogleMapは、外部のアプリやブラウザから起動させることができます。
その際に、パラメータの指定によって、場所の表示だけでなく、出発地点と目的地を指定して、ナビゲーションを開始させることもできます。

ですので、まずはブラウザから、サイクリングで回りたいチェックポイントのリストを作って、GoogleMapを起動して次のチェックポイントまでナビゲーションしてもらいます。
チェックポイントまで到着したら、またブラウザを立ち上げて、今度は次のチェックポイントを指定してGoogleMapを起動させる、これを最後の目的地まで繰り返すわけです。

# GoogleMapのナビゲーション機能

ナビゲーション中は、以下のことを適宜音声で教えてくれます。

・予定通りの道を進んでいるかどうか
・次の曲がり角まで何メートルか
・今曲がるべき曲がり角か
・予定の道を外れたか
・目的地の近くに来たか
・目的地に着いたか

上記は、ナビを開始すると、設定が選べるようになり、「詳しい音声案内」のスイッチをOnにした場合です。Offの場合はもうちょっと少ない気がします。

もろもろGitHubに上げておきました。

poruruba/orientation_navigator
　https://github.com/poruruba/orientation_navigator


# 画面説明

ブラウザを起動するとこんな感じの画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/349b57b1-fa10-4e4c-88e3-49308182005d.png)

まずは、チェックポイントを追加します。タブ「チェックポイント」を選択し、チェックポイントを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f60e67e9-bed1-7772-63c1-c15d078d91ae.png)

GoogleMapがはめ込まれて表示されるので、出発地点を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/66391dbe-6c5d-1cf4-d892-682a8bdb4535.png)

さらに、同じように次のチェックポイントを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/bc3badd1-274b-ca06-d375-ca9db1a9e9fc.png)
 
経由地っていうチェックボックスがあります。通常は次のチェックポイントに到着するとGoogleMapのナビゲーションが終わってしまうのですが、経由地は次のチェックポイントまでの途中の経由地であって、経由地を通過しただけでは、GoogleMapのナビゲーションは終わらないようにしています。
ちなみに経由地は、ネイティブのGoogleMapの場合は9箇所、ブラウザの場合は1箇所のみ指定できます。
オリエンテーションタブを選択すると、マーキングされているのがわかります。
これで準備完了です。
さっそく、「オリエンテーション開始」ボタンを押下してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e4f82184-6618-4960-d5a7-3fb69fc3df2c.png)

そうすると、こんな感じでGoogleMapが立ち上がり、ナビゲーション開始待ちとなります。
ちなみに、PCのChromeブラウザからの画面ですが、Androidから使うと、ブラウザのGoogleMapか、ネイティブのGoogleMapアプリ、どちらを起動するかの選択肢が出てきます。もちろん、ネイティブのGoogleMapアプリの方がナビゲーションとしては使い勝手が良いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/860652f7-3ebc-d0fe-2c74-ba2a7a6b711d.png)

あとは、開始してしまえば、いつものナビゲーションが始まります。
イヤホンで、GoogleMusic（Youtube Music）でも聞きながら、サイクリングしましょう。

チェックポイントに到着したら、もう一度ブラウザに戻りましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a6f00aa1-7cb8-03a9-738d-4ff067669491.png)

「チェックポイントに到着しましたか？」ボタンを押下すると、次のチェックポイントに出発のボタンに代わりますので、押下すると、またGoogleMapが立ち上がります。今度は、1つ目のチェックポイントから、2つ目のチェックポイントへのナビゲーションです。

おおよそ、イメージはつかめましたでしょうか？

# マイスポット機能

毎度毎度、場所を選択するのはめんどうです。特に家の周りはいつものコースを決めていますが、毎度ポケストップを指定するのは面倒です。
そこで、あらかじめよくいくスポットをマイスポットとして登録しておけば、それを選択するだけで、チェックポイントに追加されるようになります。

マイスポットのタブを選択して、登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/703719cf-2704-0548-25b8-6715d7d10e17.png)

そうすると、こんな感じで、チェックポイント登録する際に、マイスポットから選択することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f75509b5-bf15-affc-5582-59a341ba6a3e.png)

#サーバ同期

実は、チェックポイントを追加したり、マイスポットを追加したりしたら、サーバ側にデータを保持するようにしています。ですので、ブラウザを立ち上げなおしても、以前の状態が復元されるようにしています。また、ナビゲーション中にチェックポイントに到達したりした時もサーバ側に同期するようにしています。

とはいってもクライアント・サーバいずれもかなり手抜きしています。
クライアント側は、Vueのwatchを使って、対象のデータが変更されたら、サーバに一括アップロードしているだけです。
取得も、ブラウザでの起動時だけです。

# GoogleMap起動のURL生成

大事なところをピックアップしました。
travelmode、origin、destination、waypointsを指定しているのがわかります。

travelmode
　歩きの移動か、車の移動か、電車の移動かを指定します。自転車がありますが、日本では使えないようです。

origin
　出発地点です。ブラウザで設定したチェックポイントに相当します。経由地ではありません。

destination
　これもブラウザで設定したチェックポイントですが、originのチェックポイントの次のチェックポイントです。

waypoints
　これが経由地です。

```js:start.js
            // GoogleMap起動のパラメータを生成
            var params = "";
            var origin = this.checkpoints[this.origin_index];
            params += "&travelmode=" + this.travelmode;
            params += "&origin=" + encodeURIComponent(origin.lat + ',' + origin.lng);
            if( destination_index <= (this.checkpoints.length - 1)){
                var destination = this.checkpoints[destination_index];
                params += "&destination=" + encodeURIComponent(destination.lat + ',' + destination.lng);
            }
            if( (this.origin_index + 1) < destination_index ){
                var waypoints = "";
                for( var i = (this.origin_index + 1) ; i < destination_index ; i++ ){
                    if( i != (this.origin_index + 1) ) waypoints += '|';
                    waypoints += this.checkpoints[i].lat + ',' + this.checkpoints[i].lng;
                }
                params += "&waypoints=" + encodeURIComponent(waypoints);
            }
            var href = 'https://www.google.com/maps/dir/?api=1' + params;
            console.log(href);
            this.destination_completed = false;
            // GoogleMapを起動
            window.open(href, '_blank');
```

詳しくは以下を参照してください。

GoolgeMap Developers Guide Univarsal cross-platform syntax
　https://developers.google.com/maps/documentation/urls/guide?hl=ja#directions-action

# ソース一式

Javascriptのソースです。


```js:start.js
'use strict';

//var vConsole = new VConsole();

const default_lat = 35.465878;
const default_lng = 139.622329;
const base_url = "http://localhost:10080";

var vue_options = {
    el: "#top",
    data: {
        progress_title: '', // for progress-dialog

        origin_index: 0, // 出発のインデックス
        destination_completed : true, // チェックポイントに到着したかどうか
        checkpoints: [], // チェックポイントのリスト
        map_markers: [], // Map1に配置のマーカ
        myspots: [], // マイスポットのリスト
        dialog_params: {}, // モーダルダイアログの入出力パラメタ
        map2_markers: [], // Map2に配置のマーカ
        default_latlng: new google.maps.LatLng(default_lat, default_lng), // デフォルトのロケーション(現在地に上書き)
        travelmode: 'walking', // GoogleMapに指定するtravelmode
    },
    computed: {
        // ボタンに表示するテキスト
        orientation_text: function(){
            if( !this.destination_completed )
                return 'チェックポイントに到着しましたか？';

            if( this.origin_index == 0 )
                return 'オリエンテーション開始';
            else if( this.origin_index >= (this.checkpoints.length - 1) )
                return '最終目的地に到着しました。';
            else
                return '次のチェックポイントに出発';
        }
    },
    watch: {
        checkpoints: function(){
            // Mapに配置のマーカを再設定
            for( var i = 0 ; i < this.map_markers.length; i++ )
                this.map_markers[i].setMap(null);
            this.map_markers = [];
            for( var i = 0 ; i < this.checkpoints.length ; i++ ){
                var latlng = new google.maps.LatLng(this.checkpoints[i].lat, this.checkpoints[i].lng); 
                var mopts = {
                    position: latlng,
                    map: this.map,
                    label: String(i + 1)
                };
                var marker = new google.maps.Marker(mopts);
                this.map_markers.push(marker);
            }

            try{
                // サーバに同期
                update_data('checkpoints', this.checkpoints);
            }catch(error){
                this.toast_show("サーバにデータをアップロードできませんでした。");
            };
        },
        myspots: function(){
            try{
                // サーバに同期
                update_data('myspots', this.myspots);
            }catch(error){
                this.toast_show("サーバにデータをアップロードできませんでした。");
            };
        },
        origin_index: function(){
            try{
                // サーバに同期
                update_data('orientation', { origin_index: this.origin_index, destination_completed: this.destination_completed });
            }catch(error){
                this.toast_show("サーバにデータをアップロードできませんでした。");
            };
        },
        destination_completed: function(){
            try{
                // サーバに同期
                update_data('orientation', { origin_index: this.origin_index, destination_completed: this.destination_completed });
            }catch(error){
                this.toast_show("サーバにデータをアップロードできませんでした。");
            };
        },
    },
    methods: {
        // デフォルトのロケーションに移動
        map_goto_current_location: function(){
            var latlng = this.default_latlng;
            this.map.setCenter(latlng);                
        },
        // オリエンテーションタブ選択時にデフォルトのロケーションまたは出発位置に移動
        orientation_update_view: function(){
            var latlng = this.default_latlng;
            if( this.checkpoints.length > 0 )
                latlng = new google.maps.LatLng(this.checkpoints[this.origin_index].lat, this.checkpoints[this.origin_index].lng);
            this.map.setCenter(latlng);                
        },
        // オリエンテーションを指定位置からリスタート
        orientation_restart: function(index){
            if( index < 0 )
                if( !window.confirm('本当に最初から初めてもいいですか？') )
                    return;

            this.origin_index = (index < 0) ? 0 : index;
            this.destination_completed = true;
            this.orientation_next();
        },
        // 次の目的地(経由地を除く)を取得
        get_next_destination: function(){
            var destination_index = this.origin_index + 1;
            for( ; destination_index < this.checkpoints.length ; destination_index++ )
                if( !this.checkpoints[destination_index].waypoint )
                    break;
            if( destination_index >= this.checkpoints.length )
                destination_index = this.checkpoints.length - 1;
            return destination_index;
        },
        // 次へのボタンを押下
        orientation_next: function(){
            if( this.checkpoints.length == 0 ){
                alert('チェックポイントを追加してください。');
                return;
            }else if( this.checkpoints.length == 1 ){
                alert('次のチェックポイントを追加してください。');
                return;
            }
            
            if( this.origin_index >= (this.checkpoints.length - 1) ){
                alert('すでに目的地に到達しています。');
                return;
            }

            var destination_index = this.get_next_destination();
            if( (destination_index - (this.origin_index + 1)) > 9){
                alert('経由地の数が多すぎます。(9以下)');
                return;
            }

            if( !this.destination_completed ){
                // チェックポイントに到達
                this.origin_index = destination_index;
                this.destination_completed = true;

                // 目的位置に到達
                if( destination_index >= (this.checkpoints.length - 1) )
                    this.dialog_open('#orientation_complete_dialog');

                return;
            }

            // GoogleMap起動のパラメータを生成
            var params = "";
            var origin = this.checkpoints[this.origin_index];
            params += "&travelmode=" + this.travelmode;
            params += "&origin=" + encodeURIComponent(origin.lat + ',' + origin.lng);
            if( destination_index <= (this.checkpoints.length - 1)){
                var destination = this.checkpoints[destination_index];
                params += "&destination=" + encodeURIComponent(destination.lat + ',' + destination.lng);
            }
            if( (this.origin_index + 1) < destination_index ){
                var waypoints = "";
                for( var i = (this.origin_index + 1) ; i < destination_index ; i++ ){
                    if( i != (this.origin_index + 1) ) waypoints += '|';
                    waypoints += this.checkpoints[i].lat + ',' + this.checkpoints[i].lng;
                }
                params += "&waypoints=" + encodeURIComponent(waypoints);
            }
            var href = 'https://www.google.com/maps/dir/?api=1' + params;
            console.log(href);
            this.destination_completed = false;
            // GoogleMapを起動
            window.open(href, '_blank');
        },

        // Map2のマーカをクリアし、指定場所に移動
        map2_cleanup: function(latlng){
            for( var i = 0 ; i < this.map2_markers.length; i++ )
                this.map2_markers[i].setMap(null);
            this.map2_markers = [];
            if( this.map2_default_marker ){
                this.map2_default_marker.setMap(null);
                this.map2_default_marker = null;
            }
            this.map2.setCenter(latlng);
        },
        // モーダルダイアログの結果処理
        dialog_submit: function(){
            if( this.dialog_params.title == 'マイスポットの追加' ){
                var location = this.map2.getCenter();
                var name = this.dialog_params.name;
                this.myspots.push({
                    name, lat: location.lat(), lng: location.lng()
                });
            }else if( this.dialog_params.title == 'チェックポイントの追加' ){
                var location = this.map2.getCenter();
                var name = this.dialog_params.name;
                this.checkpoints.push({
                    name, lat: location.lat(), lng: location.lng()
                });
            }else if( this.dialog_params.title == 'マイスポットの変更'){
                var location = this.map2.getCenter();
                this.myspots[this.dialog_params.index].lat = location.lat();
                this.myspots[this.dialog_params.index].lng = location.lng();
            }else if( this.dialog_params.title == 'チェックポイントの変更' ){
                var location = this.map2.getCenter();
                this.checkpoints[this.dialog_params.index].lat = location.lat();
                this.checkpoints[this.dialog_params.index].lng = location.lng();
            }
            this.dialog_close('#select_location_dialog');
        },

        // マイスポットの追加(地図から)のためのモーダルダイアログ表示
        do_myspot_append: function(){
            this.map2_cleanup(this.default_latlng);
            this.map2_default_marker = new google.maps.Marker({
                position: this.default_latlng,
                map: this.map2,
            });

            this.dialog_params = {
                title: 'マイスポットの追加',
                is_input_name: true,
                is_input_submit: true,
            };
            this.dialog_open('#select_location_dialog');
        },
        // マイスポットの削除
        do_myspot_delete: function(index){
            if( !window.confirm('本当に削除していいですか？') )
                return;

            Vue.delete(this.myspots, index);
        },
        // マイスポットの名前変更
        do_myspot_change_name: function(index){
            var name = window.prompt('新しい名前', this.myspots[index].name);
            if( !name )
                return;
            this.myspots[index].name = name;
        },
        // マイスポットのロケーション変更
        do_myspot_change_location: function(index){
            var latlng = new google.maps.LatLng(this.myspots[index].lat, this.myspots[index].lng);
            this.map2_cleanup(latlng);
            this.map2_default_marker = new google.maps.Marker({
                position: latlng,
                map: this.map2,
            });

            this.dialog_params = {
                title: 'マイスポットの変更',
                index: index,
                is_input_submit: true,
            };
            this.dialog_open('#select_location_dialog');
        },

        // チェックポイントリストのリセット
        do_checkpoints_reset: function(){
            if( !window.confirm('本当にリセットしていいですか？') )
                return;
            this.origin_index = 0;
            this.destination_completed = true;
            this.checkpoints = [];
        },
        // チェックポイントの追加(マイスポットから)のためのモーダルダイアログ表示
        do_checkpoint_append_myspot: function(){
            if( this.myspots.length == 0 ){
                alert('マイスポットが登録されていません。');
                return;
            }

            this.map2_cleanup(this.default_latlng);

            this.dialog_params = {
                title: 'チェックポイントの追加(マイスポット)',
            };
            var _this = this;
            for( var i = 0 ; i < this.myspots.length ; i++ ){
                var mopts = {
                  position: new google.maps.LatLng(this.myspots[i].lat, this.myspots[i].lng),
                  map: this.map2,
                };
                var marker = new google.maps.Marker(mopts);
                this.map2_markers.push(marker);
                marker.addListener('click', function(e){
                    for( var i = 0 ; i < _this.map2_markers.length ; i++ ){
                        if( _this.map2_markers[i] == this ){
                            _this.checkpoints.push(_this.myspots[i]);
                            _this.dialog_close('#select_location_dialog');
                            return;
                        }
                    }
                });
            }
            this.dialog_open('#select_location_dialog');
        },
        // チェックポイント追加(地図から)のためのモーダルダイアログ表示
        do_checkpoint_append: function(){
            this.map2_cleanup(this.default_latlng);
            this.map2_default_marker = new google.maps.Marker({
                position: this.default_latlng,
                map: this.map2,
            });

            this.dialog_params = {
                title: 'チェックポイントの追加',
                is_input_name: true,
                is_input_submit: true,
                name : (this.checkpoints.length == 0) ? '現在地' : '',
            };
            this.dialog_open('#select_location_dialog');
        },
        // チェックポイントの削除
        do_checkpoint_delete: function(index){
            if( !window.confirm('本当に削除していいですか？') )
                return;

            Vue.delete(this.checkpoints, index);
        },
        // チェックポイントの名前変更
        do_checkpoint_change_name: function(index){
            var name = window.prompt('新しい名前', this.checkpoints[index].name);
            if( !name )
                return;
            this.checkpoints[index].name = name;
        },
        // チェックポイントのロケーション変更
        do_checkpoint_change_location: function(index){
            var latlng = new google.maps.LatLng(this.checkpoints[index].lat, this.checkpoints[index].lng);
            this.map2_cleanup(latlng);
            this.map2_default_marker = new google.maps.Marker({
                position: latlng,
                map: this.map2,
            });

            this.dialog_params = {
                title: 'チェックポイントの変更',
                index: index,
                is_input_submit: true,
            };
            this.dialog_open('#select_location_dialog');
        },
        // チェックポイントの順番変更
        do_checkpoint_change_index: function(index, event){
            var newIndex = event.target.selectedIndex;
            var temp = this.checkpoints[index];
            Vue.set(this.checkpoints, index, this.checkpoints[newIndex]);
            Vue.set(this.checkpoints, newIndex, temp);
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        // 現在地情報の取得
        navigator.geolocation.getCurrentPosition((pos) =>{
            this.default_latlng = new google.maps.LatLng(pos.coords.latitude, pos.coords.longitude);
            this.map_goto_current_location();
        }, (error) =>{
            this.toast_show("現在地を取得できませんでした。");
        });

        // Mapの生成
        var myOptions = { 
          zoom: 15, 
          center: this.default_latlng, 
          mapTypeId: google.maps.MapTypeId.ROADMAP,
          mapTypeControl: false,
          streetViewControl: false,
        }; 
        var canvas = $('#map_canvas')[0];
        this.map = new google.maps.Map(canvas, myOptions);

        // Map2(モーダルダイアログ用)の生成
        var canvas2 = $('#map_canvas2')[0];
        this.map2 = new google.maps.Map(canvas2, myOptions);
        google.maps.event.addListener(this.map2, 'center_changed', () =>{
            if( !this.map2_default_marker )
                return;
            var location = this.map2.getCenter();
            this.map2_default_marker.setPosition(location);
        });

        // サーバ保持データの取得
        get_data('myspots')
        .then(data =>{
            this.myspots = data;
            return get_data('checkpoints');
        })
        .then(data => {
            this.checkpoints = data;
            return get_data('orientation');
        })
        .then(data => {
            if( data.origin_index != undefined )
                this.origin_index = data.origin_index;
            if( data.destination_completed != undefined )
                this.destination_completed = data.destination_completed;
        })
        .catch(error =>{
            this.toast_show("サーバからデータを取得できませんでした。");
        });
    }
};
vue_add_methods(vue_options, methods_bootstrap);
vue_add_components(vue_options, components_bootstrap);
var vue = new Vue( vue_options );

function do_post(url, body) {
    const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });
  
    return fetch(new URL(url).toString(), {
        method: 'POST',
        body: JSON.stringify(body),
        headers: headers
      })
      .then((response) => {
        if (!response.ok)
          throw 'status is not 200';
        return response.json();
      });
}

async function get_data(type){
    return do_post(base_url + '/get-data', { type: type })
    .then(json =>{
        if( json.status != 'OK' )
            throw "post failed";
        return json.result.data;
    });
}

async function update_data(type, data){
    var body = {
        type: type,
        data: data,
    }
    return do_post(base_url + '/update-data', body)
    .then(json =>{
        if( json.status != 'OK' )
            throw "post failed";
    });
}
```

次はHTMLです。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>オリエンテーション ナビゲータ</title>

  <link rel="stylesheet" href="css/start.css">
  <script src="js/methods_bootstrap.js"></script>
  <script src="js/components_bootstrap.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>

  <script type="text/javascript" src="//maps.google.com/maps/api/js?key=【GoogleAPIキー】"></script> 
</head>
<body>
    <div id="top" class="container">
        <h1>オリエンテーション ナビゲータ</h1>

        <ul class="nav nav-tabs">
          <li role="presentation" class="active"><a href="#oriatation" v-on:click="orientation_update_view" data-toggle="tab">オリエンテーション</a></li>
          <li role="presentation"><a href="#checkpoint" data-toggle="tab">チェックポイント</a></li>
          <li role="presentation"><a href="#myspot" data-toggle="tab">マイスポット</a></li>
        </ul>

        <div class="tab-content">
          <div id="oriatation" class="tab-pane fade in active">
            <br>
            <span class="form-inline">
              <select class="form-control" v-model="travelmode">
                <option value="walking">walking</option>
                <option value="bicycling">bicycling</option>
                <option value="driving">driving</option>
                <option value="transit">transit</option>
              </select>
            </span>
            <div class="btn-group">
              <button class="btn btn-primary" v-on:click="orientation_next">{{orientation_text}}</button>
              <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>              
              <ul class="dropdown-menu">
                <li><a v-on:click="orientation_restart(-1)">最初から再開</a></li>
                <li><a v-on:click="orientation_restart(origin_index)">今のチェックポイントを再開</a></li>
              </ul>
            </div>
            <span v-if="checkpoints.length >= 2">
              <br><br>
              <label>現在地：</label>{{origin_index + 1}} {{checkpoints[origin_index].name}},
              <label>目的地：</label>{{get_next_destination() + 1}} {{checkpoints[get_next_destination()].name}}
            </span>
            <button class="btn btn-default btn-xs pull-right" v-on:click="map_goto_current_location">現在地へ</button>
            <br>
            <div class="row" id="map_canvas" style="margin: 15px; height:600px"></div>
          </div>

          <div id="checkpoint" class="tab-pane fade in">
            <br>
            <button class="btn btn-primary" v-on:click="do_checkpoints_reset">チェックポイントのリセット</button>
            <table class="table table-striped">
              <thead>
                <tr><th>#</th><th>名前</th><th>経由地</th><th>編集</th></tr>
              </thead>
              <tbody>
                <tr v-for="(point, index) in checkpoints">
                  <td width="1px">
                    <div class="form-inline">
                      <select v-bind:value="index" v-on:change="do_checkpoint_change_index(index, $event)">
                        <option v-for="(point2, index2) in checkpoints" v-bind:value="index2" v-bind:selected="index==index2">{{index2 + 1}}</option>
                      </select>
                    </div>
                  </td>
                  <td>
                    <button class="btn btn-default btn-xs" v-on:click="do_checkpoint_delete(index)">削除</button> {{point.name}}
                  </td>
                  <td>
                    <input v-if="index!=0 && index!=(checkpoints.length-1)" type="checkbox" v-model="point.waypoint">
                  </td>
                  <td>
                    <div class="btn-group">
                      <button class="btn btn-default btn-sm" v-on:click="do_checkpoint_change_name(index)">名前</button>
                      <button class="btn btn-default btn-sm" v-on:click="do_checkpoint_change_location(index)">場所</button>
                    </div>
                  </td>
                </tr>
                <tr>
                  <td></td>
                  <td>
                    <div class="btn-group">
                      <button class="btn btn-default btn-sm" v-on:click="do_checkpoint_append">チェックポイント追加</button>
                      <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>              
                      <ul class="dropdown-menu">
                        <li><a v-on:click="do_checkpoint_append_myspot">マイスポットから追加</a></li>
                      </ul>
                    </div>
                  </td><td></td><td></td>
                </tr>
              </tbody>
            </table>            
          </div>

          <div id="myspot" class="tab-pane fade in">
            <br>
            <table class="table table-striped">
              <thead>
                <tr><th>#</th><th>名前</th><th>緯度</th><th>経度</th><th>編集</th></tr>
              </thead>
              <tbody>
                <tr v-for="(spot, index) in myspots">
                  <td width="1px">{{index + 1}}</td>
                  <td><button class="btn btn-default btn-xs" v-on:click="do_myspot_delete(index)">削除</button> {{spot.name}}</td>
                  <td>{{spot.lat.toFixed(7)}}</td><td>{{spot.lng.toFixed(7)}}</td>
                  <td>
                    <div class="btn-group">
                      <button class="btn btn-default btn-sm" v-on:click="do_myspot_change_name(index)">名前</button>
                      <button class="btn btn-default btn-sm" v-on:click="do_myspot_change_location(index)">場所</button>
                    </div>
                  </td>
                </tr>
                <tr>
                  <td></td>
                  <td>
                    <button class="btn btn-default btn-sm" v-on:click="do_myspot_append">地図から追加</button>
                  </td>
                  <td></td><td></td><td></td>
                </tr>
              </tbody>
            </table>            
          </div>
          <br>
          <br>
        </div>

        <modal-dialog size="lg" id="orientation_complete_dialog">
          <div slot="content">
            <div class="modal-header">
              <h4 class="modal-title">オリエンテーション達成</h4>
            </div>
            <div class="modal-body">
              <center>
                オリエンテーション達成です。おめでとうございます。<br>
                <img src="img/goal_figure.png">
              </center>
            </div>
            <div class="modal-footer">
              <button class="btn btn-default" v-on:click="dialog_close('#orientation_complete_dialog')">閉じる</button>
            </div>
          </div>
        </modal-dialog>

        <modal-dialog size="lg" id="select_location_dialog">
          <div slot="content">
            <div class="modal-header">
              <h4 class="modal-title">{{dialog_params.title}}</h4>
            </div>
            <div class="modal-body">
              <div class="form-inline">
                <button class="btn btn-default" v-on:click="dialog_submit" v-if="dialog_params.is_input_submit">この場所にする</button>
                <span v-if="dialog_params.is_input_name">
                  <label>名前</label> <input type="text" class="form-control" v-model="dialog_params.name">
                </span>
              </div>
              <div class="row" id="map_canvas2" style="margin: 20px; height:300px"></div>
            </div>
            <div class="modal-footer">
              <button class="btn btn-default" v-on:click="dialog_close('#select_location_dialog')">キャンセル</button>
            </div>
          </div>
        </modal-dialog>


        <!-- for progress-dialog -->
        <progress-dialog v-bind:title="progress_title"></progress-dialog>
    </div>

    <script src="js/start.js"></script>
</body>
```

もう長すぎてわけわかんないですよね。。。
その他ユーティリティのファイル等含めて、GitHubに上げています。

#セットアップ：GoogleMap API利用の準備

GoogleMapを利用するには、GoogleからAPIキーを払い出してもらう必要があります。
以下のサイトの通りに実施すれば、特に問題はなかったです。

Get Started with Google Maps Platform
　https://developers.google.com/maps/gmp-get-started?hl=ja

最後に、API Keyを生成するのですが、Web APIsのMaps Embed APIを採用しました。

#セットアップ：サーバの展開

以下のGitHubから一式ダウンロードしておきます。

poruruba/orientation_navigator
　https://github.com/poruruba/orientation_navigator

そして、以下の通りに実行します。

> unzip orientation_navigator.zip
> cd orientation_navigator
> npm install
> mkdir data
> node app.js

以下、修正が必要です。

public/start.js
　7行目のあたり
　```const base_url = "http://localhost:10080";```

上記を立ち上げたサーバのURLを指定します。

public/index.html
　34行目あたり
　```<script type="text/javascript" src="//maps.google.com/maps/api/js?key=【GoogleAPIキー】"></script>```

また、HTML5の現在地情報取得機能を使っているのですが、それを利用するには、HTTPSである必要があります。

> mkdir certs

このディレクトリに、SSL証明書類を配置しましょう。

以下が参考になります。
　[SSL証明書を取得しよう](https://qiita.com/poruruba/items/59d6197166bb09049f53)


以上