🐕

Google Map API を導入してMapを表示

2023/04/24に公開

Google Map APIを導入し、投稿の際に該当場所を地図上に表示したいと思います。
自身が導入に苦戦しましたので、自己理解のためにも記録を残します。

0. APIキーの取得

APIキーの取得方法は省略させていただきます。

APIキーの取得ができたら、忘れずに行うこと

  • .envファイル に記述する (ファイル要作成)

    → このファイルに記述することで、データベース上でAPIキーが公開されなくなります。

    ★ gem ‘dotenv-rails’導入の必要あり

    " 例)キー = 123456 の場合

    API_KEY='123456'

    このように記述することで、
    どのファイルからでもENV['API_KEY']と記述することでキー = 123456を呼び出すことができます。

  • .envファイルを gitignoreファイルに加える

    → これを怠ると、せっかく.envファイルに記述することで
     公開されなくなったAPIキーをGitHub上で見ることができてしまいます。

Geocordingとは

Mapから緯度と経度を算出して、特定の位置(住所)を見つけてくれる機能

必要な準備は、
 gem ‘geocorder’ のインストール
 API: Maps JavaScript API のインストール
      Geocording API のインストール

1. ライブラリをインストール

Gemfile
 gem ‘geocoder’
 ↓
$ bundle install

2. 必要なカラムの確認

該当テーブル
:address, string
:latitude, float   緯度  float = 小数値
:longitude, float   経度

手順としては、
addressの入力フォームを作成し、住所を入力します

② インストールしたライブラリgeocoderが、
カラムaddressに格納された住所や施設名の情報から緯度と経度を自動計算し、それぞれのカラムに格納してくれます。

そのため、該当するPostモデルに以下の記述をしましょう。

post.rb

# Map用
  geocoded_by :address
  after_validation :geocode, if: :address_changed?

3. Google Mapを読み込む

Map API を読み込むためには、JSファイル(もしくはHTMLファイルの<script>タグ)内に以下の記述が必要です。

<script src="https://maps.googleapis.com/maps/api/js?language=ja&key=<%= ENV['API_KEY'] %>&callback=initMap" async defer></script>

解説:これは、Maps JavaScript API をファイルに読み込むための記述です。

  • URLhttps://maps.googleapis.com/maps/api/js の後の ?language=ja は、
    読み込んだGoogleMapの住所等の表示言語を日本語(ja)に設定するために追記しました。
  • key=<%= ENV['API_KEY'] %>
     取得したAPIキーを登録します ( “.env”ファイルに記述したキーを呼び出します)
     もし“.env”ファイルを使用しない場合は、key=123456と直接記述します。
  • callback=initMap
     Google Map API の読み込みが完了したら関数initMap を呼び出すという記述です。
  • 通常、HTMLの読み込みと実行を全て終えてからJS <script>の読み込みと実行が行われますが、async 属性や defer 属性を加えることで、HTMLと並行して非同期で処理できるようになります。

…ではこの2つの違いは??
 ⇒ 読み込んだ後、実行するタイミングの違いです

  • async属性は、HTMLと並行してJSを読み込み、JSの読み込みが終わった時点でHTMLの処理を中断して、読み込んだJSの実行を開始します。JSの実行が終わった時点で、HTMLの処理を再開します。
  • defer属性も同じくHTMLと並行してJSを読み込みますが、そのJSを実行するタイミングは読み込み直後ではなく、HTMLの読み込みと実行が完了した後です。
  • ブラウザによっては対応していない場合もあるようです。
    async 属性と defer属性の両方を記述しておけば、async に対応していなかった場合には deferが適用されるようになります。
  • <script src=”” async defer> <script async defer src=””> どちらの書き方でもOKです。

4. Viewファイルに記述

新規投稿画面(posts/new.html.erb)
  :
  :
①
<div class="form-group row">
  <%= f.label :address, '施設住所', class: 'col-md-4' %>
  <%= f.text_area :address, id: :address, autofocus: true, placeholder: '施設名を入力していただくとMapと詳細住所が表示されます)', class: 'col-md-6  d-flex align-items-stretch', size: '30x2' %>
  <%= f.button 'Map更新', type: 'button', class: 'btn btn-sm btn-secondary', onclick: 'codeAddress()' %>
</div><div class="row">
  <%= f.label :latitude, '緯度', class: 'col-md-2 offset-1', style: 'display: none;' %>
  <%= f.text_field :latitude, id: :latitude, class: 'col-md-3', style: 'display: none;' %>
  <%= f.label :longitude, '経度', class: 'col-md-2 offset-1', style: 'display: none;' %>
  <%= f.text_field :longitude, id: :longitude, class: 'col-md-3', style: 'display: none;' %>
  <%= f.hidden_field :latitude, id: 'post_latitude' %>
  <%= f.hidden_field :longitude, id: 'post_longitude' %>
</div>
  :
  :
<%= render 'map_new' %>  //Map作成のファイル読み込み

内容を確認しましょう。

<div class="form-group row">
  <%= f.label :address, '施設住所', class: 'col-md-4' %>
  <%= f.text_area :address, id: :address, autofocus: true, placeholder: '施設名を入力していただくとMapと詳細住所が表示されます)', class: 'col-md-6  d-flex align-items-stretch', size: '30x2' %>
  <%= f.button 'Map更新', type: 'button', class: 'btn btn-sm btn-secondary', onclick: 'codeAddress()' %>
</div>
  • 以下のように入力欄とMapが表示されます。

  • <%= %> 内でIDを指定するための記述が id: :ID名 です。
    ② でも同様にIDを指定しています。

    id: :ID名 のID名はカラムです。
    id: 'ID名' のID名は任意の名称です。

  • IDを指定するのは、
    JSファイルでこれらのID名を指定することで、HTMLの要素を読み込むためのものです。

    (HTML) id: :address (JS) document.getElementById('address')
    ⇒ 住所入力欄に入力した情報が、JSファイルに読み込まれます

<div class="row">
  <%= f.label :latitude, '緯度', class: 'col-md-2 offset-1', style: 'display: none;' %>
  <%= f.text_field :latitude, id: :latitude, class: 'col-md-3', style: 'display: none;' %>
  <%= f.label :longitude, '経度', class: 'col-md-2 offset-1', style: 'display: none;' %>
  <%= f.text_field :longitude, id: :longitude, class: 'col-md-3', style: 'display: none;' %>
  <%= f.hidden_field :latitude, id: 'post_latitude' %>
  <%= f.hidden_field :longitude, id: 'post_longitude' %>
</div>
  • 全て画面上に表示されていません。
  • ② の上4行には style: 'display: none;' というコードがあります。
    これは画面上に表示しないということです。
  • この4行では、Mapから “緯度と経度” を取得し表示します。(geocoder)
    画面上に緯度と経度を表示したい場合には非表示にする必要はないですが、
    今回は表示不要なのでstyle: 'display: none;'と記述しています。
  • 下2行のhidden_field は、 “form_with” にネストします。
    hidden_field は何か値を送るのではなく、それぞれ直後に書かれている :latitude 緯度 , :longitude 経度 をform_withのPostモデルに送ります。
_map_new.html.erb
<div class="row w-100">
  <div class="offset-0 offset-md-4 col-md-8 w-100">
    <small class="text-black-50">マーカーをドラック&ドロップで位置の調整ができます。</small>
    <div id="map"></div>
  </div>
</div>

<style>
  #map {       //id='map'のcss指定
    height: 300px;
    width: 100%;
  }
</style>

<script>
  let map; // マップ
  let marker; // ピン
  let geocoder; // 住所
  let aft; // 一回ピンを作ったか判定用

  //①初期マップの設定
  function initMap(){
    geocoder = new google.maps.Geocoder()
    map = new google.maps.Map(document.getElementById('map'), {
      center:  {lat: 35.68123620000001, lng: 139.7671248},  //東京
      zoom: 15,
      fullscreenControl: false, //マップを全画面モードで表示するボタンを非表示
      streetViewControl: false, //ストリートビューに切り替えるボタンを非表示
      scaleControl: true, //地図のスケールを表示(デフォルトは非表示)
      mapTypeControl: false //地図と航空写真を切り替えるボタンを非表示
    });
  }

  //②位置情報を入力してEnter(keyCode13)押したらMapに反映される
  document.getElementById('address').addEventListener('keydown',(event) => {
    //エンターキーがクリックされたか
    if (event.keyCode == 13) {
      codeAddress();
    }
    return false;
  });

  //③検索後のマップ作成
  function codeAddress(){
    let inputAddress = document.getElementById('address').value;
    geocoder.geocode( { 'address': inputAddress}, function(results, status) {
      if (status == 'OK') { //住所が取得できた場合//すでにマーカーがあれば初期化
        if (aft == true){
          marker.setMap(null);
        }//新しくマーカーを作成する result[0]は
        map.setCenter(results[0].geometry.location);
          marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location,
          draggable: true	// ドラッグ可能にする
        });//マーカー作成済
        aft = true//検索した時に緯度経度を入力する
        document.getElementById('latitude').value = results[0].geometry.location.lat();
        document.getElementById('longitude').value = results[0].geometry.location.lng();// hidden_fieldに入れる緯度経度
        document.getElementById('post_latitude').value = results[0].geometry.location.lat();
        document.getElementById('post_longitude').value = results[0].geometry.location.lng();

        $('#address').val(formattedAddress(results[0].formatted_address))// マーカーのドロップ(ドラッグ終了)時のイベント
        google.maps.event.addListener( marker, 'dragend', function(ev){
          // イベントの引数evの、プロパティ.latLngが緯度経度
          document.getElementById('post_latitude').value = ev.latLng.lat();
          document.getElementById('post_longitude').value = ev.latLng.lng();

          // ドラッグでのピン移動で住所を求めるコーディング
          let geocode = new google.maps.Geocoder();

          geocoder.geocode({
            latLng: ev.latLng
          }, function (results, status) {

            if (results[0].geometry) {
              $('#address').val(formattedAddress(results[0].formatted_address))
            } else {
              alert('該当する詳細住所はありません。検索時のアドレスでそのまま登録されます。')
            }
          });

        });
      } else {
        alert('該当する詳細住所はありません:' + status);
      }
    });
  }

  function formattedAddress(address)
  {
    let splitAddress = address.split(' ')
    splitAddress.shift();

    return splitAddress.join(' ')
  }
</script>
<script src="https://maps.googleapis.com/maps/api/js?language=ja&key=<%= ENV['API_KEY'] %>&callback=initMap" async defer></script>

解説しましょう。

①初期マップの設定
function initMap(){
  geocoder = new google.maps.Geocoder()
  map = new google.maps.Map(document.getElementById('map'), {
    center:  {lat: 35.68123620000001, lng: 139.7671248},  //東京
    zoom: 15,
    fullscreenControl: false, //マップを全画面モードで表示するボタンを非表示
    streetViewControl: false, //ストリートビューに切り替えるボタンを非表示
    scaleControl: true, //地図のスケールを表示(デフォルトは非表示)
    mapTypeControl: false //地図と航空写真を切り替えるボタンを非表示
  });
}
  • initMap という関数を定義しています。
    MapAPIを読み込む際のcallback=initMapで呼び出されるのはこの関数です。

  • 変数”geocoder” はまだ空っぽです。
    Mapから取得する住所」の情報を格納します。

  • 変数”map” はまだ空っぽです。
    新しく作成するMap」の情報を格納します。

    HTMLファイルの id=”map” の部分に作成したMapを表示。
    この関数内でMapの初期設定としてMapの中心地(東京)、縮尺、レイアウトを指定しています。

  • document.getElementById('ID名')と同様にdocument.querySelector('ID名')もID名からHTMLの要素を読み込みます。異なる点は、後者はID名以外にclass名も読み込み可能ということです。
    ただし同じclass名が複数あったとしても読み込むのは最初の1つのみです。

    参照:JS Id 取得

②位置情報を入力してEnter(keyCode13)押したらMapに反映される
document.getElementById('address').addEventListener('keydown',(event) => {
  //エンターキーがクリックされたか
  if (event.keyCode == 13) {
    codeAddress();
  }
  return false;
});
  • addEventListenerにより、’keydown’イベントが発生します。
    ID名’address’で読み込むHTML要素(住所の入力欄)でキーが押されたら(keydown)、結果となる処理が実行されます。その処理がif文で記述されています。
  • イベントのきっかけとなるkeydownですが、そのキーがkeyCode == 13(Enter)の場合のみ処理が実行されます。その処理とは、関数codeAddress() です。
  • 押したキーがEnter以外の場合、return false;:イベントは発生しません。
③検索後のマップ作成

関数 codeAddress() = ②でkeydownイベントの結果として実行される処理です。
長いので、区切って少しずつ…

function codeAddress(){
  let inputAddress = document.getElementById('address').value;
  geocoder.geocode( { 'address': inputAddress}, function(results, status) {
    if (status == 'OK') { 
        //住所が取得できた場合の処理 
    } else {
        alert('該当する詳細住所はありません:' + status);
      }
    });
  }

まず関数 codeAddress の全体像です。

変数 “inputAdress” にはID名#addressから読み込むHTML要素を格納します。
要素とは <%= f.text_area :address, id: :address, ...... %> = 住所の入力欄ですが、.value により入力された値を読み込みます。

geocoder は①で変数宣言した住所(新しくMapから取得)です。そこから、
{'address': inputAddress} ’address’ = 入力欄に入力した値

if文の条件分岐により、
 ” if ” 入力した値から住所が取得できた場合の処理
 ” else ” 住所が取得できなかった場合にはアラートメッセージを表示するという処理

status ” とは?? → コンソールで確認してみよう

JSのconsole

上記のように console.log(results, status); と記述すると、このコードの処理が実行されたときに以下のようにconsoleに表示されます。

一番下に ‘OK’ と表示されています。
status == ‘OK’ (住所が取得できた) の状態です。
その上に表示されているのが、results = 取得できた住所の詳細です。

つまり住所が取得できたら status == ‘OK’ ということです。

次に、if文の中身(住所取得できた場合)を理解しましょう。

マーカーの作成

//⑴すでにマーカー作成済であれば初期化
if (aft == true){
  marker.setMap(null);
}

//⑵新しくマーカーを作成する
map.setCenter(results[0].geometry.location);
  marker = new google.maps.Marker({
  map: map,
  position: results[0].geometry.location,
  draggable: true
});

//⑶マーカー作成済
aft = true
  • aft は関数の前で宣言している変数ですが、
    宣言する時点ではまだ値は格納されておらず空っぽです。

aft == trueの場合マーカーを初期化するという処理を指定しています。
 (複数のマーカーが表示されてしまわないようにするための処理です。)

つまりaft == trueとは、すでにMapを作成済でマーカーが表示されている場合ということです。マーカーは⑵で作成されます。
(新規投稿画面ですが、投稿前にMapは何回でも修正できます。)
・Map作成が一度目であれば、
 aft == falseであり、マーカー初期化の処理は実行されません。
・Mapを一度表示してから変更する場合は二度目なので、
 aft == trueとなり、⑵のマーカー作成前に初期化されます。

⑵ Mapの中心地を指定します。
results[0].geometry.location について、コンソールを再度確認してみましょう。
results[0] とは、複数結果があった場合に最初の結果ということです。
(このコンソールを見ると、結果は一つだけです)

geometry.locationがMapの中心となります。
ここには、lat: = 緯度 / lng: = 経度 が含まれていることがわかります。

・ 変数”marker” はまだ空っぽです。
  「新しく作成したマーカー」の情報を格納します。
 ・ “map” には変数”map”=作成したMap を格納します。
   map: map; mapて何? : 既に宣言しているmapです
   つまり、どのmapにマーカー置く? : 上記で作成されたnewMapにマーカー置く
 ・ “position” マーカーを置く位置です。results[0].geometry.location
   Mapの中心に指定した場所にマーカーを置きます。
 ・ “draggable: true; マーカーをドラッグして動かすことを可能にします。

⑶ Mapにマーカーを作成したという記述です。
これにより、Mapを再表示する際には⑴if文がtrueとなり、マーカーが初期化されます。

◆ 緯度経度の取得

//⑷検索した時に緯度経度を入力する
document.getElementById('latitude').value = results[0].geometry.location.lat();
document.getElementById('longitude').value = results[0].geometry.location.lng();

//⑸hidden_fieldに入れる緯度経度
document.getElementById('post_latitude').value = results[0].geometry.location.lat();
document.getElementById('post_longitude').value = results[0].geometry.location.lng();

$('#address').val(formattedAddress(results[0].formatted_address)) 

⑷ ID名’latitude’ ‘longitude’ からHTML要素を取得します。
 <%= f.text_field :latitude, id: :latitude, ...... %>
 <%= f.text_field :longitude, id: :longitude, ...... %>
そして、このに Mapから取得した結果の中から緯度/経度をそれぞれ代入します。
 = results[0].geometry.location.lat();
 = results[0].geometry.location.lng();
HTML要素の text_field は非表示にしていますが、非表示を解除するとここに検索結果の緯度と経度が表示されていることが確認できます。

⑸ ID名’post_latitude’ ‘post_longitude’ からHTML要素を取得します。
 <%= f.hidden_field :latitude, id: 'post_latitude' %>
 <%= f.hidden_field :longitude, id: 'post_longitude' %>
そして、⑷と同様このに Mapから取得した結果の中から緯度/経度をそれぞれ代入します。
これはhidden_field なので、取得した緯度/経度の情報をそれぞれ :latitude :longitude に代入してform_withに入力した内容に含めてPostモデルへ渡します。

$('#address').val(formattedAddress(results[0].formatted_address))

  • ここまでで緯度経度は取得できているので、
    あとはMapの情報から得られた正確な住所を表示したいです。

    このコードを記述することで、Mapから得られる住所を入力欄に表示することができます。

まず$('#address').valですが、ID名’address’ のHTML要素は住所入力欄です。
その値(.val = value属性) なので、入力欄に表示される住所のことです。

表示される住所は何か? ⇒ formattedAddress(results[0].formatted_address)
formatted_address は、以下参照のように
APIによってフォーマットされた住所のことです。

よって、HTML要素の住所入力欄に表示される住所は、マーカーを移動した後で取得する住所情報の “日本、〒” を除外した “・・県・・市~~~” という住所となります。

formattedAddressについては以下参照。

formattedAddress とは? → 後の方で関数定義されています。


splitAddress.shift() により、最初の日本、〒を除外します。
その後 ’、’で区切られていれば ’ ’(=半角スペース)でつなげられます(=join)

◆ マーカーのドラッグ&ドロップ

//⑹ マーカーのドロップ(ドラッグ終了)時のイベント
google.maps.event.addListener( marker, 'dragend', function(ev){
  // イベントの引数evの、プロパティ.latLngが緯度経度
  document.getElementById('post_latitude').value = ev.latLng.lat();
  document.getElementById('post_longitude').value = ev.latLng.lng();

  // ドラッグでのピン移動で住所を求めるコーディング
  let geocoder = new google.maps.Geocoder();

  geocoder.geocode({
    latLng: ev.latLng
  }, function (results, status) {

    if (results[0].geometry) {
      $('#address').val(formattedAddress(results[0].formatted_address))
    } else {
      alert('該当する詳細住所はありません。検索時のアドレスでそのまま登録されます。')
    }
  });
});

dragend イベントが発生します。このイベントはマーカーをドラッグで動かし、ドロップした時に発生します。イベント発生の結果として関数 function(ev) {} が実行されます。

関数 function(ev) {} の処理内容

ドラッグ&ドロップの結果取得する情報はev に格納されます。
document.getElementById によりID名'post_latitude'からHTML要素を取得し、その値に ev.latLng.lat(); を代入します。

ev.latLng.lat(); て何?
ev(ドラッグ&ドロップ後の住所情報に含まれる).latLng(緯度経度).lat()(の緯度)
つまり、ドラッグ&ドロップするたびに、そのマーカーの位置情報をそれぞれ格納しHTML要素に代入します。ID名でHTML要素を読み込んでいるので、HTML側でその新しい位置情報を取得でき流ということです。

let geocoder = new google.maps.Geocoder();
マーカーの移動により住所情報が変わったので、
変数”geocoder” に(マーカー移動後の)「Mapから取得した住所」の情報を格納します。

const 定数ではなくlet 変数なので、内容は変化可能。

geocoder.geocode({ })
 この変数”geocoder”には住所情報が格納されており、 .geocode() メソッドがその住所から緯度経度といった詳細情報に変換して返してくれます。

latLng: ev.latLng
 この時返される緯度経度は、マーカー移動後のものです。

function (results, status) {} というコールバック関数が指定されています。
・ コールバック関数とは、ある関数が実行された際に引数として呼びされ実行される関数のことです。今回の場合は、関数 ev の引数として呼び出されています。
・ このコールバック関数は結果としてマーカー移動後の結果を返しますが、内容は再度コンソールを参照しましょう。

if文の (results[0].geometry)は マーカー移動後のMapから取得できる位置情報であり、位置情報が取得できた場合と取得できなかった場合の条件分岐となっています。
true の場合の結果の処理は、すでに上の方で記述してあるように、取得した情報からフォーマットした住所をHTML要素の住所入力欄に表示させます。
false の場合の結果の処理は、アラートメッセージを表示させるというものになっています。

_map_edit.html.erb
<div class="row w-100">
  <div class="offset-0 offset-md-4 col-md-8 w-100">
    <small class="text-black-50">マーカーをドラック&ドロップで位置の調整ができます。</small>
    <div id="map"></div>
  </div>
</div>

<style>
  #map {       id='map'のCSS指定
    height: 300px;
    width: 100%;
  }
</style>

<script>
  let map;
  let marker;
  let geocoder;

  //初期マップの設定
  function initMap(){
    geocoder = new google.maps.Geocoder()
    map = new google.maps.Map(document.getElementById('map'), {
      center:  {lat: <%= post.latitude %>, lng: <%= post.longitude%>}, 
      zoom: 14,
      fullscreenControl: false, //マップを全画面モードで表示するボタンを非表示
      streetViewControl: false, //ストリートビューに切り替えるボタンを非表示
      scaleControl: true, //地図のスケールを表示(デフォルトは非表示)
      mapTypeControl: false //地図と航空写真を切り替えるボタンを非表示
    });

    marker = new google.maps.Marker({
      map: map,
      position:  {lat: <%= post.latitude %>, lng: <%= post.longitude %>},
      draggable: true	// ドラッグ可能にする
    });

    // マーカーのドロップ(ドラッグ終了)時のイベント
    google.maps.event.addListener( marker, 'dragend', function(ev){
      // イベントの引数evの、プロパティ.latLngが緯度経度
      document.getElementById('post_latitude').value = ev.latLng.lat();
      document.getElementById('post_longitude').value = ev.latLng.lng();

      // ドラッグでのピン移動で住所を求めるコーディング
      let geocoder = new google.maps.Geocoder();

      geocoder.geocode({
        latLng: ev.latLng
      }, function (results, status) {

        if (results[0].geometry) {
          $('#address').val(formattedAddress(results[0].formatted_address))
        } else {
          alert('該当する詳細住所はありません。検索時のアドレスでそのまま登録されます。')
        }
      });

    });
  }

  // 位置情報を入力してEnter(keyCode13)押したらMapに反映される
  document.getElementById('address').addEventListener('keydown',(event) => {
    if (event.keyCode == 13) {
      codeAddress();
    }
    return false;
  });

  //検索後のマップ作成
  function codeAddress(){
    let inputAddress = document.getElementById('address').value;
    geocoder.geocode( { 'address': inputAddress}, function(results, status) {
      if (status == 'OK') {
        //マーカーを初期化する
        marker.setMap(null);

        //新しくマーカーを作成する
        map.setCenter(results[0].geometry.location);
          marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location,
          draggable: true	
        });

        //検索した時に緯度経度を入力する
        document.getElementById('latitude').value = results[0].geometry.location.lat();
        document.getElementById('longitude').value = results[0].geometry.location.lng();

        // hidden_fieldに入れる緯度経度
        document.getElementById('post_latitude').value = results[0].geometry.location.lat();
        document.getElementById('post_longitude').value = results[0].geometry.location.lng();

        $('#address').val(formattedAddress(results[0].formatted_address))

        // マーカーのドロップ(ドラッグ終了)時のイベント
        google.maps.event.addListener( marker, 'dragend', function(ev){
          // イベントの引数evの、プロパティ.latLngが緯度経度
          document.getElementById('post_latitude').value = ev.latLng.lat();
          document.getElementById('post_longitude').value = ev.latLng.lng();

          // ドラッグでのピン移動で住所を求めるコーディング
          let geocoder = new google.maps.Geocoder();

          geocoder.geocode({
            latLng: ev.latLng
          }, function (results, status) {

            if (results[0].geometry) {
              $('#address').val(formattedAddress(results[0].formatted_address))
            } else {
              alert('該当する詳細住所はありません。検索時のアドレスでそのまま登録されます。')
            }
          });

        });
      } else {
        alert('該当する詳細住所はありません:' + status);
      }
    });
  }

  function formattedAddress(address)
  {
    let splitAddress = address.split(' ')
    splitAddress.shift()

    return splitAddress.join(' ')
  }
</script>
<script src="https://maps.googleapis.com/maps/api/js?language=ja&key=<%= ENV['API_KEY'] %>&callback=initMap" async defer></script>

map*new.html.erb” は “posts/new.html.erb” 新規投稿画面に読み込まれ、
“_map
_*edit.html.erb” は “posts/edit.html.erb” 投稿編集画面に読み込まれます。

新規投稿と編集画面は同じフォームなので、基本的に内容は同じです。

異なる点は、

① edit の方は初期値として、Mapの中心は投稿した場所の住所

geocoder = new google.maps.Geocoder()
*ma = new google.maps.Map(document.getElementById('map'), {
  center:  {lat: <%= post.latitude %>, lng: <%= post.longitude%>}, 
  zoom: 14,
});

marker = new google.maps.Marker({
  map: map,
  position:  {lat: <%= post.latitude %>, lng: <%= post.longitude %>},
  draggable: true
});

② edit の方はMapにマーカーが作成済(投稿した場所に)

if (status == 'OK') {
	//マーカーを初期化する
	marker.setMap(null);

この記述に関してnewの方では ”変数aft” を定義してマーカーを初期化するかどうか判断していましたが、editでは必ずマーカー作成済なので ”変数aft” は不要です。

_map_show.html.erb
<div id='map'></div>

<style>
  #map {       id='map'のCSS指定
    height: 300px;
    width: auto;
  }
</style>

<script>
  // 初期マップの設定
  let map    let=宣言
  let marker
  function initMap(){
    geocoder = new google.maps.Geocoder()

    map = new google.maps.Map(document.getElementById('map'), {
      center:  {lat: <%= post.latitude %>, lng: <%= post.longitude%>},
      zoom: 15,
      fullscreenControl: false, //マップを全画面モードで表示するボタンを非表示
      streetViewControl: false, //ストリートビューに切り替えるボタンを非表示
      scaleControl: true, //地図のスケールを表示(デフォルトは非表示)
      mapTypeControl: false //地図と航空写真を切り替えるボタンを非表示
    });

    marker = new google.maps.Marker({
      map: map,
      position:  {lat: <%= post.latitude %>, lng: <%= post.longitude %>}
    });
  }
</script>
<script src="https://maps.googleapis.com/maps/api/js?language=ja&key=<%= ENV['API_KEY'] %>&callback=initMap" async defer></script>

詳細画面では、その投稿の場所の住所が中心地としてマーカーがある。
Mapの表示のみ。変更や移動はできない。

_map_index.html.erb
<div id='map'></div>

<style>
#map {       id='map'のCSS指定
  height: 300px;
  width: auto;
}
</style>

<script>
  function initMap() {

    // 初期表示位置
    let latlng = new google.maps.LatLng(37.5, 137.5);

    // Map設定
    let map = new google.maps.Map(document.getElementById('map'), {
      zoom: 4.7,
      center: latlng,
      fullscreenControl: false, //マップを全画面モードで表示するボタンを非表示
      streetViewControl: false, //ストリートビューに切り替えるボタンを非表示
      scaleControl: true, //地図のスケールを表示(デフォルトは非表示)
      mapTypeControl: false //地図と航空写真を切り替えるボタンを非表示
    });

    // 複数マーカー ここから
    <% posts.each do |post|%>
      ( function() {
        let markerLatLng = { lat: <%= post.latitude %>, lng: <%= post.longitude %> }; // 緯度経度のデータ作成
        let marker = new google.maps.Marker({
          position: markerLatLng,
          map: map
        });
        // マーカーをクリックしたとき、詳細情報(infowindow)を表示
        let infowindow = new google.maps.InfoWindow({
          position: markerLatLng,
          <% if admin_signed_in? %> //詳細ページへのリンク(別タブで開く)
            content: "<a href='<%= admin_post_path(post.id) %>' target='_blank'><%= post.name %></a>"
          <% else %>
            content: "<a href='<%= post_path(post.id) %>' target='_blank'><%= post.name %></a>"
          <% end %>       
        }); //infowindowをクリックすると開く
        marker.addListener('click', function() {
          infowindow.open(map, marker);
        });
      })();
    <% end %>
    // 複数マーカー ここまで
  }
</script>

<script src="https://maps.googleapis.com/maps/api/js?language=ja&key=<%= ENV['API_KEY'] %>&callback=initMap" async defer></script>

一覧画面では、一つのMapに複数のマーカーが存在します。
マーカーをクリックすると、その場所の情報を見ることができるようになっています。

では確認していきましょう。

// 複数マーカー ここから
<% posts.each do |post|%>
  ( function() {
    let markerLatLng = { lat: <%= post.latitude %>, lng: <%= post.longitude %> };
    let marker = new google.maps.Marker({
      position: markerLatLng,
      map: map
    });
    // マーカーをクリックしたとき、詳細情報(infowindow)を表示
    let infowindow = new google.maps.InfoWindow({
      position: markerLatLng,
      <% if admin_signed_in? %> //詳細ページへのリンク(別タブで開く)
        content: "<a href='<%= admin_post_path(post.id) %>' target='_blank'><%= post.name %></a>"
      <% else %>
        content: "<a href='<%= post_path(post.id) %>' target='_blank'><%= post.name %></a>"
      <% end %>
    }); //infowindowをクリックすると開く
    marker.addListener('click', function() {
      infowindow.open(map, marker);
    });
  })();
<% end %>

まず <% posts.each do |post|%> というeach文で投稿の分だけマーカーを作成します。

let markerLatLng この変数に、マーカーの位置(各投稿の緯度経度)を格納します。

let marker この変数に作成したマーカーを格納します。
この変数の内容として、
 ・マーカーの位置は、変数markerLatLng
 ・どのMapに表示するかというと、変数map (作成されたMap)

let infowindow Mapに情報ウィンドウを作成し、この変数に格納します。
この変数の内容として、
 ・情報ウィンドウを表示する位置は、 markerLatLng = 各マーカーのある位置
 ・情報ウィンドウが詳細を確認できるリンクであるということを以下のように記述

<% if admin_signed_in? %> //詳細ページへのリンク(別タブで開く)
  content: "<a href='<%= admin_post_path(post.id) %>' target='_blank'><%= post.name %></a>"
<% else %>
  content: "<a href='<%= post_path(post.id) %>' target='_blank'><%= post.name %></a>"
<% end %>
  • まず、これは<script>ダグで囲った中のJSに関する記述ですが、このファイル自体は “~~~.html.erb” ファイルなので、<% %> で囲うことでRubyの記述が可能となります。
    これが ”~~~.js” ファイルであればNGです。
  • content: という記述がありますが、このプロパティを使用することで、JavaScriptの記述である変数infoWindow にHTML要素を含ませることが可能となります。
  • content: "<a href='<%= 遷移先path %>' target='_blank'><%= infoWindowに表示する名称 %></a>"
    <a href= … ></a> により情報ウィンドウがリンクになります。
    target='_blank'は別タグで開くという指定です。

次は情報ウィンドウclickイベントです。

イベント発生には addEventListener メソッドを使用しますが、GoogleMapAPIでのイベント発生(Mapのマーカー等)には addListener メソッドを使用するようです。

marker.addListener('click', function() {
  infowindow.open(map, marker);
});

このイベントの内容は、
マーカーが click されたら、関数が実行される」というものです。

そして実行される関数の内容は、「情報ウィンドウを開く」というものです。

とても長くなりましたが、以上です。
最初の投稿なので慣れない部分もあり、見にくい箇所ありましたらご容赦ください。

間違いや修正箇所ありましたらご指摘いただけますと嬉しいです。

Discussion