🌏

[Rails] Google JavaScript APIを使い投稿機能を実装する

2023/08/19に公開

gemを使わずGoogle JavaScript APIをRailsで使用する


行いたいこと

  • 投稿に緯度、経度を保存させる
  • 保存した位置情報を地図上にピンで一覧表示させる
  • ピンをクリックした際に登録済みの投稿のタイトルを吹き出し表示させる
  • 投稿内容をリンク設定し地図のピンから詳細ページまで遷移させる

前提

  • Google アカウント作成済み

APIキーの取得

Google Maps APIを利用するには、APIキーが必要なので取得します。
※既に取得済みの方は飛ばしてください。
Googleアカウントが必要になるので必要な方はそちら作成を先に準備します。
https://qiita.com/nagase_toya/items/e49977efb686ed05eadb
この方の記事がとても参考になります。
私もこの方の記事で問題なく取得できました。
※仕様変更等により表示が異なる場合があります。

取得したキーは管理にご注意ください。
dotenv-rails等を使い、環境変数にしておくのがおすすめです。
詳しくは過去に記事に残したので是非↓
https://zenn.dev/yuna0960740/articles/12d33231b8a961#環境変数の設定(dotenv-rails)

アプリケーションの準備

今回はサクッとscaffoldで大枠を作ってしまいます。

 map_test_app % rails g scaffold map lat:float lng:float text:string
-map_test_app % rails g scaffold モデル名 カラム名1:データ型1 カラム名2:データ型 2 …
 map_test_app % rails db:migrate

これでviewもcontrollerもrootも自動で生成できます。
最低限の機能を自動生成できましたら、試しにindexにマップが表示できるか確認していきます。

views/maps/index.html.erb
 ...
 <div id="map" class="show-map"></div>
 
 <script>
  if (typeof map == "undefined") {
    let map
    let marker
  }

  function initMap(){
    geocoder = new google.maps.Geocoder()

    map = new google.maps.Map(document.getElementById('map'), {
      center: { lat: 35.6803997, lng: 139.7690174 },
      zoom: 10,
    });

    marker = new google.maps.Marker({
      position:  { lat: 35.6803997, lng: 139.7690174 },
      map: map
    });
  }
 </script>
 <script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API_KEY'] %>&libraries=places&callback=initMap" async defer></script>
assets/stylesheets/application.css
 .show-map {
  height: 80vh;
  width: 90%;
  margin: 0 auto;
 }

まず<div id="map"></div>でマップを表示する場所を指定します。
<script>タグの中は基本的なgoogle mapの定義で、たったこれだけでmapを表示することができます。

center,positionの部分で試しに東京の緯度と経度を指定しています。

zoomで地図の初期表示範囲を設定できます。数字が大きくなれば詳細な施設などが見える様になり、逆に数字が小さくなると広範囲を表示することができます。お好みの範囲を指定してください。

APIキーを環境変数にしているので、<%= ENV['GOOGLE_MAP_API_KEY'] %>としています。

cssで地図の大きさを指定する必要があるので、こちらもお好みの大きさに指定してください。
できましたら確認します。

問題なく表示されました。
こちらの機能だけで、ECサイトのアクセスページ等でgoogle mapを使用して住所の表示ができる様になります。
では、mapが表示できることが確認できたら、機能として実装していきます。

投稿機能で登録した場所をピンで表示する

先ほどの記述をviewのshowに移し,少し記述を変更します。

 views/maps/show.html.erb
 ...
 <div id="map"></div>

 <script>
  if (typeof map == "undefined") {
    let map
    let marker
  }

  function initMap(){
    geocoder = new google.maps.Geocoder()

    map = new google.maps.Map(document.getElementById('map'), {
      center: { lat: <%= @map.lat %>, lng: <%= @map.lng %> },
      zoom: 10,
    });

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

変わったのはlat lngの部分くらいでそのほかに変更は特にありません。
では、新しく投稿して地図を表示させます。
先ほどの東京の緯度と経度
これで投稿してみます。

表示されました。
これでユーザーが任意の場所にピンを刺すことができる様になりました。
ただこれだと毎回緯度、経度を打たなくてはならないのでform内で緯度経度を検索できる様にします。

formで緯度経度を検索し投稿する

ここからは少しコードが多くなるので一緒に頑張りましょう。
まずformがあるviewファイルを開き,大まかに完成させます。

 views/maps/new.html.erb
...
 <%= form_with model: @map do |f| %>
    <%= f.label :text, style: "display: block" %>
   <%= f.text_field :text %>
   <%= f.hidden_field :lat, id: :lat %>
   <%= f.hidden_field :lng, id: :lng %>
   <%= f.submit %>
  <% end %>
 
 <input id="address" type="textbox" placeholder="ピンを刺したい地名を検索">
 <input type="button" value="ピンを刺す" onclick="codeAddress()">
 <div id="map" class="form-map"></div>

通常と少し違うのはhidden_fieldを使っていることぐらいでこちらもシンプルです。
検索した緯度、経度の情報をパラメータとして送信し、データを登録するために使用しています。

検索用のフォームとmapを表示するためのdivを用意しています。
次にjavascriptを書いていきます。

views/maps/new.html.erb
 ...
  <script>
  if (typeof map == "undefined") {
    let map;
    var marker;
    var geocoder;
    var afterPinned;
  }

  function initMap(){
    geocoder = new google.maps.Geocoder()

    map = new google.maps.Map(document.getElementById('map'),{
      center: { lat: 35.6803997, lng: 139.7690174 }, # 東京
      zoom: 10,
    });
  }

  function codeAddress(){ # 検索時発火
    let inputAddress = document.getElementById('address').value; # 入力した住所情報を取得
    geocoder.geocode( { 'address': inputAddress }, function(results, status) {
      if (status == 'OK') {
        if (afterPinned == true) {
          marker.setMap(null);
        }
        map.setCenter(results[0].geometry.location);
          marker = new google.maps.Marker({
            map: map,
            position: results[0].geometry.location,
            draggable: true # ピンを移動できる様にする
          });

        afterPinned = true;
        # ピンは一本しか刺せない

        document.getElementById('lat').value = results[0].geometry.location.lat();
        document.getElementById('lng').value = results[0].geometry.location.lng();
	# 情報をフォームのhidden_fieldへ送る

        google.maps.event.addListener(marker, 'dragend', function(ev){
          document.getElementById('lat').value = ev.latLng.lat();
          document.getElementById('lng').value = ev.latLng.lng();
	  # ピンが移動されたら新しい情報をフォームのhidden_fieldへ送る
        });
      } else {
        alert('該当する結果はありませんでした');
      }
    });
  }
 </script>
 <script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API_KEY'] %>&libraries=places&callback=initMap" async defer></script>
if (typeof map == "undefined") について

先ほどのshowページのコードにもあったif (typeof map == "undefined")で変数をまとめて宣言している理由は、ターボリンクスの影響で他画面から遷移してきた際に地図の情報が引き継がれてしまうバグの対策です。これ以外の解決方法は単純にターボリンクスを切ってしまう方法です。大規模なアプリケーションでない限りはそこまで影響がない(最近の通信は早いため)と聞いたので切る方が早いかもしれません。

assets/stylesheets/application.css
 ...
 .form-map {
  height: 50vh;
  width: 50%;
 }

cssも忘れずに当ててあげます。これを忘れると何も表示されなくなってしまいます。
これで投稿formの完成です。確認してみましょう。

うまく表示されました。投稿して正常に動くか確認します。

うまく投稿できました。
それでは、投稿をmapに一覧表示していきます。

投稿を地図上で一覧表示する

ルーティングの設定

まずはルーティングの設定を行います。
現在のルーティングはこの様になっています。

config/routes.rb
 Rails.application.routes.draw do
   resources :maps
  ...
 end

こちらに追記します。

config/routes.rb
 Rails.application.routes.draw do
   resources :maps do 
     get 'map', on: :collection
  end
  ...
 end

今回地図上に投稿のピンを一覧表示するだけなのでルーティングに:idが必要ないのでon: :collectionを使っています。:idが必要な場合はzon: :memberを使います。
複数ある場合はこれらで do を使って囲む書き方がベターです。

ルーティングを確認します。

 ...
 map_maps GET  /maps/map(.:format)  maps#map
 ...

コントローラーの設定

次にコントローラーの設定を行います。

app/controller/maps_controller.rb
 class MapsController < ApplicationController
  ...
  def map
    @maps = Map.all
  end
  
  private
 ...

新しくmapアクションを作成します。投稿をピンで一覧表示するので全ての投稿を取得します。

viewファイルの作成

こちらでは新たにmap.html.erbを作成しました。
作成したviewファイルに記述していきます。

views/maps/map.html.erb
 <h1>Map Maps</h1>
 <div id="map" class="map-maps"></div>

 <script>
  function initMap() {
      # 地図の表示初期値(東京)
    let latlng = new google.maps.LatLng(35.6803997, 139.7690174);
    let styles = [
      {
        featureType: "all",
	stylers: [
          { "saturation": -70 },
          { "lightness": 0 },
          { gamma: 0.6 } # 練習用に地図のデザインを変更
        ]
      }
    ];

  let map = new google.maps.Map(document.getElementById('map'), {
    zoom: 5.8,
    styles: styles, # 定義したデザインの適用
    center: latlng
  });
  let transitLayer = new google.maps.TransitLayer();
  transitLayer.setMap(map); # 公共交通機関の情報を地図上に表示するための機能

  <% @maps.each do |map| %> 
    ( function(){
      let markerLatLng = { lat: <%= map.lat %>, lng: <%= map.lng %> };
      let marker = new google.maps.Marker({
        position: markerLatLng,
        map: map
      });

      let infowindow = new google.maps.InfoWindow({ # 情報ウィンドウの定義
        position: markerLatLng,
        content: "<a href='<%= map_path(map.id) %>' target='_blank'><%= map.text %></a>" # 'target='_blank''リンクを新しいタブまたはウィンドウで開くための属性
      });
      marker.addListener('click', function() {
        infowindow.open(map, marker);
      });
    })();
  <% end %>
  }
 </script>
 <script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API_KEY'] %>&libraries=places&callback=initMap" async defer></script>

今回mapのデザインを変更してみました。お好みのデザインにしてみてください。
最後にcssを当てます。

assets/stylesheets/application.css
 .map-maps {
  height: 100vh;
  width: 100%;
 }

これで投稿を地図上で一覧表示する準備は整いました。実際に確認していきます。

無事表示できました。デザインを変更したので先ほどのmapより少しシックになっていますね。
ピンをクリックして情報ウィンドウ上にtextに登録したものがリンクとして表示されています。
リンクから詳細ページに遷移できたら正常に動いています。
新しいタブで開きたくない場合はtarget='_blank'を消せば現在のタブで遷移することができます。

こちらで実装は以上となります。

最後に

大変長らく実装お疲れ様でした。

おそらくコンソールにエラーが出てしまっていると思うのですが、ターボリンクスを切らないと解決はなかなか難しい様です。※この様なエラー↓
(You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.)
解決方法は先ほど述べたターボリンクスを切る。と、Google mapが配置されているページへ遷移するリンクにターボリンクスを切る記述をする。の二つしか今のところ解決策が見当たりませんでした。例:<%= link_to map_path(map), data: {turbolinks: false} %>

ただ特にこれが原因で致命的なエラーになったりはしないらしい(定かではない。。)のでとりあえずは心配ないとは思われます。

今回初めてAPIの記事を書きましたが、もう一つ学習したAPIがあるので次はそちらを記事にしたいと思います。

なにか間違い(誤字脱字、技術面等)があればご指摘ください。
最後まで見てくださりありがとうございました。一緒に勉強がんばりましょう!

Discussion