BFT名古屋 TECH BLOG

日々の業務で得た知識を所属するエンジニアたちがアウトプットしていきます。

【Google Maps Platform】【Vue.js】 APIから取得した情報をもとに地図上に情報ウィンドウを表示する

初めに

こんにちは。株式会社BFT名古屋支店新人エンジニアのないとうです。
今回は、APIから取得したデータをもとに地図上にピンの設置と情報ウィンドウの表示を行う方法について紹介したいと思います。

前提条件

・Vue.jsのプロジェクトが作成されている
Google Maps Platform(GMP)のAPIキーが取得されている
GMPを用いて地図の表示ができる
GMPを用いて地図を表示する方法について詳しくはこちら→【GMP】0から始めるGoogle Maps Platform ③ Google Mapを表示するプログラムの作成 - BFT名古屋 TECH BLOG

システムについて

システム概要

今回作成するシステムは、地図の表示範囲内のデータをDBから取得しそれをもとにピンと情報ウィンドウを設置します。
DB内に保存されているデータは、「画像ファイル名」、「日付」、「緯度」、「経度」、「状態」、「画像サイズ(横)」、「画像サイズ(縦)」が保存されています。
要件としては以下の通りです。
①DBから情報を取得するAPIを用いる
②S3から画像を取得するAPIを用いる
③地図表示範囲内のデータをすべて取得する
④ピンはDBに保存されている「状態」をもとに種類を変える
⑤ピンをクリックすると情報ウィンドウを表示し、記載する情報は、「画像」、「状態」、「日付」、「緯度」、「経度」とする
⑥1つのウィンドウに複数の画像と情報を載せられるようにする
⑦画像をクリックすると、別タブに大きい画像を表示する
⑧距離の近いピンはクラスター化する
⑨地図の表示範囲が変わった場合、表示するピンも変更する

f:id:bftnagoya:20211029175224p:plain
完成イメージ図

①、②APIについて

①は座標範囲をgetメソッドで送信することで範囲内のデータを取得するAPIです。
詳しくはこちら→【AWS】【API Gateway】【Lambda】 API GatewayとLambdaでDynamoDBのデータを取得する - BFT名古屋 TECH BLOG  
②はファイル名をgetメソッドで送信することで写真を取得するAPIです。
詳しくはこちら→【AWS】【API Gateway】【Lambda】API GatewayとLambdaでS3の画像を表示する - BFT名古屋 TECH BLOG   

③地図表示範囲内のデータをすべて取得する

画面上に表示されている地図から表示範囲のデータを取得する方法として、GMPでは、getBounds()が用意されています。
こちらは画面右上(北東)と左下(南西)の座標を取得します。
日本では、最大緯度が北、最大経度が東、最小緯度が南、最小経度が西になるのでこのデータをAPIに送信して、DBの情報を得ることとしました。
まずAPIから得たデータを配列に格納する関数、getdata.jsを以下のように作成しました。

import axios from 'axios'

  async function getdata(url,list){
       //送信処理
       await axios.get(
          url,
          {
            headers:{
              'content-type':'application/json'
            },
            responseType:'json'

          //送信成功時処理
          }).then(responce =>{
             list.push(responce["data"]);
             console.log(list)
          //送信失敗時処理
          }).catch(e=>{
             alert(e);
        });
      }

export { getdata }

次にこの関数とgetBoundsを用いてDBの情報を取得する機能を作成しました。

//地図範囲取得
var latlngBounds = this.map.getBounds();
var swLatlng = latlngBounds.getSouthWest();
var swlat = String(swLatlng.lat());
var swlng = String(swLatlng.lng());

var neLatlng = latlngBounds.getNorthEast();
var nelat = String(neLatlng.lat());
var nelng = String(neLatlng.lng());

//情報取得
var exifdata=[];
var url='【APIのURL】/?minlat='+swlat+'&minlng='+swlng+'&maxlat='+nelat+'&maxlng='+nelng;
await getdata(url,exifdata);

④ピンの表示

DBから取得した情報にある「情報」の値によってif文でピンの画像を変更します。
またピンを指す場所は、「緯度」、「経度」をラベルには「日付」を用います。
ピンの設置はMarker()を用いて以下のように作成しました。

//marker設置
marker=new google.maps.Marker({
  position:{lat: parseFloat(【「緯度」】), lng:parseFloat(【「経度」】)} ,
  map: this.map,
  icon: {
    url: require('【ピンの画像パス】'),
    scaledSize: new google.maps.Size(45, 45),
    labelOrigin: new this.google.maps.Point(15,30)
  },
  label: {
    text: 【「日付」】,         
    color: '#ff0000',          
    fontFamily: 'sans-serif',  
    fontWeight: 'bold',       
    fontSize: '14px'           
  }
});

⑤、⑥、⑦情報ウィンドウを表示する

情報ウィンドウを表示するためにGMPではInfoWindow()が用意されています。
こちらは、ピンとメッセージを入力することで情報ウィンドウを表示することができます。
今回はクリックすることで表示したいのでクリックイベントを追加し以下のように情報ウィンドウ表示用の関数を作成しました。

attachMessage:function(marker, msg) {
  google.maps.event.addListener(marker, 'click', function() {
    new google.maps.InfoWindow({
       content: msg  //表示する内容
    }).open(marker.getMap(), marker);

  });
}

次に上記関数に引数として与える、「表示する内容(msg)」を作成します。
InfoWindowのcontentでは文字列を表示する他にHTMLを用いて内容を表示することも可能です。
ですので今回はHTMLを用いて表示する内容を作成していきたいと思います。
今回「状態」を記載する必要があるのでこちらもif文で内容を変更します。
HTMLの内容は以下のようにしました。

<a target='_blank' herf='【APIのURL】'>
  <img style='vertical-align:top;' width='【「画像サイズ(横)」】/4' height='【「画像サイズ(縦)」】/4' src='【「画像」】'>
</a>
<div style='display:inline-block'>
  <p>状態:【「状態」】</p>
  <p>日付:【「日付」】</p>
  <p>緯度:【「緯度」】</p>
  <p>経度:【「経度」】</p>
</div>
<br>
<br>

<a>を用いて②のAPIを呼び出すことで、画像を別タブで表示できるようにしています。
こちらをgetdata関数で取得した情報を格納しているexifdata配列を用いて記載するとと以下のようになります。

contents = contents + "<a target='_blank' href='【APIのURL】?name=" + exifdata[0][0]["filename"][0] + "'><img style='vertical-align:top;' width=" + exifdata[0][0]["w"][0]/4 + " height=" + exifdata[0][0]["h"][0]/4 + " src=" + imgurl[exifdata[0][0]["filename"][0]] + "></a> <div style='display:inline-block'><p>状態:【「状態」】</p><p>日付:"+exifdata[0][0]["time"][0]+"</p><p>緯度:"+exifdata[0][0]]["lat"][0]+"</p><p>経度:"+exifdata[0][0]["lng"][0]+"</p></div><br><br>";

今回同じピンに複数情報載せられるようにしたいので、これをexifdata配列に格納してある画像分ループさせcontentsを追加していきます。
またピンの数だけ情報ウィンドウを生成したいので、こちらもexifdata配列をもとにループさせます。

for(var i=0; i < exifdata[0].length; i++){
    // var imgurl = [];
    var contents = "";
    // ファイル名リストの分だけループ
    for(var j=0; j < exifdata[0][i]["filename"].length; j++){

        // 情報ウインドに出すコンテンツ作成
        if(exifdata[0][i]["status"][j] == 0){
            contents = contents + "<a target='_blank' href=【APIのURL】?name=" + exifdata[0][i]["filename"][j] + "'><img style='vertical-align:top;' width=" + exifdata[0][i]["w"][j]/4 + " height=" + exifdata[0][i]["h"][j]/4 + " src=" + imgurl[exifdata[0][i]["filename"][j]] + "></a> <div style='display:inline-block'><p>状態:【「状態」】</p><p>日付:"+exifdata[0][i]["time"][j]+"</p><p>緯度:"+exifdata[0][i]["lat"][j]+"</p><p>経度:"+exifdata[0][i]["lng"][j]+"</p></div><br><br>"; 
        }
        if(exifdata[0][i]["status"][j] == 1){
            contents = contents + "<a target='_blank' href=【APIのURL】?name=" + exifdata[0][i]["filename"][j] + "'><img style='vertical-align:top;' width=" + exifdata[0][i]["w"][j]/4 + " height=" + exifdata[0][i]["h"][j]/4 + " src=" + imgurl[exifdata[0][i]["filename"][j]] + "></a> <div style='display:inline-block'><p>状態:【「状態」】</p><p>日付:"+exifdata[0][i]["time"][j]+"</p><p>緯度:"+exifdata[0][i]["lat"][j]+"</p><p>経度:"+exifdata[0][i]["lng"][j]+"</p></div><br><br>"; 
        }

    }
  【④マーカー設置】
    this.attachMessage(marker,contents);
}

⑧ピンのクラスター化

ピンをクラスター化する方法として、GMPにはMarkerCluster()が用意されています。
こちらは、地図とピンの入った配列を指定することで、距離の近いピンをクラスター化することができます。
MarkerCluster()を用いて以下のような関数を作成しました。

 //クラスター設置関数//
makeClusterer:function(){
  if(this.clusterer.length != 0){
    this.marker=new MarkerClusterer(this.map,this.clusterer, {
       imagePath:'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'
    });
  }
}

ピンの配列(this.cluster)については④で作ったピンを配列に格納して使いたいと思います。
そこで⑤、⑥、⑦で作成したループにピンを格納する部分と、関数起動部分を追加することとしました。

・・・
        }

    }
  【④マーカー設置】
    this.attachMessage(marker,contents);
    this.clusterer.push(marker);  //ピン格納部分追加
}
this.makeClusterer();  //関数起動部分追加

⑨地図の表示範囲が変わった場合、表示するピンも変更する

表示範囲が変わった場合に関数を起動するために、addListener()を用いてを追加します。
よく使われるclickイベント以外に、様々なイベントがあります。
その中でも地図の表示範囲に関するものとして、3つのイベントがありました。
・中心が変化した場合に起動するcenter_changeイベント
・地図の見える範囲が変化した場合に起動するbounds_changedイベント
・地図がアイドル状態になった場合に起動するidleイベント
今回は、表示範囲の変更が完了したときにイベントを起動したいので、idleイベントを用いることとしました。
このイベントが発生するように、地図設置関数を作成します。

initMap() {
  const mapElement = document.getElementById("map");
  this.google = google;
  this.map = new google.maps.Map(mapElement, {
    center: 【中央座標】,
    zoom: 【拡大度】,
    disableDefaultUI: true
  });
  //マップが移動したら関数起動
  this.google.maps.event.addListener(this.map,"idle", 【起動する関数】);
}

スクリプト作成

これまでの内容をスクリプトにまとめていきます。
まず画像の取得するAPIから取得した画像を配列に格納する関数をgetimg.jsとして作成します。

import axios from 'axios'

  async  function getimg(filename,imgurl){
       var url='/v3/?name='+filename;
       //送信処理
       await axios.get(
          //送信URL
          url,
          {
            headers:{
              'content-type':'image/jpeg'
            },responseType:'arraybuffer'

          //送信成功時処理
          }).then(response =>{

             const prefix = `data:${response.headers["content-type"]};base64,`;
             const base64 = new Buffer(response.data, "binary").toString("base64");

             imgurl[filename]=prefix+base64;


             return imgurl;

          //送信失敗時処理
          }).catch(e=>{
             alert(e);
        });
      }


export { getimg }

次にgetdata.jsとgetimg.jsを用いてピンと情報ウィンドウを表示するスクリプトを作成します。

・・・
<script>
・・・
  import $Scriptjs from "scriptjs";
  import MarkerClusterer from '@google/markerclusterer';
  import { getdata } from "./script/getdata";
  import { getimg } from "./script/getimg";
  const API_KEY = process.env.VUE_APP_MAP_API_KEY;

  const API_URL =
  "https://maps.googleapis.com/maps/api/js?key=" +
  API_KEY +
  "&libraries=places";
  export default {
    data() {
      return {
        initialPlace: {
        lat: 35.681236,
        lng: 139.767125
        },
        zoom: 17,
        map: null,
        google: null,
        exifdata:[],
        load:0,
        clusterer:[],
        marker:""
      };
    },
    created() {
      //非同期でURLを読み込み関数実行
      $Scriptjs(API_URL, () => this.initMap());
    },
    methods: {

      //マップ設置関数//

      initMap() {
        const mapElement = document.getElementById("map");
        this.google = google;
        this.map = new google.maps.Map(mapElement, {
          center: this.initialPlace,
          zoom: this.zoom,
          disableDefaultUI: true
        });

        //マップが移動したら関数起動
        this.google.maps.event.addListener(this.map,"idle", this.putMarker);

        },

        //マーカー設置関数//

        putMarker:async function(){

          if(this.load==1){
            return;
          }
          this.load=1;
          //初めにマーカー削除

          this.clusterer.forEach(function (marker) { marker.setMap(null); });
          this.clusterer=[];


          //地図範囲取得
          var latlngBounds = this.map.getBounds();
          var swLatlng = latlngBounds.getSouthWest();
          var swlat = String(swLatlng.lat());
          var swlng = String(swLatlng.lng());

          var neLatlng = latlngBounds.getNorthEast();
          var nelat = String(neLatlng.lat());
          var nelng = String(neLatlng.lng());

          //EXIFdata取得
          var exifdata=[];
          var url='/v2/?minlat='+swlat+'&minlng='+swlng+'&maxlat='+nelat+'&maxlng='+nelng;
          await getdata(url,exifdata);
          
          //非同期処理で画像をすべて取得
          var imgurl={};
          var promise_ary=[];
          for(var s = 0; s<exifdata[0].length; s++){
            for(var t=0; t<exifdata[0][s]["filename"].length;t++){
              var promise = getimg(exifdata[0][s]["filename"][t],imgurl);
              promise_ary.push(promise);
            }
          }

          Promise.all(promise_ary).then((result) =>{

          for(var i = 0; i<exifdata[0].length; i++){
            //var imgurl=[];
            var contents="";
            //ファイル名リストの分だけループ
            for(var j=0; j<exifdata[0][i]["filename"].length;j++){
              //画像取得
              //await getimg(exifdata[0][i]["filename"][j],imgurl);
              //情報ウインドに出すコンテンツ作成
              if(exifdata[0][i]["status"][j]==0){
                 contents = contents + "<a target='_blank' href=【APIのURL】?name=" + exifdata[0][i]["filename"][j] + "'><img style='vertical-align:top;' width=" + exifdata[0][i]["w"][j]/4 + " height=" + exifdata[0][i]["h"][j]/4 + " src=" + imgurl[exifdata[0][i]["filename"][j]] + "></a> <div style='display:inline-block'><p>状態:【「状態」】</p><p>日付:"+exifdata[0][i]["time"][j]+"</p><p>緯度:"+exifdata[0][i]["lat"][j]+"</p><p>経度:"+exifdata[0][i]["lng"][j]+"</p></div><br><br>"; 

              }
              if(exifdata[0][i]["status"][j]==1){
                 contents = contents + "<a target='_blank' href=【APIのURL】?name=" + exifdata[0][i]["filename"][j] + "'><img style='vertical-align:top;' width=" + exifdata[0][i]["w"][j]/4 + " height=" + exifdata[0][i]["h"][j]/4 + " src=" + imgurl[exifdata[0][i]["filename"][j]] + "></a> <div style='display:inline-block'><p>状態:【「状態」】</p><p>日付:"+exifdata[0][i]["time"][j]+"</p><p>緯度:"+exifdata[0][i]["lat"][j]+"</p><p>経度:"+exifdata[0][i]["lng"][j]+"</p></div><br><br>"; 
             }

            }


            var marker ="";

            //マーカー設置
            if(exifdata[0][i]["status"][j-1]==0){
              marker=new google.maps.Marker({
                position:{lat: parseFloat(exifdata[0][i]["lat"][j-1]), lng:parseFloat(exifdata[0][i]["lng"][j-1])} ,
                map: this.map,
                icon: {
                  url: require('【ピンの画像パス】'),
                  scaledSize: new google.maps.Size(45, 45),
                  labelOrigin: new this.google.maps.Point(15,30)
                },
                label: {
                  text: exifdata[0][i]["time"][j-1],         //ラベル文字
                  color: '#ff0000',          //ラベル文字の色
                  fontFamily: 'sans-serif',  //フォント
                  fontWeight: 'bold',        //フォントの太さ
                  fontSize: '14px'           //フォントのサイズ
               }
              });
            }

            if(exifdata[0][i]["status"][j-1]==1){
              marker=new google.maps.Marker({
                position:{lat: parseFloat(exifdata[0][i]["lat"][j-1]), lng:parseFloat(exifdata[0][i]["lng"][j-1])} ,
                map: this.map,
                icon: {
                  url: require('【ピンの画像パス】'),
                  scaledSize: new google.maps.Size(45, 45),
                  labelOrigin: new this.google.maps.Point(15,30)
                },
                label: {
                  text: exifdata[0][i]["time"][j-1],
                  color: '#ff0000',
                  fontFamily: 'sans-serif',
                  fontWeight: 'bold',
                  fontSize: '14px'
               }

              });
            }

            //コンテンツ設置関数起動
            this.attachMessage(marker,contents);
            //マーカーリストにマーカを追加
            this.clusterer.push(marker);

          }

          this.makeClusterer();
          this.load=0;
          console.log(result);

          });

      },

      //情報ウィンド設置関数//

      attachMessage:function(marker, msg) {
        google.maps.event.addListener(marker, 'click', function() {
          new google.maps.InfoWindow({
            content: msg
          }).open(marker.getMap(), marker);

        });

      },

      //クラスター設置関数//

      makeClusterer:function(){
        if(this.clusterer.length != 0){

          this.marker=new MarkerClusterer(this.map,this.clusterer, {
            imagePath:'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'
          });
        }
      }
     ・・・
}
・・・
</script>

終わりに

今回は地図にピンと情報ウィンドウを表示する方法を紹介しました。
ピンや情報ウィンドウは見た目やサイズ等変更ができるので、試してみてください。
それでは。


参照

developers.google.com

www.javadrive.jp

developers-jp.googleblog.com