// © 2025 Alessio Severi — vedi licenza nel file Main.java package weather; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URI; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * Classe principale dell'applicazione console «Weather Station». * *

Interroga il servizio open-meteo.com per un capoluogo * di regione italiano, estrae un sottoinsieme di misure meteorologiche * correnti e giornaliere e produce un report testuale formattato.

* *

Incapsula tre responsabilità principali:

* * *

Inoltre implementa l'interfaccia {@link Coordinates}, che fornisce * l'enum {@link Coordinates.ItalianCapital} con l'elenco dei capoluoghi * supportati e le relative coordinate geografiche.

* *

L'oggetto è pensato come "stazione" locale: una volta creato con una * città valida, fornisce il metodo {@link #buildReport()} che restituisce * il report meteorologico.

*/ public class WeatherStation implements Coordinates { /** * Capoluogo regionale selezionato dall'utente. *

Contiene nome della città, regione e coordinate geografiche * in formato decimale.

*/ private ItalianCapital CITY; /** * Lista ordinata di righe testuali già formattate. *

Ogni elemento rappresenta una sezione del report * (data/ora, temperatura, vento, ecc.).

*/ private final List dataList = new ArrayList<>(); /** * Crea una nuova stazione meteo per il capoluogo indicato. * *

Il nome della città viene normalizzato (elimina spazi e apici, * convertendolo poi in maiuscolo) e confrontato con le costanti * dell'enumerazione {@link ItalianCapital}.

* *

Se non esiste nessuna corrispondenza, il costruttore segnala * l'errore a console e termina il programma con codice di uscita * diverso da zero.

* * @param cityName nome del capoluogo regionale italiano inserito * dall'utente (es. "Roma", "Milano", "Cagliari"). */ public WeatherStation(String city) { city = city.trim().replace("'", "").toUpperCase(); for(ItalianCapital c : ItalianCapital.values()){ if(c.name().equals(city)) CITY = c; } if (CITY == null) { System.out.println("Unsupported city: " + city.toLowerCase()); System.exit(1); } } /** * Restituisce una copia non modificabile dei dati meteo * attualmente memorizzati in {@link #dataList}. * *

* La lista contiene tutti i campi già estratti e formattati dal JSON * (data/ora, orario dell'alba, orario del tramonto, temperature, vento, ecc.), * nello stesso ordine utilizzato da {@link #formatReport()}. *

* *

* La lista restituita è indipendente dalla lista interna: chi la riceve * non può aggiungere, rimuovere, sostituire né riordinare elementi * (ogni tentativo di modifica solleva {@link UnsupportedOperationException}) * e quindi non può alterare lo stato interno di {@code WeatherStation}. *

* * @return una lista non modificabile con i dati meteo formattati. */ public List getDataList() { return List.copyOf(dataList); } /** * Stabilisce una connessione HTTP con il servizio open-meteo e * recupera i dati meteo in formato JSON per la città selezionata. * *

* L'URL, comprensivo di query string, viene costruito a partire dalle coordinate associate al * {@link ItalianCapital} corrente e include: *

* * * *

* Il metodo apre una connessione HTTP di tipo GET, controlla il codice * di risposta e, in caso di esito positivo, acquisisce il corpo della risposta * in un {@link StringBuilder}. *

* *

* In caso di risposta HTTP diversa da {@code 200: OK} oppure se si * verifica un problema di I/O, il metodo stampa un messaggio di * errore e termina il programma con codice di uscita {@code 1}. *

* * @return il contenuto della risposta JSON come {@code StringBuilder} */ private StringBuilder connect(){ StringBuilder response = new StringBuilder(); String urlString = "https://api.open-meteo.com/v1/forecast" + "?latitude=" + CITY.getLatitude() + "&longitude=" + CITY.getLongitude() + "&timezone=Europe%2FRome" + "&daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,uv_index_max" + "¤t=temperature_2m,apparent_temperature," + "dewpoint_2m,surface_pressure,relative_humidity_2m," + "wind_speed_10m,wind_direction_10m,weather_code," + "cloudcover,visibility,precipitation,uv_index,snowfall"; try { URI uri = URI.create(urlString); // valida la stringa come URI HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); conn.setRequestMethod("GET"); int status = conn.getResponseCode(); if (status != HttpURLConnection.HTTP_OK) { System.out.println("HTTP Error: " + status); System.exit(1); } // Leggo il JSON come pura stringa try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { String line; while ((line = in.readLine()) != null) { response.append(line); } conn.disconnect(); } } catch (IOException e) { System.out.println("I/O Error: " + e.getMessage()); System.exit(1); } return response; } /** * Popola {@link #dataList} con tutti i dati necessari alla generazione * del bollettino meteo. * *

In particolare il metodo:

* * *

* Al termine dell'esecuzione, {@code dataList} contiene tutte le voci * in un ordine ben definito, esattamente quello atteso da {@link #formatReport()}, * che si occupa di restituire il bollettino meteo formattato. *

* * @param response contenuto JSON restituito dall'API, rappresentato * tramite un {@link StringBuilder}. */ private void buildDataList(StringBuilder response){ int index = response.indexOf("\"daily_units\""); String jsonCurrent = response.toString().substring(0, index); String jsonDaily = response.toString().substring(index); String marker = "\"time\":"; int hour24 = formatDateTime((extractData(marker, "time", jsonCurrent))); marker = "\"sunrise\":"; int sunriseHour24 = formatDailyTime(extractData(marker, "sunrise", jsonDaily), "sunrise"); marker = "\"sunset\":"; int sunsetHour24 = formatDailyTime(extractData(marker, "sunset", jsonDaily), "sunset"); marker = "\"temperature_2m_max\":"; dataList.add(extractData(marker, "Max temp", jsonDaily)); marker = "\"temperature_2m_min\":"; dataList.add(extractData(marker, "Min temp", jsonDaily)); marker = "\"weather_code\":"; dataList.add(extractData(marker, "weather", jsonCurrent, hour24, sunriseHour24, sunsetHour24)); marker = "\"temperature_2m\":"; dataList.add(extractData(marker, "temperature", jsonCurrent)); marker = "\"apparent_temperature\":"; dataList.add(extractData(marker, "apparent temperature", jsonCurrent)); marker = "\"surface_pressure\":"; dataList.add(extractData(marker, "surface pressure", jsonCurrent)); marker = "\"relative_humidity_2m\":"; dataList.add(extractData(marker, "relative humidity", jsonCurrent)); marker = "\"wind_speed_10m\":"; dataList.add(extractData(marker, "wind speed", jsonCurrent)); marker = "\"wind_direction_10m\":"; dataList.add(extractData(marker, "wind direction", jsonCurrent)); marker = "\"visibility\":"; dataList.add(extractData(marker, "visibility", jsonCurrent)); marker = "\"cloudcover\":"; dataList.add(extractData(marker, "cloudcover", jsonCurrent)); marker = "\"dewpoint_2m\":"; dataList.add(extractData(marker, "dew point", jsonCurrent)); marker = "\"precipitation\":"; dataList.add(extractData(marker, "precipitation", jsonCurrent)); marker = "\"uv_index\":"; dataList.add(extractData(marker, "UV index", jsonCurrent)); } /** * Estrae e formatta un singolo valore dalla sezione di risposta JSON corrispondente, * aggiornando contestualmente {@link #dataList}. * *

* La ricerca avviene tramite il marcatore testuale {@code marker} * (ad esempio {@code "\"temperature_2m\":"}) e utilizza indici di * sottostringa sulla sezione di risposta JSON per individuare il * valore compreso tra il marcatore e la virgola successiva. *

* *

* Nel caso più semplice il metodo costruisce e restituisce una stringa del tipo: *

*
     *     Temperature:   11.4°C
     * 
*

* Per grandezze come temperatura, umidità e direzione del vento le unità di * misura (ad esempio {@code °C}, {@code %} e {@code °}) vengono accodate * al valore senza spazi superflui, in modo da mantenere una resa visiva uniforme. *

*

* Per le altre grandezze numeriche rimanenti (pressione, intensità del vento, visibilità, * copertura nuvolosa, punto di rugiada, precipitazioni, ecc.), * il metodo produce una riga etichettata nel formato * {@code ": valore [unità]"}. *

* *

Infine per alcune chiavi JSON la logica di estrazione e formattazione * è specializzata:

* * * @param marker marcatore JSON da cercare (chiave del campo). * @param msg etichetta testuale da associare al valore estratto * (ad esempio {@code "Temperature"}, {@code "Wind speed"}). * @param json porzione di risposta JSON in cui effettuare la ricerca * (ad esempio {@code jsonCurrent} o {@code jsonDaily}). * @param hours eventuali ore di contesto (ora corrente, ora dell'alba, * ora del tramonto) utilizzate per distinguere scenario * diurno/notturno nella descrizione del codice meteo; * può essere omesso per i campi che non ne fanno uso. * * @return stringa formattata corrispondente al campo estratto; in caso * di marcatore non trovato, una stringa di errore descrittiva * (ad esempio {@code "Field 'UV index' not found in JSON."}). */ private String extractData(String marker, String msg, String json, int... hours){ int idx; String value = ""; String tempStr; int stop = 2; OUTER: for (int i = 0; i < stop; i++) { if(i==0) idx = json.lastIndexOf(marker); else idx = json.indexOf(marker); if (idx != -1) { int start = idx + marker.length(); int end = json.indexOf(",", start); // fine del numero tempStr = json.substring(start, end); if(i==0) value = Character.toUpperCase(msg.charAt(0)) + msg.substring(1) + ": " + tempStr; else { if(tempStr.charAt(1) != '°' && tempStr.charAt(1) != '%') value = value + " "; value = value + tempStr.substring(1, tempStr.length() - 1) + "\000"; } } else return "Field '" + msg + "' not found in JSON."; if (i==0) switch (marker) { case "\"time\":" : marker = "\"timezone_abbreviation\":"; break; case "\"weather_code\":" : value = value.replace(tempStr, describeWeatherCode(Integer.parseInt(tempStr), hours)); break OUTER; case "\"sunrise\":", "\"sunset\":" : i++; value = value.replace("\"", ""); case "\"temperature_2m_max\":", "\"temperature_2m_min\":" : value = value.replace("[", ""); } else if(marker.equals("\"timezone_abbreviation\":")) { value = value.replace("\"", "") .replace("\000", "") .replace("Time: ", ""); char c = value.charAt(value.length() - 1); value = value.replace(" GMT+" + c , ":00+0" + c + ":00"); marker = "\"timezone\":"; stop = 3; } } return value; } /** * Traduce il codice meteo WMO in una descrizione testuale arricchita * da emoji. * *

* In base all'ora corrente rispetto all'orario dell'alba e del tramonto, locali, * il metodo distingue tra contesto "diurno" e "notturno" e seleziona di * conseguenza le emoji correlate (sole, luna, nuvole con sole, ecc.). *

* * @param code codice meteo WMO restituito da open-meteo. * @param hours array di tre elementi che rappresentano, nell'ordine: * * @return descrizione del fenomeno, con emoji coerente con la fascia * oraria e le condizioni meteo (ad esempio {@code "🌤 Mainly clear"}). */ private String describeWeatherCode(int code, int[] hours) { boolean moon = !(hours[0] >= hours[1] && hours[0] < hours[2]); return switch (code) { case 0 -> ((moon) ? "🌙":"☀️") + " Clear sky"; case 1 -> ((moon) ? "🌑☁️":"🌤") + " Mainly clear"; case 2 -> ((moon) ? "🌑💨":"⛅") + " Partly cloudy"; case 3 -> "☁️ Overcast"; case 45 -> "🌫️ Fog"; case 48 -> "🌫️ Depositing rime fog"; case 51 -> ((moon) ? "🌑🌧️":"🌦️") + " Light drizzle"; case 53 -> ((moon) ? "🌑🌧️":"🌦️") + " Moderate drizzle"; case 55 -> ((moon) ? "🌑🌧️":"🌦️") + " Dense drizzle"; case 56 -> ((moon) ? "🌑🌧️":"🌦") + " ❄️ Light freezing drizzle"; case 57 -> ((moon) ? "🌑🌧️":"🌦") + " ❄️ Dense freezing drizzle"; case 61 -> "🌧️ Slight rain"; case 63 -> "🌧️ Moderate rain"; case 65 -> "🌧️ Heavy rain"; case 66 -> "🌧️ ❄️ Light freezing rain"; case 67 -> "🌧️ ❄️ Heavy freezing rain"; case 71 -> "🌨 Slight snowfall"; case 73 -> "🌨 Moderate snowfall"; case 75 -> "🌨 Heavy snowfall"; case 77 -> "🌨 Snow grains"; case 80 -> "🌧️ Slight rain showers"; case 81 -> "🌧️ Moderate rain showers"; case 82 -> "🌧️ Violent rain showers"; case 85 -> "🌨 Slight snow showers"; case 86 -> "🌨 Heavy snow showers"; case 95 -> "⛈️ Thunderstorm"; case 96 -> "⛈️ ❄️ Thunderstorm with slight hail"; case 99 -> "⛈️ ❄️ Thunderstorm with heavy hail"; default -> "Unknown weather code: " + code; }; } /** * Converte la data/ora ISO-8601 della risposta API in una rappresentazione * leggibile per l'utente. * *

* La stringa in ingresso è in un formato compatibile con * {@link java.time.ZonedDateTime#parse(CharSequence)} e contiene * data, ora, offset e ID di fuso orario, ad esempio: *

* *
     *     2026-01-22T12:30:00+01:00[Europe/Rome]
     * 
* *

* Il metodo costruisce una rappresentazione del tipo: *

* *
     *     Thursday 22 January 2026  12:30  (GMT+1, Europe/Rome)
     * 
* * La stringa formattata viene inserita come primo elemento * della {@link #dataList}. Inoltre il metodo restituisce l'ora * corrente come intero 0–23, utile per distinguere giorno/notte. *

* * @param dateTime data/ora in formato ISO-8601 compatibile con * {@code ZonedDateTime}, derivata dal JSON. * @return ora corrente 0–23 nel fuso orario locale. */ private int formatDateTime(String dateTime){ dateTime = dateTime.replace("\000", "]") .replace(" ", "["); ZonedDateTime zone = ZonedDateTime.parse(dateTime); DateTimeFormatter format = DateTimeFormatter.ofPattern("EEEE dd MMMM YYYY HH:mm (O, VV)", Locale.ENGLISH); String st = zone.format(format); dataList.add(st.replace(st.charAt(0), Character.toUpperCase(st.charAt(0)))); return Integer.parseInt(zone.format(DateTimeFormatter.ofPattern("HH"))); } /** * Estrae e formatta l'orario giornaliero (ad esempio dell'alba o del tramonto) * e ne restituisce l'ora come intero. * *

* Il parametro {@code dailyTime} è una stringa composta da una * etichetta testuale e da una data/ora separate da tre spazi, * ad esempio: *

* *
     *     "Sunrise:   2026-01-22T07:31"
     * 
* *

* La parte di data/ora non include i secondi; il metodo aggiunge * {@code ":00"} per ottenere una stringa compatibile con * {@link java.time.LocalDateTime#parse(CharSequence)} e quindi * in formato ISO-8601 completo. *

* *

* In dettaglio, il metodo: *

* * * @param dailyTime stringa contenente etichetta e data/ora separate * da tre spazi; la parte di data/ora viene completata * con i secondi per diventare ISO-8601 compatibile. * @param dailyText descrittore logico dell'evento (ad es. {@code "sunrise"} * o {@code "sunset"}); viene usato come informazione * semantica per distinguere le due tipologie di orario. * @return ora dell'evento 0–23. */ private int formatDailyTime(String dailyTime, String dailyText){ String[] array = dailyTime.split(" "); LocalDateTime time = LocalDateTime.parse(array[1] + ":00"); String hours = time.format(DateTimeFormatter.ofPattern("HH:mm")); dataList.add(array[0] + " " + hours); if(dailyText.equals("sunrise")) return Integer.parseInt(time.format(DateTimeFormatter.ofPattern("HH"))); else return Integer.parseInt(time.format(DateTimeFormatter.ofPattern("HH"))); } /** * Costruisce il report testuale completo, arricchito da emoji, * a partire dai dati presenti in {@link #dataList}. * *

Il report comprende:

* * *

* Il metodo assume che {@link #dataList} sia già stata popolata in modo * coerente da {@code getDataList}, con gli elementi nelle posizioni * previste dalla formattazione. *

* * @return stringa contenente il bollettino meteo formattato, * pronta per la stampa in console. */ public String formatReport(){ String header = "\n\n------------------ WEATHER STATION ------------------\n\n"; String dataDateTime = dataList.get(0); String info = CITY.getCityName() + ", " + CITY.getRegionName() + " (IT) 🌍\n\n" + dataDateTime; String formattedReport = info + "\n\n\n\n" + dataList.get(5) + "\n\n" + dataList.get(1) + " • " + dataList.get(2) + "\n\n" + dataList.get(3) + " • " + dataList.get(4) + "\n\n\n" + dataList.get(6).replace(" ", " 🌡️ ") + " " + "(a"+ dataList.get(7) .replace(" temperature: ","") .substring(1) + ")" + "\n\n" + dataList.get(8).replace(" ", " 🌡️ ") + "\n\n" + dataList.get(9).replace(" ", " 🌡️ ") + "\n\n" + dataList.get(10).replace(" speed", "") .replace(" ", " 🌬️ ") + dataList.get(11) .replace("Wind direction: ", " from ") + "\n\n\n" + dataList.get(12) + " • " + dataList.get(13) + "\n\n" + dataList.get(14) + " • " + dataList.get(15) + "\n\n\n" + dataList.get(16).replace(" ", " 🔆 ") + "\n\n"; String footer = "-----------------------------------------------------\n\n"; return header + formattedReport + footer; } /** * Metodo di alto livello che, con un'unica chiamata, * restituisce il bollettino meteo formattato. * *

La sequenza eseguita è:

*
    *
  1. recupero della risposta JSON tramite {@link #connect()};
  2. *
  3. riempie {@link #dataList} mediante {@link #buildDataList(StringBuilder)};
  4. *
  5. generazione del report finale tramite {@link #formatReport()}.
  6. *
* * @return stringa contenente il report meteorologico completo. */ public String buildReport() { StringBuilder response = connect(); buildDataList(response); return formatReport(); } }