11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Rails6.1.6】「日記投稿機能」にGoogle Mapsを実装する(投稿確認画面 付)

Last updated at Posted at 2022-07-18

この記事を書いた背景

DIC(プログラミングスクール)の卒業課題として作成をしたオリジナルアプリで、日記投稿機能にGoogle Mapsを実装しました。その際にGoogle Mapsの実装に苦労をしたので「自分の為に、また同様な事に悩んでいる人の為に」改めてまとめてみました。

この記事の概要

実際に作成をしたオリジナルアプリ(下記URL)では、「ユーザー」「グループ」「日記」「地図」の機能がありますが、今回は記事としての汎用性が高くなるように、「日記」「地図」の機能のみを改めて実装した内容をまとめました。日記を投稿する前に確認ができる「投稿確認画面」の機能付きです。それでは記事をご覧ください!
※「日記1つの投稿に対して、地図情報は1つのみ」の実装です。
※API導入に関してはこちらでは取り上げていません、参考文献など他の記事をご覧ください。また、APIキーの取り扱いにはお気をつけください!
※実務未経験者です、間違い等ありましたらご指摘をいただけると助かります。

実際に作成をしたオリジナルアプリはこちら

開発環境

  • macOS Monterey 12.1
  • Ruby 3.0.1
  • Rails 6.1.6
  • psql (PostgreSQL) 14.3

目次

1. 使用したgem

Gemfile
# --- 省略 ---
# 追加
gem 'geocoder'
gem 'gon'

group :development, :test do
  # 追加
  gem 'dotenv-rails'
end

gem geocoder
「ジオコーディング」の為に使用します。

ジオコーディングとは、住所や地名、駅名などの地理的情報を、緯度・経度の座標値に変換する技術のことである。日常生活で使用している、場所を示すキーワードを用いて地図上の場所を特定することができるため、地図データの利用効率を向上させることができる。(IT用語辞典バイナリより 参考文献*1にURLを掲載)

gem gon
今回「controller」で取得した値を「viewsで使用されているJavaScript」に渡す為に利用をしています。

gem dotenv-rails
環境変数の管理に使用します。
こちらの記事では詳しく取り上げていませんが、APIキーの取り扱いにはお気をつけください!
※「gem dotenv-rails」についての参考になるよう、参考文献*2にURLを掲載しています。

2. diariesテーブル

今回作成をする「diariesテーブル」はこちらです。

カラム論理名 カラム物理名 データ型 オプション
タイトル title string null: false
内容 content text
住所 address string
緯度 latitude float
経度 longitude float

「addressカラム」に実際に入る内容は「〒105-0011 東京都港区芝公園4丁目2−8といった『住所』」や「東京タワーなどの『シンボル名』」が入る事になります。

3. ルーティング

routes.rb
Rails.application.routes.draw do
  # 追加
  resources :diaries do
    post 'confirm', on: :collection
  end
end

今回は「投稿確認画面」実装の為にpost 'confirm', on: :collectionを記述しています。

4. モデル

diary.rb
class Diary < ApplicationRecord
  # 日記タイトルが空の場合は、バリデーションを発生させる
  validates :title, presence: true
  # モデルの中で、オブジェクトの住所がどこにあるかをgeocoderに伝える
  geocoded_by :address
  # addressカラムに入った情報を元に「latitude」「longitude」に緯度・経度の情報が入る
  after_validation :geocode
end

5. コントローラー

diaries_controller.rb
class DiariesController < ApplicationController
  before_action :set_diary, only: %i[show edit update destroy]
  before_action :assignment_to_diary_instance_and_gon_lat_lng, only: %i[confirm create]

  # --- 省略 ---
  def new
    @diary = Diary.new
  end

  def edit
    # 「@diary.addressに値が存在する場合」は...
    return unless @diary.address.present?

    # 「gon.lat_lng」に [@diary.latitude, @diary.longitude]を代入する
    gon.lat_lng = [@diary.latitude, @diary.longitude]
  end

  def confirm
    render :new if @diary.invalid?
  end

  def create
    return render :new if params[:back]

    if @diary.save
      redirect_to diaries_path
    else
      render :new
    end
  end

  def update
    if @diary.update(diary_params)
      redirect_to diaries_path
    else
      render :edit
    end
  end

  private

  def set_diary
    @diary = Diary.find(params[:id])
  end

  def diary_params
    params.require(:diary).permit(:title, :content, :address)
  end

  def assignment_to_diary_instance_and_gon_lat_lng
    @diary = Diary.new(diary_params)
    # 「gon.lat_lng」に緯度・経度を代入
    # 仮に「@diary.address = '東京タワー'」だった場合
    # 「gon.lat_lng = [35.6585805, 139.7454329]」と代入される
    # 「@diary.address = ""」だった場合は「nil」が代入される
    gon.lat_lng = Geocoder.coordinates(@diary.address)
  end
end

6. ビュー

今回紹介する箇所は下記の4ファイルとなります。

(1) 「views/layouts/application.html.erb」を編集します。
views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>GoogleMaps</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%# 「include_gon(init: true)」を追加 %>
    <%# 【(init: true)】を記述する事により、「リクエストごとにwindow.gon = {};を初期化」 %>
    <%# ↓【(init: true)】の記述が無いと「未定義(今回は値が無い)」時に【JavaScript】のエラーが発生する %>
    <%# 【エラー内容】Uncaught (in promise) ReferenceError: gon is not defined %>
    <%= include_gon(init: true) %>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
(2) 「views/diaries/_form.html.erb」を編集します。

「new.html.erb」「edit.html.erb」を「_form.html.erb」(パーシャル)を利用して、共通化しています。「choose_new_or_edit」のコードについては「7. ヘルパー」で定義をしていますので、そちらをご覧ください。

views/diaries/_form.html.erb
<%= form_with(model: @diary, local: true, url: choose_new_or_edit) do |form| %>
  <% if @diary.errors.any? %>
    <div>
      <%= @diary.errors.count %>のエラーが発生<br>
      <% @diary.errors.full_messages.each do |msg| %>
        <%= msg %><br>
      <% end %>
    </div>
  <% end %>

  <div>
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :content %>
    <%= form.text_area :content %>
  </div>

  <%# script内の「hidden_address.setAttribute("value", inputAddress)」を
  介して、【住所入力用のテキストボックス】の値を受け取る %>
  <%= form.hidden_field :address, id: "hidden_address" %>

  <div>
    イベント先
    <%# 住所入力用のテキストボックスを作成 %>
    <input id="address" type="textbox" value="">
    <%# 地図検索ボタンを作成 %>
    <input type="button" value="地図検索" onclick="codeAddress()">
  </div>

  <%# 地図検索ボタンを押すと、ここに【経度・緯度】が表示される %>
  <div id="display"></div>

  <%# 地図が表示される %>
  <div id='map' style='height: 600px; width: 600px;' ></div>

  <script>
    const display = document.getElementById('display')
    let map
    let marker

    // 地図の初期設定
    function initMap() {
      // 地図の作成
      map = new google.maps.Map(document.getElementById('map'), {
        center: {
          lat: 35.6458437,
          lng: 139.7046171
        },
        zoom: 9,
      });

      // 緯度・経度が存在する場合はif文の中身を実行
      // 緯度・経度が存在する状況は【日記新規作成確認画面から「戻る」場合】と【「日記編集」をする場合】がある
      // gonを使用してcontrollerから緯度・経度を取得
      if (!!gon.lat_lng) {
        const lat_lng = {lat: gon.lat_lng[0], lng: gon.lat_lng[1]};
         // 指定された座標でマップを中央に表示
        map.setCenter(lat_lng);
        // 指定された座標でマーカーを立てる
        marker = new google.maps.Marker({
          map: map,
          position: lat_lng
        });
      }
    }
    
    // 地図検索ボタンを押すと実行される関数
    function codeAddress() {
      // 【住所入力用のテキストボックス】の値を取得
      const inputAddress = document.getElementById('address').value;

      const geocoder = new google.maps.Geocoder()
      geocoder.geocode({
        'address': inputAddress
      // resultsに「ジオコーディングの結果」、statusに『ステータスコード』を渡す
      }, function (results, status) {
        // statusが「OK」の場合は...
        if (status == 'OK') {
          // 指定された座標でマップを中央に表示
          map.setCenter(results[0].geometry.location); 
          // 「既にmarkerに値がある」場合は...
          if (typeof marker != 'undefined') {
            // markerをnullにする(前のマーカーが削除される)
            marker.setMap(null);
          }
          // 指定された座標でマーカーを立てる
          marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location
        });
        // 「hidden_address」に【住所入力用のテキストボックス】の値を送る
        const hidden_address = document.getElementById('hidden_address');
        hidden_address.setAttribute("value", inputAddress);

        // 検索結果が存在する場合は「display」に緯度・経度を表示させる
        display.textContent = "<%= '検索結果' %>" + results[0].geometry.location
        } else {
          // 検索結果が無い場合はその旨を表示させる 
          alert("<%= '検索結果がありません' %>" + status);
        }
      });
    }
  </script>

  <%# APIキーの読み込み %>
  <%# callback=initMap … APIが読み込まれたらinitMap()という関数を実行 %>
  <%# ※下記の説明は引用です ------- %>
    <%# async … async属性は、外部のスクリプト・ファイルのスクリプトが使用可能になったら実行する属性 %>
    <%# defer … defer属性は、ページ読み込み時に、外部のスクリプト・ファイルのスクリプトを実行する属性 %>
    <%# 「async属性」と「defer属性」を両方指定した場合、 「async属性」に対応しているブラウザは「async属性」に従う。「async属性」に対応していないブラウザは「defer属性」に従う %>
    <%# 参考文献*3にURLを掲載 %>
  <%# -------------------------- %>

  <%# 「GOOGLE_MAP_KEY」には、自分で定義をした環境変数を入れる %>
  <script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_KEY'] %>&callback=initMap" async defer></script>

  <%= form.submit %>
<% end %>
(3) 「views/diaries/confirm.html.erb」を編集します。
views/diaries/confirm.html.erb
<h2>日記新規投稿確認画面</h2>

<%= form_with(model: @diary, local: true) do |form| %>
  <div>
    <%= form.label :title %>
    <%= form.text_field :title, disabled: true %>
  </div>

  <div>
    <%= form.label :content %>
    <%= form.text_area :content, disabled: true %>
  </div>

  <div>
    <%= form.label :address %>
    <%= form.text_field :address, disabled: true %>
  </div>

  <%= form.hidden_field :title %>
  <%= form.hidden_field :content %>
  <%= form.hidden_field :address %>

  <%# @diary.addressが存在する場合のみ地図が表示される %>
  <% if @diary.address.present? %>
    <div id='map' style='height: 600px; width: 600px;' ></div>

    <script>
      const lat_lng = {lat: gon.lat_lng[0], lng: gon.lat_lng[1]};
      function initMap() {
        const map = new google.maps.Map(document.getElementById('map'), {
          center: lat_lng,
          zoom: 9,
        });
        var marker = new google.maps.Marker({
          position: lat_lng,
          map: map
        });
      }
    </script>

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

  <%= form.submit %>
  <%= form.submit '戻る', name: 'back' %>
<% end %>
(4) 「views/diaries/index.html.erb」を編集します。
views/diaries/index.html.erb
<h2>日記一覧</h2>

<%# 省略 %>

<%# 【data: {turbolinks: false}】の記述によりAPIの再読み込むを防ぐ %>
<%# 記述が無いと【JavaScript】のエラーが発生する↓ %>
<%# 【エラー内容】<%# You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors. %>
<%= link_to '日記新規投稿へ', new_diary_path, data: {turbolinks: false} %>

今回に限らず「turbolinks」を無効にしておかないと「2回目にAPIキーを使用している画面」に遷移した際に、再度APIキーを読み込んでしまい【JavaScript】でエラーが発生します。

7. ヘルパー

diaries_helper.rb
module DiariesHelper
  def choose_new_or_edit
    case action_name
    # 【確認画面から『戻る』】→ もう一度【確認画面】
    # 【日記新規投稿】 → 【バリデーション発生】 → もう一度【確認画面】
    # 以上の2点にも対応ができるように『'create'』『'confirm'』の両方を記述している
    when 'new', 'create', 'confirm'
      confirm_diaries_path
    when 'edit'
      diary_path
    end
  end
end

8. 「geocoder.rb」の設定

検索精度を向上させる為「geocoder.rb」の設定を行います。今回は下記のように設定をしました。「geocoder.rb」の他の詳しい設定に関しては、gem geocoderの「公式サイトのREADME.md」やgem geocoder - lib/generators/geocoder/config/templates/initializer.rb、または他の記事をご覧ください。
※API導入に関してはこちらでは取り上げていません、参考文献など他の記事をご覧ください。また、APIキーの取り扱いにはお気をつけください!

config/initializers/geocoder.rb
Geocoder.configure(
  lookup: :google,
  # 「GOOGLE_MAP_KEY」には、自分で定義をした環境変数を入れる
  api_key: ENV['GOOGLE_MAP_KEY'],
)

9. 参考文献

IT用語辞典 - バイナリ-ジオコーディング *1
【Rails】 初心者向け!gem 'dotenv-rails'の使い方 *2
HTML5入門 - async属性 *3
HTML5入門 - defer属性 *3
【rails】google maps api 地図情報含んだ投稿をして表示させる方法
Google Mapを「投稿画面」と「詳細画面」の2か所に実装してみた。
【Rails6 / Google Map API】初学者向け!Ruby on Railsで簡単にGoogle Map APIの導入する
Google Maps API を使ってみた
Rails5でGoogleMapを表示してみるまで
RailsのGeocoderとあそぼ

11
8
2

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
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?