diff --git a/src/main/java/eu/m724/wtapi/object/Coordinates.java b/src/main/java/eu/m724/wtapi/object/Coordinates.java index f38f6ce..ddbab63 100644 --- a/src/main/java/eu/m724/wtapi/object/Coordinates.java +++ b/src/main/java/eu/m724/wtapi/object/Coordinates.java @@ -1,21 +1,14 @@ package eu.m724.wtapi.object; /** - * represents geographic coordinates - * contains fields latitude and longitude + * Represents coordinates + * + * @param latitude The latitude (-90-90) + * @param longitude The longitude (-180-180) */ -public class Coordinates { - public final double latitude, longitude; - public String country, city; // TODO should it stay here? - +public record Coordinates(double latitude, double longitude) { public Coordinates(double latitude, double longitude) { this.latitude = (((latitude + 90) % 180) + 180) % 180 - 90; this.longitude = (((longitude + 180) % 360) + 360) % 360 - 180; } - - public Coordinates setAddress(String country, String city) { - this.country = country; - this.city = city; - return this; - } } diff --git a/src/main/java/eu/m724/wtapi/object/GeoLocation.java b/src/main/java/eu/m724/wtapi/object/GeoLocation.java new file mode 100644 index 0000000..3ba4e4e --- /dev/null +++ b/src/main/java/eu/m724/wtapi/object/GeoLocation.java @@ -0,0 +1,17 @@ +package eu.m724.wtapi.object; + +/** + * Represents a geolocation + * + * @param coordinates The coordinates + * @param city the city + * @param country The country name + * @param countryCode The ISO 3166-1 alpha-2 country code + */ +public record GeoLocation( + Coordinates coordinates, + String city, + String country, + String countryCode +) { +} diff --git a/src/main/java/eu/m724/wtapi/object/Quota.java b/src/main/java/eu/m724/wtapi/object/Quota.java new file mode 100644 index 0000000..0b1f7e1 --- /dev/null +++ b/src/main/java/eu/m724/wtapi/object/Quota.java @@ -0,0 +1,24 @@ +package eu.m724.wtapi.object; + +import java.util.concurrent.TimeUnit; + +public record Quota( + int maxRequests, + TimeUnit timeUnit +) { + public static Quota hourly(int maxRequestsHourly) { + return new Quota(maxRequestsHourly, TimeUnit.HOURS); + } + + public static Quota daily(int maxRequestsDaily) { + return new Quota(maxRequestsDaily, TimeUnit.DAYS); + } + + public int getMinuteQuota() { + return maxRequests / (int) timeUnit.toMicros(1); + } + + public int getHourlyQuota() { + return maxRequests / (int) timeUnit.toHours(1); + } +} diff --git a/src/main/java/eu/m724/wtapi/object/Severity.java b/src/main/java/eu/m724/wtapi/object/Severity.java deleted file mode 100644 index e6ad4fd..0000000 --- a/src/main/java/eu/m724/wtapi/object/Severity.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.m724.wtapi.object; - -public enum Severity { - LIGHT, MODERATE, HEAVY -} diff --git a/src/main/java/eu/m724/wtapi/object/Weather.java b/src/main/java/eu/m724/wtapi/object/Weather.java index 76400b9..b4ac0f5 100644 --- a/src/main/java/eu/m724/wtapi/object/Weather.java +++ b/src/main/java/eu/m724/wtapi/object/Weather.java @@ -1,53 +1,33 @@ package eu.m724.wtapi.object; +import java.time.LocalDateTime; + /** - * contains weather conditions + * Represents current weather + * + * @param coordinates The coordinates + * @param timestamp The timestamp of this data + * @param raining Whether it's raining + * @param thundering Whether it's a thunderstorm + * @param snowing Whether it's snowing + * @param temperatureCelsius The temperature in Celsius + * @param temperatureApparentCelsius The apparent temperature (feels like) in Celsius + * @param relativeHumidityPercentage Percentage of relative humidity, 0.0-1.0 + * @param cloudCoverPercentage Percentage of cloud cover (cloudiness), 0.0-1.0 */ -public class Weather { - public Coordinates coordinates; - - public Severity rainSeverity, thunderstormSeverity, snowSeverity; - - // secondary conditions - public Severity sleetSeverity, drizzleSeverity; - - public boolean shower; - - /** - * in kelvin - */ - public float temperature, temperatureApparent; - - /** - * in meters per second - */ - public float windSpeed, windGust; - - /** - * 0.0 - 1.0 - */ - public float humidity, cloudiness; - - /** - * as unix timestamp - */ - public long sunrise, sunset; - - /** - * short name of weather - */ - public String description; - - - public boolean isRaining() { - return rainSeverity != null; - } - - public boolean isThundering() { - return thunderstormSeverity != null; - } - - public boolean isSnowing() { - return snowSeverity != null; - } +public record Weather( + Coordinates coordinates, + LocalDateTime timestamp, + + boolean raining, + boolean thundering, + boolean snowing, + + float temperatureCelsius, + float temperatureApparentCelsius, + + float relativeHumidityPercentage, + float cloudCoverPercentage +) { + } diff --git a/src/main/java/eu/m724/wtapi/provider/Providers.java b/src/main/java/eu/m724/wtapi/provider/Providers.java index 0dfe691..9aea931 100644 --- a/src/main/java/eu/m724/wtapi/provider/Providers.java +++ b/src/main/java/eu/m724/wtapi/provider/Providers.java @@ -1,40 +1,68 @@ package eu.m724.wtapi.provider; -import eu.m724.wtapi.provider.exception.NoSuchProviderException; import eu.m724.wtapi.provider.thunder.ThunderProvider; import eu.m724.wtapi.provider.thunder.impl.blitzortung.BlitzortungProvider; import eu.m724.wtapi.provider.twilight.impl.approximate.ApproximateTwilightTimeProvider; import eu.m724.wtapi.provider.twilight.TwilightTimeProvider; import eu.m724.wtapi.provider.weather.WeatherProvider; -import eu.m724.wtapi.provider.weather.impl.openweathermap.OpenWeatherMapProvider; +import eu.m724.wtapi.provider.weather.impl.openmeteo.OpenMeteoProvider; public class Providers { - - public static ThunderProvider getThunderProvider(String name, String apiKey) throws NoSuchProviderException { + + /** + * Gets a thunder provider by name.
+ * Currently: blitzortung + * + * @param name The provider name + * @param apiKey The API key, or null if not needed + * @return The thunder provider + * @throws NoSuchProviderException If invalid provider name + */ + public static ThunderProvider getThunderProvider(String name, String apiKey) { switch (name.toLowerCase()) { - case "blitzortung": - return new BlitzortungProvider(); + case "blitzortung" -> new BlitzortungProvider(); } - throw new NoSuchProviderException(); + throw new NoSuchProviderException(name); } - - public static WeatherProvider getWeatherProvider(String name, String apiKey) throws NoSuchProviderException { + + /** + * Gets a weather provider by name.
+ * Currently: openmeteo + * + * @param name The provider name + * @param apiKey The API key, or null if not needed + * @return The weather provider + * @throws NoSuchProviderException If invalid provider name + */ + public static WeatherProvider getWeatherProvider(String name, String apiKey) { switch (name.toLowerCase()) { - case "openweathermap": - return new OpenWeatherMapProvider(apiKey); + case "openmeteo" -> new OpenMeteoProvider(); } - throw new NoSuchProviderException(); + throw new NoSuchProviderException(name); } - public static TwilightTimeProvider getTwilightTimeProvider(String name, String apiKey) throws NoSuchProviderException { + /** + * Gets a twilight time provider by name.
+ * Currently: approximate + * + * @param name The provider name + * @param apiKey The API key, or null if not needed + * @return The twilight time provider + * @throws NoSuchProviderException If invalid provider name + */ + public static TwilightTimeProvider getTwilightTimeProvider(String name, String apiKey) { switch (name.toLowerCase()) { - case "approximate": - return new ApproximateTwilightTimeProvider(); + case "approximate" -> new ApproximateTwilightTimeProvider(); } - throw new NoSuchProviderException(); + throw new NoSuchProviderException(name); + } + + public static class NoSuchProviderException extends RuntimeException { + public NoSuchProviderException(String provider) { + super(provider); + } } - } diff --git a/src/main/java/eu/m724/wtapi/provider/exception/AuthorizationException.java b/src/main/java/eu/m724/wtapi/provider/exception/AuthorizationException.java deleted file mode 100644 index 18d56b2..0000000 --- a/src/main/java/eu/m724/wtapi/provider/exception/AuthorizationException.java +++ /dev/null @@ -1,15 +0,0 @@ -package eu.m724.wtapi.provider.exception; - -/** - * when you specified an incorrect api key - */ -public class AuthorizationException extends ProviderException { - - private static final long serialVersionUID = -2258293509429607176L; - - public AuthorizationException(String message) { - super(message); - // TODO Auto-generated constructor stub - } - -} diff --git a/src/main/java/eu/m724/wtapi/provider/exception/NoSuchProviderException.java b/src/main/java/eu/m724/wtapi/provider/exception/NoSuchProviderException.java deleted file mode 100644 index 621a4cb..0000000 --- a/src/main/java/eu/m724/wtapi/provider/exception/NoSuchProviderException.java +++ /dev/null @@ -1,10 +0,0 @@ -package eu.m724.wtapi.provider.exception; - -/** - * thrown when there's no known provider with that name - */ -public class NoSuchProviderException extends Exception { - - private static final long serialVersionUID = -2740598348303023762L; - -} diff --git a/src/main/java/eu/m724/wtapi/provider/exception/ProviderAuthorizationException.java b/src/main/java/eu/m724/wtapi/provider/exception/ProviderAuthorizationException.java new file mode 100644 index 0000000..8d630c6 --- /dev/null +++ b/src/main/java/eu/m724/wtapi/provider/exception/ProviderAuthorizationException.java @@ -0,0 +1,7 @@ +package eu.m724.wtapi.provider.exception; + +public class ProviderAuthorizationException extends ProviderException { + public ProviderAuthorizationException(String message) { + super(message); + } +} diff --git a/src/main/java/eu/m724/wtapi/provider/exception/ProviderException.java b/src/main/java/eu/m724/wtapi/provider/exception/ProviderException.java index ee5e7f7..015984c 100644 --- a/src/main/java/eu/m724/wtapi/provider/exception/ProviderException.java +++ b/src/main/java/eu/m724/wtapi/provider/exception/ProviderException.java @@ -3,11 +3,12 @@ package eu.m724.wtapi.provider.exception; import java.util.concurrent.CompletionException; public class ProviderException extends CompletionException { - - private static final long serialVersionUID = -841882181122537157L; - public ProviderException(String message) { super(message); } + public ProviderException(Throwable cause) { + super(cause); + } + } diff --git a/src/main/java/eu/m724/wtapi/provider/exception/ProviderRequestException.java b/src/main/java/eu/m724/wtapi/provider/exception/ProviderRequestException.java new file mode 100644 index 0000000..1da2f7e --- /dev/null +++ b/src/main/java/eu/m724/wtapi/provider/exception/ProviderRequestException.java @@ -0,0 +1,14 @@ +package eu.m724.wtapi.provider.exception; + +/** + * When unable to connect to the provider. + */ +public class ProviderRequestException extends ProviderException { + public ProviderRequestException(String message) { + super(message); + } + + public ProviderRequestException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/eu/m724/wtapi/provider/exception/QuotaExceededException.java b/src/main/java/eu/m724/wtapi/provider/exception/QuotaExceededException.java index 9f26b0f..10c8897 100644 --- a/src/main/java/eu/m724/wtapi/provider/exception/QuotaExceededException.java +++ b/src/main/java/eu/m724/wtapi/provider/exception/QuotaExceededException.java @@ -1,22 +1,16 @@ package eu.m724.wtapi.provider.exception; -/** - * too many api requests, or ratelimited - */ public class QuotaExceededException extends ProviderException { - - private static final long serialVersionUID = 7550042052176034614L; - - private int retryMs; + private final int retryInSeconds; /** - * - * @param message - * @param retryMs SUGGESTION after how many ms to retry. it can be null + * Instantiates a new QuotaExceededException. + * + * @param retryInSeconds A suggestion after how many seconds to retry. */ - public QuotaExceededException(String message, int retryMs) { - super(message); - this.retryMs = retryMs; + public QuotaExceededException(int retryInSeconds) { + super("Rate limited. Try again in " + retryInSeconds + "s"); + this.retryInSeconds = retryInSeconds; } /** @@ -24,7 +18,7 @@ public class QuotaExceededException extends ProviderException { * @return SUGGESTION after how many ms to retry. it can be null */ public int getRetryIn() { - return this.retryMs; + return this.retryInSeconds; } } diff --git a/src/main/java/eu/m724/wtapi/provider/exception/ServerProviderException.java b/src/main/java/eu/m724/wtapi/provider/exception/ServerProviderException.java deleted file mode 100644 index 4aaa9fe..0000000 --- a/src/main/java/eu/m724/wtapi/provider/exception/ServerProviderException.java +++ /dev/null @@ -1,16 +0,0 @@ -package eu.m724.wtapi.provider.exception; - -/** - * when the provider is at fault - */ -public class ServerProviderException extends ProviderException { - - private static final long serialVersionUID = 4461164912391786673L; - - public ServerProviderException(String message) { - super(message); - // TODO Auto-generated constructor stub - } - - -} diff --git a/src/main/java/eu/m724/wtapi/provider/thunder/impl/blitzortung/BlitzortungWebsocketClient.java b/src/main/java/eu/m724/wtapi/provider/thunder/impl/blitzortung/BlitzortungWebsocketClient.java index bdb7bef..4a06e21 100644 --- a/src/main/java/eu/m724/wtapi/provider/thunder/impl/blitzortung/BlitzortungWebsocketClient.java +++ b/src/main/java/eu/m724/wtapi/provider/thunder/impl/blitzortung/BlitzortungWebsocketClient.java @@ -62,7 +62,7 @@ class BlitzortungWebsocketClient extends WebSocketClient { JsonParser.parseString(decode(message)) .getAsJsonObject(); - long time = json.getAsJsonPrimitive("time").getAsLong() / 1000000; + long time = json.getAsJsonPrimitive("timestamp").getAsLong() / 1000000; double lat = json.getAsJsonPrimitive("lat").getAsDouble(); double lon = json.getAsJsonPrimitive("lon").getAsDouble(); diff --git a/src/main/java/eu/m724/wtapi/provider/twilight/TwilightTimeProvider.java b/src/main/java/eu/m724/wtapi/provider/twilight/TwilightTimeProvider.java index 92fd24d..5805652 100644 --- a/src/main/java/eu/m724/wtapi/provider/twilight/TwilightTimeProvider.java +++ b/src/main/java/eu/m724/wtapi/provider/twilight/TwilightTimeProvider.java @@ -4,10 +4,11 @@ import eu.m724.wtapi.object.Coordinates; import eu.m724.wtapi.object.Twilight; import java.time.LocalDate; +import java.time.LocalDateTime; // TODO make this an interface? also consider new name /** - * Twilight refers to sunset and sunrise time
+ * Twilight refers to sunset and sunrise timestamp
* Is it the correct term? I don't know */ public abstract class TwilightTimeProvider { @@ -19,4 +20,6 @@ public abstract class TwilightTimeProvider { * @return {@link Twilight} */ public abstract Twilight calculateTwilightTime(LocalDate date, Coordinates coordinates); + + public abstract double calculateSolarAltitude(LocalDateTime time, Coordinates coordinates); } diff --git a/src/main/java/eu/m724/wtapi/provider/twilight/impl/approximate/ApproximateTwilightTimeProvider.java b/src/main/java/eu/m724/wtapi/provider/twilight/impl/approximate/ApproximateTwilightTimeProvider.java index db1ef2b..44e5070 100644 --- a/src/main/java/eu/m724/wtapi/provider/twilight/impl/approximate/ApproximateTwilightTimeProvider.java +++ b/src/main/java/eu/m724/wtapi/provider/twilight/impl/approximate/ApproximateTwilightTimeProvider.java @@ -21,8 +21,8 @@ public class ApproximateTwilightTimeProvider extends CacheableTwilightTimeProvid double declination = cache.declination(); double equationOfTime = cache.equationOfTime(); - double latRad = toRadians(coordinates.latitude); - double solarNoon = 720 - 4 * coordinates.longitude - equationOfTime; + double latRad = toRadians(coordinates.latitude()); + double solarNoon = 720 - 4 * coordinates.longitude() - equationOfTime; LocalDateTime solarNoonDateTime = cache.date().atStartOfDay().plusSeconds((long) (solarNoon * 60)); // 90.833 deg = 1.5853349194640094 rad @@ -94,15 +94,9 @@ public class ApproximateTwilightTimeProvider extends CacheableTwilightTimeProvid public ApproximateTwilightTimeCache initializeCache(LocalDate date) { double fractionalYear = getFractionalYear(date); - double equationOfTime = 229.18 * (0.000075 - + 0.001868 * cos(fractionalYear) - - 0.032077 * sin(fractionalYear) - - 0.014615 * cos(2 * fractionalYear) - - 0.040849 * sin(2 * fractionalYear)); - return new ApproximateTwilightTimeCache( date, - equationOfTime, + getEquationOfTime(fractionalYear), getDeclination(fractionalYear) ); } @@ -115,6 +109,14 @@ public class ApproximateTwilightTimeProvider extends CacheableTwilightTimeProvid return 0.01721420632103996 * dayOfYear; } + private double getEquationOfTime(double fractionalYear) { + return 229.18 * (0.000075 + + 0.001868 * cos(fractionalYear) + - 0.032077 * sin(fractionalYear) + - 0.014615 * cos(2 * fractionalYear) + - 0.040849 * sin(2 * fractionalYear)); + } + private double getDeclination(double fractionalYear) { return 0.006918 - 0.399912 * cos(fractionalYear) @@ -124,4 +126,24 @@ public class ApproximateTwilightTimeProvider extends CacheableTwilightTimeProvid - 0.002697 * cos(3 * fractionalYear) + 0.00148 * sin(3 * fractionalYear); } + + @Override + public double calculateSolarAltitude(LocalDateTime time, Coordinates coordinates) { + double latRad = toRadians(coordinates.latitude()); + double declination = getDeclination(getFractionalYear(time.toLocalDate())); + + double hourAngle = toRadians(15 * (lst(time, coordinates.longitude()) - 12)); + return asin(sin(declination) * sin(latRad) + cos(declination) * cos(latRad) * cos(hourAngle)); + } + + private double lst(LocalDateTime dateTime, double longitude) { + double hours = dateTime.toLocalTime().toSecondOfDay() / 3600.0; + return hours + longitude + longitude / 15 + getEquationOfTime(getFractionalYear(dateTime.toLocalDate())); + } + + private double G_year(int year) { + double G_ref = 6.6768410277777778; + int year_ref = 2024; + return (G_ref + 0.0657098244 * (year - year_ref)) % 24; + } } diff --git a/src/main/java/eu/m724/wtapi/provider/weather/WeatherProvider.java b/src/main/java/eu/m724/wtapi/provider/weather/WeatherProvider.java index 24b8eda..4b2e56b 100644 --- a/src/main/java/eu/m724/wtapi/provider/weather/WeatherProvider.java +++ b/src/main/java/eu/m724/wtapi/provider/weather/WeatherProvider.java @@ -1,8 +1,10 @@ package eu.m724.wtapi.provider.weather; +import java.time.Duration; import java.util.concurrent.CompletableFuture; import eu.m724.wtapi.object.Coordinates; +import eu.m724.wtapi.object.Quota; import eu.m724.wtapi.object.Weather; import eu.m724.wtapi.provider.exception.ProviderException; @@ -15,40 +17,12 @@ public abstract class WeatherProvider { public abstract void init() throws ProviderException; /** - * get weather for a single point - * @param coordinates - * @throws NullPointerException if coordinates is null - * @return a future that CAN throw {@link ProviderException} + * Get the current weather of a single or multiple points + * + * @param coordinates The coordinates + * @return A future that CAN throw {@link ProviderException} */ - public abstract CompletableFuture getWeather(Coordinates coordinates); - - /** - * get weather for multiple points at bulk - * it can be called one at a time if the api doesn't support this or if there's too many - * @param coordinateses array of coordinates - * @throws IllegalArgumentException if array is empty - * @return a future that CAN throw {@link ProviderException} - */ - public abstract CompletableFuture getWeatherBulk(Coordinates[] coordinateses); - - /** - * get hourly quota that is max requests per hour - * @return hourly quota - */ - public abstract int getQuotaHourly(); - - /** - * how many coordinates in one bulk request - * this is because some apis don't support bulk or limit it - * @return amount of coordinates per bulk request - */ - public abstract int getBulkLimit(); - - /** - * estimates minimum delay between calls given last request - * @return milliseconds - */ - public int estimateDelay() { - return (int) Math.ceil(this.getQuotaHourly() / 2.0 / 60 / 60 / 1000); - } + public abstract CompletableFuture getWeather(Coordinates... coordinates); + + public abstract Quota getQuota(); } diff --git a/src/main/java/eu/m724/wtapi/provider/weather/impl/openmeteo/OpenMeteoProvider.java b/src/main/java/eu/m724/wtapi/provider/weather/impl/openmeteo/OpenMeteoProvider.java new file mode 100644 index 0000000..b27d667 --- /dev/null +++ b/src/main/java/eu/m724/wtapi/provider/weather/impl/openmeteo/OpenMeteoProvider.java @@ -0,0 +1,89 @@ +package eu.m724.wtapi.provider.weather.impl.openmeteo; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import eu.m724.wtapi.object.Coordinates; +import eu.m724.wtapi.object.Quota; +import eu.m724.wtapi.object.Weather; +import eu.m724.wtapi.provider.exception.ProviderException; +import eu.m724.wtapi.provider.exception.QuotaExceededException; +import eu.m724.wtapi.provider.exception.ProviderRequestException; +import eu.m724.wtapi.provider.weather.WeatherProvider; + +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class OpenMeteoProvider extends WeatherProvider { + @Override + public void init() throws ProviderException { + + } + + @Override + public CompletableFuture getWeather(Coordinates... coordinates) { + String latitudes = Arrays.stream(coordinates) + .map(Coordinates::latitude) + .map(Number::toString) + .collect(Collectors.joining(",")); + + String longitudes = Arrays.stream(coordinates) + .map(Coordinates::longitude) + .map(Number::toString) + .collect(Collectors.joining(",")); + + String url = "https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s¤t=temperature_2m,relative_humidity_2m,rain,snowfall,apparent_temperature,cloud_cover,weather_code&timeformat=unixtime&wind_speed_unit=ms" + .formatted(latitudes, longitudes); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .build(); + + CompletableFuture> responseFuture = + HttpClient.newBuilder().build() + .sendAsync(request, HttpResponse.BodyHandlers.ofString()); + + return responseFuture.thenApply((response) -> { + int code = response.statusCode(); + + JsonObject jsonResponse; + try { + jsonResponse = JsonParser.parseString(response.body()).getAsJsonObject(); + } catch (JsonParseException e) { + throw new ProviderRequestException(e); + } + + if (code == 429) { + long now = System.currentTimeMillis(); + int retryIn = -1; + + String reason = jsonResponse.get("reason").getAsString(); + + if (reason.contains("Minutely")) { + retryIn = (int) (60 - (now % 60)); + } else if (reason.contains("Daily")) { + retryIn = (int) (3600 - (now % 3600)); + } + + throw new QuotaExceededException(1000); + } + + if (code != 200) + throw new ProviderRequestException("Status code: " + code); + + return OpenMeteoResponseConverter.convert(jsonResponse); + }); + } + + @Override + public Quota getQuota() { + return Quota.daily(10000); + } +} diff --git a/src/main/java/eu/m724/wtapi/provider/weather/impl/openmeteo/OpenMeteoResponseConverter.java b/src/main/java/eu/m724/wtapi/provider/weather/impl/openmeteo/OpenMeteoResponseConverter.java new file mode 100644 index 0000000..d32f7a7 --- /dev/null +++ b/src/main/java/eu/m724/wtapi/provider/weather/impl/openmeteo/OpenMeteoResponseConverter.java @@ -0,0 +1,53 @@ +package eu.m724.wtapi.provider.weather.impl.openmeteo; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import eu.m724.wtapi.object.Coordinates; +import eu.m724.wtapi.object.Weather; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +public class OpenMeteoResponseConverter { + static Weather[] convert(JsonElement jsonElement) { + Weather[] weathers; + + if (jsonElement.isJsonArray()) { + weathers = jsonElement.getAsJsonArray().asList().stream() + .map(JsonElement::getAsJsonObject) + .map(OpenMeteoResponseConverter::convertSingle) + .toArray(Weather[]::new); + } else { + weathers = new Weather[] { + convertSingle(jsonElement.getAsJsonObject()) + }; + } + + return weathers; + } + + private static Weather convertSingle(JsonObject jsonObject) { + JsonObject current = jsonObject.getAsJsonObject("current"); + + Coordinates coordinates = new Coordinates( + jsonObject.get("latitude").getAsDouble(), + jsonObject.get("longitude").getAsDouble() + ); + + LocalDateTime timestamp = LocalDateTime.ofEpochSecond(current.get("time").getAsLong(), 0, ZoneOffset.UTC); + + int weatherCode = current.get("weather_code").getAsInt(); + + boolean raining = current.get("rain").getAsDouble() > 0; + boolean thundering = weatherCode >= 90 && weatherCode < 100; + boolean snowing = current.get("snowfall").getAsDouble() > 0; + + float temperatureCelsius = current.get("temperature").getAsFloat(); + float temperatureApparentCelsius = current.get("apparent_temperature").getAsFloat(); + + float relativeHumidityPercentage = current.get("relative_humidity_2m").getAsInt() / 100f; + float cloudCoverPercentage = current.get("cloud_cover").getAsInt() / 100f; + + return new Weather(coordinates, timestamp, raining, thundering, snowing, temperatureCelsius, temperatureApparentCelsius, relativeHumidityPercentage, cloudCoverPercentage); + } +} diff --git a/src/main/java/eu/m724/wtapi/provider/weather/impl/openweathermap/OWMResponseConverter.java b/src/main/java/eu/m724/wtapi/provider/weather/impl/openweathermap/OWMResponseConverter.java index ea6649c..c464e62 100644 --- a/src/main/java/eu/m724/wtapi/provider/weather/impl/openweathermap/OWMResponseConverter.java +++ b/src/main/java/eu/m724/wtapi/provider/weather/impl/openweathermap/OWMResponseConverter.java @@ -9,6 +9,10 @@ import eu.m724.wtapi.object.Weather; public class OWMResponseConverter { static Weather convert(JsonObject json) { + Coordinates coordinates = new Coordinates( + json.getAsJsonObject("coord").getAsJsonPrimitive("lon").getAsDouble(), + json.getAsJsonObject("coord").getAsJsonPrimitive("lat").getAsDouble() + ); Weather weather = new Weather(); int conditionCode = json.getAsJsonArray("weather") @@ -178,12 +182,12 @@ public class OWMResponseConverter { json.getAsJsonObject("coord").getAsJsonPrimitive("lat").getAsDouble() ).setAddress(country, city); - weather.temperature = json + weather.temperatureCelsius = json .getAsJsonObject("main") .getAsJsonPrimitive("temp") .getAsFloat(); - weather.temperatureApparent = json + weather.temperatureApparentCelsius = json .getAsJsonObject("main") .getAsJsonPrimitive("feels_like") .getAsFloat(); @@ -200,12 +204,12 @@ public class OWMResponseConverter { if (pri != null) weather.windSpeed = pri.getAsFloat(); - weather.humidity = json + weather.relativeHumidity = json .getAsJsonObject("main") - .getAsJsonPrimitive("humidity") + .getAsJsonPrimitive("relativeHumidityPercentage") .getAsInt() / 100f; - weather.cloudiness = json + weather.cloudCover = json .getAsJsonObject("clouds") .getAsJsonPrimitive("all") .getAsInt() / 100f; diff --git a/src/main/java/eu/m724/wtapi/provider/weather/impl/openweathermap/OpenWeatherMapProvider.java b/src/main/java/eu/m724/wtapi/provider/weather/impl/openweathermap/OpenWeatherMapProvider.java index eabf18a..eff20ed 100644 --- a/src/main/java/eu/m724/wtapi/provider/weather/impl/openweathermap/OpenWeatherMapProvider.java +++ b/src/main/java/eu/m724/wtapi/provider/weather/impl/openweathermap/OpenWeatherMapProvider.java @@ -6,21 +6,27 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; import java.util.ArrayList; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import eu.m724.wtapi.object.Coordinates; import eu.m724.wtapi.object.Weather; -import eu.m724.wtapi.provider.exception.AuthorizationException; +import eu.m724.wtapi.provider.exception.ProviderAuthorizationException; import eu.m724.wtapi.provider.exception.ProviderException; import eu.m724.wtapi.provider.exception.QuotaExceededException; -import eu.m724.wtapi.provider.exception.ServerProviderException; +import eu.m724.wtapi.provider.exception.ProviderRequestException; import eu.m724.wtapi.provider.weather.WeatherProvider; +import eu.m724.wtapi.provider.weather.impl.openmeteo.OpenMeteoProvider; +/** + * @deprecated + * Development will focus on {@link OpenMeteoProvider} instead. + */ +@Deprecated public class OpenWeatherMapProvider extends WeatherProvider { private String apiKey; @@ -32,56 +38,49 @@ public class OpenWeatherMapProvider extends WeatherProvider { public void init() throws ProviderException { CompletableFuture weatherFuture = getWeather(new Coordinates(0, 0)); - - try { - weatherFuture.join(); - } catch (CompletionException e) { - throw (ProviderException) e; - } - } - @Override - public CompletableFuture getWeather(Coordinates coordinates) { + weatherFuture.join(); + } + + private CompletableFuture getWeather(Coordinates coordinates) { String url = "https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s" - .formatted(coordinates.latitude, coordinates.longitude, apiKey); - + .formatted(coordinates.latitude(), coordinates.longitude(), apiKey); + HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .build(); - + CompletableFuture> responseFuture = HttpClient.newBuilder() .proxy(ProxySelector.getDefault()).build() .sendAsync(request, BodyHandlers.ofString()); - + CompletableFuture weatherFuture = responseFuture.thenApply((response) -> { int code = response.statusCode(); - + if (code == 401) - throw new AuthorizationException("invalid api key"); + throw new ProviderAuthorizationException("Invalid api key"); if (code == 429) - throw new QuotaExceededException("ratelimited", 1000); + throw new QuotaExceededException("Ratelimited", 1000); if (code != 200) - throw new ServerProviderException(String.format("status code %d", code)); - + throw new ProviderRequestException("Status code: " + code); + JsonObject jsonResponse = JsonParser.parseString(response.body()) .getAsJsonObject(); - - Weather weather = OWMResponseConverter.convert(jsonResponse); - - return weather; + + return OWMResponseConverter.convert(jsonResponse); }); - + return weatherFuture; } @Override - public CompletableFuture getWeatherBulk(Coordinates[] coordinateses) { + public CompletableFuture getWeather(Coordinates... coordinates) { ArrayList> weatherFutures = new ArrayList<>(); - for (Coordinates coordinates : coordinateses) { + for (Coordinates coordinates : coordinatesArray) { weatherFutures.add(getWeather(coordinates)); } @@ -97,8 +96,8 @@ public class OpenWeatherMapProvider extends WeatherProvider { } @Override - public int getQuotaHourly() { - return 1370; + public int getQuota() { + return Duration.ofHours(1370); } @Override diff --git a/src/test/java/eu/m724/wtapi/object/CoordinateTest.java b/src/test/java/eu/m724/wtapi/object/CoordinateTest.java index 4e2388a..4c87bfe 100644 --- a/src/test/java/eu/m724/wtapi/object/CoordinateTest.java +++ b/src/test/java/eu/m724/wtapi/object/CoordinateTest.java @@ -9,14 +9,14 @@ public class CoordinateTest { public void testCoordinates() { Coordinates coordinates = new Coordinates(-91.1, 180.01); - System.out.println(coordinates.longitude); + System.out.println(coordinates.longitude()); - assert coordinates.latitude == 88.9; - assert coordinates.longitude == -179.99; + assert coordinates.latitude() == 88.9; + assert coordinates.longitude() == -179.99; coordinates = new Coordinates(-91.1, 180.1); - System.out.printf("Precision loss expected: %f\n", coordinates.longitude); - assert coordinates.longitude != -179.9; // TODO fix precision loss + System.out.printf("Precision loss expected: %f\n", coordinates.longitude()); + assert coordinates.longitude() != -179.9; // TODO fix precision loss assertNull(coordinates.city); assertNull(coordinates.country); diff --git a/src/test/java/eu/m724/wtapi/thunder/BlitzortungTest.java b/src/test/java/eu/m724/wtapi/thunder/BlitzortungTest.java index 8015522..51ce79e 100644 --- a/src/test/java/eu/m724/wtapi/thunder/BlitzortungTest.java +++ b/src/test/java/eu/m724/wtapi/thunder/BlitzortungTest.java @@ -25,7 +25,7 @@ public class BlitzortungTest { provider.tick(); int size = coordinatesList.size(); if (size > 0) - System.out.printf("Last from tick: %f %f (total %d)\n", coordinatesList.get(size-1).latitude, coordinatesList.get(size-1).longitude, size); + System.out.printf("Last from tick: %f %f (total %d)\n", coordinatesList.get(size - 1).latitude(), coordinatesList.get(size - 1).longitude(), size); Thread.sleep(25); } diff --git a/src/test/java/eu/m724/wtapi/thunder/ThunderProviderTest.java b/src/test/java/eu/m724/wtapi/thunder/ThunderProviderTest.java index 5c6b299..6778d7d 100644 --- a/src/test/java/eu/m724/wtapi/thunder/ThunderProviderTest.java +++ b/src/test/java/eu/m724/wtapi/thunder/ThunderProviderTest.java @@ -24,7 +24,7 @@ public class ThunderProviderTest { provider.tick(); int size = coordinatesList.size(); if (size > 0) - System.out.printf("Last from tick: %f %f (total %d)\n", coordinatesList.get(size-1).latitude, coordinatesList.get(size-1).longitude, size); + System.out.printf("Last from tick: %f %f (total %d)\n", coordinatesList.get(size - 1).latitude(), coordinatesList.get(size - 1).longitude(), size); Thread.sleep(20); } @@ -33,6 +33,6 @@ public class ThunderProviderTest { System.out.printf("Strikes in the last 1s: %d\n", coordinatesList.size()); System.out.printf("Latency: %dms\n", provider.getLatency()); - assert coordinatesList.size() == 20; // TODO this is time sensitive and fails under loaded system. Also, the entire test is suboptimal + assert coordinatesList.size() == 20; // TODO this is timestamp sensitive and fails under loaded system. Also, the entire test is suboptimal } } diff --git a/src/test/java/eu/m724/wtapi/twilight/ApproximateTwilightTimeTest.java b/src/test/java/eu/m724/wtapi/twilight/ApproximateTwilightTimeTest.java index 7845ec8..e964c50 100644 --- a/src/test/java/eu/m724/wtapi/twilight/ApproximateTwilightTimeTest.java +++ b/src/test/java/eu/m724/wtapi/twilight/ApproximateTwilightTimeTest.java @@ -22,6 +22,10 @@ public class ApproximateTwilightTimeTest { public void approximateTest() { TwilightTimeProvider provider = new ApproximateTwilightTimeProvider(); + double solarAltitude = provider.calculateSolarAltitude(LocalDateTime.now(), new Coordinates(51, 0)); + System.out.println(Math.toDegrees(solarAltitude)); + assert false; + // used https://gml.noaa.gov/grad/solcalc/index.html for reference values testLocation(provider, 26, 6, 2023, 53.123394, 23.0864867, LocalDateTime.of(2023, 6, 26, 2, 2), LocalDateTime.of(2023, 6, 26, 18, 59), 0); testLocation(provider, 13, 11, 2040, 45.432427, -122.3899276, LocalDateTime.of(2040, 11, 13, 15, 7), LocalDateTime.of(2040, 11, 14, 0, 41), 0); @@ -39,7 +43,7 @@ public class ApproximateTwilightTimeTest { Twilight twilight = provider.calculateTwilightTime(date, coordinates); System.out.println(date); - System.out.println(coordinates.latitude + " " + coordinates.longitude); + System.out.println(coordinates.latitude() + " " + coordinates.longitude()); System.out.println("Solar noon: " + twilight.solarNoon()); System.out.println("Calculated sunrise: " + twilight.sunrise()); diff --git a/src/test/java/eu/m724/wtapi/twilight/MockTwilightTimeProvider.java b/src/test/java/eu/m724/wtapi/twilight/MockTwilightTimeProvider.java index c3ca302..bc32656 100644 --- a/src/test/java/eu/m724/wtapi/twilight/MockTwilightTimeProvider.java +++ b/src/test/java/eu/m724/wtapi/twilight/MockTwilightTimeProvider.java @@ -4,15 +4,13 @@ import eu.m724.wtapi.object.Coordinates; import eu.m724.wtapi.object.Twilight; import eu.m724.wtapi.provider.twilight.TwilightTimeProvider; -import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; public class MockTwilightTimeProvider extends TwilightTimeProvider { @Override public Twilight calculateTwilightTime(LocalDate date, Coordinates coordinates) { - int time = (int) (coordinates.latitude + coordinates.longitude); + int time = (int) (coordinates.latitude() + coordinates.longitude()); return new Twilight( date, date.atStartOfDay().plusSeconds(-time), @@ -21,4 +19,9 @@ public class MockTwilightTimeProvider extends TwilightTimeProvider { false ); } + + @Override + public double calculateSolarAltitude(LocalDateTime time, Coordinates coordinates) { + return coordinates.latitude(); + } } diff --git a/src/test/java/eu/m724/wtapi/weather/MockWeatherProvider.java b/src/test/java/eu/m724/wtapi/weather/MockWeatherProvider.java index 3e74bd0..ef8fe3c 100644 --- a/src/test/java/eu/m724/wtapi/weather/MockWeatherProvider.java +++ b/src/test/java/eu/m724/wtapi/weather/MockWeatherProvider.java @@ -5,7 +5,7 @@ import eu.m724.wtapi.object.Coordinates; import eu.m724.wtapi.object.Weather; import eu.m724.wtapi.provider.exception.ProviderException; import eu.m724.wtapi.provider.exception.QuotaExceededException; -import eu.m724.wtapi.provider.exception.ServerProviderException; +import eu.m724.wtapi.provider.exception.ProviderRequestException; import eu.m724.wtapi.provider.weather.WeatherProvider; public class MockWeatherProvider extends WeatherProvider { @@ -26,13 +26,13 @@ public class MockWeatherProvider extends WeatherProvider { throw new NullPointerException("no coordinates passed"); System.out.printf("mock getting weather for %f, %f\n", - coordinates.latitude, coordinates.longitude); + coordinates.latitude(), coordinates.longitude()); CompletableFuture completableFuture = new CompletableFuture<>(); if (faulty) - completableFuture.completeExceptionally(new ServerProviderException("server is on vacation rn")); + completableFuture.completeExceptionally(new ProviderRequestException("Imagined server is down")); else completableFuture.complete(new Weather()); @@ -40,13 +40,13 @@ public class MockWeatherProvider extends WeatherProvider { } @Override - public CompletableFuture getWeatherBulk(Coordinates[] coordinateses) { - int len = coordinateses.length; + public CompletableFuture getWeatherBulk(Coordinates[] coordinatesArray) { + int len = coordinatesArray.length; if (len == 0) throw new IllegalArgumentException("no coordinates passed"); - System.out.printf("mock getting weather for multiple coords"); + System.out.println("mock getting weather for multiple coords"); CompletableFuture completableFuture = new CompletableFuture<>(); @@ -55,14 +55,14 @@ public class MockWeatherProvider extends WeatherProvider { Weather[] weathers = new Weather[len]; for (int i=0; i weatherFuture = - provider.getWeather(new Coordinates(53.2232, -4.2008)); - - Weather weather = weatherFuture.get(); - assertNotNull(weather); - - System.out.printf("current weather in %s, %s: %s\n", weather.coordinates.city, weather.coordinates.country, weather.description); - CompletableFuture weatherBulkFuture = - provider.getWeatherBulk( - new Coordinates[] { - new Coordinates(54.6606714, -3.3827237), - new Coordinates(47.5705952, -53.5556464), - new Coordinates(34.2073721, -84.1402857), - }); + CompletableFuture weathersFuture = + provider.getWeather( + new Coordinates(54.6606714, -3.3827237), + new Coordinates(47.5705952, -53.5556464), + new Coordinates(34.2073721, -84.1402857)); - Weather[] weathers = weatherBulkFuture.get(); + Weather[] weathers = weathersFuture.get(); assert weathers.length == 3; - for (Weather weather1 : weathers) { - System.out.printf("current weather in %s, %s: %s\n", weather.coordinates.city, weather.coordinates.country, weather1.description); + for (Weather weather : weathers) { + System.out.printf("Current weather in %s, %s: %s\n", weather.coordinates().city, weather.coordinates().country, weather.description); } } } diff --git a/src/test/java/eu/m724/wtapi/weather/ProviderTest.java b/src/test/java/eu/m724/wtapi/weather/ProviderTest.java index cee26a7..76b5d66 100644 --- a/src/test/java/eu/m724/wtapi/weather/ProviderTest.java +++ b/src/test/java/eu/m724/wtapi/weather/ProviderTest.java @@ -22,7 +22,7 @@ public class ProviderTest { WeatherProvider provider = new MockWeatherProvider(false); provider.init(); - assert provider.getQuotaHourly() == 5; + assert provider.getQuota() == 5; assert provider.getBulkLimit() == 1; CompletableFuture weatherFuture =