実現したいこと
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)は使います.
省略
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
を忘れずに.
= 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"
住所の緯度経度にを取得
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
というモデルを作ってある程度のロジックを書いていきます。
# 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
続いてコントローラ
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と紐付いているコントローラに以下のように記述します。
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で取り出します
# 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のドキュメントを参照しながら複数のマーカーを立てるという実装をしていきます
ループ以外の書き方はドキュメントに従っています
//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など個人のプロジェクトで違うと思いますが、この記事が何かの参考になれば幸いです。