LoginSignup
7
5

More than 1 year has passed since last update.

【Rails6 x Google Maps API】フォームから住所を入力してviewのGoogle Mapで複数のマーカーを表示する方法

Posted at

実現したいこと

demo_4.gif

スクリーンショット 2021-04-28 13.07.10.png

GIFやスクリーンショットのように、view上のGoogle Mapにおいて複数のマーカー(赤いピン)を立てることを目標とします。
データの流れとしては、
フォームから住所を入力 -> Geocoding APIで住所を緯度経度に変換 -> 緯度経度をデータベースに保存 -> 保存した緯度経度

Railsのバージョン/使ったAPI

この記事では、以下のバージョンを使用しました

  • Ruby on Rails 6.1.3

  • Google Maps JavaScript API
    -> webアプリケーション上でGoogle Mapをカスタマイズして表示するために必要

  • Geocoding API
    -> 住所を緯度経度に変換するために必要

記事を読む前に終わっておくべきこと

以下のことについては本記事では解説しないので本編では終わっている前提で進めていきます。

  • Google Maps JavaScript API/ Geocoding APIの導入

APIを導入するには下記記事が参考になると思います。

  • モデル/テーブルの作成

今回は Cafeモデルcafesテーブル を持たせることを想定しています。
cafesテーブルには以下のレコードが存在します
後で緯度(longitude), 経度(latitude)は使います.

scheme.rb
省略

  create_table "cafes", force: :cascade do |t|
    t.string "address"
    t.float "longitude"
    t.float "latitude"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "name"
  end

省略
  • 任意のviewでGoogle Mapが表示されている状態

↑までが終わった状態で本編へお進みください

本編

実装の確認

今回は以下のような実装を目指します。
フォームから住所を入力すると、viewのGoogle Mapで複数のマーカーが表示されているという状態

フォームから住所を入力する

まずはフォームから作っていきましょう.
form_withを使って実装します
非同期なのでlocal: trueを忘れずに.

new.html.slim
  = form_with model:@cafe, url: admin_cafes_path, local: true do |f|
    .form-group
      p 店舗名を入力
      = f.text_field :name, placeholder: "XX COFFEE ROASTER", class: "form-control"
      br
      p 住所を入力
      = f.text_field :address, placeholder: "東京都港区芝公園4-2−8", class: "form-control"
      br
      = f.submit "登録する", class: "btn btn-primary"

そうするとこのようなフォームが完成します。
スクリーンショット 2021-04-28 14.22.43.png

住所の緯度経度にを取得

APIを叩いて帰ってくる結果を確認

Geocoding APIを使って住所を緯度経度に変換する処理を書く前に、APIを使って取得できる結果を確認しておきましょう。

https://maps.googleapis.com/maps/api/geocode/json?address=東京都港区芝公園4-2-8&key=取得したAPIキー

にアクセスすると、レスポンスが返ってきます

{
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "8",
               "short_name" : "8",
               "types" : [ "premise" ]
            },
            {
               "long_name" : "2",
               "short_name" : "2",
               "types" : [ "political", "sublocality", "sublocality_level_4" ]
            },
            {
               "long_name" : "4丁目",
               "short_name" : "4丁目",
               "types" : [ "political", "sublocality", "sublocality_level_3" ]
            },
            {
               "long_name" : "芝公園",
               "short_name" : "芝公園",
               "types" : [ "political", "sublocality", "sublocality_level_2" ]
            },
            {
               "long_name" : "港区",
               "short_name" : "港区",
               "types" : [ "locality", "political" ]
            },
            {
               "long_name" : "東京都",
               "short_name" : "東京都",
               "types" : [ "administrative_area_level_1", "political" ]
            },
            {
               "long_name" : "日本",
               "short_name" : "JP",
               "types" : [ "country", "political" ]
            },
            {
               "long_name" : "105-0011",
               "short_name" : "105-0011",
               "types" : [ "postal_code" ]
            }
         ],
         "formatted_address" : "日本、〒105-0011 東京都港区芝公園4丁目2−8",
         "geometry" : {
            "location" : {
               "lat" : 35.6585769,
               "lng" : 139.7454506
            },
            "location_type" : "ROOFTOP",
            "viewport" : {
               "northeast" : {
                  "lat" : 35.6599258802915,
                  "lng" : 139.7467995802915
               },
               "southwest" : {
                  "lat" : 35.6572279197085,
                  "lng" : 139.7441016197085
               }
            }
         },
         "place_id" : "ChIJL9dIIZeLGGARMHFc9xtDEhM",
         "plus_code" : {
            "compound_code" : "MP5W+C5 日本、東京都港区",
            "global_code" : "8Q7XMP5W+C5"
         },
         "types" : [ "street_address" ]
      }
   ],
   "status" : "OK"
}

今回欲しいのは、["geometry"]["location"]の["lat"]と["lng"]です。
フォームで入力した住所をもとに緯度経度を取得し、データベースに保存する処理を書いていきます。

入力された住所の緯度経度を取得して保存する

↑で叩いたように入力された住所の緯度経度を取得します。

map_query.rb というモデルを作ってある程度のロジックを書いていきます。

map_query.rb
# app > models > map_query.rb を作成

class MapQuery
  # 変数を初期化
  def initialize(cafe_param)
    @cafe_param = cafe_param
  end
 
  def uri
    # フォームから飛んできた住所をエスケープして変数に格納
    address = URI.encode_www_form({address: @cafe_param})
    # Geocoding APIを叩く
    URI.parse("https://maps.googleapis.com/maps/api/geocode/json?#{address}&key=#{ENV["MAP_API_KEY"]}")
  end

  def result
    # 返ってきたJSONをパースしてapi_response という変数に格納
    api_response = Net::HTTP.get_response(uri)
    response_body = JSON.parse(api_response.body)
    response_body["results"][0]["geometry"]["location"]
  end
end

続いてコントローラ

cafes_controller.rb
class Admin::CafesController < Admin::BaseController
  # Cafeクラスのインスタンスを作成
  def new
    @cafe = Cafe.new
  end
  # 住所、緯度、経度をデータベースに保存
  def create
    # MapQueryのresultメソッドを呼び出して@resultに格納
    @result = MapQuery.new(params[:cafe]).result

    # Cafeモデルの各カラムに住所、緯度、経度を格納
    @cafe = Cafe.new(
      name: cafe_params["name"],
      address: cafe_params["address"],
      latitude: @result["lat"],
      longitude: @result["lng"]
    )

    # 保存
    if @cafe.save
      flash[:notice] = "保存しました"
      redirect_to cafes_path
    else
      flash.now[:danger] = "保存に失敗しました"
      render action: :new
    end
  end

  private

    def cafe_params
      params.require(:cafe).permit(:name, :address, :latitude, :longitude)
    end
end

緯度経度をviewで取り出してGoogle Map上で表示する

ここまでで、フォームから住所を入力 -> 緯度経度を取得 -> データベースに保存 までは実装できました.
ここからは保存したデータをもとに、任意のviewのGoogle Mapで複数のマーカーを立てるように実装していきます.

まず、どういう処理が必要か考えていきます

  • データベースから緯度経度を取りだす
  • 取り出した緯度経度をviewに渡す
  • 渡された緯度経度をもとにGoogle Map上に複数のマーカーを立てる

データベースから緯度経度を取りだす

Google Mapを表示させたいviewと紐付いているコントローラに以下のように記述します。

cafes_controller.rb
class CafesController < ApplicationController
  def index
    # pluckを使って緯度経度を取り出し、JSONにして変数に格納
    # 保存されているすべてのlatitudeカラムとlongitudeカラムを配列として取得し、JSONに変換
    @all_cafe_position = Cafe.all.pluck(:latitude, :longitude).to_json
  end
end

pluckメソッドについて

カラムを指定して、配列として取得することが出来ます

説明: 指定したカラムのレコードの配列を取得
使い方: モデル.pluck(カラム名 [, ...])

取り出した緯度経度をviewに渡す

緯度経度がJSONになって変数に格納されています
それをviewで取り出します

任意のview.html.slim
#roaster
  h1 コーヒーロースター一覧
  #map
  /データ属性として@all_cafe_positionを渡す
  #marker-data data-position=@all_cafe_position

  /GoogleMapをカスタマイズするためのJavaScriptファイルを別で定義してここで読み込む
  =javascript_pack_tag 'home/index'

  /viewでGoogleMapを表示させるため
  script src="https://maps.googleapis.com/maps/api/js?key=#{ENV['MAP_API_KEY']}&callback"

渡された緯度経度をもとにGoogle Map上に複数のマーカーを立てる

viewにはDOMとして緯度経度が渡っています
Google Maps JavaScript APIのドキュメントを参照しながら複数のマーカーを立てるという実装をしていきます

ループ以外の書き方はドキュメントに従っています

index.js
//app > javascript > packs > home > index.js

let marker = [];
//DOMの #marker-data に緯度経度が渡っているので、取得してmarkerData変数に格納
let markerData = JSON.parse(document.querySelector("#marker-data").dataset.position);

function initMap() {

    //ループを使ってすべての緯度経度を変数に格納
    for (var i = 0; i < markerData.length; i++) {
      markerLatLng = new google.maps.LatLng({
        lat: markerData[i][0],
        lng: markerData[i][1]
      });

      marker[i] = new google.maps.Marker({
        position: markerLatLng,
        map: map
      });
    }
  });
}

window.onload = function() {
  initMap();
}

まとめ

データベースに保存する処理まではWeb APIを叩いて保存するというよくある流れなのですんなりと実装することができると思います

個人的に解決するまでに時間がかかったポイント

  • pluckメソッドを使って緯度経度を取得する点
  • data属性としてviewに渡す点

この2点は糸口を掴むまでかなり時間を要しました。

controllerやviewなど個人のプロジェクトで違うと思いますが、この記事が何かの参考になれば幸いです。

7
5
0

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
7
5