前提
・Railsのアプリケーションであること
※私のRailsのバージョンは6.0.3です
・記事作成機能が存在すること
・GoogleMapを自分のアプリケーション上で表示することができていること
※下記記事などが非常に参考になると思います。
目標
記事作成の際、マップにマーカーを1つだけ立てられるようにし、その位置情報をDBに保存すること。
マーカーの立て方としては以下を想定。
・マップをクリック
・位置を検索(GeocodingAPIを使用)
※記事最下部に完成版のgifあり
それではやっていきましょう!
gemのインストール ※いらない方はスキップしてください
gem 'geocoder'
gem 'gon'
※この記事においてはこの2つのgemは使用しません(次の記事で使用予定)。しかし、これらのgemが入っているという前提で進めていきます。
geocoderは便利なメソッドが豊富です。
RailsでGoogleMapを使用のであれば入れておいたほうが良いです。
gonはController内でセットした変数をJavascript内で使う事ができます。
DBに保存した位置情報を記事詳細画面で表示させる際に非常に便利です。
geocoder
https://github.com/alexreisner/geocoder
gon
https://github.com/gazay/gon
これらを記述した後、bundle install します。
マップの情報保存用テーブル作成
指定した緯度、経度を保存するテーブルを作成します。
まず、マイグレーションファイルを作成しましょう。
私の場合は、記事と地図を関連付けさせたいのでこのようになっています。
マップ単品の方は
t.references :article, null: false, foreign_key: true
の部分はいらないかと思います。
class CreateMaps < ActiveRecord::Migration[6.0]
def change
create_table :maps do |t|
t.references :article, null: false, foreign_key: true
t.string :address
t.float :latitude
t.float :longitude
t.timestamps
end
end
end
その後、rails db:migrate します。
モデルの関連付け
関連付けさせなくて良い方はスキップしても大丈夫です。
class Map < ApplicationRecord
belongs_to :article
geocoded_by :address
after_validation :geocode
end
has_one :map, dependent: :destroy
マップの表示
表示だけならこれだけでできると思います。
<div id='map'></div>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API'] %>&callback=initMap" async defer></script>
<script>
let map
function initMap (){
myLatLng = {lat: 35.68090045006303, lng: 139.76690798417752}
map = new google.maps.Map(document.getElementById('map'), {
center: myLatLng,
zoom: 12
});
</script>
マップ上でクリックしたところにマーカーを立てることができるようにする
記事作成の際に、マップ上でクリックした箇所にマーカーを立てられるようにします。
また、マーカーが立っている箇所の経度、緯度を保存できるようにします。
ついでにマーカーを削除できるボタンも追加しました。
<div id="article_form" class="field">
<%= f.label :title, "タイトル" %>
<%= f.text_area :title %>
マップ
<div id='map'></div>
<%= f.fields_for :map, @article.build_map do |map| %>
<%= map.hidden_field :latitude %>
<%= map.hidden_field :longitude %>
<% end %>
<%= f.label :content, "本文" %>
<%= f.rich_text_area :content,placeholder:"入力してください" %>
<%= f.label :tag_list %>
<%= f.file_field :image, accept: "image/jpeg,image/gif,image/png" %>
</div>
<div id="article_button"><%= f.submit "投稿!",class:"btn"%></div>
<% end %>
<button id="del" class="btn" onclick="deleteMarker();">マーカー削除</button>
<script>
var marker
var myLatLng
var map
var map_lat
var map_lng
function initMap(){
myLatLng = {lat: 35.68090045006303, lng: 139.76690798417752}
marker = new google.maps.Marker();
map_lat = document.getElementById('article_map_latitude')
map_lng = document.getElementById('article_map_longitude')
map = new google.maps.Map(document.getElementById('map'), {
center: myLatLng,
zoom: 8
});
google.maps.event.addListener(map, 'click', mylistener);
//クリックしたときの処理
function mylistener(event){
//markerの位置を設定
//event.latLng.lat()でクリックしたところの緯度を取得
marker.setPosition(new google.maps.LatLng(event.latLng.lat(), event.latLng.lng()));
//marker設置
marker.setMap(map);
map_lat.value = event.latLng.lat();
map_lng.value = event.latLng.lng();
}
}
function deleteMarker(){
marker.setMap(null);
map_lat.value = "";
map_lng.value = "";
}
</script>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API'] %>&callback=initMap" async defer></script>
30
下記のコードによってarticleと関連付けたmapにデータを渡すことができます。
<%= f.fields_for :map, @article.build_map do |map| %>
<%= map.hidden_field :latitude %>
<%= map.hidden_field :longitude %>
<% end %>
また、これらに値を入れるコードはこの部分です。
map_lat = document.getElementById('article_map_latitude')
map_lng = document.getElementById('article_map_longitude')
//↑map.hidden_fieldによって生成された<input>のidです。
//人によって違うかもしれないのでブラウザの開発者モードで確認してください。
//〜〜省略〜〜
map_lat.value = event.latLng.lat();
map_lng.value = event.latLng.lng();
そして、値をコントローラに渡すため、StrongParameterの設定も必要です。
binding.pryなどで、どのようなparamsが渡ってきているか確認しておいたほうが良いかと思います。
def article_params
params.require(:article)
.permit(:content, :title,
map_attributes: [:id, :latitude, :longitude])
end
これを保存するコードはこちらです。
def create
@article = current_user.articles.build(article_params)
if @article.save
latitude = params[:article][:map][:latitude]
longitude = params[:article][:map][:longitude]
unless latitude.empty && longitude.empty?
@map = @article.build_map(
latitude: latitude,
longitude: longitude
)
@map.save
end
#〜〜〜〜〜〜省略〜〜〜〜〜〜〜〜〜〜〜
end
位置検索でもマーカーを立てることができるようにする
この場合、GoogleMapAPIだけでなく、Geocoding APIも必要になってきます。
紆余曲折あり、最終的に完成したのがこちらです。
<div class="row">
<div class="col s12 m10 push-m1">
<div class="card">
<div class="card-content article-card">
<%= form_with(model:@article,local: true,class:"margin-30") do |f|%>
<%= render 'shared/error_messages',object: f.object %>
<div id="article_form" class="field">
<div class="input-field">
<%= f.text_field :title,class: "materialize-textarea" %>
<%= f.label :title, "タイトル" %>
</div>
<div class="input-field">
<%= f.text_field :tag_list, value: @article.tag_list.join(','), class: "materialize-textarea" %>
<%= f.label :tag_list,"タグ ※コンマ区切りで記入してください" %>
</div>
<!-- 追加したイベント -->
<div class="input-field">
<input type="text" id="address" placeholder="地名、施設名などを入力するか、地図をクリックしてマーカーを立ててください">
<label>場所</label>
<a class="btn" onclick="codeAddress()">地図検索</a>
<a id="del" class="btn marker-delete right" onclick="deleteMarker()">
<i class="fas fa-map-marker-alt fas-2x" style="color:red"></i>削除
</a>
</div>
<!-- ここまで -->
<div id='map' class="margin-t-b-14"></div>
<%= f.fields_for :map, @article.build_map do |map| %>
<%= map.hidden_field :latitude %>
<%= map.hidden_field :longitude %>
<% end %>
<%= f.rich_text_area :content,placeholder:"入力してください",class: "materialize-textarea" %>
<%= f.label :content, "本文" %>
<%= f.label :thumbnail, "サムネイル" %>
<%= f.file_field :thumbnail, accept: "image/jpeg,image/gif,image/png",class:"file_field" %>
<div id="article_button"><%= f.submit "投稿!",class:"btn col s6 m6 article-form-btn push-s3 push-m3"%></div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API'] %>&callback=initMap" async defer></script>
<script>
var marker
var myLatLng
var map_lat
var map_lng
var geocoder
var map_result
let map
function initMap (){
myLatLng = {lat: 35.68090045006303, lng: 139.76690798417752}
map_lat = document.getElementById('article_map_latitude')
map_lng = document.getElementById('article_map_longitude')
marker = new google.maps.Marker();
map = new google.maps.Map(document.getElementById('map'), {
center: myLatLng,
zoom: 12
});
google.maps.event.addListener(map, 'click', mylistener);
//クリックしたときの処理
function mylistener(event){
marker.setPosition(new google.maps.LatLng(event.latLng.lat(), event.latLng.lng()));
marker.setMap(map);
console.log(event.latLng.lat(), event.latLng.lng());
map_lat.value = event.latLng.lat();
map_lng.value = event.latLng.lng();
}
}
function deleteMarker(){
marker.setMap(null);
map_lat.value = "";
map_lng.value = "";
}
//追加した関数
function codeAddress(){
geocoder = new google.maps.Geocoder()
let inputAddress = document.getElementById('address').value;
geocoder.geocode( {
'address': inputAddress,
'region': 'jp'
}, function(results, status) {
if (status == 'OK') {
map.setCenter(results[0].geometry.location);
map_result = results[0].geometry.location;
map_lat.value = map_result.lat();
map_lng.value = map_result.lng();
marker.setPosition(new google.maps.LatLng(map_result.lat(), map_result.lng()));
marker.setMap(map);
console.log(map_lat.value,map_lng.value);
} else {
alert('該当する結果がありませんでした');
initMap();
}
});
}
</script>
随分長いコードになってしまいました。
しかし、実際は関数とイベントが1つ増えただけなのでその部分を見てもらえればわかるかと思います!
これによって、マップをクリックor位置を検索することで地図上にマーカーを立て、その位置情報をDBに保存することができるようになりました!
これらを利用する方法についてはまた後日記事を書こうかと思います。