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

# はじめに

Nuxt.jsでライブラリ(vue2-google-mapsなど)は使わずにGoogle Mapsを表示してみます
作りたい地図はこんな感じで、マーカーをつけてみます
地図からはわからないですが、地図とマーカーにそれぞれマウスクリックイベントもつけてみます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1022607/c03ae430-934c-6538-c2cf-ef644a0ebbd5.png)

## ここに書いてあること

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

## 環境

* macOS Monterey
* VS Code Version: 1.63.2

```zsh
$ npm -v
6.14.15

$ node -v
v14.15.4
```

```json: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1022607/e4d82bb8-fe90-b12f-5737-cd5408439f34.png)

### TypeScriptの型を定義する

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

```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` クラスに必要なパラメータを渡しておくことで、地図のカスタマイズができます
        * （参考：[Maps JavaScript API -  Map Options Interface \|  Google Developers](https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions)）
    * `google.maps.Map` クラスで作成された `google` オブジェクトは `GMap` に代入しています
        * 地図にマーカーをつけたりするのに `google` オブジェクトが必要になるためです
3. ここまででGoogleマップの表示ができています

### 補足

* TypeScriptで必要なGoogle JS API等の型定義はドキュメントに記載されています
* チュートリアルはこちら
    * [Overview  \|  Maps JavaScript API  \|  Google Developers](https://developers.google.com/maps/documentation/javascript/overview)

```vue: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](https://developers.google.com/maps/documentation/javascript/reference/event#MapsEventListener) ）

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

### 手順

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

### 参考

* チュートリアルはこちら
    * [Events  \|  Maps JavaScript API  \|  Google Developers](https://developers.google.com/maps/documentation/javascript/events)
    * ホバーなどほかにもイベントが用意されています

```vue: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` に該当 )
1. マーカーのカスタム値を追加します( `mapMarker` 関数の `(marker as google.maps.Marker).setValues` に該当 )
    * `customValMarker` にマーカーのカスタム値を用意していますが、複数のカスタムIDを用意できます
1. 複数のマーカーを表示したいので、もう一つ関数を用意します( `addAllMarkers` 関数 )
1. `addAllMarkers` 関数に `storeData` (マーカー表示したい店の情報の配列) をわたせば地図上にマーカーを追加されます

### 参考

* チュートリアルはこちら
    * [Events  \|  Maps JavaScript API  \|  Google Developers](https://developers.google.com/maps/documentation/javascript/events)

```vue: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)` オブジェクトにイベントリスナーを追加します
1. `markerStoreId` にクリックされたマーカーのカスタムID (ここではお店のID) を取得しています
1. 取得したIDで表示するお店を更新する、みたいなことができそうです
    * コードではVuexにあるお店の情報を更新しています ( `updateSelectedStore` がVuexにアクションを実行)

### 参考

* チュートリアルはこちら
    * [Events  \|  Maps JavaScript API  \|  Google Developers](https://developers.google.com/maps/documentation/javascript/events)
    * ホバーなどほかにもイベントが用意されています

```vue: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` )を取得します
        * `findPlaceQuery` の `query` は検索したいお店の情報を指定（ここではお店の名前と住所を指定)
        * `findPlaceQuery` の `fields` は必要なお店の情報を指定（ここでは `place_id` を指定）
    1. `placeId` でより詳細なお店の情報を `placesService` クラスの `getDetails` 関数で取得します
        * `place_id` をクエリに指定して、1つのお店の詳細情報を取得しています
    1. `getDetails` 関数の `results` からお店の写真のURLが取得できます
        * `results!.photos![0].getUrl()`

### 参考

* チュートリアルはこちら
    * [Places Library  \|  Maps JavaScript API  \|  Google Developers](https://developers.google.com/maps/documentation/javascript/places)
* Places Service クラスのドキュメントはこちら
    * [Places Service  \|  Maps JavaScript API  \|  Google Developers](https://developers.google.com/maps/documentation/javascript/reference/places-service)

```vue: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>

```