【Vue.js】単一ファイルコンポーネントでGoogleマップのマーカークラスタリングとvue-router

# Googleマップ表示とマーカークラスタリングと情報ウインドウからのページ遷移
単一ファイルコンポーネントでGoogleマップを表示して、複数マーカーをまとめて表示（マーカークラスタリング）したうえで、情報ウインドウ内から非同期でページ遷移する（vue-router）

Vueの単一ファイルコンポーネントでGoogleMapsAPIを利用した地図を表示します。
npmに `vue2-google-maps` というのがあるので、単純な地図ならこれでよいのですが
今回は

- 複数のマーカーをまとめて表示（[マーカークラスタリング](https://developers.google.com/maps/documentation/javascript/marker-clustering?hl=ja)）をしたい
- [vue-router](https://router.vuejs.org/ja/)を使っていてシングルページアプリケーションとして作成
- マップ上の[情報ウインドウ（infoWindow）](https://developers.google.com/maps/documentation/javascript/examples/infowindow-simple?hl=ja)の中からのリンクも非同期にページ遷移させたかった。

等々ありましたので、自前でコンポーネントを作成しました。
それについてのメモです。

# 環境
- サーバサイド：Laravel5.5
- フロントエンド：Vue.js
- 単一ファイルコンポーネントのコンパイル：Laravel Mix

## 準備

### GoogleMapの読み込み

```
<script src="https://maps.googleapis.com/maps/api/js?key=[API_KEY]"></script>
```
いつものGoogleMapAPIのやつです。

### マーカークラスタリング
マーカーを沢山表示するときに、近くのマーカーが重なって見えにくくなってしまうのを回避する手段です。
詳しくは
[MarkerClusterer](https://developers.google.com/maps/documentation/javascript/marker-clustering?hl=ja)

npm からインストール

```
$ npm i js-marker-clusterer
```
ライブラリを読み込んでおきます。

```js:app.js
import MarkerClusterer from 'js-marker-clusterer';
```

## 前提条件

- GoogleマップのinfoWindow（マーカーから出る吹き出しみたいなやつ）から、リンクを貼って、詳細ページへ遷移させたい。
- 詳細ページのURLは `/999` みたいな詳細を表すID

vue-routerを使って事前にルーティング

```js:app.js
const router = new VueRouter({
  mode: 'history',
  routes: [
    {
      path: '/',
      name:'home',
      component: require('./components/Home.vue')
    },
    {
      path: '/:post_id(\\d+)',
      name: 'description',
      component: require('./components/Description.vue')
    }
    //　↑こういう感じでIDをURLにした詳細ページ的なのをつくると仮定して進めます
  ]
});
```
こんな感じでルーティングしています。

## コンポーネントファイル

```html:Map.vue
<template lang="html">
  <div>
    <!-- テンプレートは単純にマップだけ -->
    <div id="map"></div>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      mapName: "map", //mapを紐付ける要素のID
      map: null, //GoogleMapのインスタンスを入れる変数
      markers: [], //マーカー群を入れる配列

      // サンプルデータです。指している場所には意味はありません。
      // ロケーション（緯度・軽度）と、id、タイトルです。
      locations: [
        {
          location:{lat: 35.681, lng: 139.761},
          id:1,
          title:"たいとる1"
        },
        {
          location:{lat: 35.682, lng: 139.762},
          id:2,
          title:"たいとる2"
        },
        {
          location:{lat: 35.683, lng: 139.763},
          id:3,
          title:"たいとる3"
        },
        {
          location:{lat: 35.684, lng: 139.764},
          id:4,
          title:"たいとる4"
        },
        {
          location:{lat: 35.685, lng: 139.765},
          id:5,
          title:"たいとる5"
        }

      ],
    }
  },
  methods: {
    // マップを表示するメソッド
    initMap: function () {
      var my_this = this; //thisを別名で退避しておく（あとでつかうので）

      // 中心データ
      var center_lat = 35.6810409;
      var center_lng = 139.7670516;
      var zoom = 16;

      // マップを初期化
      this.map = new google.maps.Map(document.getElementById(this.mapName), {
        center: {lat:center_lat, lng: center_lng},
        zoom: zoom
      });

      // this.locations をループしてマーカー群を作成
      this.markers = this.locations.map( function(data, i) {
        var marker = new google.maps.Marker({
          position: data.location,
        });

        // 情報ウインドウ（infoWindow）を作成 idとデータのID番号を設定しておく
        var infowindow = new google.maps.InfoWindow({
          content: '<div id="infobox_' + data.id + '" data-item-id="'+ data.id +'">' + data.title + '</div>',
          maxWidth: 300
        });

        // 情報ウインドウ（infoWindow）にイベントを設定
        // 情報ウインドウにclickイベントが設置できなさそうなので、読み込み時に、中身にクリックイベントを設置する
        google.maps.event.addListener(infowindow, 'domready', function(){

          //infoWindowが持っている内部文字列をDOMに変換
          var dumy_dom =  document.createElement('div');//ダミーの要素を作成
          dumy_dom.innerHTML = this.content;//ダミー要素にinfoWindow内のHTML文字列を読み込ませてDOMに変換

          // 内部のDOMを取得
          var id_name = dumy_dom.children[0].id; //infoWindow内の要素のID名を取得
          var inner_dom = document.getElementById(id_name); //ID名からinfoWindow内の要素オブジェクトを取得

          // アイテムのid番号を取得
          var item_id = inner_dom.dataset.itemId; //同じくリンク先用の番号を取得（data-item-idの値）

          // infoWindow内の要素にクリックイベントを設置
          inner_dom.addEventListener('click', function(){
            my_this.go(item_id);
          });
        });

        // infoWindowをマーカーに紐付け
        marker.addListener('click', function() {
          infowindow.open(this.map, marker);
        });
        return marker;
      });

      // マーカークラスタ設定
      var markerCluster = new MarkerClusterer(this.map, this.markers,
        {imagePath: '/images/markerclusterer/m'} // マーカークラスタ画像のパス（予めDLしておく）
      );
    },

    // Vue-routeをつかって非同期にページ遷移させるメソッド
    go: function(item_id){
      this.$router.push({ name: 'description', params: { post_id: item_id }});
    }
  },
  mounted: function () {
    this.initMap();
  }
}
</script>

<style lang="css">
#map {
  width:100%;
  height:400px;
}
</style>
```

### ポイント（というか詰まったところ）
- infoWindow内の要素からリンクさせたかったのですが、infoWindowのcontentで、vue-routerを使う方法がわかりませんでした。なので、infoWindowの読み込みイベントから、内部DOMを取得してクリックイベントを設定する。という強引な方法で実現しました。
- たぶん、もっとスマートなVueっぽい方法があるのではないかと思案中。
- マーカークラスタ用の画像については、下記からダウンロードできますが、画像ファイルなだけなので、自作するとよいかも。
- そこまでしてSPAにこだわらなくても・・というのは言わないで。

##　参考

- [Google Maps API マーカークラスタリング](https://developers.google.com/maps/documentation/javascript/marker-clustering?hl=ja)
- [Google Maps API infoWindow](https://developers.google.com/maps/documentation/javascript/examples/infowindow-simple?hl=ja)
- [github:js-marker-clusterer](https://github.com/googlemaps/js-marker-clusterer) ※マーカークラスタの画像はここから入手できます。
- [npm js-marker-clusterer](https://www.npmjs.com/package/js-marker-clusterer) ※npm
- [vue-router](https://router.vuejs.org/ja/)