From 09c5d13d377dfecafc89b3a310fd68182061db65 Mon Sep 17 00:00:00 2001 From: Julien Papasian Date: Sun, 15 Jan 2023 17:00:42 +0100 Subject: [PATCH] Implement MET Norway weather provider --- README.md | 22 +- app/build.gradle | 2 + .../models/options/provider/WeatherSource.kt | 4 + .../common/rxjava/BaseObserver.java | 2 +- .../weather/WeatherServiceSet.java | 10 +- .../weather/apis/MetNoApi.java | 46 ++ .../weather/apis/NominatimApi.java | 43 ++ .../weather/converters/CommonConverter.java | 9 + .../converters/MetNoResultConverter.java | 593 ++++++++++++++++++ .../weather/converters/MfResultConverter.java | 12 +- .../weather/di/ApiModule.java | 28 + .../metno/MetNoLocationForecastResult.java | 73 +++ .../weather/json/metno/MetNoSunsetResult.java | 34 + .../nominatim/NominatimLocationResult.java | 24 + .../weather/services/MetNoWeatherService.java | 198 ++++++ app/src/main/res/values-cs/arrays.xml | 7 +- app/src/main/res/values-el/arrays.xml | 1 + app/src/main/res/values-es/arrays.xml | 7 +- app/src/main/res/values-fi/arrays.xml | 1 + app/src/main/res/values-fr/arrays.xml | 8 + app/src/main/res/values-ja/arrays.xml | 1 + app/src/main/res/values-ko/arrays.xml | 1 + app/src/main/res/values-pl/arrays.xml | 1 + app/src/main/res/values-pt-rBR/arrays.xml | 1 + app/src/main/res/values-ro/arrays.xml | 1 + app/src/main/res/values-ru/arrays.xml | 1 + app/src/main/res/values-zh-rCN/arrays.xml | 2 + app/src/main/res/values-zh-rHK/arrays.xml | 2 + app/src/main/res/values-zh-rTW/arrays.xml | 2 + app/src/main/res/values/arrays.xml | 3 + gradle.properties | 2 + 31 files changed, 1104 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/wangdaye/com/geometricweather/weather/apis/MetNoApi.java create mode 100644 app/src/main/java/wangdaye/com/geometricweather/weather/apis/NominatimApi.java create mode 100644 app/src/main/java/wangdaye/com/geometricweather/weather/converters/MetNoResultConverter.java create mode 100644 app/src/main/java/wangdaye/com/geometricweather/weather/json/metno/MetNoLocationForecastResult.java create mode 100644 app/src/main/java/wangdaye/com/geometricweather/weather/json/metno/MetNoSunsetResult.java create mode 100644 app/src/main/java/wangdaye/com/geometricweather/weather/json/nominatim/NominatimLocationResult.java create mode 100644 app/src/main/java/wangdaye/com/geometricweather/weather/services/MetNoWeatherService.java diff --git a/README.md b/README.md index 151ded9a3..58c913e87 100644 --- a/README.md +++ b/README.md @@ -22,17 +22,17 @@ There are 3 build variants now. Specifically, the `fdroid` variant dose not cont AccuWeather is the most complete provider, however you may sometimes find other providers to be more accurate for your location. -| Providers | AccuWeather | OpenWeatherMap | Météo-France | -| --- | --- | --- | --- | -| Status | Maintained | Not maintained anymore (version 3.0 requires credit card information) | Maintained | -| API key | Optional | Required (must be compatible with version 2.5) | Optional | -| Country supported | Worldwide, some features not available everywhere | Worldwide, some features not available everywhere | Mostly France, including DROM-COM. AQI restricted to Auvergne-Rhône-Alpes | -| Current | Weather, Temperature, Precipitation, Wind, UV, Air Quality, Humidity, Pressure, Visibility, Dew point, Cloud Cover, Ceiling | Weather, Temperature, Precipitation, Wind, UV, Air Quality, Humidity, Pressure, Visibility, Dew point, Cloud Cover | Weather, Temperature, Wind, UV, Air Quality | -| Yesterday | Temperature | Not available | May be available in the future | -| Daily | Weather, Temperature, Precipitation (Rain, Snow, Ice), Precipitation Probability (Thunderstorm, Rain, Snow, Ice), Precipitation Duration (Rain, Snow, Ice), Wind, Cloud Clover, Sunrise/Sunset, Moonrise/Moonset, Moon phase, Air Quality, Pollen, UV, Hours of sun | Weather, Temperature, Precipitation (Rain, Snow), Precipitation Probability, Wind, Cloud Clover, Sunrise/Sunset, Air Quality, UV | Weather, Temperature, Precipitation (Rain, Snow), Precipitation Probability (Rain, Snow, Ice), Wind, Cloud Clover, Sunrise/Sunset, Moonrise/Moonset (may be available in the future), Moon phase, Air Quality, UV, Hours of sun | -| Hourly | Weather, Temperature, Precipitation (Rain, Snow, Ice), Precipitation Probability (Thunderstorm, Rain, Snow, Ice), Wind, UV | Weather, Temperature, Precipitation (Rain, Snow), Precipitation Probability, Wind, UV | Weather, Temperature, Precipitation (Rain, Snow), Precipitation Probability (Rain, Snow, Ice), Wind | -| Realtime | Weather, Rain, Cloud Cover | Not available | Rain (intensity estimated, not available everywhere) | -| Alerts | Yes (duplicate alerts issue) | Yes | Yes (incorrect phenomen time to be fixed) | +| Providers | AccuWeather | MET Norway | OpenWeatherMap | Météo-France | +| --- | --- | --- | --- | --- | +| Status | Maintained | Alpha test | Not maintained anymore (version 3.0 requires credit card information) | Maintained | +| API key | Optional | None | Required (must be compatible with version 2.5) | Optional | +| Country supported | Worldwide, some features not available everywhere | Worldwide, some features restricted to Nordic area | Worldwide, some features not available everywhere | Mostly France, including DROM-COM. AQI restricted to Auvergne-Rhône-Alpes | +| Current | Weather, Temperature, Precipitation, Wind, UV, Air Quality, Humidity, Pressure, Visibility, Dew point, Cloud Cover, Ceiling | Weather, Temperature, Precipitation, Precipitation Probability (Thunder, Rain), Wind, UV (may be available in the future), Humidity, Pressure | Weather, Temperature, Precipitation, Wind, UV, Air Quality, Humidity, Pressure, Visibility, Dew point, Cloud Cover | Weather, Temperature, Wind, UV, Air Quality | +| Yesterday | Temperature | Not available | Not available | May be available in the future | +| Daily | Weather, Temperature, Precipitation (Rain, Snow, Ice), Precipitation Probability (Thunderstorm, Rain, Snow, Ice), Precipitation Duration (Rain, Snow, Ice), Wind, Cloud Clover, Sunrise/Sunset, Moonrise/Moonset, Moon phase, Air Quality, Pollen, UV, Hours of sun | Weather, Temperature, Precipitation (Rain), Precipitation Probability (Thunderstorm, Rain), Wind, UV (may be implemented in the future), Hours of sun | Weather, Temperature, Precipitation (Rain, Snow), Precipitation Probability, Wind, Cloud Clover, Sunrise/Sunset, Air Quality, UV | Weather, Temperature, Precipitation (Rain, Snow), Precipitation Probability (Rain, Snow, Ice), Wind, Cloud Clover, Sunrise/Sunset, Moonrise/Moonset (may be available in the future), Moon phase, Air Quality, UV, Hours of sun | +| Hourly | Weather, Temperature, Precipitation (Rain, Snow, Ice), Precipitation Probability (Thunderstorm, Rain, Snow, Ice), Wind, UV | Weather, Temperature, Precipitation, Precipitation Probability (Thunder, Rain), Wind, UV (may be implemented in the future) | Weather, Temperature, Precipitation (Rain, Snow), Precipitation Probability, Wind, UV | Weather, Temperature, Precipitation (Rain, Snow), Precipitation Probability (Rain, Snow, Ice), Wind | +| Realtime | Weather, Rain, Cloud Cover | May be available in the future for Norway, Sweden, Finland and Denmark only | Not available | Rain (intensity estimated, not available everywhere) | +| Alerts | Yes (duplicate alerts issue) | May be available in the future for Norway only | Yes | Yes (incorrect phenomen time to be fixed) | ### Weather icon extensions If you want to build your own weather icon-pack, please read this document: diff --git a/app/build.gradle b/app/build.gradle index 9d0bc4f03..a3e598be9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,6 +57,8 @@ android { it.buildConfigField "String", "IQA_ATMO_AURA_KEY", IQA_ATMO_AURA_KEY it.buildConfigField "String", "IQA_ATMO_AURA_URL", IQA_ATMO_AURA_URL it.buildConfigField "String", "IQA_ATMO_SUD_URL", IQA_ATMO_SUD_URL + it.buildConfigField "String", "METNO_BASE_URL", METNO_BASE_URL + it.buildConfigField "String", "NOMINATIM_BASE_URL", NOMINATIM_BASE_URL } lintOptions { checkReleaseBuilds false diff --git a/app/src/main/java/wangdaye/com/geometricweather/common/basic/models/options/provider/WeatherSource.kt b/app/src/main/java/wangdaye/com/geometricweather/common/basic/models/options/provider/WeatherSource.kt index 4864ff3cf..2610b1a8b 100644 --- a/app/src/main/java/wangdaye/com/geometricweather/common/basic/models/options/provider/WeatherSource.kt +++ b/app/src/main/java/wangdaye/com/geometricweather/common/basic/models/options/provider/WeatherSource.kt @@ -14,6 +14,7 @@ enum class WeatherSource( ACCU("accu", -0x10a7dd, "accuweather.com"), OWM("owm", -0x1491b5, "openweathermap.org"), + METNO("metno", -0xdba791, "met.no / nominatim.org"), MF("mf", -0xffa76e, "meteofrance.com"), CAIYUN("caiyun", -0xa14472, " caiyunapp.com"); @@ -23,6 +24,9 @@ enum class WeatherSource( fun getInstance( value: String ): WeatherSource { + if (value.lowercase().contains("metno")) { + return METNO + } if (value.lowercase().contains("owm")) { return OWM } diff --git a/app/src/main/java/wangdaye/com/geometricweather/common/rxjava/BaseObserver.java b/app/src/main/java/wangdaye/com/geometricweather/common/rxjava/BaseObserver.java index 938a7ec82..0f94ace2a 100644 --- a/app/src/main/java/wangdaye/com/geometricweather/common/rxjava/BaseObserver.java +++ b/app/src/main/java/wangdaye/com/geometricweather/common/rxjava/BaseObserver.java @@ -16,7 +16,7 @@ public Integer getStatusCode() { } public Boolean isApiLimitReached() { - return code == 429; + return code != null && code == 429; } @Override diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/WeatherServiceSet.java b/app/src/main/java/wangdaye/com/geometricweather/weather/WeatherServiceSet.java index ae589f516..df934b908 100644 --- a/app/src/main/java/wangdaye/com/geometricweather/weather/WeatherServiceSet.java +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/WeatherServiceSet.java @@ -7,6 +7,7 @@ import wangdaye.com.geometricweather.common.basic.models.options.provider.WeatherSource; import wangdaye.com.geometricweather.weather.services.AccuWeatherService; import wangdaye.com.geometricweather.weather.services.CaiYunWeatherService; +import wangdaye.com.geometricweather.weather.services.MetNoWeatherService; import wangdaye.com.geometricweather.weather.services.MfWeatherService; import wangdaye.com.geometricweather.weather.services.OwmWeatherService; import wangdaye.com.geometricweather.weather.services.WeatherService; @@ -19,18 +20,23 @@ public class WeatherServiceSet { public WeatherServiceSet(AccuWeatherService accuWeatherService, CaiYunWeatherService caiYunWeatherService, MfWeatherService mfWeatherService, - OwmWeatherService owmWeatherService) { + OwmWeatherService owmWeatherService, + MetNoWeatherService metNoWeatherService) { mWeatherServices = new WeatherService[] { accuWeatherService, caiYunWeatherService, mfWeatherService, - owmWeatherService + owmWeatherService, + metNoWeatherService }; } @NonNull public WeatherService get(WeatherSource source) { switch (source) { + case METNO: + return mWeatherServices[4]; + case OWM: return mWeatherServices[3]; diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/apis/MetNoApi.java b/app/src/main/java/wangdaye/com/geometricweather/weather/apis/MetNoApi.java new file mode 100644 index 000000000..e3e685f62 --- /dev/null +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/apis/MetNoApi.java @@ -0,0 +1,46 @@ +package wangdaye.com.geometricweather.weather.apis; + +import java.util.Date; + +import io.reactivex.Observable; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Query; +import wangdaye.com.geometricweather.weather.json.metno.MetNoLocationForecastResult; +import wangdaye.com.geometricweather.weather.json.metno.MetNoSunsetResult; + +/** + * MET Weather API. + */ + +public interface MetNoApi { + + @GET("locationforecast/2.0/compact.json") + Observable getLocationForecast(@Header("User-Agent") String userAgent, + @Query("lat") Float lat, + @Query("lon") Float lon); + + @GET("sunrise/2.0/.json") + Observable getSunset(@Header("User-Agent") String userAgent, + @Query("date") String date, + @Query("days") int days, + @Query("lat") Float lat, + @Query("lon") Float lon, + @Query("offset") String offset); + + // Only available in Nordic area + /*@GET("nowcast/2.0/complete.json") + Observable getMinutely(@Header("User-Agent") String userAgent, + @Query("lat") Float lat, + @Query("lon") Float lon);*/ + + /*@GET("airqualityforecast/0.1/") + Observable getAirQuality(@Header("User-Agent") String userAgent, + @Query("lat") Float lat, + @Query("lon") Float lon);*/ + + /*@GET("metalerts/1.1/") + Observable> getAlerts(@Header("User-Agent") String userAgent, + @Query("lat") Float lat, + @Query("lon") Float lon);*/ +} \ No newline at end of file diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/apis/NominatimApi.java b/app/src/main/java/wangdaye/com/geometricweather/weather/apis/NominatimApi.java new file mode 100644 index 000000000..737eecdc9 --- /dev/null +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/apis/NominatimApi.java @@ -0,0 +1,43 @@ +package wangdaye.com.geometricweather.weather.apis; + +import java.util.List; + +import io.reactivex.Observable; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Query; +import wangdaye.com.geometricweather.weather.json.nominatim.NominatimLocationResult; + +/** + * Nominatim API. + */ + +public interface NominatimApi { + + @GET("search") + Call> callWeatherLocation(@Header("User-Agent") String userAgent, + @Query("q") String q, + @Query("featuretype") String featureType, + @Query("addressdetails") Boolean addressDetails, + @Query("accept-language") String acceptLanguage, + @Query("format") String format); + + @GET("search") + Observable> getWeatherLocation(@Header("User-Agent") String userAgent, + @Query("q") String q, + @Query("featuretype") String featureType, + @Query("addressdetails") Boolean addressDetails, + @Query("accept-language") String acceptLanguage, + @Query("format") String format); + + @GET("search") + Observable getWeatherLocationByGeoPosition(@Header("User-Agent") String userAgent, + @Query("lat") Float lat, + @Query("lon") Float lon, + @Query("featuretype") String featureType, + @Query("addressdetails") Boolean addressDetails, + @Query("accept-language") String acceptLanguage, + @Query("format") String format); + +} \ No newline at end of file diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/converters/CommonConverter.java b/app/src/main/java/wangdaye/com/geometricweather/weather/converters/CommonConverter.java index 9f19db9b4..867a2f82b 100644 --- a/app/src/main/java/wangdaye/com/geometricweather/weather/converters/CommonConverter.java +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/converters/CommonConverter.java @@ -146,4 +146,13 @@ public static UV getCurrentUV(int dayMaxUV, Date currentDate, Date sunriseDate, return new UV(Math.toIntExact(Math.round(currentUV)), null, null); } + + public static float getHoursOfDay(Date sunrise, Date sunset) { + return (float) ( + (sunset.getTime() - sunrise.getTime()) // get delta millisecond. + / 1000 // second. + / 60 // minutes. + / 60.0 // hours. + ); + } } diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/converters/MetNoResultConverter.java b/app/src/main/java/wangdaye/com/geometricweather/weather/converters/MetNoResultConverter.java new file mode 100644 index 000000000..671d143a8 --- /dev/null +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/converters/MetNoResultConverter.java @@ -0,0 +1,593 @@ +package wangdaye.com.geometricweather.weather.converters; + +import android.content.Context; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.TimeZone; + +import wangdaye.com.geometricweather.common.basic.models.Location; +import wangdaye.com.geometricweather.common.basic.models.options.provider.WeatherSource; +import wangdaye.com.geometricweather.common.basic.models.weather.AirQuality; +import wangdaye.com.geometricweather.common.basic.models.weather.Astro; +import wangdaye.com.geometricweather.common.basic.models.weather.Base; +import wangdaye.com.geometricweather.common.basic.models.weather.Current; +import wangdaye.com.geometricweather.common.basic.models.weather.Daily; +import wangdaye.com.geometricweather.common.basic.models.weather.HalfDay; +import wangdaye.com.geometricweather.common.basic.models.weather.Hourly; +import wangdaye.com.geometricweather.common.basic.models.weather.MoonPhase; +import wangdaye.com.geometricweather.common.basic.models.weather.Pollen; +import wangdaye.com.geometricweather.common.basic.models.weather.Precipitation; +import wangdaye.com.geometricweather.common.basic.models.weather.PrecipitationDuration; +import wangdaye.com.geometricweather.common.basic.models.weather.PrecipitationProbability; +import wangdaye.com.geometricweather.common.basic.models.weather.Temperature; +import wangdaye.com.geometricweather.common.basic.models.weather.UV; +import wangdaye.com.geometricweather.common.basic.models.weather.Weather; +import wangdaye.com.geometricweather.common.basic.models.weather.WeatherCode; +import wangdaye.com.geometricweather.common.basic.models.weather.Wind; +import wangdaye.com.geometricweather.common.basic.models.weather.WindDegree; +import wangdaye.com.geometricweather.weather.json.metno.MetNoLocationForecastResult; +import wangdaye.com.geometricweather.weather.json.metno.MetNoSunsetResult; +import wangdaye.com.geometricweather.weather.json.nominatim.NominatimLocationResult; +import wangdaye.com.geometricweather.weather.services.WeatherService; + +public class MetNoResultConverter { + + @NonNull + public static Location convert(@Nullable Location location, NominatimLocationResult result) { + if (location != null + && !TextUtils.isEmpty(location.getProvince()) + && !TextUtils.isEmpty(location.getCity()) + && !TextUtils.isEmpty(location.getDistrict())) { + return new Location( + result.place_id.toString(), + result.lat, + result.lon, + TimeZone.getDefault(), + result.address.country, + location.getProvince(), + location.getCity(), + location.getDistrict(), + null, + WeatherSource.METNO, + false, + false, + !TextUtils.isEmpty(result.address.country_code) + && (result.address.country_code.equals("CN") + || result.address.country_code.equals("cn") + || result.address.country_code.equals("HK") + || result.address.country_code.equals("hk") + || result.address.country_code.equals("TW") + || result.address.country_code.equals("tw")) + ); + } else { + return new Location( + result.place_id.toString(), + result.lat, + result.lon, + TimeZone.getDefault(), + result.address.country, + result.address.state == null ? "" : result.address.state, + result.display_name, + "", + null, + WeatherSource.METNO, + false, + false, + !TextUtils.isEmpty(result.address.country_code) + && (result.address.country_code.equals("CN") + || result.address.country_code.equals("cn") + || result.address.country_code.equals("HK") + || result.address.country_code.equals("hk") + || result.address.country_code.equals("TW") + || result.address.country_code.equals("tw")) + ); + } + } + + @NonNull + public static WeatherService.WeatherResultWrapper convert(Context context, + Location location, + MetNoLocationForecastResult locationForecastResult, + MetNoSunsetResult sunsetResult) { + try { + HashMap sunsetList = getSunsetResultAsHashMap(sunsetResult.location.time); + List hourly = getHourlyList(context, locationForecastResult, sunsetList); + + Weather weather = new Weather( + new Base( + location.getCityId(), + System.currentTimeMillis(), + locationForecastResult.properties.meta.updatedAt, + locationForecastResult.properties.meta.updatedAt.getTime(), + new Date(), + System.currentTimeMillis() + ), + new Current( + "", + getWeatherCode(getSymbolCode(locationForecastResult.properties.timeseries.get(0).data)), + new Temperature( + toInt(Double.valueOf(locationForecastResult.properties.timeseries.get(0).data.instant.details.airTemperature)), + null, + null, + null, + null, + null, + null + ), + new Precipitation( + getPrecipitationAmount(locationForecastResult.properties.timeseries.get(0).data), + null, + null, + null, + null + ), + getPrecipitationProbability(locationForecastResult.properties.timeseries.get(0).data), + new Wind( + getWindDirection(locationForecastResult.properties.timeseries.get(0).data.instant.details.windFromDirection), + new WindDegree(locationForecastResult.properties.timeseries.get(0).data.instant.details.windFromDirection, false), + locationForecastResult.properties.timeseries.get(0).data.instant.details.windSpeed * 3.6f, + CommonConverter.getWindLevel(context, locationForecastResult.properties.timeseries.get(0).data.instant.details.windSpeed * 3.6f) + ), + new UV(null,null,null), // FIXME: Use ultravioletIndexClearSky + new AirQuality( + null, null, null, null, + null, null, null, null + ), + locationForecastResult.properties.timeseries.get(0).data.instant.details.relativeHumidity, + locationForecastResult.properties.timeseries.get(0).data.instant.details.airPressureAtSeaLevel, + null, + null, + null, + null, + null, + null + ), + null, + getDailyList(context, hourly, sunsetList), + hourly, + new ArrayList<>(), + new ArrayList<>() + ); + return new WeatherService.WeatherResultWrapper(weather); + } catch (Exception ignored) { + /*Log.d("GEOM", ignored.getMessage()); + for (StackTraceElement stackTraceElement : ignored.getStackTrace()) { + Log.d("GEOM", stackTraceElement.toString()); + }*/ + return new WeatherService.WeatherResultWrapper(null); + } + } + + protected static Float getMaxTemperature(List timeResults) { + Float temperature = null; + for (MetNoLocationForecastResult.Properties.Timeseries timeResult : timeResults) { + if (temperature == null || timeResult.data.instant.details.airTemperature > temperature) { + temperature = timeResult.data.instant.details.airTemperature; + } + } + + return temperature; + } + + protected static Float getMinTemperature(List timeResults) { + Float temperature = null; + for (MetNoLocationForecastResult.Properties.Timeseries timeResult : timeResults) { + if (temperature == null || timeResult.data.instant.details.airTemperature < temperature) { + temperature = timeResult.data.instant.details.airTemperature; + } + } + + return temperature; + } + + private static HalfDay getHalfDay(Context context, boolean isDaytime, Date date, List hourly, HashMap sunsetList) { + Integer temp = null; + + Float precipitationTotal = 0.0f; + Float precipitationRain = 0.0f; + + Float probPrecipitationTotal = 0.0f; + Float probPrecipitationThunderstorm = 0.0f; + Float probPrecipitationRain = 0.0f; + + WeatherCode weatherCode = WeatherCode.CLOUDY; + + Wind wind = new Wind("Pas d’info", new WindDegree(0, false), null, "Pas d’info"); + + // In AccuWeather provider, a day is considered from 6:00 to 17:59 and night from 18:00 to 5:59 the next day + // So we implement it this way (same as Météo France provider) + for (Hourly hour : hourly) { + // For temperatures, we loop through all hours from 6:00 to 5:59 (next day) to avoid having no max temperature after 18:00 + if ((hour.getTime() / 1000) >= (date.getTime() / 1000) + 6 * 3600 && (hour.getTime() / 1000) < (date.getTime() / 1000) + 30 * 3600) { + if (isDaytime) { + if (temp == null || hour.getTemperature().getTemperature() > temp) { + temp = hour.getTemperature().getTemperature(); + } + } + if (!isDaytime) { + if (temp == null || hour.getTemperature().getTemperature() < temp) { + temp = hour.getTemperature().getTemperature(); + } + } + } + + // For weather code, we look at 12:00 and 00:00 + if (isDaytime && (hour.getTime() / 1000) == (date.getTime() / 1000) + 12 * 3600) { + weatherCode = hour.getWeatherCode(); + } + if (!isDaytime && (hour.getTime() / 1000) == (date.getTime() / 1000) + 24 * 3600) { + weatherCode = hour.getWeatherCode(); + } + + if ((isDaytime && (hour.getTime() / 1000) >= (date.getTime() / 1000) + 6 * 3600 && (hour.getTime() / 1000) < (date.getTime() / 1000) + 18 * 3600) + || (!isDaytime && (hour.getTime() / 1000) >= (date.getTime() / 1000) + 18 * 3600 && (hour.getTime() / 1000) < (date.getTime() / 1000) + 30 * 3600)) { + // Precipitation + precipitationTotal += (hour.getPrecipitation().getTotal() == null) ? 0 : hour.getPrecipitation().getTotal(); + precipitationRain += (hour.getPrecipitation().getRain() == null) ? 0 : hour.getPrecipitation().getRain(); + + // Precipitation probability + if (hour.getPrecipitationProbability().getTotal() != null && hour.getPrecipitationProbability().getTotal() > probPrecipitationTotal) { + probPrecipitationTotal = hour.getPrecipitationProbability().getTotal(); + } + if (hour.getPrecipitationProbability().getThunderstorm() != null && hour.getPrecipitationProbability().getThunderstorm() > probPrecipitationThunderstorm) { + probPrecipitationThunderstorm = hour.getPrecipitationProbability().getThunderstorm(); + } + if (hour.getPrecipitationProbability().getRain() != null && hour.getPrecipitationProbability().getRain() > probPrecipitationRain) { + probPrecipitationRain = hour.getPrecipitationProbability().getRain(); + } + + // Wind + if ((hour.getWind() != null && hour.getWind().getSpeed() != null) && (wind.getSpeed() == null || hour.getWind().getSpeed() > wind.getSpeed())) { + wind = hour.getWind(); + } + } + } + + // Return null so we don't add a garbage day + return temp == null ? null : new HalfDay( + "", + "", + weatherCode, + new Temperature( + temp, + null, + null, + null, + null, + null, + null + ), + new Precipitation( + precipitationTotal, + null, + precipitationRain, + null, + null + ), + new PrecipitationProbability( + probPrecipitationTotal, + probPrecipitationThunderstorm, + probPrecipitationRain, + null, + null + ), + new PrecipitationDuration( + null, + null, + null, + null, + null + ), + wind, + null + ); + } + + private static List getDailyList(Context context, List hourlyConverted, HashMap sunsetList) { + List dateList = new ArrayList<>(); + List dailyList = new ArrayList<>(); + for (Hourly hourly : hourlyConverted) { + SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); + String dateString = fmt.format(hourly.getDate()); + Date date; + try { + date = fmt.parse(dateString); // Setup Date at 00:00 + } catch (ParseException ignored) { + // Should never happen + date = hourly.getDate(); + } + if (!dateList.contains(dateString)) { + dateList.add(dateString); + + HalfDay halfDayDaytime = getHalfDay(context, true, hourly.getDate(), hourlyConverted, sunsetList); + HalfDay halfDayNighttime = getHalfDay(context, false, hourly.getDate(), hourlyConverted, sunsetList); + + // Don’t add to the list if we have no data on it + if (halfDayDaytime != null && halfDayNighttime != null) { + dailyList.add( + new Daily( + date, + date.getTime(), + halfDayDaytime, + halfDayNighttime, + new Astro( + sunsetList.get(dateString).sunrise.time, + sunsetList.get(dateString).sunset.time + ), + new Astro( + sunsetList.get(dateString).moonrise.time, + sunsetList.get(dateString).moonset.time + ), + new MoonPhase( + toInt(Double.valueOf(sunsetList.get(dateString).moonposition.phase)), + sunsetList.get(dateString).moonposition.desc + ), + new AirQuality( + null, null, null, null, + null, null, null, null + ), + new Pollen(null, null, null, null, null, null, null, null, null, null, null, null), + new UV(null, null, null), + CommonConverter.getHoursOfDay( + sunsetList.get(dateString).sunrise.time, + sunsetList.get(dateString).sunset.time + ) + ) + ); + } + } + } + return dailyList; + } + + private static boolean isDaytime(Date time, HashMap sunsetList) { + SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); + if (!sunsetList.containsKey(fmt.format(time)) + || sunsetList.get(fmt.format(time)).sunrise == null + || sunsetList.get(fmt.format(time)).sunset == null + || sunsetList.get(fmt.format(time)).sunrise.time == null + || sunsetList.get(fmt.format(time)).sunset.time == null) { + return true; + } + return (time.getTime() > sunsetList.get(fmt.format(time)).sunrise.time.getTime()) + && (time.getTime() < sunsetList.get(fmt.format(time)).sunset.time.getTime()); + } + + private static List getHourlyList(Context context, MetNoLocationForecastResult resultList, HashMap sunsetList) { + List hourlyList = new ArrayList<>(resultList.properties.timeseries.size()); + for (MetNoLocationForecastResult.Properties.Timeseries result : resultList.properties.timeseries) { + hourlyList.add( + new Hourly( + result.time, + result.time.getTime(), + isDaytime(result.time, sunsetList), + null, + getWeatherCode(getSymbolCode(result.data)), + result.data.instant == null || result.data.instant.details == null || result.data.instant.details.airTemperature == null ? null : new Temperature( + toInt(Double.valueOf(result.data.instant.details.airTemperature)), + null, + null, + null, + null, + null, + null + ), + new Precipitation( + getPrecipitationAmount(result.data), + null, + null, + null, + null + ), + getPrecipitationProbability(result.data), + result.data.instant == null || result.data.instant.details == null ? null : new Wind( + getWindDirection(result.data.instant.details.windFromDirection), + new WindDegree(result.data.instant.details.windFromDirection, false), + result.data.instant.details.windSpeed * 3.6f, + CommonConverter.getWindLevel(context, result.data.instant.details.windSpeed * 3.6f) + ), + new UV(null, null, null) // FIXME: Use ultravioletIndexClearSky + ) + ); + } + return hourlyList; + } + + /*private static List getMinutelyList(long sunrise, long sunset, + @Nullable MetNoLocationForecastResult minuteResult) { + if (minuteResult == null) { + return new ArrayList<>(); + } + List minutelyList = new ArrayList<>(minuteResult.properties.timeseries.size()); + for (MetNoLocationForecastResult.Property.Timeseries interval : minuteResult.properties.timeseries) { + minutelyList.add( + new Minutely( + interval.StartDateTime, + interval.StartEpochDateTime, + CommonConverter.isDaylight(new Date(sunrise * 1000), new Date(sunset * 1000), interval.StartDateTime), + interval.ShortPhrase, + getWeatherCode(interval.IconCode), + interval.Minute, + toInt(interval.Dbz), + interval.CloudCover + ) + ); + } + return minutelyList; + }*/ + + private static int toInt(Double value) { + return (int) (value + 0.5); + } + + private static Float getPrecipitationAmount(MetNoLocationForecastResult.Properties.Timeseries.Data timeData) { + if (timeData.next1Hours != null && timeData.next1Hours.details != null) { + return timeData.next1Hours.details.precipitationAmount; + } else if (timeData.next6Hours != null && timeData.next6Hours.details != null) { + return timeData.next6Hours.details.precipitationAmount; + } else if (timeData.next12Hours != null && timeData.next12Hours.details != null) { + return timeData.next12Hours.details.precipitationAmount; + } else { + return null; + } + } + + private static PrecipitationProbability getPrecipitationProbability(MetNoLocationForecastResult.Properties.Timeseries.Data timeData) { + if (timeData.next1Hours != null && timeData.next1Hours.details != null) { + List allProbabilities = new ArrayList<>(); + allProbabilities.add(timeData.next1Hours.details.probabilityOfThunder != null ? timeData.next1Hours.details.probabilityOfThunder : 0f); + allProbabilities.add(timeData.next1Hours.details.probabilityOfPrecipitation != null ? timeData.next1Hours.details.probabilityOfPrecipitation : 0f); + + return new PrecipitationProbability( + Collections.max(allProbabilities, null), + timeData.next1Hours.details.probabilityOfThunder, + timeData.next1Hours.details.probabilityOfPrecipitation, + null, + null + ); + } else if (timeData.next6Hours != null && timeData.next6Hours.details != null) { + List allProbabilities = new ArrayList<>(); + allProbabilities.add(timeData.next6Hours.details.probabilityOfThunder != null ? timeData.next6Hours.details.probabilityOfThunder : 0f); + allProbabilities.add(timeData.next6Hours.details.probabilityOfPrecipitation != null ? timeData.next6Hours.details.probabilityOfPrecipitation : 0f); + + return new PrecipitationProbability( + Collections.max(allProbabilities, null), + timeData.next6Hours.details.probabilityOfThunder, + timeData.next6Hours.details.probabilityOfPrecipitation, + null, + null + ); + } else if (timeData.next12Hours != null && timeData.next12Hours.details != null) { + List allProbabilities = new ArrayList<>(); + allProbabilities.add(timeData.next12Hours.details.probabilityOfThunder != null ? timeData.next12Hours.details.probabilityOfThunder : 0f); + allProbabilities.add(timeData.next12Hours.details.probabilityOfPrecipitation != null ? timeData.next12Hours.details.probabilityOfPrecipitation : 0f); + + return new PrecipitationProbability( + Collections.max(allProbabilities, null), + timeData.next12Hours.details.probabilityOfThunder, + timeData.next12Hours.details.probabilityOfPrecipitation, + null, + null + ); + } else { + return new PrecipitationProbability(null, null, null, null, null); + } + } + + private static String getSymbolCode(MetNoLocationForecastResult.Properties.Timeseries.Data timeData) { + if (timeData.next1Hours != null && timeData.next1Hours.summary != null && timeData.next1Hours.summary.symbolCode != null) { + return timeData.next1Hours.summary.symbolCode; + } else if (timeData.next6Hours != null && timeData.next6Hours.summary != null && timeData.next6Hours.summary.symbolCode != null) { + return timeData.next6Hours.summary.symbolCode; + } else if (timeData.next12Hours != null && timeData.next12Hours.summary != null && timeData.next12Hours.summary.symbolCode != null) { + return timeData.next12Hours.summary.symbolCode; + } else { + return ""; + } + } + + private static WeatherCode getWeatherCode(String icon) { + switch (icon) { + case "clearsky": + case "fair": + return WeatherCode.CLEAR; + + case "partlycloudy": + return WeatherCode.PARTLY_CLOUDY; + + case "cloudy": + return WeatherCode.CLOUDY; + + case "fog": + return WeatherCode.FOG; + + case "heavyrain": + case "heavyrainshowers": + case "lightrain": + case "lightrainshowers": + case "rain": + case "rainshowers": + return WeatherCode.RAIN; + + case "heavyrainandthunder": + case "heavyrainshowersandthunder": + case "heavysleetandthunder": + case "heavysleetshowersandthunder": + case "heavysnowandthunder": + case "heavysnowshowersandthunder": + case "lightrainandthunder": + case "lightrainshowersandthunder": + case "lightsleetandthunder": + case "lightsleetshowersandthunder": + case "lightsnowandthunder": + case "lightsnowshowersandthunder": + case "rainandthunder": + case "rainshowersandthunder": + case "sleetandthunder": + case "sleetshowersandthunder": + case "snowandthunder": + case "snowshowersandthunder": + return WeatherCode.THUNDERSTORM; + + case "heavysnow": + case "heavysnowshowers": + case "lightsnow": + case "lightsnowshowers": + case "snow": + case "snowshowers": + return WeatherCode.SNOW; + + case "heavysleet": + case "heavysleetshowers": + case "lightsleet": + case "lightsleetshowers": + case "sleet": + case "sleetshowers": + return WeatherCode.SLEET; + + default: + return WeatherCode.CLOUDY; + } + } + + private static String getWindDirection(float degree) { + if (degree < 0) { + return "Variable"; + } + if (22.5 < degree && degree <= 67.5) { + return "NE"; + } else if (67.5 < degree && degree <= 112.5) { + return "E"; + } else if (112.5 < degree && degree <= 157.5) { + return "SE"; + } else if (157.5 < degree && degree <= 202.5) { + return "S"; + } else if (202.5 < degree && degree <= 247.5) { + return "SO"; + } else if (247.5 < degree && degree <= 292.5) { + return "O"; + } else if (292. < degree && degree <= 337.5) { + return "NO"; + } else { + return "N"; + } + } + + protected static HashMap getSunsetResultAsHashMap(List sunsetTimeResults) { + HashMap sunsetList = new HashMap<>(); + for (MetNoSunsetResult.Location.Time sunsetTimeResult : sunsetTimeResults) { + sunsetList.put(sunsetTimeResult.date, sunsetTimeResult); + } + return sunsetList; + } +} diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/converters/MfResultConverter.java b/app/src/main/java/wangdaye/com/geometricweather/weather/converters/MfResultConverter.java index d4afa56e6..bfc0a37e5 100644 --- a/app/src/main/java/wangdaye/com/geometricweather/weather/converters/MfResultConverter.java +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/converters/MfResultConverter.java @@ -506,7 +506,7 @@ private static List getDailyList(Context context, MfForecastResult foreca getAirQuality(context, new Date(dailyForecast.dt * 1000), aqiAtmoAuraResult, false), new Pollen(null, null, null, null, null, null, null, null, null, null, null, null), new UV(dailyForecast.uv, null, null), - getHoursOfDay(new Date(dailyForecast.sun.rise * 1000), new Date(dailyForecast.sun.set * 1000)) + CommonConverter.getHoursOfDay(new Date(dailyForecast.sun.rise * 1000), new Date(dailyForecast.sun.set * 1000)) ) ); } @@ -835,16 +835,6 @@ private static WeatherCode getWeatherCode(@Nullable String icon) { } } - private static float getHoursOfDay(Date sunrise, Date sunset) { - return (float) ( - (sunset.getTime() - sunrise.getTime()) // get delta millisecond. - / 1000 // second. - / 60 // minutes. - / 60.0 // hours. - ); - } - - /*private static AirQuality getDailyAirQuality(Context context, List list) { MfDailyResult.DailyForecasts.AirAndPollen aqi = getAirAndPollen(list, "AirQuality"); diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/di/ApiModule.java b/app/src/main/java/wangdaye/com/geometricweather/weather/di/ApiModule.java index 246f8d25c..08eea67e6 100644 --- a/app/src/main/java/wangdaye/com/geometricweather/weather/di/ApiModule.java +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/di/ApiModule.java @@ -12,7 +12,9 @@ import wangdaye.com.geometricweather.weather.apis.AccuWeatherApi; import wangdaye.com.geometricweather.weather.apis.AtmoAuraIqaApi; import wangdaye.com.geometricweather.weather.apis.CaiYunApi; +import wangdaye.com.geometricweather.weather.apis.MetNoApi; import wangdaye.com.geometricweather.weather.apis.MfWeatherApi; +import wangdaye.com.geometricweather.weather.apis.NominatimApi; import wangdaye.com.geometricweather.weather.apis.OwmApi; @InstallIn(SingletonComponent.class) @@ -32,6 +34,19 @@ public AccuWeatherApi provideAccuWeatherApi(OkHttpClient client, .create((AccuWeatherApi.class)); } + @Provides + public MetNoApi provideMetNoApi(OkHttpClient client, + GsonConverterFactory converterFactory, + RxJava2CallAdapterFactory callAdapterFactory) { + return new Retrofit.Builder() + .baseUrl(BuildConfig.METNO_BASE_URL) + .client(client) + .addConverterFactory(converterFactory) + .addCallAdapterFactory(callAdapterFactory) + .build() + .create((MetNoApi.class)); + } + @Provides public OwmApi provideOpenWeatherMapApi(OkHttpClient client, GsonConverterFactory converterFactory, @@ -83,4 +98,17 @@ public AtmoAuraIqaApi provideAtmoAuraIqaApi(OkHttpClient client, .build() .create((AtmoAuraIqaApi.class)); } + + @Provides + public NominatimApi provideNominatimApi(OkHttpClient client, + GsonConverterFactory converterFactory, + RxJava2CallAdapterFactory callAdapterFactory) { + return new Retrofit.Builder() + .baseUrl(BuildConfig.NOMINATIM_BASE_URL) + .client(client) + .addConverterFactory(converterFactory) + .addCallAdapterFactory(callAdapterFactory) + .build() + .create((NominatimApi.class)); + } } diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/json/metno/MetNoLocationForecastResult.java b/app/src/main/java/wangdaye/com/geometricweather/weather/json/metno/MetNoLocationForecastResult.java new file mode 100644 index 000000000..e9967567d --- /dev/null +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/json/metno/MetNoLocationForecastResult.java @@ -0,0 +1,73 @@ +package wangdaye.com.geometricweather.weather.json.metno; + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.List; + +/** + * MET Norway location forecast. + **/ +public class MetNoLocationForecastResult { + public Properties properties; + + public static class Properties { + public Meta meta; + public List timeseries; + + public static class Meta { + @SerializedName("updated_at") + public Date updatedAt; + } + public static class Timeseries { + public Date time; + public Data data; + + public static class Data { + public Instant instant; + @SerializedName("next_12_hours") + public NextHours next12Hours; + @SerializedName("next_1_hours") + public NextHours next1Hours; + @SerializedName("next_6_hours") + public NextHours next6Hours; + + public static class Summary { + @SerializedName("symbol_code") + public String symbolCode; + } + // All of them are nullable + public static class Details { + @SerializedName("air_pressure_at_sea_level") + public Float airPressureAtSeaLevel; + @SerializedName("air_temperature") + public Float airTemperature; + @SerializedName("precipitation_amount") + public Float precipitationAmount; + @SerializedName("probability_of_precipitation") + public Float probabilityOfPrecipitation; + @SerializedName("probability_of_thunder") + public Float probabilityOfThunder; + @SerializedName("relative_humidity") + public Float relativeHumidity; + @SerializedName("ultraviolet_index_clear_sky") + public Float ultravioletIndexClearSky; // Nullable + @SerializedName("wind_from_direction") + public Float windFromDirection; + @SerializedName("wind_speed") + public Float windSpeed; + } + + public static class Instant { + public Details details; + } + + public static class NextHours { + public Summary summary; + public Details details; + + } + } + } + } +} diff --git a/app/src/main/java/wangdaye/com/geometricweather/weather/json/metno/MetNoSunsetResult.java b/app/src/main/java/wangdaye/com/geometricweather/weather/json/metno/MetNoSunsetResult.java new file mode 100644 index 000000000..84e333f23 --- /dev/null +++ b/app/src/main/java/wangdaye/com/geometricweather/weather/json/metno/MetNoSunsetResult.java @@ -0,0 +1,34 @@ +package wangdaye.com.geometricweather.weather.json.metno; + +import java.util.Date; +import java.util.List; + +/** + * MET Norway sun/moon rise/set forecast. + **/ +public class MetNoSunsetResult { + public Location location; + + public static class Location { + public List