GoogleMapsAPI
vue.js
vue-router
Vue.js #3Day 14

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

More than 1 year has passed since last update.

Googleマップ表示とマーカークラスタリングと情報ウインドウからのページ遷移

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

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

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

環境

  • サーバサイド:Laravel5.5
  • フロントエンド:Vue.js
  • 単一ファイルコンポーネントのコンパイル:Laravel Mix

準備

GoogleMapの読み込み

<script src="https://maps.googleapis.com/maps/api/js?key=[API_KEY]"></script>

いつものGoogleMapAPIのやつです。

マーカークラスタリング

マーカーを沢山表示するときに、近くのマーカーが重なって見えにくくなってしまうのを回避する手段です。
詳しくは
MarkerClusterer

npm からインストール

$ npm i js-marker-clusterer

ライブラリを読み込んでおきます。

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

前提条件

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

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

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にした詳細ページ的なのをつくると仮定して進めます
  ]
});

こんな感じでルーティングしています。

コンポーネントファイル

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にこだわらなくても・・というのは言わないで。

 参考