【Vue.js】vue2-google-mapsを使って地図検索を実装してみた

投稿者: | 2018年12月16日

はじめに

Vue.jsでWebサービス開発しているのですが、GoogleMapを埋め込んでみたくなったので、vue2-google-mapsを触りながらサンプルプログラムを作ってみました。色々試行錯誤しながらやったので、備忘と共有に記事にしておきます。今回は地図に表示する位置情報の取得にホットペッパーのAPIを使いましたが、本記事ではAPI利用部分は割愛し、vue2-google-mapsの導入にフォーカスしています。

作ったもの

  • Vue.jsで作ったWebサイトにGoogleマップを埋め込む
  • 地図をドラッグして表示エリアを移動した際に、中心のピンも合わせて移動する(Uberっぽい動き)
  • 現在表示している地図の中心の座標(Center)、表示エリア(Bounds)を取得し、ホットペッパーのグルメAPIで検索する
  • ホットペッパーのAPIレスポンス(お店情報)をMaker表示する。MarkerをクリックするとinfoWindowに情報表示する

vue2-google-mapsのインストール、設定

インストール

まずはプロジェクトフォルダにvue2-google-mapsをインストールします。プロジェクトフォルダに移動して以下のコマンドを実行です。

npm install vue2-google-maps --save

設定

main.js へコンポーネントの読み込みと、設定情報を記載します。

import * as VueGoogleMaps from 'vue2-google-maps'
Vue.use(VueGoogleMaps, {
load: {
key: 'Your API Key',
libraries: 'places'
}
})

API KEYはGoogleCloudConsoleで取得が必要です。まだお持ちでない方は以下の記事が参考になります。

■参考=>【2018年7月16日版】Google Maps の APIキー を取得する

設定は以上です。あとはアプリケーションの開発です。

アプリケーションへの組込み(App.vue)

① 地図、マーカーの表示

まずはページを表示した際に地図とマーカーを表示する部分です。

出来上がりのイメージ

ソースコード(App.vue)

<template>
<div class="app">
<div id="map">
<GmapMap :center="center" :zoom="zoom" style="width: 100%; height: 100%;">
<GmapMarker v-for="(m,id) in marker_items"
:position="m.position"
:title="m.title"
:clickable="true" :draggable="false" :key="id"></GmapMarker>
</GmapMap>
</div>
</div>
</template>
<script>
export default {
data () {
return {
center: {lat: 35.71, lng: 139.72},
zoom: 14,
marker_items: [
{position: {lat: 35.71, lng: 139.72}, title: 'marker_1'},
{position: {lat: 35.72, lng: 139.73}, title: 'marker_2'},
{position: {lat: 35.70, lng: 139.71}, title: 'marker_3'},
{position: {lat: 35.71, lng: 139.70}, title: 'marker_4'}
]
}
}
}
</script>
<style scoped>
省略
</style>

とてもシンプルに実装できました。Markerをv-forで繰り返しで表示できるのがとても楽です。

② 地図の中心座標(Center)、表示領域(Bounds)を取得する

地図の表示中領域のお店情報を取得したいとした場合、以下の情報が必要となります。

  • 地図の中心座標
  • 表示エリアの緯度(min、max)、経度(min、max)

これらの取得をやってみました。2通りの方法でやれそうです。

②-1 必要なときに関数を呼び出す方法

出来上がりのイメージ

ソースコード(App.vue)

<template>
<div class="app">
<div id="map">
<GmapMap :center="center" :zoom="zoom" ref="map" style="width: 100%; height: 100%;">
<GmapMarker v-for="(m,id) in marker_items"
:position="m.position"
:title="m.title"
:clickable="true" :draggable="false" :key="id"></GmapMarker>
</GmapMap>
<p><button @click="getInfo">地図情報の取得</button></p>
<p v-for="(info,id) in infos" :key="id">{{info}}</p>
</div>
</div>
</template>
<script>
export default {
data () {
return {
center: {lat: 35.71, lng: 139.72},
zoom: 14,
marker_items: [
{position: {lat: 35.71, lng: 139.72}, title: 'marker_1'},
{position: {lat: 35.72, lng: 139.73}, title: 'marker_2'},
{position: {lat: 35.70, lng: 139.71}, title: 'marker_3'},
{position: {lat: 35.71, lng: 139.70}, title: 'marker_4'}
],
infos: []
}
},
methods: {
getInfo () {
let mapBounds = this.$refs.map.$mapObject.getBounds()
this.infos.push(`[bounds-lat:] ${mapBounds.la.j} ~ ${mapBounds.la.l}`)
this.infos.push(`[bounds-lng:] ${mapBounds.ea.j} ~ ${mapBounds.ea.l}`)
let center = this.$refs.map.$mapObject.getCenter()
this.infos.push(`[center] lat: ${center.lat()} lng: ${center.lng()}`)
}
}
}
</script>
  • <GmapMap>タグに「ref=”map”」を追加
  • ボタンクリック時のgetInfo()にてcenterとboundsを取得します。

②-2 イベントで取得する方法

色々調べてみると、以下がイベントで取得できました。イベントハンドラで緯度経度が取得できるので、これを利用することも可能です。変更をリアルタイムに追いかけるときとかには使えます。

  • 地図の中心座標が動いたタイミング ⇒ @center_changed
  • 地図の表示エリアが変化したタイミング ⇒ @bounds_changed

出来上がりのイメージ

ソースコード(App.vue)

<template>
<div class="app">
<div id="map">
<GmapMap :center="center" :zoom="zoom" ref="map" style="width: 100%; height: 100%;"
@center_changed="onCenterChanged"
@bounds_changed="onBoundsChanged"
>
<GmapMarker v-for="(m,id) in marker_items"
:position="m.position"
:title="m.title"
:clickable="true" :draggable="false" :key="id"></GmapMarker>
</GmapMap>
<p>【center】lat:{{info.center.lat}} lng:{{info.center.lng}}</p>
<p>【bounds_lat】lat:{{info.lat.min}} lng:{{info.lat.max}}</p>
<p>【bounds_lng】lat:{{info.lng.min}} lng:{{info.lng.max}}</p>
</div>
</div>
</template>
<script>
export default {
data () {
return {
center: {lat: 35.71, lng: 139.72},
zoom: 14,
marker_items: [
{position: {lat: 35.71, lng: 139.72}, title: 'marker_1'},
{position: {lat: 35.72, lng: 139.73}, title: 'marker_2'},
{position: {lat: 35.70, lng: 139.71}, title: 'marker_3'},
{position: {lat: 35.71, lng: 139.70}, title: 'marker_4'}
],
info: {
center: {lat:null, lng:null},
lat: {min: null, max: null},
lng: {min: null, max: null},
}
}
},
methods: {
onCenterChanged (center) {
this.info.center = {lat: center.lat(), lng: center.lng()}
},
onBoundsChanged (bounds) {
this.info.lat = {min: bounds.la.j, max: bounds.la.l}
this.info.lng = {min: bounds.ea.j, max: bounds.ea.l}
}
}
}
</script>

GmapMapに@center_changedと@bound_changedの2つのイベントハンドラを追加しています。関数の引数にそれぞれcenterとboundsのオブジェクトが渡されるので、オブジェクトから緯度経度を取得することができます。

②-3 中心にオリジナルアイコン画像を表示してみる(Uberっぽいやつ)

出来上がりのイメージ

ソースコード

どっちかというとアイコン画像とサイズの設定方法に悩んだかな…

<template>
<div class="app">
<div id="map">
<GmapMap :center="center" :zoom="zoom" ref="map" style="width: 100%; height: 100%;"
@center_changed="onCenterChanged"
@bounds_changed="onBoundsChanged"
>
<GmapMarker v-for="(m,id) in marker_items"
:position="m.position"
:title="m.title"
:clickable="true" :draggable="false" :key="id"></GmapMarker>
<GmapMarker v-show="marker_center" :position="marker_center" :clickable="true" :icon="icon_center" :draggable="false"></GmapMarker>
</GmapMap>
</div>
</div>
</template>
<script>
export default {
data () {
return {
center: {lat: 35.71, lng: 139.72},
zoom: 14,
marker_items: [
{position: {lat: 35.71, lng: 139.72}, title: 'marker_1'},
{position: {lat: 35.72, lng: 139.73}, title: 'marker_2'},
{position: {lat: 35.70, lng: 139.71}, title: 'marker_3'},
{position: {lat: 35.71, lng: 139.70}, title: 'marker_4'}
],
icon_center: {
url: require('@/assets/image/icon/pin_black.png'),
size: {width: 44, height: 70, f: 'px', b: 'px'},
scaledSize: {width: 22, height: 35, f: 'px', b: 'px'}
},
marker_center: {lat: 35.71, lng: 139.72}
}
},
methods: {
onCenterChanged (center) {
this.marker_center = {lat: center.lat(), lng: center.lng()}
}
}
}
</script>

③ APIの取得イメージをもとにmarkerとinfoWindowを表示する

長くなってしまいましたが、ようやく最終成果物の作りです。

出来上がりのイメージ

上述したイメージになります。

ソースコード

<template>
<div class="app">
<h1>vue2-googlemap-sample</h1>
<div id="map">
<GmapMap :center="center" :zoom="zoom" ref="map" style="width: 100%; height: 100%;"
@center_changed="centerChanged"
>
<GmapInfoWindow :options="infoOptions" :position="infoWindowPos" :opened="infoWinOpen" @closeclick="infoWinOpen=false">
<table>
<tr>
<td><img :src="infoContent.imageurl"></td>
<td style="text-align:left;">
<p><a :href="infoContent.url" target="_blank">{{infoContent.title}}</a></p>
<p>{{infoContent.address}}</p>
</td>
</tr>
</table>
</GmapInfoWindow>
<GmapMarker v-for="(m,id) in marker_items"
:position="m.position"
:title="m.title"
@click="toggleInfoWindow(m,id)"
:clickable="true" :draggable="false" :key="id"></GmapMarker>
<GmapMarker v-show="marker_center" :position="marker_center" :clickable="true" :icon="icon_center" :draggable="false"></GmapMarker>
</GmapMap>
<p><strong>Center:</strong>lat:{{marker_center.lat}},lng:{{marker_center.lng}}</p>
<button @click="mapTest">現在地周辺のお店を検索</button>
<button @click="currentPosition">現在地へ移動</button>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data () {
return {
center: {lat: 35.7, lng: 139.7},
zoom: 17,
marker_center: {lat: 35.7, lng: 139.7},
marker_items: [
{position: {lat: 35.7, lng: 139.69}}
],
icon_center: {
url: require('@/assets/image/icon/pin_black.png'),
size: {width: 44, height: 70, f: 'px', b: 'px'},
scaledSize: {width: 22, height: 35, f: 'px', b: 'px'}
},
infoOptions: {
pixelOffset: {
width: 0,
height: -35
}
},
infoWindowPos: null,
infoWinOpen: false,
infoContent: {
imageurl: null,
title: null,
address: null
}
}
},
methods: {
centerChanged (latLng) {
this.marker_center = {lat: latLng.lat(), lng: latLng.lng()}
},
async mapTest () {
let mapBounds = this.$refs.map.$mapObject.getBounds()
let center = this.$refs.map.$mapObject.getCenter()
this.infoWinOpen = false
let response = await this.search()
let shops = response.data.results.shop
let items = []
for (let i = 0; i < shops.length; i++) {
let item = {
position: {
lat: Number(shops[i].lat),
lng: Number(shops[i].lng)
},
content: {
title: shops[i].name,
imageurl: shops[i].logo_image,
address: shops[i].address,
url: shops[i].urls.pc
}
}
items.push(item)
}
this.marker_items = items
console.log(this.marker_items)
},
toggleInfoWindow (marker, id) {
this.infoWinOpen = false
this.infoWindowPos = marker.position
this.infoContent = marker.content
this.infoWinOpen = true
},
async search () {
let apiResponse = await axios.get('http://localhost:5000/app/api/restaurantSearch/', {
params: {
lat: this.marker_center.lat,
lng: this.marker_center.lng
}})
console.log(apiResponse)
let shops = apiResponse.data.results.shop
console.log(shops)
return apiResponse
},
currentPosition () {
navigator.geolocation.getCurrentPosition(this.getCurrentPositionSuccess)
},
getCurrentPositionSuccess (position) {
let lat = position.coords.latitude
let lng = position.coords.longitude
this.$refs.map.panTo({lat: lat, lng: lng})
}
}
}
</script>

こんな感じです!API連携の部分は参考になりませんが、infoWindowの使い方を参考にしていただければと思います。

参考にしたサイト

さいごに さわってみた感想

とりあえず最低限使ってみたいものは実装できそうだなと感じました。ソースは短く実装できるのでちょっと使うとかだと便利だと思います。

ただアニメーションとかどうやって組み込むのか謎な部分もあります。詰まったときにもドキュメントがほぼなく、あってもstackOverFlowとかしかないのは辛いですね。


カツオが開発したWebサービスです。

平成の想い出を気軽に年表にしてシェアすることができるサービスです。ぜひ使ってみてください。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です