天気予報日付ごとに表示したい
- OpenWEatherMapAPIを使用して、天気の情報を取得しているのですが取得した予報データが1つの大きな配列に格納されたままとなっており、ビューで日付ごとにデータを扱うの難しいという問題が発生しておりました。
-
group_by
メソッドを使用して、同じ日付の予報データをグループ化してまとめ、配列として取り出すことで、各日の天気情報を区別して表示できるようにしました。 - これにより、ユーザーが一目で知りたい日にちの情報を確認できるようになっております。
- 加えて、取得できる天気のアイコンには2種類あり、取得した時間帯により異なるアイコンが表示されてしまっていたため、gsub(ジーサブ)メソッドを使用して、天気予報のアイコンを常に昼間のアイコンが表示されるようにしました。
def show
@weather_data = get_weather_forecast(@post.latitude, @post.longitude)
end
def get_weather_forecast(latitude, longitude)
api_key = ENV['OPENWEATHERMAP_API_KEY']
api_url = "https://api.openweathermap.org/data/2.5/forecast?lat=#{latitude}&lon=#{longitude}&appid=#{api_key}&lang=ja&units=metric"
response = HTTParty.get(api_url)
weather_data = JSON.parse(response.body)
return weather_data
end
取得したデータの例
[1] pry(#<PostsController>)> weather_data
=> {"cod"=>"200",
"message"=>0,
"cnt"=>40,
"list"=>
[{"dt"=>1699520400,
"main"=>{"temp"=>19.49, "feels_like"=>18.96, "temp_min"=>19.49, "temp_max"=>19.76, "pressure"=>1023, "sea_level"=>1023, "grnd_level"=>1021, "humidity"=>56, "temp_kf"=>-0.27},
"weather"=>[{"id"=>801, "main"=>"Clouds", "description"=>"薄い雲", "icon"=>"02n"}],
"clouds"=>{"all"=>20},
"wind"=>{"speed"=>4.19, "deg"=>186, "gust"=>4.47},
"visibility"=>10000,
"pop"=>0,
"sys"=>{"pod"=>"n"},
"dt_txt"=>"2023-11-09 09:00:00"},
{"dt"=>1699531200,
"main"=>{"temp"=>19.33, "feels_like"=>18.81, "temp_min"=>19.01, "temp_max"=>19.33, "pressure"=>1024, "sea_level"=>1024, "grnd_level"=>1022, "humidity"=>57, "temp_kf"=>0.32},
"weather"=>[{"id"=>801, "main"=>"Clouds", "description"=>"薄い雲", "icon"=>"02n"}],
"clouds"=>{"all"=>16},
"wind"=>{"speed"=>3.13, "deg"=>178, "gust"=>4.05},
"visibility"=>10000,
"pop"=>0,
"sys"=>{"pod"=>"n"},
"dt_txt"=>"2023-11-09 12:00:00"},
{"dt"=>1699542000,
"main"=>{"temp"=>18.87, "feels_like"=>18.41, "temp_min"=>18.56, "temp_max"=>18.87, "pressure"=>1023, "sea_level"=>1023, "grnd_level"=>1020, "humidity"=>61, "temp_kf"=>0.31},
"weather"=>[{"id"=>801, "main"=>"Clouds", "description"=>"薄い雲", "icon"=>"02n"}],
"clouds"=>{"all"=>13},
"wind"=>{"speed"=>2.44, "deg"=>165, "gust"=>3.64},
"visibility"=>10000,
"pop"=>0,
"sys"=>{"pod"=>"n"},
"dt_txt"=>"2023-11-09 15:00:00"},
{"dt"=>1699552800,
"main"=>{"temp"=>18.37, "feels_like"=>18.01, "temp_min"=>18.37, "temp_max"=>18.37, "pressure"=>1022, "sea_level"=>1022, "grnd_level"=>1019, "humidity"=>67, "temp_kf"=>0},
"weather"=>[{"id"=>802, "main"=>"Clouds", "description"=>"雲", "icon"=>"03n"}],
"clouds"=>{"all"=>29},
"wind"=>{"speed"=>2.45, "deg"=>182, "gust"=>4.09},
"visibility"=>10000,
"pop"=>0,
"sys"=>{"pod"=>"n"},
"dt_txt"=>"2023-11-09 18:00:00"},
{"dt"=>1699563600,
"main"=>{"temp"=>18.42, "feels_like"=>18.07, "temp_min"=>18.42, "temp_max"=>18.42, "pressure"=>1021, "sea_level"=>1021, "grnd_level"=>1017, "humidity"=>67, "temp_kf"=>0},
"weather"=>[{"id"=>804, "main"=>"Clouds", "description"=>"厚い雲", "icon"=>"04n"}],
"clouds"=>{"all"=>100},
"wind"=>{"speed"=>1.97, "deg"=>189, "gust"=>3.52},
"visibility"=>10000,
"pop"=>0,
"sys"=>{"pod"=>"n"},
"dt_txt"=>"2023-11-09 21:00:00"},
{"dt"=>1699574400,
"main"=>{"temp"=>18.92, "feels_like"=>18.56, "temp_min"=>18.92, "temp_max"=>18.92, "pressure"=>1020, "sea_level"=>1020, "grnd_level"=>1017, "humidity"=>65, "temp_kf"=>0},
"weather"=>[{"id"=>804, "main"=>"Clouds", "description"=>"厚い雲", "icon"=>"04d"}],
<% if @weather_data.present? %>
<h2>5日間の天気予報</h2>
<% daily_forecasts = @weather_data['list'].group_by { |forecast| Time.at(forecast['dt']).strftime('%Y-%m-%d') } %>
<div class="daily-forecasts-container">
<% daily_forecasts.each do |date, forecasts| %>
<div class="daily-forecast">
<div class="forecast-entries">
<% forecasts.each do |forecast| %>
<% if Time.at(forecast['dt']).hour == 12 %>
<div class="forecast-entry">
<p><strong><%= Time.at(forecast['dt']).strftime('%m-%d %H:%M') %></strong></p>
<p>気温: <%= forecast['main']['temp'].to_i %>度</p>
<p>天気: <%= forecast['weather'][0]['description'] %></p>
<img src="https://openweathermap.org/img/wn/<%= forecast['weather'][0]['icon'].gsub('n', 'd') %>@2x.png"
alt="Weather Icon">
<% if forecast['pop'].present? %>
<p>降水確率: <%= (forecast['pop'] * 100).round(-1) %>%</p>
<% else %>
<p>降水確率: 0%</p>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
<% end %>
</div>
<% else %>
<p>天気情報が取得できませんでした。</p>
<% end %>
slickを使用した
スマートフォンで投稿詳細画面を表示した際に、複数枚画像が表示されると画像だけで画面を埋めてしまいスマホで見ると圧迫感があり、スクロールの量も増え、ユーザーの使用感が良くないと考えたためjQueryのプラグインであるslickを使用して、サムネイル画像をクリックするとメインの画像が切り替わるように設計しました。
これにより、画面を圧迫することなく、ユーザーがみたい画像を確認しやすくなっております。
AWS-s3で画像を永続的に保存
renderを用いてアプリの公開を行なっていましたが、アップロードされた画像はアプリケーションのディレクトリに保存されており、ユーザーがアップロードした画像データは、アプリがデプロイまたは再起動(24時間に1回自動で行われる)される度に、消えてしまう仕様になっていました。
そこで、ストレージサービスであるAWS-s3を利用することで、画像を永続的にに保存することができるように設計しました。
これにより、サーバー側で障害等が発生した際にも、画像を保存し続けることができ、安全にデータを利用することが可能となっています。
地名、住所から自動で緯度経度を取得させたい
GoogleMapsAPI,OpenWEatherMapAPIを使用する際に、経度と緯度を使用するため緯度経度を新規投稿された際にデータベースに保存する必要がありました。
ユーザーに入力させるのは、ユーザビリティの低下を招くため、地名,例えば東京タワーが入力され、投稿された際にその入力された地名から自動で緯度経度を算出し保存できるように設計しました。
具体的には、gemのgeocoderを使用して、住所を保存するaddressカラムに対してジオコーディングを行うように指定しました。
住所に変更があった場合にも対応できるように、addressカラムに変更があった場合に変更後の緯度経度を保存できるようafter_validationをかけました。
class Post < ApplicationRecord
# addressカラムの値(住所、地名など)からgeocoded_byが緯度経度を算出し保存してくれる
geocoded_by :address
# after_validation :geocodeでaddressカラムに変更があった場合に変更後の緯度経度を保存してくれる
after_validation :geocode, if: :address_changed?
end
投稿内容を更新した際に緯度経度が取得できない住所の場合のバリデーション
緯度経度のカラムに対して、バリデーションpresence: true
をかけ、ジオコーディングが失敗して緯度経度の値がとれていない場合にエラーメッセージが表示され保存できないように設計しておりましたが、ジオコーディングが成功してもエラーメッセージが表示され、保存されないという問題がありました。
そこでbefore_save
コールバックを使用して、新規投稿の場合や住所が変更された場合に、緯度と経度のバリデーションを行い、問題がある場合は保存を中止させるメソッド(validate_latitude_and_longitudeメソッド)を保存前に呼び出すことで、バリデーションを行えるように修正しました。
具体的なメソッド(validate_latitude_and_longitudeメソッド)の内容は、
if new_record? || address_changed?:で新規投稿の場合または住所が変更された場合にsuccess = geocode
でgeocodeメソッドの真偽値を返します。その結果をsuccess変数に代入しています。
unless success && latitude.present? && longitude.present?
で緯度、経度が取得できているかを判定し、加えて緯度経度が存在するかどうかを確認しています。
真偽値がtrueかつ緯度と経度が存在している場合、緯度と経度が正常に取得されたと判定しています。
緯度経度を取得できていないか、存在しない場合はエラーメッセージを表示させthrow(:abort)
を呼び出して保存を中止させています。
before_save :validate_latitude_and_longitude
def validate_latitude_and_longitude
if new_record? || address_changed?
success = geocode
unless success && latitude.present? && longitude.present?
errors.add(:base, 'この住所からは緯度と経度が取得できないため保存できません。')
throw(:abort)
end
end
end
スマートフォンでも直感的に操作が行いやすいように画面下部にメニューバーの実装を行いました。
スマホサイズの時だけ表示 というより、スマホサイズ以外の場合は消えるという記述にしています。
ページネーションを使用して処理速度をあげた
投稿一覧画面で、全ての投稿がトップページに表示されると読み込むまでに時間がかかりますし、かなり読みにくくなってしまいます。
そのためページネーション機能を実装して表示される項目が8投稿分を越えると自動的に次のページへ内容を移行してくれるのでものすごい便利になります。