Google Mapでハザードマップを表示するまで３ - WebサイトにGoogle Mapを埋め込む



『Google Mapでハザードマップを表示するまで』の第３回の記事です

第１回は[こちら](https://qiita.com/mjusui/items/9940a6abeb16e3fc2f59)
第２回は[こちら](https://qiita.com/mjusui/items/584591763aa66d931738)


# Google Maps APIの概要


2023/03/14現在 [料金プランと API の費用 - Google Maps Platform](https://mapsplatform.google.com/pricing/?hl=ja) によると、Google MapのAPIには以下の選択肢があります


- マップ
  - 地図の画像や、スクロールできる動的な地図をWebサイトやアプリに埋め込む
- ルート
  - 経路(線)の検索と表示
- プレイス
  - 場所(点)の検索と表示

今回は **マップ** でWebサイトに動的な地図を埋め込み、**プレイス** で検索した点に地図を移動するところを記載します


## 手順

動的な地図をWebサイトに埋め込むのに [Maps JavaScript API](https://developers.google.com/maps/documentation/javascript?hl=ja) を利用していきます

- APIキーの発行
- `<script>` タグでライブラリの読み込み
- オブジェクトの初期化
- イベントハンドラの設定
- 検索
- 指定位置へ移動
- タイルの描画

### APIキーの発行

- 1. [Google Cloud プロジェクトをセットアップする  |  Maps JavaScript API  |  Google Developers](https://developers.google.com/maps/documentation/javascript/cloud-setup?hl=ja)
- 2. [API キーを使用する  |  Maps JavaScript API  |  Google Developers](https://developers.google.com/maps/documentation/javascript/get-api-key?hl=ja)

### `<script>` タグでライブラリの読み込み

```html
<script>
window.initMap=function(){
  // Google Map ライブラリが読み込まれたら実行する
};
</script>

<script src='https://maps.googleapis.com/maps/api/js
?key=<上記の手順で発行したAPI key>
&libraries=drawing,places,marker
&callback=initMap' ></script>
```

- マップ以外の機能を利用する場合は `libraries` に利用する機能を **カンマ区切り** で指定する必要があります
今回利用するのは図形(タイル)の描画、プレイス(地名検索)、マーカー(指定位置に移動)なので `libraries=drawing,places, marker` を指定しています。指定できるライブラリの一覧は[こちら](https://developers.google.com/maps/documentation/javascript/libraries?hl=ja)
- `callback` には、Google Mapのライブラリが読み込まれたあとに実行する関数名を指定します
ここではサンプルコードにしたがって `window` オブジェクトに `initMap` 関数をセットしています
この関数の中で、地図のオブジェクトやイベントハンドラを初期化していきます


(`libraries` はクソ真面目に指定しなくても使えますが、余計なライブラリの読み込まないことで、パフォーマンスの改善になっているみたいです)

### オブジェクトの初期化

```html
<script>
let map=null;
let marker=null;
let place=null;
window.initMap=function(){
  // mapオブジェクトの初期化
  // 初期の中心点は、なんとなく中央区
  const chuo_ku={
    lat: 35.67066340446589,
    lng: 139.77196419558666,
  };

  map=new google.maps.Map(maproot, {
    center: chuo_ku,
    zoom: 15,
    minZoom: 14,
    gestureHandling: 'greedy',
  });

  // merkerオブジェクトの初期化
  marker=new google.maps.Marker({
    position: chuo_ku,
    map: map,
  });

  // places(検索)オブジェクトの初期化
  place=new google.maps.places.PlacesService(map);
};
</script>
```

- マップ、マーカー、ブレイスオブジェクトの詳細は[リファレンス](https://developers.google.com/maps/documentation/javascript/reference/map?hl=ja)を参照
ここでは地図の初期の中心点を中央区の座標に、ズーム率を15、最小ズーム率を14にしています。あまり広い範囲を表示できてしまうと、タイルの描画量が増えてパフォーマンスに影響するので、最小ズーム率を制限しています
１本指で全ての操作ができるようにしたいので `gestureHndling: 'greedy'` を指定しています


### イベントハンドラの設定

Google Maps APIでは、地図に表示されている範囲を、東西南北の辺の経緯度で示した `bounds` として提供しています
今回は、表示範囲が変更されるたびに、表示範囲にあわせてタイルを描画するため、表示範囲の変更を `bounds_changed` というイベントで検知します


```html
<script>
window.initMap=function(){
  // 前略

  let bounds_last=null;
  map.addListener('bounds_changed', ()=>{
    const curr=new Date() // このイベントが発生した時間
      .getTime();
    bounds_last=curr;  // 最後にイベントが発生した時間を更新

    setTimeout(()=>{  // boundsの変更から600ms待つ
      if(curr < bounds_last){ // このイベントが発生して600msの間に、追加でイベントが発生していた場合は、何も処理しない
        return;
      }
      const bounds=map.getBounds() // 現在のboundsを取得
        .toJSON();

      fetchAndRenderTiles(bounds); // サーバからbounds内に含まれるタイルデータを取得して、地図に描画
    }, 600);
  });
}
</script>
```

- `bounds` が変更されるたびにタイルを描画し直すと、地図をスクロールした際などは無駄な描画処理が増えてしまうので、最後に `bounds` が変更されてから600ms待って、boundsの値が安定してからタイルを描画するよう `setTimeout` で調整しています

### 検索

地名などの文字列から、場所の座標(経緯度)を検索するにはプレイスオブジェクトの `findPlaceFromQuery` 関数を使います

```html
<script>
  place.findPlaceFromQuery({ query: query,  // クエリ文字列(地名など)から、場所の座標(経緯度)を検索する
    fields: ['geometry',], }, (results, status)=>{
    if( !(status === google.maps.places.PlacesServiceStatus.OK) ){  // 検索が成功しなかった場合は何も処理しない
      return;
    }
    const center=results[0].geometry.location;  // 検索結果の１番目の座標を取得する

    move(center);  // 上記の座標に地図の中心点を移動する
  });
</script>
```

### 指定位置への移動

地図の中心点を移動してから、中心点にマーカーをセットします

```html
<script>
  let move=(center)=>{
    map.setCenter(center);
    marker.setPosition(center);
  };
</script>
```

### タイルの描画

新しくタイルを描画する際、一つ前に描画したタイルを削除してから描画するという処理にしました
タイルの描画はそれなりに重い処理なので `setTimeout` で時間をバラけさせて実行しています
タイルの描画中に、次のタイルの描画が始まってしまった場合、そのタイルの描画処理はキャンセルします

```html
<script>
  let shape_rects=[];
  let shape_last=null;
  const renderTiles=(items)=>{  // サーバから取得したタイルの配列を受け取り、地図上に描画する
    const curr=new Date()  // この描画が始まった時間
      .getTime();
    shape_last=curr;  // 最後にタイルを描画した時間を更新

    const reset_rects=shape_rects;  // 最後に描画したタイルの配列(これを消して、今回分のタイルを新しく描画する)
    shape_rects=[];  // 今回描画するタイルは、次回の描画時に消す必要があるため、この配列に保持しておく

    // 前回描画したタイルを消す
    reset_rects.forEach(rect =>{
      const delay=Math.floor(Math.random() * 11);  // タイルの描画(削除)処理は重いので、適当な時間遅延して、処理をバラけさせる
      setTimeout(()=>{
        rect.setMap(null);  // タイルの削除
      }, delay);
    });

    items.forEach(item =>{  // 今回分のタイルを描画する
      const { north, south, east, west, }=item;
      const rank=(item.weight || item.rank);  // 浸水深(rank)に応じて、タイルの色を決める
      if(!rank){
        return;
      }
      const color=(
        rank < 1 ? null
      : rank < 2 ? '#f7f5a9'
      : rank < 3 ? '#ffd8c0'
      : rank < 4 ? '#ffb7b7'
      : rank < 5 ? '#ff9191'
      : rank < 6 ? '#f285c9'
      : '#dc7adc' );

      const delay=Math.floor(Math.random() * 22);  // タイルの描画処理は重いので、適当な時間遅延して、処理をバラけさせる
      setTimeout(()=>{
        if(curr < shape_last){　// この描画が開始する前に、次の描画が開始していた場合は、描画をキャンセルする
          return;
        }
        const rect=new google.maps.Rectangle({  // 四角形オブジェクト
          strokeColor: color,
          strokeOpacity: 0,
          strokeWeight: 0,
          fillColor: color,
          fillOpacity: 0.66,
          map, bounds: { north, south, east, west, },

          clickable: false,  // ここをtrueにしてしまうと、タイルの裏に隠れた地図をクリックできなくなる
          draggable: false,
          editable: false,
          visible: true,
        });
        shape_rects=[...shape_rects, rect, ];  // 次回描画時に、このタイルを削除するため、この配列に保持しておく
      }, delay);
    });
  };
</script>
```


# おわりに

『Google Mapでハザードマップを表示するまで』に必要な作業は、ひととおり紹介できたと思います