Open Map WeatherのAPIを用いて不快指数を算出してみた

#はじめに
どうも、こんにちは。
最近Web開発に興味を持ち、Ruby on Railsを一通り学習した結果、あることに気が付きました。
...Rubyでのコーディング力が足りないことに。

というわけで、まずは手を動かしてRubyでプログラムを組んでみました。

#開発環境など
OS：macOS High Sierra
version：Ruby2.5.1
editor：Atom
API：Open Map Weather 5 day / 3 hour weather forecast

#概要
今回作成するのは、天気予報APIを用いて取得した情報から、天気情報や不快指数を出力するプログラムです。

#Open Map WeatherのAPIから天気予報を取得する
天気予報は[Open Map Weather](https://home.openweathermap.org/)から取得しました。
Open Map WeatherのAPIは無料ライセンス登録をすれば利用できます。
※ライセンス取得後、APIが使用可能になるまで1時間ほどかかる場合もあるみたいなので注意が必要です。

ベースとなるURLは以下の通りです。
 ```https://api.openweathermap.org/data/2.5/forecast/```

リクエストする際にはURLの末尾にAPPIDパラメータを用いて、自分のAPPIDを追加してください。

APPIDは[Open Map Weather](https://home.openweathermap.org/)にログイン後、「API keys」の「Key」から確認できます。
<img width="1440" alt="openmapweather_key.png" src="https://qiita-image-store.s3.amazonaws.com/0/310331/7ef15c87-4f33-f9fc-7dae-60a241586ee9.png">

**By city name**
天気情報を取得したい都市名を指定できます。
 ```api.openweathermap.org/data/2.5/forecast?q={city name},{country code}&APPID={your APPID}```

東京の天気情報を取得するならこうなります。
 ```api.openweathermap.org/data/2.5/forecast?q=Tokyo,jp&APPID=xxxxx```

**By city ID**
天気情報を取得したい都市名を固有のIDから指定できます。
 ```api.openweathermap.org/data/2.5/forecast?id={city ID}&APPID={your APPID}```

東京の天気予報
 ```api.openweathermap.org/data/2.5/forecast?id=1850147&APPID=xxxxx```

Open Map Weatherが公式で推奨している取得方法のようです。

**By geographic coordinates**
緯度と経度から天気情報を取得する地域を指定することもできます。
 ```api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&APPID={your APPID}```

東京の天気予報
 ```api.openweathermap.org/data/2.5/forecast?lat=35.6895&lon=139.6917&APPID=xxxxx```

**By ZIP code**
郵便番号を指定して天気情報を取得できます。
 ```api.openweathermap.org/data/2.5/forecast?zip={zip code},{country code}&APPID={your APPID}```
東京の天気予報
 ```api.openweathermap.org/data/2.5/forecast?zip=100-0001,jp&APPID=xxxxx```

# 取得した天気情報から不快指数を算出する

実際に作成したソースコードを説明します。

**作成したクラスファイル**
1.weather_info.rb
2.answer.rb

**weather_info**
Open Map Weather APIからレスポンスを受け取るクラスです。

1.コンストラクタ：initialize(area_name)
天気情報を取得する地域名(area_name)を受け取ります。
地域名の指定がなかった場合はデフォルト地域として東京を地域名に代入しています。

2.メソッド：answer_clothes
天気情報を取得し、answerインスタンスを作成して呼び出し元に返却します。

3.メソッド：guess_area
コンストラクタで受け取ったarea_nameと該当する地域名がAPIに存在しなかった場合、APIに存在する似た地域名一覧を返却します。

```weather_info.rb
require 'bundler/setup'
require 'json'
require 'open-uri'
require 'yaml'
require_relative './Answer'

class WeatherInfo
  @@base_url = YAML.load_file('./conf/config.yml')['BASE_URL']
  @@api_key = YAML.load_file('./conf/key.yml')['API_KEY']
  @@messages = File.open('./json/message.json') { |j| hash = JSON.load(j) }
  @@area_list = File.open('./json/city.list.json') { |j| hash = JSON.load(j) }

  # コンストラクタ
  def initialize(area_name)
    if area_name.empty?
      @area_name = YAML.load_file('./conf/config.yml')['DEFAULT_AREA']
    elsif
      @area_name = area_name.collect(&:capitalize).join(' ')
    end
  end

  # ベストな服装配列を返却
  def answer_clothes
    match_area = @@area_list.map do |area|
      area if area.find { |_k, v| v == @area_name }
    end.compact

    if match_area[0]
      area_id = match_area[0]['id']
      response = JSON.parse(open(@@base_url + "?id=#{area_id}&APPID=#{@@api_key}").read)
      puts response['city']['name']
      answer = Answer.new(response)
      answer.return_answer
    end
  end

  # 入力された地域名から前方3文字で該当地域検索
  def guess_area
    search_str = @area_name.slice(0..3)
    area_candidate = @@area_list.map do |area|
      area if area.find { |_k, v| v =~ /#{search_str}/ }
    end.compact
    area_candidate.map do |candidate|
      candidate['name']
    end.unshift('該当する地域名がありません。該当候補の地域名を表示します。') << '--------------------'
  end
end
```
リクエスト用のベースURLとデフォルト地域名はconfig.ymlに記載しています。
APPIDや地域名リストなどの定義ファイルも読み込んでいます。

```config.yml
# 天気情報取得apiURL（OpenWeatherMap）
BASE_URL : https://api.openweathermap.org/data/2.5/forecast/
# 引数なしで実行した場合の情報取得対象地域
DEFAULT_AREA : Tokyo
```


***answer***
天気情報や不快指数の返却クラスです。

1.コンストラクタ：initialize(response)
天気情報を受け取り、reject_timezoneメソッドを呼び出します。

2.メソッド：reject_timezone
対象外の時間帯の情報を削除します。

3.メソッド：return_answer
必要な情報をハッシュとして返却します。

4.メソッド：calculate_DI(response)
気温と湿度から不快指数を算出します。

```answer.rb
require 'bundler/setup'
require 'json'
require 'open-uri'

class Answer
  # 回答対象外時刻
  @@exclude_time = ['03:00:00', '09:00:00', '15:00:00', '21:00:00']
  # 回答メッセージリスト
  @@messages = File.open('./json/message.json') { |j| hash = JSON.load(j) }

  # コンストラクタ
  def initialize(response)
    @response = response.dup
    reject_timezone
  end

  # 不要な時間帯の情報を削除
  def reject_timezone
    @response['list'].delete_if do |list|
      list['dt_txt'].include?(@@exclude_time[0]) || list['dt_txt'].include?(@@exclude_time[1]) || list['dt_txt'].include?(@@exclude_time[2]) || list['dt_txt'].include?(@@exclude_time[3])
    end
  end

  # 回答を返却
  def return_answer
    answer = @response['list'].map do |res|
      datetime = res['dt_txt']
      weather = res['weather'][0]['description']
      temp = res['main']['temp'] - 273.15
      humidity = res['main']['humidity']
      di = calculate_DI(res)
      message = @@messages.fetch(di.floor(-1).to_s)
      { datetime: datetime, weather: weather, temp: temp, humidity: humidity, message: message, di: di }
    end
  end

  # DI（不快指数）計算
  # DI = 0.81T + 0.01H * (0.99T - 14.3) + 46.3
  def calculate_DI(response)
    t = response['main']['temp'] - 273.15
    h = response['main']['humidity']
    di = 0.81 * t + 0.01 * h * (0.99 * t - 14.3) + 46.3
    return di if di >= 0
    return 0 if di < 0
  end
end
```

***main***
コンソールから実行時に入力された地域名を受け取り、結果を出力します。

```main.rb
require_relative 'weather_info'

area_name = ARGV
weather_info = WeatherInfo.new(area_name)
answer = weather_info.answer_clothes

if answer
  answer.each do |ans|
    puts "日付：#{ans[:datetime]} 天気：#{ans[:weather]} 気温：#{ans[:temp].floor(2)}度 湿度：#{ans[:humidity]}％ 服装：#{ans[:message]} 不快指数：#{ans[:di].floor(2)}"
  end
else
  puts weather_info.guess_area
end
```

#実行結果
main.rbを実行した結果です。
area_nameはOkinawaで指定しました。
<img width="1126" alt="main結果.png" src="https://qiita-image-store.s3.amazonaws.com/0/310331/f7a19c42-87ab-565b-ccf9-fe9b105a1a9f.png">

無事、天気予報も取得できて不快指数も算出されました。

#今後の課題
このプログラムは作成当初、不快指数を算出することを目的としたものではありませんでした。
本来の目的は、天気情報から不快指数や体感温度などを用いて最適な服装が何かを出力することでした。
しかし、作り込む時間も限られていたため、今回はひとまずここら辺で一度手を止めてみます。

今後は、最適な服装を算出するロジックの作成とリファクタリングを行いたいと思います。