// © 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 ListIl 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* 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 "
Infine per alcune chiavi JSON la logica di estrazione e formattazione * è specializzata:
** 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: *hours[0]: ora corrente come intero nell'intervallo 0–23;hours[1]: ora dell'alba come intero nell'intervallo 0–23;hours[2]: ora del tramonto come intero nell'intervallo 0–23.* 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: *
*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 è:
*