5
1

More than 1 year has passed since last update.

Nuxt と TypeScript で Google Maps を表示する (Maps JavaScript API, Places API)

Posted at

はじめに

Nuxt.jsでライブラリ(vue2-google-mapsなど)は使わずにGoogle Mapsを表示してみます
作りたい地図はこんな感じで、マーカーをつけてみます
地図からはわからないですが、地図とマーカーにそれぞれマウスクリックイベントもつけてみます
image.png

ここに書いてあること

  • Nuxt/VueでGoogle Mapsを表示する方法
  • Maps JavaScript APIとPlaces APIの使い方
  • 地図とマーカーのクリックイベント登録方法
  • マーカーにカスタムIDをつける方法

環境

  • macOS Monterey
  • VS Code Version: 1.63.2
$ npm -v
6.14.15

$ node -v
v14.15.4
package.json(抜粋)
{
  "dependencies": {
    "@nuxt/types": "^2.15.8",
    "@nuxt/typescript-build": "^2.1.0",
    "@nuxtjs/axios": "^5.13.6",
    "@nuxtjs/dotenv": "^1.4.1",
    "@nuxtjs/eslint-config-typescript": "^8.0.0",
    "@nuxtjs/eslint-module": "^3.0.2",
    "@nuxtjs/proxy": "^2.1.0",
    "@types/google.maps": "^3.47.2",
    "@types/node": "^17.0.7",
    "nuxt": "^2.15.8",
    "typescript": "^4.5.4",
  },
}

事前準備

Google Maps APIキーを用意する

  • Google Maps APIを使用するためのAPIキーを取得しておきます
  • リクエスト数が心配な時はQuotasに制限値をつけておくと安心かも

image.png

TypeScriptの型を定義する

TypeScriptの開発に必要な @nuxt/types と、
Google Mapsの開発に必要な @types/google.mapstsconfig.json に追加する

tsconfig.json(抜粋)
{
  "compilerOptions": {
    "types": [
      "@types/node",
      "@nuxt/types",         // →追加
      "@nuxtjs/axios",
      "@types/google.maps",  // →追加
    ]
  }
}

Google Mapsを表示する(Maps JavaScript API)

手順

  1. Google Maps APIを読み込むため、 script タグを用意する
    • 今回はplacesAPIも使うため、 &libraries=places をURL末尾に追加しています
    • APIキーはクライアント側にも見えるようになるので、APIキーごとの制限値や利用できるAPIを必要最低限にします
  2. Google Mapsを表示するために用意した <div ref="map"></div> に地図を表示します
    • コードでは initMap 関数と createMap 関数によって地図が表示されます
    • google.maps.Map クラスに必要なパラメータを渡しておくことで、地図のカスタマイズができます
    • google.maps.Map クラスで作成された google オブジェクトは GMap に代入しています
      • 地図にマーカーをつけたりするのに google オブジェクトが必要になるためです
  3. ここまででGoogleマップの表示ができています

補足

Map.vue
<template>
  <div class="map">
    <div ref="map" class="map__google-maps_embedded"></div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

const apiKeyGoogleMaps = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

export default Vue.extend({
  data() {
    return {
      GMap: {} as google.maps.Map, // window.google オブジェクト
    }
  },
  head() {
    return {
      script: [
        {
          hid: 'srcMap',
          src: `https://maps.googleapis.com/maps/api/js?key=${apiKeyGoogleMaps}&libraries=places`
        }
      ]
    }
  },
  mounted() {
    this.initMap()
  },
  methods: {

    // Create Google Map
    initMap(): void {
      const elMap = this.$refs.map
      this.GMap = this.createMap(elMap, (this as any).$mapCenterLatLng, 9, process.env.GOOGLE_MAPS_MAP_ID as string)
    },

    // Create Map
    createMap(mapEl: HTMLDivElement, center: google.maps.LatLngLiteral, zoom: number, mapId: string): google.maps.Map {
      return new window.google.maps.Map(mapEl, { center, zoom, mapId, })
    },
  }
})
</script>

地図にクリックイベントを追加する( google.maps.MapsEventListener

  • Googleマップ上でマウスクリックがあったときに、イベントを発火させてみます

手順

  1. google オブジェクトにイベントリスナーを追加します

参考

Map.vue
<template> ... </template>

<script lang="ts">
import Vue from 'vue'
const apiKeyGoogleMaps = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

export default Vue.extend({
  data() { return { GMap: {} as google.maps.Map, } },
  head() { return { ... } },
  mounted() {
    this.initMap()
    this.mapClickEvents()
  },
  methods: {
    initMap(): { ... },
    createMap( ... ): { ... },

    // 地図のクリックイベント
    mapClickEvents(): void {
      /* eslint-disable @typescript-eslint/no-unused-vars */
      this.GMap.addListener('click', (event: google.maps.MapsEventListener) => {
        console.log('Map Clicked!')
      })
    },

  }
})
</script>

地図にカスタムID付きのマーカーを追加する

  • 地図にマーカー(赤いピンマーク)を追加してみます
    • 今回は複数のマーカーを地図に追加しています( addAllMarkers 関数 )
  • マーカーごとにカスタムIDもつけてみます
    • マーカーをクリック時に、カスタムIDでお店を検索、みたいなことができそうです
    • マーカーはpngやsvgのアイコンをカスタムで指定も可能です
      • マーカーの数が多いと地図のズームイン/アウトの動作がカクカクします
      • 特にこだわりがなければ、デフォルトのマーカーを使うのが良さそうです

手順

  1. マーカーを追加する関数を用意します( mapMarker 関数の window.google.maps.Marker に該当 )
  2. マーカーのカスタム値を追加します( mapMarker 関数の (marker as google.maps.Marker).setValues に該当 )
    • customValMarker にマーカーのカスタム値を用意していますが、複数のカスタムIDを用意できます
  3. 複数のマーカーを表示したいので、もう一つ関数を用意します( addAllMarkers 関数 )
  4. addAllMarkers 関数に storeData (マーカー表示したい店の情報の配列) をわたせば地図上にマーカーを追加されます

参考

Map.vue
<template> ... </template>

<script lang="ts">
import Vue from 'vue'
const apiKeyGoogleMaps = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

export default Vue.extend({
  data() { return { GMap: {} as google.maps.Map, } },
  head() { return { ... } },
  computed: {
    ...mapGetters({
      allStores: 'stores/allStores',
    })
  },
  mounted() {
    this.initMap()
    await this.addAllMarkers(this.allStores, this.GMap)
    this.mapClickEvents()
  },
  methods: {
    initMap(): { ... },
    createMap( ... ): { ... },
    mapClickEvents(): { ... },

    // add marker to Map
    mapMarker(storeId: string, storeName: string, position: google.maps.LatLngLiteral, map: google.maps.Map, icon?: google.maps.Symbol): void {
      const self = this
      let marker = {}
      const customValMarker = { storeId: storeId, name: storeName }
      if (!icon) {
        marker = new window.google.maps.Marker({ position, map })
      } else {
        marker = new window.google.maps.Marker({ position, map, icon })
      }
      ;(marker as google.maps.Marker).setValues(customValMarker)
    },

    // Add all markers to Map
    addAllMarkers(storeData: [], map: google.maps.Map, markerIcon?: google.maps.Symbol): void {
      for (let i = 0; i < storeData.length; i++) {
        const item: any = storeData[i]
        const storeName: string = item.name
        const storeId: string = item.eoGourmetId
        const latlng: mapCenterLatLng = { lat: Number(item.latitude), lng: Number(item.longitude) }
        if (!markerIcon) {
          this.mapMarker(storeId, storeName, latlng, map)
          continue
        }
        this.mapMarker(storeId, storeName, latlng, map, markerIcon)
      }
    },

  }
})
</script>

マーカーにクリックイベントを追加する

マーカーにマウスクリックがあったときに、イベントを発火させてみます
カスタムIDを指定しているので、クリックされたお店の情報をDBから取ってきて表示する、みたいなことができます

手順

  1. (marker as google.maps.Marker) オブジェクトにイベントリスナーを追加します
  2. markerStoreId にクリックされたマーカーのカスタムID (ここではお店のID) を取得しています
  3. 取得したIDで表示するお店を更新する、みたいなことができそうです
    • コードではVuexにあるお店の情報を更新しています ( updateSelectedStore がVuexにアクションを実行)

参考

Map.vue
<template> ... </template>

<script lang="ts">
import Vue from 'vue'
const apiKeyGoogleMaps = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

export default Vue.extend({
  data() { return { GMap: {} as google.maps.Map, } },
  head() { return { ... } },
  computed: {
    ...mapGetters({
      allStores: 'stores/allStores',
    })
  },
  mounted() {
    this.initMap()
    await this.addAllMarkers(this.allStores, this.GMap)
    this.mapClickEvents()
  },
  methods: {

    ...mapActions({
      updateSelectedStore: 'stores/updateSelectedStore',
    }),

    initMap(): { ... },
    createMap( ... ): { ... },
    mapClickEvents(): { ... },

    // add marker to Map
    mapMarker(storeId: string, storeName: string, position: google.maps.LatLngLiteral, map: google.maps.Map, icon?: google.maps.Symbol): void {
      let marker = {}
      const customValMarker = { storeId: storeId, name: storeName }
      if (!icon) { ... } else { ... }
      ;(marker as google.maps.Marker).setValues(customValMarker)


      /* eslint-disable @typescript-eslint/no-unused-vars */
      ;(marker as google.maps.Marker).addListener('click', async (event: google.maps.MapMouseEvent) => {
        const markerStoreId = (marker as google.maps.Marker).get('storeId')
        await this.updateSelectedStore(markerStoreId)
      })


    },

    addAllMarkers(storeData: [], map: google.maps.Map, markerIcon?: google.maps.Symbol): void { ... },

  }
})
</script>

お店等の情報を取得する(Places API)

ここまでは Maps JavaScript API を使ってきましたが、 Places API を使ってGoogle Mapsのお店の情報を取得してみます
Places APIでお店の写真( img タグで使用する写真のURL)を1つ取得してみます

手順

  • マーカーのクリックイベント内に追記していきます
    1. placesService クラスの findPlaceFromQuery 関数でお店の情報(ここでは placeId )を取得します
      • findPlaceQueryquery は検索したいお店の情報を指定(ここではお店の名前と住所を指定)
      • findPlaceQueryfields は必要なお店の情報を指定(ここでは place_id を指定)
    2. placeId でより詳細なお店の情報を placesService クラスの getDetails 関数で取得します
      • place_id をクエリに指定して、1つのお店の詳細情報を取得しています
    3. getDetails 関数の results からお店の写真のURLが取得できます
      • results!.photos![0].getUrl()

参考

Map.vue
<template> ... </template>

<script lang="ts">
import Vue from 'vue'
const apiKeyGoogleMaps = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

export default Vue.extend({
  data() { return { GMap: {} as google.maps.Map, } },
  head() { return { ... } },
  computed: {
    ...mapGetters({
      allStores: 'stores/allStores',
    })
  },
  mounted() {
    this.initMap()
    await this.addAllMarkers(this.allStores, this.GMap)
    this.mapClickEvents()
  },
  methods: {

    ...mapActions({
      updateSelectedStore: 'stores/updateSelectedStore',
      addStoreImg: 'stores/addStoreImg',
    }),

    initMap(): { ... },
    createMap( ... ): { ... },
    mapClickEvents(): { ... },

    // add marker to Map
    mapMarker(storeId: string, storeName: string, position: google.maps.LatLngLiteral, map: google.maps.Map, icon?: google.maps.Symbol): void {
      const self = this
      let marker = {}
      const customValMarker = { storeId: storeId, name: storeName }
      if (!icon) { ... } else { ... }
      ;(marker as google.maps.Marker).setValues(customValMarker)


      /* eslint-disable @typescript-eslint/no-unused-vars */
      ;(marker as google.maps.Marker).addListener('click', async (event: google.maps.MapMouseEvent) => {
        const markerStoreId = (marker as google.maps.Marker).get('storeId')
        await this.updateSelectedStore(markerStoreId)


        // find store in Google Maps Places API
        const findPlaceQuery = {
          query: `${this.selectedStoreName} ${this.selectedStoreAddress}`,
          fields: ['place_id']
        }
        const placesService = new google.maps.places.PlacesService(map)
        let plusCode: {name: string, data: string}, placeId: string
        await placesService.findPlaceFromQuery(findPlaceQuery, async function (results: Array<google.maps.places.PlaceResult> | null, status: google.maps.places.PlacesServiceStatus): Promise<void> {
          // ↓ の[ 'ALL' ]の指定は本番環境では非推奨 必要なフィールドだけ指定する
          const getDetailsQuery = { placeId, fields: [ 'ALL' ] }
          await placesService.getDetails(getDetailsQuery, async function (results: google.maps.places.PlaceResult | null, status: google.maps.places.PlacesServiceStatus) {
            const isStatusOk = status === 'OK'
            if (isStatusOk) {
              const photoUrl = await results!.photos![0].getUrl()
              // photoUrlをimgタグのsrcに指定すれば写真を表示できる
              self.addStoreImg(photoUrl)
            }
          })
        })
      })


      })


    },

    addAllMarkers(storeData: [], map: google.maps.Map, markerIcon?: google.maps.Symbol): void { ... },

  }
})
</script>

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1