/*
* Copyright (C) 2014-2016 VersoBit
*
* This file is part of Weather Doge.
*
* Weather Doge is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Weather Doge is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Weather Doge. If not, see <http://www.gnu.org/licenses/>.
*/
package com.versobit.weatherdoge;
import android.text.TextUtils;
import android.util.Log;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.HttpsURLConnection;
final class WeatherUtil {
private static final String TAG = WeatherUtil.class.getSimpleName();
private static final Pattern YAHOO_TIME = Pattern.compile("([0-9]{1,2}):([0-9]{1,2}) (am|pm)");
private static final SimpleDateFormat YAHOO_DATE_FORMAT = new SimpleDateFormat("EEE, d MMM yyyy h:m a zzz", Locale.US);
private WeatherUtil() {
//
}
// Fetch weather from a general location search string. Source dependent. Your mileage may vary.
static WeatherResult getWeather(String location, Source source) {
return getWeather(Double.MIN_VALUE, Double.MIN_VALUE, location, source);
}
// Fetch weather from geographic coordinates
static WeatherResult getWeather(double latitude, double longitude, Source source) {
return getWeather(latitude, longitude, null, source);
}
private static WeatherResult getWeather(double latitude, double longitude, String location, Source source) {
if (source == Source.OPEN_WEATHER_MAP) {
//noinspection ConstantConditions
if (TextUtils.isEmpty(BuildConfig.OWM_APPID)) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE,
"OpenWeatherMap App ID has not been set. Select a different weather source.", new IllegalStateException());
}
return getWeatherFromOWM(latitude, longitude, location);
}
if (source == Source.YAHOO) {
return getWeatherFromYahoo(latitude, longitude, location);
}
if (source == Source.ACCUWEATHER) {
//noinspection ConstantConditions
if (TextUtils.isEmpty(BuildConfig.ACCUWEATHER_KEY)) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE,
"AccuWeather API key has not been set. Select a different weather source.", new IllegalStateException());
}
return getWeatherFromAccuWeather(latitude, longitude, location);
}
throw new IllegalArgumentException("No supported weather source provided.");
}
private static WeatherResult getWeatherFromYahoo(double latitude, double longitude, String location) {
try {
String yqlText;
if(latitude == Double.MIN_VALUE && longitude == Double.MIN_VALUE) {
if(location == null) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE,
"No valid location parameters.", new IllegalArgumentException());
}
yqlText = location.replaceAll("[^\\p{L}\\p{Nd} ,-]+", "");
} else {
yqlText = String.format(Locale.US, "(%.6f, %.6f)", latitude, longitude);
}
HttpsURLConnection connection = (HttpsURLConnection) openGzipConnection(
new URL("https://query.yahooapis.com/v1/public/yql?q="
+ URLEncoder.encode("select location.city, units, item.condition, item.link, astronomy from weather.forecast where woeid in (select woeid from geo.places(1) where text = \""
+ yqlText + "\") limit 1", "UTF-8") + "&format=json")
);
try {
JSONObject response = new JSONObject(getUncompressedResponse(connection));
if(connection.getResponseCode() != HttpsURLConnection.HTTP_OK) {
JSONObject error = response.getJSONObject("error");
return new WeatherResult(null, WeatherResult.ERROR_API, error.getString("description"), null);
}
JSONObject query = response.getJSONObject("query");
if(query.getInt("count") == 0) {
return new WeatherResult(null, WeatherResult.ERROR_API, "No results found for that location.", null);
}
JSONObject channel = query.getJSONObject("results").getJSONObject("channel");
JSONObject units = channel.getJSONObject("units");
JSONObject item = channel.getJSONObject("item");
JSONObject condition = item.getJSONObject("condition");
JSONObject astronomy = channel.getJSONObject("astronomy");
double temp = condition.getDouble("temp");
if("F".equals(units.getString("temperature"))) {
temp = (temp - 32d) * 5d / 9d;
}
String text = condition.getString("text");
String code = condition.getString("code");
String date = condition.getString("date");
String sunrise = astronomy.getString("sunrise");
String sunset = astronomy.getString("sunset");
String link = item.getString("link");
String owmCode = convertYahooCode(code, date, sunrise, sunset);
if(location == null || location.isEmpty()) {
location = channel.getJSONObject("location").getString("city");
}
return new WeatherResult(new WeatherData(
temp, text, owmCode, latitude, longitude, location, new Date(),
Source.YAHOO, link
), WeatherResult.ERROR_NONE, null, null);
} finally {
connection.disconnect();
}
} catch (Exception ex) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE, ex.getMessage(), ex);
}
}
private static WeatherResult getWeatherFromOWM(double latitude, double longitude, String location) {
try {
String query;
if(latitude == Double.MIN_VALUE && longitude == Double.MIN_VALUE) {
if(location == null) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE,
"No valid location parameters.", new IllegalArgumentException());
}
query = "q=" + URLEncoder.encode(location, "UTF-8");
} else {
query = "lat=" + URLEncoder.encode(String.format(Locale.US, "%.6f", latitude), "UTF-8")
+ "&lon=" + URLEncoder.encode(String.format(Locale.US, "%.6f", longitude), "UTF-8");
}
query += "&APPID=" + URLEncoder.encode(BuildConfig.OWM_APPID, "UTF-8");
HttpURLConnection connection = (HttpURLConnection) openGzipConnection(
new URL("http://api.openweathermap.org/data/2.5/weather?" + query)
);
try {
JSONObject response = new JSONObject(getUncompressedResponse(connection));
if(response.getInt("cod") != HttpURLConnection.HTTP_OK) {
// OWM has HTTP error codes that are passed through an API field, the actual HTTP
// error code is always 200...
return new WeatherResult(null, WeatherResult.ERROR_API,
response.getString("cod") + ": " + response.getString("message"), null);
}
JSONObject weather = response.getJSONArray("weather").getJSONObject(0);
JSONObject main = response.getJSONObject("main");
double temp = main.getDouble("temp") - 273.15d;
String condition = WordUtils.capitalize(weather.getString("description").trim());
// Sky Is Clear -> Sky is Clear
condition = condition.replaceAll("(?<=[^\\w])Is(?=[^\\w]?)", "is");
String image = weather.getString("icon");
if(location == null || location.isEmpty()) {
location = response.getString("name");
}
String link = "https://openweathermap.org/city/" + response.getInt("id");
return new WeatherResult(new WeatherData(
temp, condition, image, latitude, longitude, location, new Date(),
Source.OPEN_WEATHER_MAP, link
), WeatherResult.ERROR_NONE, null, null);
} finally {
connection.disconnect();
}
} catch (Exception ex) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE, ex.getMessage(), ex);
}
}
private static WeatherResult getWeatherFromAccuWeather(double latitude, double longitude, String location) {
String query;
if (latitude == Double.MIN_VALUE && longitude == Double.MIN_VALUE) {
if (location == null) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE,
"No valid location parameters.", new IllegalArgumentException());
}
query = location;
} else {
query = String.format(Locale.US, "%.6f, %.6f", latitude, longitude);
}
String locationKey;
String place;
try {
HttpsURLConnection connection = (HttpsURLConnection) openGzipConnection(
new URL("https://dataservice.accuweather.com/locations/v1/search?apikey="
+ URLEncoder.encode(BuildConfig.ACCUWEATHER_KEY, "UTF-8")
+ "&q=" + URLEncoder.encode(query, "UTF-8"))
);
try {
JSONArray response = new JSONArray(getUncompressedResponse(connection));
if (response.length() == 0) {
return new WeatherResult(null, WeatherResult.ERROR_API, "No results found for that location.", null);
}
// We'll just use the first location result
JSONObject jsonLocation = response.getJSONObject(0);
locationKey = jsonLocation.getString("Key");
place = jsonLocation.getString("LocalizedName");
} finally {
connection.disconnect();
}
} catch (Exception ex) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE, ex.getMessage(), ex);
}
try {
HttpsURLConnection connection = (HttpsURLConnection) openGzipConnection(
new URL("https://dataservice.accuweather.com/currentconditions/v1/"
+ URLEncoder.encode(locationKey, "UTF-8")
+ "?apikey=" + URLEncoder.encode(BuildConfig.ACCUWEATHER_KEY, "UTF-8"))
);
try {
Object response = new JSONTokener(getUncompressedResponse(connection)).nextValue();
if (response instanceof JSONObject) {
return new WeatherResult(null, WeatherResult.ERROR_API,
((JSONObject) response).getString("Message"), null);
}
JSONObject obj = ((JSONArray) response).getJSONObject(0);
JSONObject jsonTemp = obj.getJSONObject("Temperature");
JSONObject jsonMetric = jsonTemp.getJSONObject("Metric");
JSONObject jsonImperial = jsonTemp.getJSONObject("Imperial");
double temp = 0d;
if (jsonMetric != null && !jsonMetric.isNull("Value")) {
temp = jsonMetric.getDouble("Value");
} else if (jsonImperial != null && !jsonImperial.isNull("Value")) {
temp = (jsonImperial.getDouble("Value") - 32d) * (5/9);
}
String condition = obj.getString("WeatherText");
String image = convertAccuWeatherCode(
obj.isNull("WeatherIcon") ? 1 : obj.getInt("WeatherIcon"),
obj.getBoolean("IsDayTime")
);
String link = obj.getString("MobileLink");
return new WeatherResult(new WeatherData(
temp, condition, image, latitude, longitude, place, new Date(),
Source.ACCUWEATHER, link
), WeatherResult.ERROR_NONE, null, null);
} finally {
connection.disconnect();
}
} catch (Exception ex) {
return new WeatherResult(null, WeatherResult.ERROR_THROWABLE, ex.getMessage(), ex);
}
}
private static String convertYahooCode(String code, String weatherTime, String sunrise, String sunset) {
Date weatherDate = new Date();
try {
weatherDate = YAHOO_DATE_FORMAT.parse(weatherTime);
} catch (ParseException ex) {
Log.e(TAG, "Yahoo date format failed!", ex);
}
Calendar weatherCal = new GregorianCalendar();
Calendar sunriseCal = new GregorianCalendar();
Calendar sunsetCal = new GregorianCalendar();
weatherCal.setTime(weatherDate);
sunriseCal.setTime(weatherDate);
sunsetCal.setTime(weatherDate);
Matcher sunriseMatch = YAHOO_TIME.matcher(sunrise);
Matcher sunsetMatch = YAHOO_TIME.matcher(sunset);
if(!sunriseMatch.matches() || !sunsetMatch.matches()) {
Log.e(TAG, "Failed to find sunrise/sunset. Using defaults.");
sunriseMatch = YAHOO_TIME.matcher("6:00 am");
sunsetMatch = YAHOO_TIME.matcher("6:00 pm");
sunriseMatch.matches();
sunsetMatch.matches();
}
// Set the sunrise to the correct hour and minute of the same day
sunriseCal.set(Calendar.HOUR, Integer.parseInt(sunriseMatch.group(1)));
sunriseCal.set(Calendar.MINUTE, Integer.parseInt(sunriseMatch.group(2)));
sunriseCal.set(Calendar.SECOND, 0);
sunriseCal.set(Calendar.MILLISECOND, 0);
sunriseCal.set(Calendar.AM_PM, "am".equals(sunriseMatch.group(3)) ? Calendar.AM : Calendar.PM);
// Set the sunset to the correct hour and minute of the same day
sunsetCal.set(Calendar.HOUR, Integer.parseInt(sunsetMatch.group(1)));
sunsetCal.set(Calendar.MINUTE, Integer.parseInt(sunsetMatch.group(2)));
sunsetCal.set(Calendar.SECOND, 0);
sunsetCal.set(Calendar.MILLISECOND, 0);
sunsetCal.set(Calendar.AM_PM, "am".equals(sunsetMatch.group(3)) ? Calendar.AM : Calendar.PM);
boolean isDaytime = true;
if(weatherCal.before(sunriseCal) || weatherCal.after(sunsetCal)) {
isDaytime = false;
}
String owmCode = "01";
switch (Integer.parseInt(code)) {
// Thunderstorms
case 0: case 1: case 2: case 3: case 4: case 17: case 37: case 38: case 39: case 45: case 47:
owmCode = "11";
break;
// Snow
case 5: case 7: case 13: case 14: case 15: case 16: case 18: case 41: case 42: case 43: case 46:
owmCode = "13";
break;
// Rain
case 6: case 10: case 35:
owmCode = "09";
break;
// Light-ish Rain
case 8: case 9: case 11: case 12: case 40:
owmCode = "10";
break;
// Fog
case 19: case 20: case 21: case 22:
owmCode = "50";
break;
// Cloudy
case 27: case 28:
owmCode = "04";
break;
// (Other) Cloudy
case 26:
owmCode = "03";
break;
// Partly Cloudy
case 23: case 24: case 25: case 29: case 30: case 44:
owmCode = "02";
break;
// Clear
case 31: case 32: case 33: case 34: case 36:
owmCode = "01";
break;
}
return owmCode + (isDaytime ? "d" : "n");
}
private static String convertAccuWeatherCode(int weatherIcon, boolean daytime) {
String owmCode = "01";
switch (weatherIcon) {
// Clear
case 1: case 2: case 5: case 30: case 33: case 34: case 37:
owmCode = "01";
break;
// Partly Cloudy
case 3: case 4: case 35: case 36:
owmCode = "02";
break;
// Mostly Cloudy
case 6:
owmCode = "03";
break;
// Cloudy
case 7: case 8:
owmCode = "04";
break;
// Fog
case 11:
owmCode = "50";
break;
// Light-ish Rain
case 12: case 13: case 14: case 39: case 40:
owmCode = "10";
break;
// Thunderstorms
case 15: case 16: case 17: case 41: case 42:
owmCode = "11";
break;
// Rain
case 18: case 26:
owmCode = "09";
break;
// Snow
case 19: case 20: case 21: case 22: case 23: case 24: case 25: case 29: case 43: case 44:
owmCode = "13";
break;
}
return owmCode + (daytime ? "d" : "n");
}
private static URLConnection openGzipConnection(URL url) throws IOException {
URLConnection connection = url.openConnection();
connection.setRequestProperty("Accept-Encoding", "gzip");
return connection;
}
private static String getUncompressedResponse(URLConnection connection) throws IOException {
InputStream input;
if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) {
input = new GZIPInputStream(connection.getInputStream());
} else {
input = connection.getInputStream();
}
return IOUtils.toString(input);
}
enum Source {
OPEN_WEATHER_MAP("0"),
YAHOO("1"),
ACCUWEATHER("2");
private final String key;
Source(String key) {
this.key = key;
}
static Source fromKey(String key) {
for (Source s : values()) {
if (s.key == null) {
continue;
}
if (s.key.equals(key)) {
return s;
}
}
throw new IllegalArgumentException("No constant with key " + key + " found.");
}
String getKey() {
return key;
}
}
final static class WeatherResult {
final static int ERROR_NONE = 0;
final static int ERROR_API = 1;
final static int ERROR_THROWABLE = 2;
final WeatherData data; // null unless error = ERROR_NONE
final int error; // corresponds to the above error codes or ERROR_NONE for no error
final String msg; // null if ERROR_NONE, may or may not be null if there's an error
final Throwable throwable; // null unless error = ERROR_THROWABLE
WeatherResult(WeatherData data, int error, String msg, Throwable throwable) {
this.data = data;
this.error = error;
this.msg = msg;
this.throwable = throwable;
}
}
final static class WeatherData implements Serializable {
private static final long serialVersionUID = 2253249035716676067L;
final double temperature; // Always in Celsius
final String condition;
final String image; // An OpenWeatherMap weather icon ID
final double latitude;
final double longitude;
final String place;
final Date time; // The system time the data was retrieved
final Source source;
final String link;
WeatherData(double temperature, String condition, String image, double latitude,
double longitude, String place, Date time, Source source, String link) {
this.temperature = temperature;
this.condition = condition;
this.image = image;
this.latitude = latitude;
this.longitude = longitude;
this.place = place;
this.time = time;
this.source = source;
this.link = link;
}
@Override
public boolean equals(Object o) {
if(o == null) {
return false;
}
if(getClass() != o.getClass()) {
return false;
}
final WeatherData other = (WeatherData)o;
return !((this.temperature != other.temperature) || !this.condition.equals(other.condition)
|| !this.image.equals(other.image) || (this.latitude != other.latitude)
|| (this.longitude != other.longitude) || !this.place.equals(other.place)
|| !this.time.equals(other.time) || (this.source != other.source)
|| !this.link.equals(other.link));
}
@Override
public String toString() {
@SuppressWarnings("StringBufferReplaceableByString")
StringBuilder sb = new StringBuilder(WeatherData.class.getSimpleName());
sb.append("[temperature=").append(temperature).append(", condition=").append(condition)
.append(", image=").append(image).append(", latitude=")
.append(String.format(Locale.US, "%.6f", latitude))
.append(", longitude=").append(String.format(Locale.US, "%.6f", longitude))
.append(", place=").append(place)
.append(", time=").append(time).append(", source=").append(source)
.append(", link=").append(link).append("]");
return sb.toString();
}
}
}