This commit is contained in:
Nemo D'ACREMONT 2024-12-14 15:50:35 +01:00
commit 4fc4374583
50 changed files with 16318 additions and 340 deletions

View File

@ -9,22 +9,56 @@ Nous vous conseillons d'utiliser le logiciel PlantUML pour générer vos diagram
Décrivez ici le schéma général de votre programme. Quels sont les composants principaux et comment interagissent-ils ?
On a 2 principaux composants :
* WeatherDataAPI
* WeatherDisplay
WeatherDisplay contient un ensemble d'instances de WeatherDisplay. Chacune de ces instances retournent des données ayant l'interface WeatherData, ce qui permet au WeatherDisplay de les afficher.
## Utilisation du polymorphisme
Comment utilisez-vous le polymorphisme dans votre programme ?
## Utilisation de la déléguation
Nous utilisons les interfaces suivantes servant à définir les parties publiques de nos Classes :
* WeatherDataAPI
* WeatherDisplay
## Utilisation de la délégation
Comment utilisez-vous la délégation dans votre programme ?
Nous avons essayé de mettre en oeuvre un maximum de forme de délégation dans le projet.
Voici les principales formes de délégations qui se trouvent dans le projet :
### JSONFetcher
Les requêtes HTTP(S) et la transformation de la réponse en JSONObject est abstraite grâce à la classe JSONFetcher
### City
Nous utilisons une classe City afin de stocker le nom d'une ville, et de faire le lien avec ses coordonnées.
Cela permet d'abstraire un appel à l'API api-adresse.data.gouv.fr pour obtenir les coordonnées depuis le nom de la ville.
## Utilisation de l'héritage
Comment utilisez-vous l'héritage dans votre programme ?
Nous avons limité au maximum l'héritage dans le projet et nous sommes concentrés sur des relations de composition.
Au final, pour permettre un système de cache, les trois classes WeatherAPI, OpenMeteo et OpenWeatherMap héritent d'une classe WeatherDataCachedAPI.
## Utilisation de la généricité
Comment utilisez-vous la généricité dans votre programme ?
Nous n'avons pas eu besoin de généricité dans notre programme.
## Utilisation des exceptions
Comment utilisez-vous les exceptions dans votre programme ?
Nous utilisons WeatherFetchingException qui est une Exception qui est envoyée lorsqu'

View File

@ -4,12 +4,26 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.time.Instant;
import eirb.pg203.weather.display.WeatherDisplay;
import eirb.pg203.weather.display.WeatherDisplayBasic;
import eirb.pg203.weather.data.api.OpenMeteo;
import eirb.pg203.weather.data.api.OpenWeatherMap;
import eirb.pg203.weather.data.api.WeatherAPI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Scanner;
/**
* Main class
*/
public class Main {
/**
* Default constructor (private)
*/
private Main() {};
private static class Option {
String flag, value;
public Option(String flag, String value){
@ -31,6 +45,11 @@ public class Main {
}
}
/**
* Main loop
* @param args list of arguments
* @throws IllegalArgumentException if arguments are not provided or in a wrong way
*/
public static void main(String[] args) throws IOException, IllegalArgumentException{
HashMap<String, Option> options = new HashMap<>();
List<String> argsList = new ArrayList<>();
@ -69,8 +88,6 @@ public class Main {
openWeatherMap.loadCacheFromFile("owm.cache.json");
openMeteo.loadCacheFromFile("om.cache.json");
System.out.println(weatherAPI.toJSON().toString());
WeatherDisplay display = new WeatherDisplayBasic();
display.addAPI(weatherAPI);

View File

@ -1,42 +0,0 @@
package eirb.pg203;
import java.time.Instant;
/**
* Representation of a temperature in a city at a specific date
*/
public class Temperature {
private City city;
private Instant date;
private float temp;
Temperature(float temp, City city, Instant date) {
this.temp = temp;
this.city = city;
this.date = date;
}
/**
* Get the name of the city from where the temperature come from
* @return city
*/
public String getCity() {
return this.city.getCityName();
}
/**
* Get the date
* @return date
*/
public Instant getDate() {
return date;
}
/**
* Get the temperature
* @return temperature
*/
public float getTemp() {
return temp;
}
}

View File

@ -1,34 +0,0 @@
package eirb.pg203;
import java.io.IOException;
import java.util.ArrayList;
public interface WeatherDataAPI {
/**
* Fetch the temperature for a specific day in a city
* @param day Since D+0
* @param city Localisation
* @return Temperature of the day from the city
* @throws IOException when request failed
*/
WeatherData getTemperature(int day, String city) throws IOException;
/**
*
* @param day Since D+0
* @param hour Since H+0
* @param city Localisation
* @return Temperature of the day for a hour from the city
* @throws IOException when request failed
*/
WeatherData getTemperature(int day, int hour, String city) throws IOException;
ArrayList<WeatherData> getTemperatures(int days, String city) throws IOException;
/***
* Name of the API
* @return Name of the API
*/
String getAPIName();
}

View File

@ -1,7 +0,0 @@
package eirb.pg203.exceptions;
public class WeatherFetchingException extends Exception {
public WeatherFetchingException(String message) {
super(message);
}
}

View File

@ -1,27 +0,0 @@
package eirb.pg203.utils;
public class Coords {
private float lat;
private float lon;
public Coords(float lat, float lon) {
this.lat = lat;
this.lon = lon;
}
public float getLat() {
return lat;
}
public float getLon() {
return lon;
}
public void setLat(float lat) {
this.lat = lat;
}
public void setLon(float lon) {
this.lon = lon;
}
}

View File

@ -1,14 +1,12 @@
package eirb.pg203;
package eirb.pg203.weather.data;
import java.time.Instant;
import java.util.concurrent.locks.Condition;
import org.json.JSONObject;
import com.sun.net.httpserver.Authenticator.Retry;
import eirb.pg203.weather.utils.City;
class WeatherData {
enum Condition {
public class WeatherData {
public enum Condition {
SUNNY("☀️"),
PARTIAL("🌤"),
CLOUDY("☁️"),
@ -115,7 +113,8 @@ class WeatherData {
return WindDirection.ERROR;
}
WeatherData(City city, Instant date, float temp, float windSpeed, float windDirectionAngle, Condition condition) {
public WeatherData(City city, Instant date, float temp, float windSpeed, float windDirectionAngle, Condition condition) {
this.city = city;
this.date = date;
this.temp = temp;

View File

@ -0,0 +1,263 @@
package eirb.pg203.weather.data;
import eirb.pg203.weather.utils.City;
import java.time.Instant;
import java.util.Locale;
/**
* Weather Data representation
* A weather data is a Temperature, on a date, in a city, with a weather condition and wind speed + direction
* The representation of the data is managed in this class.
*/
public class WeatherData {
/**
* Representation of a weather condition (with a smiley for String representation)
*/
public enum Condition {
/**
* Sunny condition
*/
SUNNY("☀️"),
/**
* A little a bit of sun and a little bit of clouds
*/
PARTIAL("🌤"),
/**
* Cloudy weather
*/
CLOUDY("☁️"),
/**
* Rainy weather -> like most of the time in Bordeaux
*/
RAINY("🌧"),
/**
* Impossible to determine a Weather Condition
*/
ERROR("E");
private final String desc;
Condition(String desc) { this.desc = desc; }
@Override
public String toString() {
return this.desc;
}
}
/**
* Representation of the wind direction with an arrow
*/
public enum WindDirection {
/**
* North direction
*/
N("🡩"),
/**
* North East direction
*/
NE("🡭"),
/**
* East direction
*/
E("🡪"),
/**
* South East direction
*/
SE("🡮"),
/**
* South direction
*/
S("🡫"),
/**
* South West direction
*/
SW("🡯"),
/**
* West direction
*/
W("🡨"),
/**
* North West direction
*/
NW("🡬"),
/**
* Error wind direction
*/
ERROR("E");
private final String desc;
WindDirection(String desc) {this.desc = desc;}
@Override
public String toString(){ return this.desc;};
}
private City city;
private Instant date;
private float temp;
private Condition condition; // cloudly, sunny ...
private float windSpeed;
private float windDirectionAngle;
private WindDirection windDirection;
/**
* Get wind direction representation based on the angle
* @param windDirectionAngle float representation of the wind direction
* @return wind direction representation
*/
private WindDirection getWindDirection(float windDirectionAngle) {
if ((windDirectionAngle >= 337.5 && windDirectionAngle <= 360) || (windDirectionAngle >= 0 && windDirectionAngle <= 22.5))
return WindDirection.N;
if (windDirectionAngle > 22.5 && windDirectionAngle <= 67.5)
return WindDirection.NE;
if (windDirectionAngle > 67.5 && windDirectionAngle <= 112.5)
return WindDirection.E;
if (windDirectionAngle > 112.5 && windDirectionAngle <= 157.5)
return WindDirection.SE;
if (windDirectionAngle > 157.5 && windDirectionAngle <= 202.5)
return WindDirection.S;
if (windDirectionAngle > 202.5 && windDirectionAngle <= 247.5)
return WindDirection.SW;
if (windDirectionAngle > 247.5 && windDirectionAngle <= 292.5)
return WindDirection.W;
if (windDirectionAngle > 292.5 && windDirectionAngle <= 337.5)
return WindDirection.NW;
return WindDirection.ERROR;
}
public WeatherData(City city, Instant date, float temp, float windSpeed, float windDirectionAngle, Condition condition) {
this.city = city;
this.date = date;
this.temp = temp;
this.condition = condition;
this.windSpeed = windSpeed;
this.windDirectionAngle = windDirectionAngle;
this.windDirection = this.getWindDirection(windDirectionAngle);
}
/**
* Get city from where the weather data come from
* @return city
*/
public City getCity() {
return city;
}
/**
* Get date of the weather data
* @return date of the Weather data
*/
public Instant getDate() {
return date;
}
/**
* Get weather condition representation
* @return Weather Condition representation
*/
public Condition getCondition() {
return condition;
}
/**
* Get temperature value
* @return float temperature
*/
public float getTemp() {
return temp;
}
/**
* Get wind speed value
* @return wind speed
*/
public float getWindSpeed() {
return windSpeed;
}
/**
* Get wind direction angle value
* @return Wind direction angle
*/
public float getWindDirectionAngle() {return this.windDirectionAngle;}
/**
* Get wind direction representation
* @return wind direction representation
*/
public WindDirection getWindDirection() {
return windDirection;
}
/**
* Set city
* @param city city of the weather representation
*/
public void setCity(City city) {
this.city = city;
}
/**
* Set date of the weather data
* @param date date
*/
public void setDate(Instant date) {
this.date = date;
}
/**
* Set weather data representation
* @param condition weather data representation
*/
public void setCondition(Condition condition) {
this.condition = condition;
}
/**
* Set temperature value
* @param temp Weather data temperature
*/
public void setTemp(float temp) {
this.temp = temp;
}
/**
* Set wind speed value
* @param windSpeed Wind speed
*/
public void setWindSpeed(float windSpeed) {
this.windSpeed = windSpeed;
}
/**
* Set wind direction angle value
* @param windDirectionAngle wind direction angle
*/
public void setWindDirectionAngle(float windDirectionAngle) {
this.windDirectionAngle = windDirectionAngle;
this.windDirection = this.getWindDirection(windDirectionAngle);
}
/**
* WeatherData representation
* 10,70° 🌧 25,80km/h 243,00° 🡯
* @return String representation of the WeatherData
*/
@Override
public String toString() {
return String.format(Locale.ENGLISH, "%05.2f° %s %05.2fkm/h %06.2f° %s",
this.getTemp(),
this.getCondition().toString(),
this.getWindSpeed(),
this.getWindDirectionAngle(),
this.getWindDirection().toString()
);
}
}

View File

@ -1,45 +1,77 @@
package eirb.pg203;
package eirb.pg203.weather.data.api;
import org.json.JSONArray;
import eirb.pg203.weather.exceptions.WeatherFetchingExceptionCityCoords;
import eirb.pg203.weather.utils.City;
import eirb.pg203.weather.exceptions.WeatherFetchingException;
import eirb.pg203.weather.exceptions.WeatherFetchingExceptionApi;
import eirb.pg203.weather.data.WeatherData;
import org.json.JSONObject;
import eirb.pg203.utils.JSONFetcher;
import eirb.pg203.weather.utils.JSONFetcher;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import eirb.pg203.WeatherData.Condition;
import java.util.Locale;
import eirb.pg203.weather.data.WeatherData.Condition;
// https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&hourly=temperature_2m
/**
* OpenMeteo implementation
*/
public class OpenMeteo extends WeatherCachedAPI {
private static final String forecastBaseURL = "https://api.open-meteo.com/v1/forecast";
private static final String dailyQuery = "weather_code,temperature_2m_max,temperature_2m_min,wind_speed_10m_max,wind_direction_10m_dominant";
JSONFetcher JSONFetcher = new JSONFetcher();
Clock clock = Clock.systemUTC();
/**
* Default constructor
*/
public OpenMeteo() {}
// https://www.nodc.noaa.gov/archive/arc0021/0002199/1.1/data/0-data/HTML/WMO-CODE/WMO4677.HTM
private JSONObject fetchWeather(int days, City city) throws IOException {
URL url = URI.create(
String.format(forecastBaseURL + "?latitude=%.2f&longitude=%.2f&forecast_days=%d&daily=" + dailyQuery,
private JSONObject fetchWeather(int days, City city) throws WeatherFetchingException{
URL url = null;
try {
url = URI.create(
String.format(Locale.ENGLISH, forecastBaseURL + "?latitude=%.2f&longitude=%.2f&forecast_days=%d&daily=" + dailyQuery,
city.getCityCoords().getLat(),
city.getCityCoords().getLon(),
days
)
).toURL();
return JSONFetcher.fetch(url);
} catch (IOException e) {
throw new WeatherFetchingExceptionCityCoords();
}
try {
return JSONFetcher.fetch(url);
} catch (IOException e) {
throw new WeatherFetchingExceptionApi();
}
}
/**
* return condition based on the WMOCode
* table can be found here : https://open-meteo.com/en/docs (at the end)
* @param WMOCode id code
* @return weather condition
*/
private static Condition getConditionFromCode(int WMOCode) {
if (WMOCode < 20)
return Condition.SUNNY;
else if (WMOCode < 30)
return Condition.RAINY;
else if (WMOCode < 50)
return Condition.CLOUDY;
else
return Condition.RAINY;
return switch (WMOCode) {
case 0, 1 -> Condition.SUNNY;
case 2 -> Condition.PARTIAL;
case 3 -> Condition.CLOUDY;
case 61, 63, 65, 80, 81, 82 -> Condition.RAINY;
default -> Condition.ERROR;
};
}
@ -67,18 +99,18 @@ public class OpenMeteo extends WeatherCachedAPI {
* @param day Day, 0 &leq; day &leq; 14
*/
@Override
public WeatherData getTemperature(int day, String cityName) throws IOException {
public WeatherData getTemperature(int day, String cityName) throws WeatherFetchingException {
JSONObject result = fetchWeather(day + 1, new City(cityName));
return getWeatherDataFromForecast(result, day, cityName);
}
@Override
public WeatherData getTemperature(int day, int hour, String cityName) throws IOException{
public WeatherData getTemperature(int day, int hour, String cityName) throws WeatherFetchingException {
return getTemperature(day, cityName);
}
public ArrayList<WeatherData> fetchTemperatures(int days, String cityName) throws IOException {
public ArrayList<WeatherData> fetchTemperatures(int days, String cityName) throws WeatherFetchingException {
JSONObject result = fetchWeather(days, new City(cityName));
ArrayList<WeatherData> weatherDatas = new ArrayList<>();
@ -95,4 +127,9 @@ public class OpenMeteo extends WeatherCachedAPI {
public String getAPIName() {
return "OpenMeteo";
}
@Override
public String toString() {
return this.getAPIName();
}
}

View File

@ -1,46 +1,65 @@
package eirb.pg203;
package eirb.pg203.weather.data.api;
import eirb.pg203.weather.utils.City;
import eirb.pg203.weather.exceptions.WeatherFetchingException;
import eirb.pg203.weather.exceptions.WeatherFetchingExceptionApi;
import eirb.pg203.weather.exceptions.WeatherFetchingExceptionCityCoords;
import eirb.pg203.weather.data.WeatherData;
import org.json.JSONObject;
import org.json.JSONArray;
import eirb.pg203.utils.JSONFetcher;
import eirb.pg203.weather.utils.JSONFetcher;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.time.Clock;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import eirb.pg203.WeatherData.Condition;
import java.util.Locale;
// https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&hourly=temperature_2m
import eirb.pg203.weather.data.WeatherData.Condition;
/**
* OpenWeatherMap api implementation
*/
public class OpenWeatherMap extends WeatherCachedAPI {
private static final String forecastBaseURL = "https://api.openweathermap.org/data/2.5/forecast";
private String APIKey;
private static Clock clock = Clock.systemUTC();
JSONFetcher JSONFetcher = new JSONFetcher();
OpenWeatherMap(String APIKey) {
public OpenWeatherMap(String APIKey) {
this.APIKey = APIKey;
}
private JSONObject fetchWeather(int days, City city) throws IOException {
URL url = URI.create(
String.format(forecastBaseURL + "?appid=%s&lat=%.2f&lon=%.2f&units=metric",
private JSONObject fetchWeather(int day, City city) throws WeatherFetchingException {
URL url = null;
try {
url = URI.create(
String.format(Locale.ENGLISH, forecastBaseURL + "?appid=%s&lat=%.2f&lon=%.2f&units=metric",
APIKey,
city.getCityCoords().getLat(),
city.getCityCoords().getLon(),
days
city.getCityCoords().getLon()
)
).toURL();
} catch (IOException e) {
throw new WeatherFetchingExceptionCityCoords();
}
try {
return JSONFetcher.fetch(url);
} catch (IOException e) {
throw new WeatherFetchingExceptionApi();
}
}
private static WeatherData getWeatherDataFromForecast(JSONObject response, int day, String cityName) {
JSONArray list = response.getJSONArray("list");
DayOfWeek targetedDay = Instant.now().plusSeconds(day * 24 * 3600).atZone(ZoneId.systemDefault()).getDayOfWeek();
DayOfWeek targetedDay = Instant.now(clock).plusSeconds(day * 24 * 3600).atZone(ZoneId.systemDefault()).getDayOfWeek();
DayOfWeek dayOfWeek;
int dataCount = 0;
float temp_c = 0;
@ -98,18 +117,18 @@ public class OpenWeatherMap extends WeatherCachedAPI {
* @param day Day, 0 &leq; day &leq; 14
*/
@Override
public WeatherData getTemperature(int day, String cityName) throws IOException {
public WeatherData getTemperature(int day, String cityName) throws WeatherFetchingException {
JSONObject result = fetchWeather(day+1, new City(cityName));
return getWeatherDataFromForecast(result, day, cityName);
}
@Override
public WeatherData getTemperature(int day, int hour, String cityname) throws IOException{
public WeatherData getTemperature(int day, int hour, String cityname) throws WeatherFetchingException {
return getTemperature(day, cityname);
}
public ArrayList<WeatherData> fetchTemperatures(int days, String cityName) throws IOException {
public ArrayList<WeatherData> fetchTemperatures(int days, String cityName) throws WeatherFetchingException {
JSONObject result = fetchWeather(days, new City(cityName));
ArrayList<WeatherData> weatherDatas = new ArrayList<>();
@ -126,4 +145,9 @@ public class OpenWeatherMap extends WeatherCachedAPI {
public String getAPIName() {
return "OpenWeatherMap";
}
@Override
public String toString() {
return this.getAPIName();
}
}

View File

@ -1,41 +1,57 @@
package eirb.pg203;
package eirb.pg203.weather.data.api;
import eirb.pg203.weather.utils.City;
import eirb.pg203.weather.exceptions.WeatherFetchingException;
import eirb.pg203.weather.exceptions.WeatherFetchingExceptionApi;
import eirb.pg203.weather.data.WeatherData;
import org.json.JSONArray;
import org.json.JSONObject;
import eirb.pg203.WeatherData.Condition;
import eirb.pg203.utils.JSONFetcher;
import eirb.pg203.weather.data.WeatherData.Condition;
import eirb.pg203.weather.utils.JSONFetcher;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Locale;
/**
* WeatherAPI implementation
*/
public class WeatherAPI extends WeatherCachedAPI {
private final String weatherAPIKey;
JSONFetcher JSONFetcher = new JSONFetcher();
private static final String forecastBaseURL = "https://api.weatherapi.com/v1/forecast.json";
WeatherAPI(String weatherAPIKey) {
public WeatherAPI(String weatherAPIKey) {
this.weatherAPIKey = weatherAPIKey;
}
private JSONObject fetchWeather(int days, String city) throws IOException {
URL url = URI.create(
String.format(forecastBaseURL + "?key=%s&q=%s&days=%d",
private JSONObject fetchWeather(int days, String city) throws WeatherFetchingException {
URL url = null;
try {
url = URI.create(
String.format(Locale.ENGLISH, forecastBaseURL + "?key=%s&q=%s&days=%d",
this.weatherAPIKey,
city,
days
)
).toURL();
return JSONFetcher.fetch(url);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
private static WeatherData.Condition getConditionFromString(String str) {
try {
return JSONFetcher.fetch(url);
} catch (IOException e) {
throw new WeatherFetchingExceptionApi();
}
}
private static Condition getConditionFromString(String str) {
if (str.toLowerCase().contains("rain"))
return Condition.RAINY;
@ -80,17 +96,17 @@ public class WeatherAPI extends WeatherCachedAPI {
* @param day Day, 0 &leq; day &leq; 14
*/
@Override
public WeatherData getTemperature(int day, String cityName) throws IOException {
public WeatherData getTemperature(int day, String cityName) throws WeatherFetchingException {
JSONObject result = fetchWeather(day+1, cityName);
return getWeatherDataFromForecast(result, day, cityName);
}
@Override
public WeatherData getTemperature(int day, int hour, String cityName) throws IOException{
public WeatherData getTemperature(int day, int hour, String cityName) throws WeatherFetchingException {
return getTemperature(day, cityName);
}
public ArrayList<WeatherData> fetchTemperatures(int days, String cityName) throws IOException {
public ArrayList<WeatherData> fetchTemperatures(int days, String cityName) throws WeatherFetchingException {
JSONObject result = fetchWeather(days, cityName);
ArrayList<WeatherData> weatherDatas = new ArrayList<>();
@ -107,4 +123,9 @@ public class WeatherAPI extends WeatherCachedAPI {
public String getAPIName() {
return "WeatherAPI";
}
@Override
public String toString() {
return this.getAPIName();
}
}

View File

@ -1,4 +1,6 @@
package eirb.pg203;
package eirb.pg203.weather.data.api;
import eirb.pg203.weather.data.WeatherData;
import eirb.pg203.weather.exceptions.WeatherFetchingException;
import java.io.File;
import java.io.FileNotFoundException;
@ -11,16 +13,17 @@ import java.util.Scanner;
import org.json.JSONArray;
abstract class WeatherCachedAPI implements WeatherDataAPI {
public abstract class WeatherCachedAPI implements WeatherDataAPI {
private final WeatherDataCache cache = new WeatherDataCache();
abstract ArrayList<WeatherData> fetchTemperatures(int days, String cityName) throws IOException;
abstract ArrayList<WeatherData> fetchTemperatures(int days, String cityName) throws WeatherFetchingException;
public void loadCache(JSONArray data) {
this.cache.fromJSON(data, this.getAPIName());
}
private void updateCache(int days, String cityName) throws IOException {
private void updateCache(int days, String cityName) throws WeatherFetchingException {
ArrayList<WeatherData> data = fetchTemperatures(days, cityName);
Instant timestamp = Instant.now();
@ -28,7 +31,7 @@ abstract class WeatherCachedAPI implements WeatherDataAPI {
this.cache.set(cityName, i, data.get(i), timestamp);
}
public ArrayList<WeatherData> getTemperatures(int days, String cityName) throws IOException
public ArrayList<WeatherData> getTemperatures(int days, String cityName) throws WeatherFetchingException
{
ArrayList<WeatherData> result = new ArrayList<>();
@ -36,7 +39,9 @@ abstract class WeatherCachedAPI implements WeatherDataAPI {
{
if (this.cache.needsUpdate(cityName, i))
{
try {
updateCache(days, cityName);
} catch(Exception e) {}
}
}

View File

@ -0,0 +1,46 @@
package eirb.pg203.weather.data.api;
import eirb.pg203.weather.exceptions.WeatherFetchingException;
import eirb.pg203.weather.data.WeatherData;
import java.util.ArrayList;
/**
* Interface to discuss with a weather API
*/
public interface WeatherDataAPI {
/**
* Fetch the temperature for a specific day in a city
* @param day Since D+0
* @param city Localisation
* @return Temperature of the day from the city
* @throws WeatherFetchingException when request failed
*/
WeatherData getTemperature(int day, String city) throws WeatherFetchingException;
/**
* Get WeatherData for a specific day
* @param day Since D+0
* @param hour Since H+0
* @param city Localisation
* @return Temperature of the day for a hour from the city
* @throws WeatherFetchingException when request failed
*/
WeatherData getTemperature(int day, int hour, String city) throws WeatherFetchingException;
/**
* Fetch the temperature for multiple day since today
* @param days number of days to fetch
* @param city name of te city
* @return List of WeatherData
* @throws WeatherFetchingException when request failed
*/
ArrayList<WeatherData> getTemperatures(int days, String city) throws WeatherFetchingException;
/***
* Name of the API
* @return Name of the API
*/
String getAPIName();
}

View File

@ -1,4 +1,5 @@
package eirb.pg203;
package eirb.pg203.weather.data.api;
import eirb.pg203.weather.data.WeatherData;
import java.time.Instant;
import java.util.ArrayList;
@ -8,6 +9,7 @@ import java.util.Locale;
import org.json.JSONArray;
import org.json.JSONObject;
public class WeatherDataCache {
private static class CacheValue {
private WeatherData value;

View File

@ -1,4 +1,6 @@
package eirb.pg203;
package eirb.pg203.weather.display;
import eirb.pg203.weather.data.api.WeatherDataAPI;
/**
* How to display weather information, make the API calls based on the collection of WheatherDataAPI

View File

@ -1,15 +1,25 @@
package eirb.pg203;
package eirb.pg203.weather.display;
import eirb.pg203.weather.exceptions.WeatherFetchingException;
import eirb.pg203.weather.data.WeatherData;
import eirb.pg203.weather.data.api.WeatherDataAPI;
import java.util.ArrayList;
import java.util.HashMap;
class WeatherDisplayBasic implements WeatherDisplay {
private ArrayList<WeatherDataAPI> apis;
WeatherDisplayBasic() {
this.apis = new ArrayList<WeatherDataAPI>();
}
public class WeatherDisplayBasic implements eirb.pg203.weather.display.WeatherDisplay {
/**
* List of apis
*/
private final ArrayList<WeatherDataAPI> apis = new ArrayList<>();
/**
* Display header
* Source J + 0 J + 1 J + 2
* @param days number of columns
* @param sourceColumnSize size of the first column
* @param dayColumnSize day column size
*/
private void displayHeader(int days, double sourceColumnSize, double dayColumnSize) {
StringBuilder line = new StringBuilder();
line.append("Source\t");
@ -26,6 +36,13 @@ class WeatherDisplayBasic implements WeatherDisplay {
System.out.println(line);
}
/**
* Calculate column size based on the WeatherData
* Check the size of the string of the weather data and return the max for the column
* WARNING : Special chars in the WeatherData string will introduce an offset
* @param weatherDataAPIArrayListHashMap list of Weather data for every WeatherDataApi
* @return day column size
*/
private double getColumnSize(HashMap<WeatherDataAPI, ArrayList<WeatherData>> weatherDataAPIArrayListHashMap) {
double max = 0;
for (WeatherDataAPI api: weatherDataAPIArrayListHashMap.keySet()) {
@ -39,6 +56,15 @@ class WeatherDisplayBasic implements WeatherDisplay {
return max;
}
/**
* Display a line of data with a column separator
* OpenWeatherMap | 14,49° 07,71km/h 254,25° 🡨 | 11,29° 04,33km/h 296,00° 🡬 | 12,06° 07,53km/h 188,88° 🡫 |
* @param apiName Name of the api if the first column
* @param weatherDatas List of Weather data to display
* @param startColumnString Separator between column
* @param sourceColumnSize Size of the first column
* @param dayColumnSize Size for day columns
*/
private void displayWeatherDatas(String apiName, ArrayList<WeatherData> weatherDatas, String startColumnString, double sourceColumnSize, double dayColumnSize) {
StringBuilder line = new StringBuilder();
String weatherDataString;
@ -61,6 +87,13 @@ class WeatherDisplayBasic implements WeatherDisplay {
}
/**
* Display in stdout the line between apis
* -------------------------------+------------------------------+------------------------------+------------------------------+
* @param days number of days to display
* @param sourceColumnSize size for the first column (where the name of the api is display)
* @param dayColumnSize column size for the days (where the temperature is displayed)
*/
private void displaySeparatorLine(int days, double sourceColumnSize, double dayColumnSize) {
String mainSeparator = "-";
String secondSeparator = "+";
@ -77,6 +110,20 @@ class WeatherDisplayBasic implements WeatherDisplay {
System.out.println(line);
}
/**
* Display an array like this in stdout
* Source J + 0 J + 1 J + 2
* -------------------------------+------------------------------+------------------------------+------------------------------+
* OpenWeatherMap | 14,49° 07,71km/h 254,25° 🡨 | 11,29° 04,33km/h 296,00° 🡬 | 12,06° 07,53km/h 188,88° 🡫 |
* -------------------------------+------------------------------+------------------------------+------------------------------+
* WeatherAPI | 12,50° 🌧 20,31km/h 238,67° 🡯 | 11,20° 🌧 17,23km/h 291,92° 🡨 | 11,00° 🌧 25,59km/h 256,88° 🡨 |
* -------------------------------+------------------------------+------------------------------+------------------------------+
* OpenMeteo | 10,70° 🌧 25,80km/h 243,00° 🡯 | 11,35° 🌧 24,30km/h 276,00° 🡨 | 11,00° 🌧 31,50km/h 238,00° 🡯 |
* -------------------------------+------------------------------+------------------------------+------------------------------+
*
* @param weatherDataAPIArrayListHashMap Hashmap with WeatherData array for each api
* @param days number of days to display
*/
private void displayAllWeatherDatas(HashMap<WeatherDataAPI, ArrayList<WeatherData>> weatherDataAPIArrayListHashMap, int days) {
double dayColumnSize = this.getColumnSize(weatherDataAPIArrayListHashMap);
@ -105,7 +152,10 @@ class WeatherDisplayBasic implements WeatherDisplay {
for (WeatherDataAPI w: apis) {
try {
weatherDatasMap.put(w, w.getTemperatures(days, city));
} catch (Exception e) {
} catch (WeatherFetchingException e) {
System.err.println(w.getAPIName() + " failed to fetch meteo");
}
catch (Exception e) {
System.err.println(e);
}
}

View File

@ -0,0 +1,14 @@
package eirb.pg203.weather.exceptions;
/**
* Exception when an error during the api call
*/
public class WeatherFetchingException extends Exception {
/**
* Weather Fetching exception
* @param message message of the exception
*/
public WeatherFetchingException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package eirb.pg203.weather.exceptions;
public class WeatherFetchingExceptionApi extends eirb.pg203.weather.exceptions.WeatherFetchingException {
public WeatherFetchingExceptionApi() {
super("An error occurred during API fetching");
}
}

View File

@ -0,0 +1,7 @@
package eirb.pg203.weather.exceptions;
public class WeatherFetchingExceptionCityCoords extends eirb.pg203.weather.exceptions.WeatherFetchingException {
public WeatherFetchingExceptionCityCoords() {
super("Impossible to get city coords");
}
}

View File

@ -1,52 +1,51 @@
package eirb.pg203;
package eirb.pg203.weather.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.Locale;
import eirb.pg203.weather.utils.Coords;
import eirb.pg203.weather.utils.JSONFetcher;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import eirb.pg203.utils.Coords;
class City {
/**
* Representation of a city
* Possibility to get city coordinates based on the name
*/
public class City {
private String cityName;
private Coords cityCoords;
JSONFetcher JSONFetcher = new JSONFetcher();
/**
* Fetch data from adresse.data.gouv.fr
* @throws IOException if the request fails
*/
private static JSONObject getDataFromName(String cityName) throws IOException {
private JSONObject getDataFromName(String cityName) throws IOException {
StringBuilder result = new StringBuilder();
URL url = URI.create(
String.format("https://api-adresse.data.gouv.fr/search/?q=%s&autocomplete=0&limit=1",
String.format(Locale.ENGLISH, "https://api-adresse.data.gouv.fr/search/?q=%s&autocomplete=0&limit=1",
cityName
)
).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
for (String line; (line = reader.readLine()) != null; ) {
result.append(line);
}
return JSONFetcher.fetch(url);
}
return new JSONObject(result.toString());
}
private static Coords getCoordsFromName(String cityName) throws IOException {
private Coords getCoordsFromName(String cityName) throws IOException {
JSONObject data = getDataFromName(cityName);
JSONArray rawCoords = data.getJSONArray("features")
JSONArray rawCoords;
try {
rawCoords = data.getJSONArray("features")
.getJSONObject(0)
.getJSONObject("geometry")
.getJSONArray("coordinates");
} catch (JSONException e) {
throw new IOException();
}
final float lon = rawCoords.getFloat(0);
final float lat = rawCoords.getFloat(1);
@ -54,10 +53,14 @@ class City {
return new Coords(lat, lon);
}
City (String cityName) {
public City (String cityName) {
this.cityName = cityName;
}
/**
* Get city name
* @return city name string
*/
public String getCityName() {
return cityName;
}
@ -80,7 +83,7 @@ class City {
public String toString() {
try {
Coords coords = this.getCityCoords();
return String.format(
return String.format(Locale.ENGLISH,
"City(%s, lat: %f, lon: %f)",
this.cityName,
coords.getLat(),
@ -88,7 +91,7 @@ class City {
);
}
catch (IOException e) {
return String.format(
return String.format(Locale.ENGLISH,
"City(%s, lat: Request failed, lon: Request Failed)",
this.cityName
);

View File

@ -0,0 +1,51 @@
package eirb.pg203.weather.utils;
/**
* Coordinates representation
*/
public class Coords {
private float lat;
private float lon;
/**
* Coordinates representation
* @param lat latitude
* @param lon longitude
*/
public Coords(float lat, float lon) {
this.lat = lat;
this.lon = lon;
}
/**
* Get latitude
* @return latitude
*/
public float getLat() {
return lat;
}
/**
* Get longitude
* @return longitude
*/
public float getLon() {
return lon;
}
/**
* Set latitude
* @param lat latitude
*/
public void setLat(float lat) {
this.lat = lat;
}
/**
* Set longitude
* @param lon longitude
*/
public void setLon(float lon) {
this.lon = lon;
}
}

View File

@ -1,4 +1,4 @@
package eirb.pg203.utils;
package eirb.pg203.weather.utils;
import java.io.BufferedReader;
import java.io.IOException;
@ -9,8 +9,21 @@ import java.net.URL;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Util for http calls
*/
public class JSONFetcher {
/**
* No need for constructor
*/
public JSONFetcher() {};
/**
* Make the request
* @param url url to fetch
* @return String of the response
* @throws IOException if the request failed
*/
private static String fetchString(URL url) throws IOException{
System.err.println("Requesting " + url);
StringBuilder result = new StringBuilder();
@ -22,20 +35,19 @@ public class JSONFetcher {
result.append(line);
}
}
System.out.println(url);
return result.toString();
}
public static JSONObject fetch(URL url) throws IOException {
/**
* Fetch an url
* @param url url
* @return Json object of the response
* @throws IOException if the request failed
*/
public JSONObject fetch(URL url) throws IOException {
String result = fetchString(url);
return new JSONObject(result);
}
public static JSONArray fetchArray(URL url) throws IOException {
String result = fetchString(url);
return new JSONArray(result);
}
}

View File

@ -1,11 +0,0 @@
package eirb.pg203;
import org.json.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class SampleTest {
}

View File

@ -1,71 +0,0 @@
package eirb.pg203;
import org.json.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class WeatherAPITest {
private static String APIKey = "cef8e1b6ea364994b5072423240111";
@Test
public void testRightAPIKey() {
WeatherAPI weatherAPI = new WeatherAPI(WeatherAPITest.APIKey);
int day = 0;
// int hour = 10;
int days = 7;
String city = "Bordeaux";
Assertions.assertAll(
() -> weatherAPI.getTemperature(day, city),
// () -> weatherAPI.getTemperature(day, hour, city),
() -> weatherAPI.getTemperatures(days, city)
);
}
@Test
public void testWrongAPIKey() {
WeatherAPI weatherAPI = new WeatherAPI("");
int day = 0;
// int hour = 10;
int days = 7;
String city = "Bordeaux";
Assertions.assertThrows(IOException.class, () -> weatherAPI.getTemperature(day, city));
Assertions.assertThrows(IOException.class, () -> weatherAPI.getTemperatures(days, city));
}
@Test
public void testWrongDay() {
WeatherAPI weatherAPI = new WeatherAPI(WeatherAPITest.APIKey);
String city = "Bordeaux";
Assertions.assertThrows(IOException.class, () -> weatherAPI.getTemperature(-1, city));
Assertions.assertThrows(IOException.class, () -> weatherAPI.getTemperature(15, city));
Assertions.assertThrows(IOException.class, () -> weatherAPI.getTemperatures(15, city));
Assertions.assertThrows(IOException.class, () -> weatherAPI.getTemperatures(-1, city));
}
@Test
public void testRightDay() {
WeatherAPI weatherAPI = new WeatherAPI(WeatherAPITest.APIKey);
String city = "Bordeaux";
Assertions.assertAll(
() -> weatherAPI.getTemperature(0, city),
() -> weatherAPI.getTemperature(5, city),
() -> weatherAPI.getTemperature(14, city),
() -> weatherAPI.getTemperatures(0, city),
() -> weatherAPI.getTemperatures(8, city),
() -> weatherAPI.getTemperatures(14, city)
);
}
@Test
public void testGetAPIName() {
WeatherAPI weatherAPI = new WeatherAPI(WeatherAPITest.APIKey);
Assertions.assertTrue(weatherAPI.getAPIName().equals("WeatherAPI"));
}
}

View File

@ -0,0 +1,84 @@
package eirb.pg203.weather;
import eirb.pg203.weather.display.WeatherDisplayBasic;
import eirb.pg203.weather.data.api.OpenMeteo;
import eirb.pg203.weather.data.api.OpenWeatherMap;
import eirb.pg203.weather.data.api.WeatherAPI;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.stream.Stream;
import static eirb.pg203.weather.data.api.WeatherDataAPITest.*;
public class WeatherDisplayBasicTest {
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
private final PrintStream originalOut = System.out;
private final PrintStream originalErr = System.err;
@BeforeEach
public void setUpStreams() {
System.setOut(new PrintStream(outContent));
System.setErr(new PrintStream(errContent));
}
@AfterEach
public void restoreStreams() {
System.setOut(originalOut);
System.setErr(originalErr);
}
private WeatherDisplayBasic weatherDisplayBasic;
private WeatherDisplayBasic setWeatherDisplayBasic() {
/* Fake apis */
OpenMeteo openMeteo = openMeteo();
OpenWeatherMap openWeatherMap = openWeatherMap();
WeatherAPI weatherAPI = weatherAPI();
WeatherDisplayBasic weatherDisplay = new WeatherDisplayBasic();
weatherDisplay.addAPI(openMeteo);
weatherDisplay.addAPI(openWeatherMap);
weatherDisplay.addAPI(weatherAPI);
return weatherDisplay;
}
private static Stream<Arguments> display() {
return Stream.of(
Arguments.arguments(1, new StringBuilder()
.append("Source J + 0 \n")
.append("------------------------------+-----------------------------+\n")
.append("OpenMeteo | 07.60° 🌤 17.60km/h 151.00° 🡮| \n")
.append("------------------------------+-----------------------------+\n")
.append("WeatherAPI | 08.10° 🌤 17.45km/h 142.08° 🡮| \n")
.append("------------------------------+-----------------------------+\n")
.append("OpenWeatherMap | 13.41° 🌤 05.74km/h 142.13° 🡮| \n")
.append("------------------------------+-----------------------------+\n")
.toString()
)
);
}
@ParameterizedTest
@MethodSource
void display(int days, String expectedDisplay) {
String city = "Bordeaux";
WeatherDisplayBasic weatherDisplayBasic = setWeatherDisplayBasic();
weatherDisplayBasic.display(days, city);
Assertions.assertEquals(expectedDisplay, outContent.toString());
}
@Test
void addAPI() {
}
}

View File

@ -0,0 +1,109 @@
package eirb.pg203.weather.data;
import eirb.pg203.weather.utils.City;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import java.time.Instant;
import java.util.stream.Stream;
public class WeatherDataTest {
private static final float epsilon = 0.01F;
private WeatherData weatherData;
@BeforeEach
public void setWeatherData() {
this.weatherData = new WeatherData(new City("f"), Instant.now(), 0f, 0, 0, WeatherData.Condition.SUNNY);
}
@Test
void city() {
String cityName = "Paris";
City city = new City(cityName);
this.weatherData.setCity(city);
/* check if the setter works */
Assertions.assertEquals(weatherData.getCity(), city);
}
@Test
void temp() {
float temp = 69.69f;
this.weatherData.setTemp(temp);
/* check if the setter works */
Assertions.assertEquals(temp, weatherData.getTemp(), epsilon);
}
@ParameterizedTest(name = "Set {0}")
@EnumSource
void condition(WeatherData.Condition condition) {
this.weatherData.setCondition(condition);
/* check if the setter works */
Assertions.assertEquals(condition, weatherData.getCondition());
}
@Test
void windSpeed() {
float windSpeed = 69.69f;
this.weatherData.setWindSpeed(windSpeed);
/* check if the setter works */
Assertions.assertEquals(windSpeed, weatherData.getWindSpeed());
}
@Test
void windAngle() {
float windAngle = 69.69f;
this.weatherData.setWindDirectionAngle(windAngle);
/* check if the setter works */
Assertions.assertEquals(windAngle, weatherData.getWindDirectionAngle());
}
@Test
void date() {
Instant instant = Instant.now();
this.weatherData.setDate(instant);
/* check if the setter works */
Assertions.assertEquals(instant, weatherData.getDate());
}
@Test
void stringRepresentation() {
this.weatherData = new WeatherData(new City("f"), Instant.now(), 0f, 0, 0, WeatherData.Condition.SUNNY);
Assertions.assertEquals("00.00° ☀️ 00.00km/h 000.00° 🡩", weatherData.toString());
}
private static Stream<Arguments> windDirectionRepresentation() {
return Stream.of(
Arguments.arguments(0f, WeatherData.WindDirection.N),
Arguments.arguments(45f, WeatherData.WindDirection.NE),
Arguments.arguments(90f, WeatherData.WindDirection.E),
Arguments.arguments(135f, WeatherData.WindDirection.SE),
Arguments.arguments(180f, WeatherData.WindDirection.S),
Arguments.arguments(225f, WeatherData.WindDirection.SW),
Arguments.arguments(270f, WeatherData.WindDirection.W),
Arguments.arguments(315f, WeatherData.WindDirection.NW),
Arguments.arguments(999f, WeatherData.WindDirection.ERROR)
);
}
@ParameterizedTest
@MethodSource
void windDirectionRepresentation(float windDirection, WeatherData.WindDirection expectedWindDirection){
weatherData.setWindDirectionAngle(windDirection);
Assertions.assertEquals(expectedWindDirection, weatherData.getWindDirection());
}
}

View File

@ -0,0 +1,23 @@
package eirb.pg203.weather.data.api;
import eirb.pg203.weather.fakeJSONFetcher.FakeJSONFetcherWeatherAPI;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class WeatherAPITest {
private static final String APIKey = "realKey";
private WeatherAPI weatherAPI;
@BeforeEach
public void setupWeatherApi() {
this.weatherAPI = new WeatherAPI(WeatherAPITest.APIKey);
this.weatherAPI.JSONFetcher = new FakeJSONFetcherWeatherAPI();
}
@Test
public void testGetAPIName() {
Assertions.assertEquals("WeatherAPI", weatherAPI.getAPIName());
}
}

View File

@ -0,0 +1,213 @@
package eirb.pg203.weather.data.api;
import eirb.pg203.weather.exceptions.WeatherFetchingExceptionApi;
import eirb.pg203.weather.exceptions.WeatherFetchingExceptionCityCoords;
import eirb.pg203.weather.fakeJSONFetcher.FakeJSONFetcherOpenMeteo;
import eirb.pg203.weather.fakeJSONFetcher.FakeJSONFetcherOpenWeatherMap;
import eirb.pg203.weather.fakeJSONFetcher.FakeJSONFetcherWeatherAPI;
import eirb.pg203.weather.exceptions.WeatherFetchingException;
import eirb.pg203.weather.data.WeatherData;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.stream.Stream;
public class WeatherDataAPITest {
private static final float epsilon = 0.01F;
private static final String APIKey = "realKey";
private static final String wrongAPIKey = "wrongKey";
private static WeatherAPI wrongWeatherAPI() {
WeatherAPI weatherAPI = new WeatherAPI(wrongAPIKey);
weatherAPI.JSONFetcher = new FakeJSONFetcherWeatherAPI();
return weatherAPI;
}
private static OpenWeatherMap wrongOpenWeatherMap() {
OpenWeatherMap weatherAPI = new OpenWeatherMap(wrongAPIKey);
weatherAPI.JSONFetcher = new FakeJSONFetcherOpenWeatherMap();
return weatherAPI;
}
public static WeatherAPI weatherAPI(){
WeatherAPI weatherAPI = new WeatherAPI(APIKey);
weatherAPI.JSONFetcher = new FakeJSONFetcherWeatherAPI();
return weatherAPI;
}
public static OpenWeatherMap openWeatherMap(){
// Fix clock for testing
String instantExpected = "2024-11-24T00:00:00.00Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.systemDefault());
OpenWeatherMap openWeatherMap= new OpenWeatherMap(APIKey);
openWeatherMap.JSONFetcher = new FakeJSONFetcherOpenWeatherMap();
openWeatherMap.clock = clock;
return openWeatherMap;
}
public static OpenMeteo openMeteo() {
// Fix clock for testing
String instantExpected = "2024-11-24T00:00:00.00Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.systemDefault());
OpenMeteo openMeteo = new OpenMeteo();
openMeteo.clock = clock;
openMeteo.JSONFetcher = new FakeJSONFetcherOpenMeteo();
return openMeteo;
}
/**
* List of args for Temperature testing
* @return Args for testing
*/
private static Stream<Arguments> testGetTemperature(){
return Stream.of(
/* WeatherAPI */
Arguments.arguments(weatherAPI(), 0, 8.1F, WeatherData.Condition.PARTIAL, 17.45F, 142.08F),
Arguments.arguments(weatherAPI(), 1, 13F, WeatherData.Condition.SUNNY, 23.03F, 142.58F),
Arguments.arguments(weatherAPI(), 2, 12.7F, WeatherData.Condition.RAINY, 13.19F, 222.92F),
Arguments.arguments(weatherAPI(), 3, 8.1F,WeatherData.Condition.CLOUDY, 17.45F, 142.08F),
/* Open Weather Map */
Arguments.arguments(openWeatherMap(), 0, 13.41F,WeatherData.Condition.PARTIAL, 5.74F, 142.13F),
Arguments.arguments(openWeatherMap(), 1, 13.29F,WeatherData.Condition.CLOUDY, 3.62F, 225.25F),
Arguments.arguments(openWeatherMap(), 2, 10.06F,WeatherData.Condition.RAINY, 2.22F, 191.75F),
Arguments.arguments(openWeatherMap(), 3, 9.88F,WeatherData.Condition.SUNNY, 2.00F, 160.00F),
/* Open Meteo */
Arguments.arguments(openMeteo(), 0, 7.6F,WeatherData.Condition.PARTIAL, 17.6F, 151F),
Arguments.arguments(openMeteo(), 1, 13.20F,WeatherData.Condition.CLOUDY, 20.6F, 149F),
Arguments.arguments(openMeteo(), 2, 12.3F,WeatherData.Condition.RAINY, 21.70F, 187F),
Arguments.arguments(openMeteo(), 3, 10.80F,WeatherData.Condition.SUNNY, 9.4F, 177F)
);
}
@ParameterizedTest(name="[{0}] Temperature fetch at Bordeaux D+{1}")
@MethodSource
public void testGetTemperature(WeatherDataAPI weatherDataAPI, int day, float expectedTemp, WeatherData.Condition expectedCond, float expectedWindSpeed, float expectedWindAngle) throws WeatherFetchingException {
String city = "Bordeaux";
WeatherData weatherData;
weatherData = weatherDataAPI.getTemperature(day, city);
/* Temperatures */
Assertions.assertEquals(expectedTemp, weatherData.getTemp(), epsilon);
/* Condition */
Assertions.assertEquals(expectedCond, weatherData.getCondition());
/* Wind */
Assertions.assertEquals(expectedWindSpeed, weatherData.getWindSpeed(), epsilon);
Assertions.assertEquals(expectedWindAngle, weatherData.getWindDirectionAngle(),epsilon);
}
private static Stream<Arguments> testGetTemperatures() {
float[] weatherAPIExpectedTemperatures = {8.1F, 13F, 12.7F, 8.1F};
WeatherData.Condition[] weatherAPIExpectedConditions = {WeatherData.Condition.PARTIAL, WeatherData.Condition.SUNNY, WeatherData.Condition.RAINY, WeatherData.Condition.CLOUDY};
float[] weatherAPIExpectedWindSpeed = {17.45F, 23.03F, 13.19F, 17.45F};
float[] weatherAPIExpectedWindDirection = {142.08F, 142.58F, 222.92F, 142.08F};
float[] openWeatherMapExpectedTemperatures = {13.41F, 13.29F, 10.06F, 9.88F};
WeatherData.Condition[] openWeatherMapExpectedConditions = {WeatherData.Condition.PARTIAL, WeatherData.Condition.CLOUDY, WeatherData.Condition.RAINY, WeatherData.Condition.SUNNY};
float[] openWeatherMapExpectedWindSpeed = {5.74F, 3.62F, 2.22F, 2.00F};
float[] openWeatherMapExpectedWindDirection = {142.13F, 225.25F, 191.75F, 160F};
float[] openMeteoExpectedTemperatures = {7.6F, 13.2F, 12.3F, 10.80F};
WeatherData.Condition[] openMeteoExpectedConditions = {WeatherData.Condition.PARTIAL, WeatherData.Condition.CLOUDY, WeatherData.Condition.RAINY, WeatherData.Condition.SUNNY};
float[] openMeteoExpectedWindSpeed = {17.6F, 20.6F, 21.7F, 9.40F};
float[] openMeteoExpectedWindDirection = {151.00F, 149F, 187F, 177F};
return Stream.of(
Arguments.arguments(weatherAPI(), 4, weatherAPIExpectedTemperatures, weatherAPIExpectedConditions, weatherAPIExpectedWindSpeed, weatherAPIExpectedWindDirection),
Arguments.arguments(openWeatherMap(), 4, openWeatherMapExpectedTemperatures, openWeatherMapExpectedConditions, openWeatherMapExpectedWindSpeed, openWeatherMapExpectedWindDirection),
Arguments.arguments(openMeteo(), 4, openMeteoExpectedTemperatures, openMeteoExpectedConditions, openMeteoExpectedWindSpeed, openMeteoExpectedWindDirection)
);
}
@ParameterizedTest(name = "[{0}] Fetch temperatures for {1} days")
@MethodSource
public void testGetTemperatures(WeatherDataAPI weatherDataAPI, int days, float[] expectedTemperatures, WeatherData.Condition[] expectedConditions, float[] expectedWindSpeed, float[] expectedWindDirection) throws WeatherFetchingException{
String city = "Bordeaux";
ArrayList<WeatherData> weatherDatas;
WeatherData weatherData;
weatherDatas = weatherDataAPI.getTemperatures(days, city);
for (int index = 0; index < days; index++) {
weatherData = weatherDatas.get(index);
/* Temperatures */
Assertions.assertEquals(expectedTemperatures[index], weatherData.getTemp(), epsilon);
/* Weather condition */
Assertions.assertEquals(expectedConditions[index], weatherData.getCondition());
/* Wind */
Assertions.assertEquals(expectedWindSpeed[index],weatherData.getWindSpeed(), epsilon);
Assertions.assertEquals(expectedWindDirection[index], weatherData.getWindDirectionAngle(), epsilon);
}
}
private static Stream<Arguments> testGetTemperatureByHours() {
return Stream.of(
Arguments.arguments(weatherAPI()),
Arguments.arguments(openWeatherMap()),
Arguments.arguments(openMeteo())
);
}
/**
* For coverage, not yet implemented
* @param weatherDataAPI api to test
*/
@ParameterizedTest(name = "[{0}] Get temperature for a specific hour")
@MethodSource
public void testGetTemperatureByHours(WeatherDataAPI weatherDataAPI) {
String city = "Bordeaux";
Assertions.assertAll(
() -> weatherDataAPI.getTemperature(0,1, city)
);
}
private static Stream<Arguments> testUnkowncity() {
return Stream.of(
Arguments.arguments(openWeatherMap()),
Arguments.arguments(openMeteo())
);
}
@ParameterizedTest(name = "[{0}] Get temperature from missing city")
@MethodSource
public void testUnkowncity(WeatherDataAPI weatherDataAPI) {
String unknownCity = "jlkfreajflkj";
Assertions.assertThrows(WeatherFetchingExceptionCityCoords.class,
() -> weatherDataAPI.getTemperature(3, unknownCity)
);
Assertions.assertThrows(WeatherFetchingExceptionCityCoords.class,
() -> weatherDataAPI.getTemperatures(3, unknownCity)
);
}
private static Stream<Arguments> testWrongApiKey() {
return Stream.of(
Arguments.arguments(wrongWeatherAPI()),
Arguments.arguments(wrongOpenWeatherMap())
);
}
@ParameterizedTest(name = "[{0}] Wrong API Key throws exception")
@MethodSource
public void testWrongApiKey(WeatherDataAPI weatherDataAPI) {
String city = "Bordeaux";
Assertions.assertThrows(WeatherFetchingExceptionApi.class,
() -> weatherDataAPI.getTemperatures(3, city)
);
Assertions.assertThrows(WeatherFetchingExceptionApi.class,
() -> weatherDataAPI.getTemperature(3, city)
);
}
}

View File

@ -0,0 +1,34 @@
package eirb.pg203.weather.fakeJSONFetcher;
import eirb.pg203.weather.utils.FileResourcesUtils;
import eirb.pg203.weather.utils.SplitQueryUrl;
import eirb.pg203.weather.utils.JSONFetcher;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class FakeJSONFetcherCity extends JSONFetcher{
JSONObject unknownCity = FileResourcesUtils.getFileFromResourceAsJson("City/fakeCity.json");
private static HashMap<String, JSONObject> cities(){
HashMap<String, JSONObject> cities = new HashMap<>();
cities.put("bordeaux", eirb.pg203.weather.utils.FileResourcesUtils.getFileFromResourceAsJson("City/bordeaux.json"));
cities.put("paris", eirb.pg203.weather.utils.FileResourcesUtils.getFileFromResourceAsJson("City/paris.json"));
cities.put("unknown", eirb.pg203.weather.utils.FileResourcesUtils.getFileFromResourceAsJson("City/fakeCity.json"));
return cities;
}
@Override
public JSONObject fetch(URL url) throws IOException {
Map<String, String> params = SplitQueryUrl.splitQuery(url);
String city = params.get("q").toLowerCase(Locale.ENGLISH);
return cities().getOrDefault(city, unknownCity);
}
}

View File

@ -0,0 +1,21 @@
package eirb.pg203.weather.fakeJSONFetcher;
import eirb.pg203.weather.utils.FileResourcesUtils;
import eirb.pg203.weather.utils.JSONFetcher;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URL;
public class FakeJSONFetcherOpenMeteo extends JSONFetcher {
private JSONObject responseExample() {
return FileResourcesUtils.getFileFromResourceAsJson("OpenMeteo/Bordeaux-partial-cloudy-rain-sunny.json");
}
@Override
public JSONObject fetch(URL url) throws IOException {
return responseExample();
}
}

View File

@ -0,0 +1,27 @@
package eirb.pg203.weather.fakeJSONFetcher;
import eirb.pg203.weather.utils.FileResourcesUtils;
import eirb.pg203.weather.utils.SplitQueryUrl;
import eirb.pg203.weather.utils.JSONFetcher;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
public class FakeJSONFetcherOpenWeatherMap extends JSONFetcher {
private final String apiKey = "realKey";
private final JSONObject wrongKeyResponse = FileResourcesUtils.getFileFromResourceAsJson("OpenWeatherMap/wrong-apikey.json");
private JSONObject responseExample() {
return eirb.pg203.weather.utils.FileResourcesUtils.getFileFromResourceAsJson("OpenWeatherMap/Bordeaux-partial-cloudy-rain-sunny.json");
}
@Override
public JSONObject fetch(URL url) throws IOException {
Map<String, String> params = SplitQueryUrl.splitQuery(url);
if (!params.getOrDefault("appid", "").contentEquals(apiKey))
throw new IOException();
return responseExample();
}
}

View File

@ -0,0 +1,37 @@
package eirb.pg203.weather.fakeJSONFetcher;
import eirb.pg203.weather.utils.FileResourcesUtils;
import eirb.pg203.weather.utils.SplitQueryUrl;
import eirb.pg203.weather.utils.JSONFetcher;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Map;
public class FakeJSONFetcherWeatherAPI extends JSONFetcher {
private final static String baseUrlFormat = "https://api.weatherapi.com/v1/forecast.json?key=%s&q=%s&days=%d";
private final String apiKey = "realKey";
private static final JSONObject wrongKeyRequest = FileResourcesUtils.getFileFromResourceAsJson("WeatherAPI/wrong-apikey.json");
private static ArrayList<JSONObject> bordeauxRequests() {
ArrayList<JSONObject> bordeauxRequest = new ArrayList<>();
bordeauxRequest.add(eirb.pg203.weather.utils.FileResourcesUtils.getFileFromResourceAsJson("WeatherAPI/Bordeaux-1-partial.json"));
bordeauxRequest.add(eirb.pg203.weather.utils.FileResourcesUtils.getFileFromResourceAsJson("WeatherAPI/Bordeaux-2-partial-sunny.json"));
bordeauxRequest.add(eirb.pg203.weather.utils.FileResourcesUtils.getFileFromResourceAsJson("WeatherAPI/Bordeaux-3-partial-sunny-rain.json"));
bordeauxRequest.add(eirb.pg203.weather.utils.FileResourcesUtils.getFileFromResourceAsJson("WeatherAPI/Bordeaux-4-partial-sunny-rain-cloudy.json"));
return bordeauxRequest;
}
@Override
public JSONObject fetch(URL url) throws IOException {
Map<String, String> params = SplitQueryUrl.splitQuery(url);
int days = Integer.parseInt(params.get("days"));
if (!params.get("key").contentEquals(apiKey))
throw new IOException();
return bordeauxRequests().get(days - 1);
}
}

View File

@ -0,0 +1,50 @@
package eirb.pg203.weather.utils;
import eirb.pg203.weather.fakeJSONFetcher.FakeJSONFetcherCity;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.io.IOException;
public class CityTest {
private final static float epsilon = 0.001F;
@ParameterizedTest(name = "{0} is located at [lat:{1}, lon:{2}]")
@CsvSource({
"Paris,48.859F,2.347F,'City(Paris, lat: 48.859001, lon: 2.347000)'",
"Bordeaux,44.851895F,-0.587877F,'City(Bordeaux, lat: 44.851894, lon: -0.587877)'"
})
void testRealCity(String cityName, float expectedLat, float expectedLon, String expectedString) throws IOException {
City city = new City(cityName);
city.JSONFetcher = new FakeJSONFetcherCity();
/* Name coherence */
Assertions.assertEquals(cityName, city.getCityName());
/* Localisation */
Coords coords = city.getCityCoords();
float lat = coords.getLat();
float lon = coords.getLon();
Assertions.assertTrue(Math.abs(lat - expectedLat) < epsilon);
Assertions.assertTrue(Math.abs(lon - expectedLon) < epsilon);
/* String representation */
Assertions.assertEquals(expectedString, city.toString());
}
@Test
void testFakeCity(){
String fakeCity = "farlmjmjfkl";
String fakeCityRepresentation = "City("+ fakeCity +", lat: Request failed, lon: Request Failed)";
City city = new City(fakeCity);
/* String representation */
Assertions.assertEquals(fakeCityRepresentation, city.toString());
/* Throw exception */
Assertions.assertThrows(IOException.class, city::getCityCoords);
}
}

View File

@ -0,0 +1,53 @@
package eirb.pg203.weather.utils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CoordsTest {
private static final float epsilon = 0.01F;
@Test
void getLat() {
float lat = 1f;
float lon = 2f;
Coords coords = new Coords(lat, lon);
assertEquals(lat, coords.getLat(), epsilon);
}
@Test
void getLon() {
float lat = 1f;
float lon = 2f;
Coords coords = new Coords(lat, lon);
assertEquals(lon, coords.getLon(), epsilon);
}
@Test
void setLat() {
float lat = 1f;
float lon = 2f;
Coords coords = new Coords(lat, lon);
float sndLat = 3f;
coords.setLat(sndLat);
assertEquals(sndLat, coords.getLat(), epsilon);
}
@Test
void setLon() {
float lat = 1f;
float lon = 2f;
Coords coords = new Coords(lat, lon);
float sndLon = 4f;
coords.setLon(sndLon);
assertEquals(sndLon, coords.getLon(), epsilon);
}
}

View File

@ -0,0 +1,53 @@
package eirb.pg203.weather.utils;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class FileResourcesUtils {
/**
* Fetch ressource file
* Code from https://mkyong.com
* @param fileName
* @return
*/
public static InputStream getFileFromResourceAsStream(String fileName) {
// The class loader that loaded the class
ClassLoader classLoader = FileResourcesUtils.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream(fileName);
// the stream holding the file content
if (inputStream == null) {
throw new IllegalArgumentException("file not found! " + fileName);
} else {
return inputStream;
}
}
public static JSONObject getFileFromResourceAsJson(String fileName) {
InputStream inputStream = getFileFromResourceAsStream(fileName);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder stringBuilder = new StringBuilder();
String line;
while(true){
try {
if (!bufferedReader.ready()) break;
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
line = bufferedReader.readLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
stringBuilder.append(line);
}
return new JSONObject(stringBuilder.toString());
}
}

View File

@ -0,0 +1,20 @@
package eirb.pg203.weather.utils;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.LinkedHashMap;
import java.util.Map;
public class SplitQueryUrl {
public static Map<String, String> splitQuery(URL url) throws UnsupportedEncodingException {
Map<String, String> query_pairs = new LinkedHashMap<String, String>();
String query = url.getQuery();
String[] pairs = query.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
query_pairs.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
}
return query_pairs;
}
}

View File

@ -0,0 +1,37 @@
{
"type": "FeatureCollection",
"version": "draft",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-0.587877,
44.851895
]
},
"properties": {
"label": "Bordeaux",
"score": 0.9608036363636362,
"id": "33063",
"banId": "f38fe08f-c87b-4b3f-8f05-1566b8da9c39",
"type": "municipality",
"name": "Bordeaux",
"postcode": "33000",
"citycode": "33063",
"x": 416627.63,
"y": 6423408.37,
"population": 261804,
"city": "Bordeaux",
"context": "33, Gironde, Nouvelle-Aquitaine",
"importance": 0.56884,
"municipality": "Bordeaux"
}
}
],
"attribution": "BAN",
"licence": "ETALAB-2.0",
"query": "Bordeaux",
"limit": 1
}

View File

@ -0,0 +1,9 @@
{
"type": "FeatureCollection",
"version": "draft",
"features": [],
"attribution": "BAN",
"licence": "ETALAB-2.0",
"query": "farmjfakj",
"limit": 1
}

View File

@ -0,0 +1,36 @@
{
"type": "FeatureCollection",
"version": "draft",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
2.347,
48.859
]
},
"properties": {
"label": "Paris",
"score": 0.9703381818181818,
"id": "75056",
"type": "municipality",
"name": "Paris",
"postcode": "75001",
"citycode": "75056",
"x": 652089.7,
"y": 6862305.26,
"population": 2133111,
"city": "Paris",
"context": "75, Paris, Île-de-France",
"importance": 0.67372,
"municipality": "Paris"
}
}
],
"attribution": "BAN",
"licence": "ETALAB-2.0",
"query": "Paris",
"limit": 1
}

View File

@ -0,0 +1,55 @@
{
"latitude": 44.84,
"longitude": -0.58000016,
"generationtime_ms": 0.1360177993774414,
"utc_offset_seconds": 0,
"timezone": "GMT",
"timezone_abbreviation": "GMT",
"elevation": 15,
"daily_units": {
"time": "iso8601",
"weather_code": "wmo code",
"temperature_2m_max": "°C",
"temperature_2m_min": "°C",
"wind_speed_10m_max": "km/h",
"wind_direction_10m_dominant": "°"
},
"daily": {
"time": [
"2024-11-23",
"2024-11-24",
"2024-11-25",
"2024-11-26"
],
"weather_code": [
2,
3,
63,
0
],
"temperature_2m_max": [
12.3,
17.2,
15.4,
13.7
],
"temperature_2m_min": [
2.9,
9.2,
9.2,
7.9
],
"wind_speed_10m_max": [
17.6,
20.6,
21.7,
9.4
],
"wind_direction_10m_dominant": [
151,
149,
187,
177
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
{
"cod": 401,
"message": "Invalid API key. Please see https://openweathermap.org/faq#error401 for more info."
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
{
"error": {
"code": 2008,
"message": "API key has been disabled."
}
}

BIN
weather-app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB