Programmare in java: 6. Weather Station

Programmare in java: 6. Weather Station

Introduzione

    Nella storia dell’umanità, il cielo è stato il primo “schermo” di dati. Prima ancora di termometri e radar, i contadini osservavano il colore delle nuvole, la direzione del vento, l’odore dell’aria prima di un temporale. Da quei segni traevano decisioni concrete: seminare, raccogliere, mettersi in viaggio oppure restare al riparo. Prevedere il tempo non era un lusso, ma una forma di sopravvivenza.

Con l’Ottocento arrivarono i primi strumenti sistematici: barometri, pluviometri, termometri disposti in una piccola capanna bianca di legno. Nacque così la “stazione meteo”: un punto fisso dove il cielo si traduceva in numeri, orari, tabelle. Collegando fra loro più stazioni, i meteorologi iniziarono a tracciare mappe di pressione e fronti, trasformando l’intuizione in scienza.

Nel Novecento la rete si allargò: palloni sonda, boe oceaniche, satelliti geostazionari che osservano la Terra dall’alto. Oggi un singolo bollettino contiene il risultato di migliaia di sensori distribuiti in tutto il pianeta. Eppure, dietro quella complessità, resta la stessa domanda di sempre: qual è lo stato attuale delle grandezze meteorologiche in un determinato luogo di interesse?

In alcuni contesti, la risposta non è solo una curiosità, ma una condizione necessaria per agire. I lanci spaziali, ad esempio, vivono dentro “finestre di lancio” strettamente legate al meteo: vento in quota, formazione di cumulonembi, rischio di fulminazione del razzo, visibilità, gradiente termico verticale. Una lettura delle condizioni atmosferiche può rinviare un lancio di ore o di giorni. Tuttavia, anche lì, come in una stazione meteo in miniatura, tutto dipende dalla precisione con cui si raccolgono e si interpretano i dati.

Nel linguaggio dell’informatica, una stazione meteo diventa un laboratorio di dati: un’API fornisce misure e istanti temporali in formato JSON. Il programma li analizza, li elabora e ne rappresenta i risultati — una data, un’ora di riferimento, una descrizione del cielo. In questo quadro, ogni risposta dell’API registra un istante dell’atmosfera terrestre: una fotografia numerica del cielo che il codice traduce in una forma leggibile per l’esperienza umana.

Risultato

    La Weather Station costituisce un’estensione rispetto ai precedenti progetti console-based Java poiché utilizza una sorgente dati esterna in tempo quasi reale — l’API pubblica di open-meteo.com — per produrre un report meteorologico strutturato.

Dal punto di vista del linguaggio, il progetto adotta alcuni strumenti caratteristici della piattaforma Java, come la costruzione dell’URL di richiesta — completo di query string che specifica coordinate geografiche, fuso orario e insiemi di variabili meteorologiche (sezioni daily e current) —, l’apertura della connessione HTTP verso il servizio remoto e l’invio della richiesta HTTP di tipo GET tramite le classi URI e HttpURLConnection, con controllo esplicito del codice di stato, e l’acquisizione del flusso di risposta in formato JSON attraverso InputStreamReader e BufferedReader. A questi aspetti si affiancano l’impiego delle API del package java.time per rappresentare data, ora, fuso orario e orari di alba e tramonto, e la definizione di un repertorio dei capoluoghi italiani tramite un enum dedicato, annidato in una interfaccia, che associa a ciascuna città le relative coordinate geografiche.

Inoltre, la scelta di effettuare un parsing del JSON senza ricorrere a librerie esterne mette in evidenza l’uso dei costrutti del linguaggio e delle API di base sulle stringhe: ricerca tramite marker testuali, gestione di indici, costruzione controllata di porzioni di testo. In questo quadro risultano centrali anche elementi del core language come i parametri variadici, l’utilizzo mirato degli switch e l’impiego di etichette di blocco per gestire casi particolari nella logica di estrazione e formattazione dei dati.

Da un punto di vista astratto, l’applicazione agisce come una funzione che associa a uno stato dell’atmosfera un vettore finito di grandezze numeriche o simboliche, in modo da rendere una rappresentazione discreta e stabile di condizioni fisiche altrimenti effimere.
 


Link utili

▶ Esegui il programma su Replit:

  1. Clicca sul link Apri su Replit per accedere al progetto.
  2. Se non hai un account Replit, registrati gratuitamente oppure accedi con Google/GitHub.
  3. Una volta nel progetto, premi “Run” per avviare l’esecuzione.
  4. Puoi consultare il file sorgente e, se desideri, scaricarlo o copiarlo dal pannello di codice a sinistra.


▶ Requisiti:

  1. Java JDK 8 o superiore installato (verifica con java -version).
  2. Console o IDE compatibile con UTF-8.


▶ Scarica la versione portatile del codice:

  1. Effettua queste tre semplici operazioni:
    • Clicca sul link Scarica File per scaricare il file Main.txt, che contiene il codice sorgente completo in formato .txt.
    • Clicca sul link Scarica File per scaricare il file Coordinates.txt, che contiene il codice sorgente completo in formato .txt.
    • Clicca sul link Scarica File per scaricare il file WeatherStation.txt, che contiene il codice sorgente completo in formato .txt.
  2. Rinomina i file cambiando l’estensione da .txt a .java.
  3. A. Se usi un IDE Java (VS Code, IntelliJ, NetBeans, Eclipse), crea un nuovo progetto java vuoto e aggiungi il package weather; poi importa i tre file e avvia la classe Main.java come Java Application.
  1. B. Se invece vuoi eseguirlo senza IDE, crea una cartella chiamata weather e inserisci al suo interno i tre file .java.
  2. B. Apri il terminale nella cartella che contiene weather/ e usa uno dei comandi seguenti:

macOS / Linux :

mkdir -p out && javac -encoding UTF-8 -d out $(find . -name "*.java") && java -Dfile.encoding=UTF-8 -cp out weather.Main


Windows (PowerShell):

mkdir out; javac -encoding UTF-8 -d out (Get-ChildItem -Recurse -Filter *.java).FullName; java -Dfile.encoding=UTF-8 -cp out weather.Main


Windows (Prompt dei comandi):
esegui i comandi uno alla volta (senza ; tra di essi). Esempio:

mkdir out
javac -encoding UTF-8 -d out *.java
java -Dfile.encoding=UTF-8 -cp out weather.Main


▶ Utilizzo:

Dopo l’avvio, la console ti guida passo-passo:

  • inserisci un il nome di un capoluogo di regione italiano e premi ‘INVIO’ per visualizzare le condizioni meteo della città.
replit.com
A

Weather Station

@alessioseveri27


Codice

    Nel complesso, il progetto mostra come, a partire dall’esigenza di ottenere un quadro delle condizioni meteorologiche per una singola città, sia possibile costruire in Java una struttura modulare che integra la gestione di richieste HTTP verso un’API esterna, la modellazione delle informazioni temporali, l’uso di coordinate geografiche e la formattazione testuale, e che mantiene distinti i livelli (dati esterni, parsing, rappresentazione) lasciando spazio a estensioni future.

Le Javadoc all’interno del codice documentano nel dettaglio le responsabilità e il flusso logico delle singole classi, enum e metodi, rendendo esplicita l’architettura complessiva e facilitando eventuali evoluzioni del progetto.

Weather Station package weather
Main.java
WeatherStation.java
Coordinates.java
1
2/*
3 Weather Station
4
5 Package: weather
6
7 Descrizione:
8 Weather Station – bollettino meteo da open-meteo.com
9
10 Applicazione console che interroga il servizio open-meteo.com
11 (https://api.open-meteo.com/) per un capoluogo di regione italiano,
12 recupera un sottoinsieme di misure meteorologiche correnti e giornaliere
13 in JSON e le presenta in un report testuale formattato.
14
15 L'utente inserisce il nome di una città tra i capoluoghi supportati;
16 il programma crea un'istanza di WeatherStation e stampa in console
17 il bollettino meteo corrispondente.
18
19
20 Autore: Alessio Severi
21 Licenza: MIT License
22
23 MIT License
24
25 Copyright (c) 2025 Alessio Severi
26
27 Permission is hereby granted, free of charge, to any person obtaining a copy
28 of this software and associated documentation files (the "Software"), to deal
29 in the Software without restriction, including without limitation the rights
30 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31 copies of the Software, and to permit persons to whom the Software is
32 furnished to do so, subject to the following conditions:
33
34 The above copyright notice and this permission notice shall be included in all
35 copies or substantial portions of the Software.
36
37 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43 SOFTWARE.
44
45*/
46
47
48
49package weather;
50
51import java.util.Scanner;
52
53/**
54 * Punto di ingresso dell'applicazione Weather Station.
55 * <p>
56 * Gestisce l'interazione da console con l'utente, acquisisce il nome
57 * del capoluogo italiano e delega la generazione del report meteo
58 * alla classe {@link WeatherStation}.
59 */
60public class Main {
61
62 /**
63 * Avvia l'applicazione da linea di comando.
64 *
65 * @param args argomenti passati da riga di comando (non utilizzati).
66 */
67 public static void main(String[] args) {
68
69 try (Scanner sc = new Scanner(System.in)) {
70 System.out.print("""
71 \n\n
72 Benvenuto in Weather Station.
73 I dati meteo sono forniti dal servizio open-meteo.com.
74 Inserisci il nome di un capoluogo di regione italiano
75 per visualizzare le condizioni meteo della città: \000""");
76
77
78 // Crea una WeatherStation associata alla città inserita
79 // dall'utente e stampa il report meteo formattato.
80 System.out.println(new WeatherStation(sc.nextLine()).buildReport());
81 }
82 }
83}
84
1// © 2025 Alessio Severi — vedi licenza nel file Main.java
2
3
4package weather;
5
6
7import java.io.BufferedReader;
8import java.io.IOException;
9import java.io.InputStreamReader;
10import java.net.HttpURLConnection;
11import java.net.URI;
12import java.time.LocalDateTime;
13import java.time.ZonedDateTime;
14import java.time.format.DateTimeFormatter;
15import java.util.ArrayList;
16import java.util.List;
17import java.util.Locale;
18
19
20/**
21 * Classe principale dell'applicazione console «Weather Station».
22 *
23 * <p>Interroga il servizio <strong>open-meteo.com</strong> per un capoluogo
24 * di regione italiano, estrae un sottoinsieme di misure meteorologiche
25 * correnti e giornaliere e produce un report testuale formattato.</p>
26 *
27 * <p>Incapsula tre responsabilità principali:</p>
28 * <ul>
29 * <li>costruzione dell'URL di richiesta a partire dalle coordinate
30 * del capoluogo selezionato;</li>
31 * <li>gestione della connessione HTTP e raccolta della risposta JSON
32 * in una struttura testuale;</li>
33 * <li>parsing manuale dei campi rilevanti e formattazione del report
34 * per la stampa in console.</li>
35 * </ul>
36 *
37 * <p>Inoltre implementa l'interfaccia {@link Coordinates}, che fornisce
38 * l'enum {@link Coordinates.ItalianCapital} con l'elenco dei capoluoghi
39 * supportati e le relative coordinate geografiche.</p>
40 *
41 * <p>L'oggetto è pensato come "stazione" locale: una volta creato con una
42 * città valida, fornisce il metodo {@link #buildReport()} che restituisce
43 * il report meteorologico.</p>
44 */
45public class WeatherStation implements Coordinates {
46
47
48 /**
49 * Capoluogo regionale selezionato dall'utente.
50 * <p>Contiene nome della città, regione e coordinate geografiche
51 * in formato decimale.</p>
52 */
53 private ItalianCapital CITY;
54
55 /**
56 * Lista ordinata di righe testuali già formattate.
57 * <p>Ogni elemento rappresenta una sezione del report
58 * (data/ora, temperatura, vento, ecc.).</p>
59 */
60 private final List<String> dataList = new ArrayList<>();
61
62
63 /**
64 * Crea una nuova stazione meteo per il capoluogo indicato.
65 *
66 * <p>Il nome della città viene normalizzato (elimina spazi e apici,
67 * convertendolo poi in maiuscolo) e confrontato con le costanti
68 * dell'enumerazione {@link ItalianCapital}.</p>
69 *
70 * <p>Se non esiste nessuna corrispondenza, il costruttore segnala
71 * l'errore a console e termina il programma con codice di uscita
72 * diverso da zero.</p>
73 *
74 * @param cityName nome del capoluogo regionale italiano inserito
75 * dall'utente (es. "Roma", "Milano", "Cagliari").
76 */
77 public WeatherStation(String city) {
78
79
80 city = city.trim().replace("'", "").toUpperCase();
81
82
83 for(ItalianCapital c : ItalianCapital.values()){
84
85 if(c.name().equals(city))
86 CITY = c;
87 }
88
89 if (CITY == null) {
90
91 System.out.println("Unsupported city: " + city.toLowerCase());
92 System.exit(1);
93
94 }
95
96 }
97
98
99 /**
100 * Restituisce una copia non modificabile dei dati meteo
101 * attualmente memorizzati in {@link #dataList}.
102 *
103 * <p>
104 * La lista contiene tutti i campi già estratti e formattati dal JSON
105 * (data/ora, orario dell'alba, orario del tramonto, temperature, vento, ecc.),
106 * nello stesso ordine utilizzato da {@link #formatReport()}.
107 * </p>
108 *
109 * <p>
110 * La lista restituita è indipendente dalla lista interna: chi la riceve
111 * non può aggiungere, rimuovere, sostituire né riordinare elementi
112 * (ogni tentativo di modifica solleva {@link UnsupportedOperationException})
113 * e quindi non può alterare lo stato interno di {@code WeatherStation}.
114 * </p>
115 *
116 * @return una lista non modificabile con i dati meteo formattati.
117 */
118 public List<String> getDataList() {
119 return List.copyOf(dataList);
120 }
121
122
123
124 /**
125 * Stabilisce una connessione HTTP con il servizio open-meteo e
126 * recupera i dati meteo in formato JSON per la città selezionata.
127 *
128 * <p>
129 * L'URL, comprensivo di query string, viene costruito a partire dalle coordinate associate al
130 * {@link ItalianCapital} corrente e include:
131 * </p>
132 *
133 * <ul>
134 * <li>latitudine e longitudine del capoluogo selezionato;</li>
135 * <li>parametri {@code daily} per l'orario dell'alba e del tramonto, temperature massima/minima;</li>
136 * <li>parametri {@code current} per le principali variabili meteorologiche istantanee;</li>
137 * <li>impostazione esplicita del fuso orario su {@code Europe/Rome}.</li>
138 * </ul>
139 *
140 * <p>
141 * Il metodo apre una connessione HTTP di tipo GET, controlla il codice
142 * di risposta e, in caso di esito positivo, acquisisce il corpo della risposta
143 * in un {@link StringBuilder}.
144 * </p>
145 *
146 * <p>
147 * In caso di risposta HTTP diversa da {@code 200: OK} oppure se si
148 * verifica un problema di I/O, il metodo stampa un messaggio di
149 * errore e termina il programma con codice di uscita {@code 1}.
150 * </p>
151 *
152 * @return il contenuto della risposta JSON come {@code StringBuilder}
153 */
154 private StringBuilder connect(){
155
156
157 StringBuilder response = new StringBuilder();
158
159 String urlString =
160 "https://api.open-meteo.com/v1/forecast"
161 + "?latitude=" + CITY.getLatitude()
162 + "&longitude=" + CITY.getLongitude()
163 + "&timezone=Europe%2FRome"
164 + "&daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,uv_index_max"
165 + "&current=temperature_2m,apparent_temperature,"
166 + "dewpoint_2m,surface_pressure,relative_humidity_2m,"
167 + "wind_speed_10m,wind_direction_10m,weather_code,"
168 + "cloudcover,visibility,precipitation,uv_index,snowfall";
169
170
171
172 try {
173 URI uri = URI.create(urlString); // valida la stringa come URI
174 HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
175 conn.setRequestMethod("GET");
176
177
178 int status = conn.getResponseCode();
179
180 if (status != HttpURLConnection.HTTP_OK) {
181 System.out.println("HTTP Error: " + status);
182 System.exit(1);
183 }
184
185
186 // Leggo il JSON come pura stringa
187 try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
188
189 String line;
190
191 while ((line = in.readLine()) != null) {
192 response.append(line);
193 }
194
195 conn.disconnect();
196 }
197
198
199
200 } catch (IOException e) {
201 System.out.println("I/O Error: " + e.getMessage());
202 System.exit(1);
203 }
204
205 return response;
206
207 }
208
209
210
211 /**
212 * Popola {@link #dataList} con tutti i dati necessari alla generazione
213 * del bollettino meteo.
214 *
215 * <p>In particolare il metodo:</p>
216 * <ul>
217 * <li>separa la risposta JSON relativa ai dati correnti da quelli
218 * giornalieri, tramite il marcatore {@code "daily_units"},
219 * in modo che {@code extractData} riceva, per ciascun campo,
220 * soltanto la sezione JSON rilevante;
221 * </li>
222 * <li>ricava data e ora correnti delegando l'estrazione a
223 * {@link #extractData(String, String, String)} e la formattazione
224 * a {@link #formatDateTime(String)}, che aggiunge anche la
225 * prima riga descrittiva in {@code dataList} e restituisce le ore nel
226 * formato 0–23 come intero;
227 * </li>
228 * <li>ricava orari di alba e tramonto delegando l'estrazione a
229 * {@link #extractData(String, String, String)} e la formattazione
230 * a {@link #formatDailyTime(String, String)}, che aggiunge anche la
231 * riga descrittiva in {@code dataList} e restituisce le ore nel
232 * formato 0–23 come intero, utilizzate insieme all'ora locale
233 * per distinguere il contesto diurno/notturno nella descrizione
234 * del codice meteo;
235 * </li>
236 * <li>per tutte le altre grandezze meteorologiche (temperature
237 * massima e minima giornaliera, temperatura corrente e percepita, pressione,
238 * umidità, vento, visibilità, copertura nuvolosa, punto di
239 * rugiada, precipitazioni, indice UV, ecc.) delega l'estrazione a
240 * {@code extractData} e aggiunge a {@code dataList} stringhe già
241 * etichettate, nel formato “nome campo: valore con unità di misura”.
242 * </li>
243 * </ul>
244 *
245 * <p>
246 * Al termine dell'esecuzione, {@code dataList} contiene tutte le voci
247 * in un ordine ben definito, esattamente quello atteso da {@link #formatReport()},
248 * che si occupa di restituire il bollettino meteo formattato.
249 * </p>
250 *
251 * @param response contenuto JSON restituito dall'API, rappresentato
252 * tramite un {@link StringBuilder}.
253 */
254 private void buildDataList(StringBuilder response){
255
256
257 int index = response.indexOf("\"daily_units\"");
258 String jsonCurrent = response.toString().substring(0, index);
259 String jsonDaily = response.toString().substring(index);
260
261
262 String marker = "\"time\":";
263 int hour24 = formatDateTime((extractData(marker, "time", jsonCurrent)));
264
265
266 marker = "\"sunrise\":";
267 int sunriseHour24 = formatDailyTime(extractData(marker, "sunrise", jsonDaily), "sunrise");
268
269 marker = "\"sunset\":";
270 int sunsetHour24 = formatDailyTime(extractData(marker, "sunset", jsonDaily), "sunset");
271
272 marker = "\"temperature_2m_max\":";
273 dataList.add(extractData(marker, "Max temp", jsonDaily));
274
275 marker = "\"temperature_2m_min\":";
276 dataList.add(extractData(marker, "Min temp", jsonDaily));
277
278
279 marker = "\"weather_code\":";
280 dataList.add(extractData(marker, "weather", jsonCurrent, hour24, sunriseHour24, sunsetHour24));
281
282
283 marker = "\"temperature_2m\":";
284 dataList.add(extractData(marker, "temperature", jsonCurrent));
285
286 marker = "\"apparent_temperature\":";
287 dataList.add(extractData(marker, "apparent temperature", jsonCurrent));
288
289
290 marker = "\"surface_pressure\":";
291 dataList.add(extractData(marker, "surface pressure", jsonCurrent));
292
293 marker = "\"relative_humidity_2m\":";
294 dataList.add(extractData(marker, "relative humidity", jsonCurrent));
295
296
297 marker = "\"wind_speed_10m\":";
298 dataList.add(extractData(marker, "wind speed", jsonCurrent));
299
300 marker = "\"wind_direction_10m\":";
301 dataList.add(extractData(marker, "wind direction", jsonCurrent));
302
303
304 marker = "\"visibility\":";
305 dataList.add(extractData(marker, "visibility", jsonCurrent));
306
307 marker = "\"cloudcover\":";
308 dataList.add(extractData(marker, "cloudcover", jsonCurrent));
309
310
311 marker = "\"dewpoint_2m\":";
312 dataList.add(extractData(marker, "dew point", jsonCurrent));
313
314 marker = "\"precipitation\":";
315 dataList.add(extractData(marker, "precipitation", jsonCurrent));
316
317
318 marker = "\"uv_index\":";
319 dataList.add(extractData(marker, "UV index", jsonCurrent));
320
321
322 }
323
324
325 /**
326 * Estrae e formatta un singolo valore dalla sezione di risposta JSON corrispondente,
327 * aggiornando contestualmente {@link #dataList}.
328 *
329 * <p>
330 * La ricerca avviene tramite il marcatore testuale {@code marker}
331 * (ad esempio {@code "\"temperature_2m\":"}) e utilizza indici di
332 * sottostringa sulla sezione di risposta JSON per individuare il
333 * valore compreso tra il marcatore e la virgola successiva.
334 * </p>
335 *
336 * <p>
337 * Nel caso più semplice il metodo costruisce e restituisce una stringa del tipo:
338 * </p>
339 * <pre>
340 * Temperature: 11.4°C
341 * </pre>
342 * <p>
343 * Per grandezze come temperatura, umidità e direzione del vento le unità di
344 * misura (ad esempio {@code °C}, {@code %} e {@code °}) vengono accodate
345 * al valore senza spazi superflui, in modo da mantenere una resa visiva uniforme.
346 * </p>
347 * <p>
348 * Per le altre grandezze numeriche rimanenti (pressione, intensità del vento, visibilità,
349 * copertura nuvolosa, punto di rugiada, precipitazioni, ecc.),
350 * il metodo produce una riga etichettata nel formato
351 * {@code "<etichetta>: valore [unità]"}.
352 * </p>
353 *
354 * <p>Infine per alcune chiavi JSON la logica di estrazione e formattazione
355 * è specializzata:</p>
356 * <ul>
357 * <li>
358 * per {@code "weather_code"}:
359 * il codice WMO estratto dal JSON viene trasformato in
360 * descrizione testuale con emoji tramite
361 * {@link #describeWeatherCode(int, int[])}, usando eventualmente
362 * le ore di contesto passate in {@code hours}
363 * (ora corrente, ora dell'alba, ora del tramonto);
364 * </li>
365 * <li>
366 * per {@code "time"}:
367 * oltre al valore temporale, il metodo estrae anche
368 * {@code "timezone_abbreviation"} e {@code "timezone"},
369 * componendo una stringa pseudo–ISO 8601 che include sia
370 * l'abbreviazione dell'offset sia l'identificatore di zona (zone ID),
371 * coerenti con il fuso orario specificato nell'URL di richiesta
372 * (questa stringa successivamente verrà ulteriormente formattata in
373 * {@link #formatDateTime(String)});
374 * </li>
375 * <li>
376 * per {@code "sunrise"} e {@code "sunset"}:
377 * la stringa estratta viene normalizzata rimuovendo virgolette e parentesi
378 * quadre, in modo da risultare adatta a
379 * {@link #formatDailyTime(String, String)};
380 * </li>
381 * </ul>
382 *
383 * @param marker marcatore JSON da cercare (chiave del campo).
384 * @param msg etichetta testuale da associare al valore estratto
385 * (ad esempio {@code "Temperature"}, {@code "Wind speed"}).
386 * @param json porzione di risposta JSON in cui effettuare la ricerca
387 * (ad esempio {@code jsonCurrent} o {@code jsonDaily}).
388 * @param hours eventuali ore di contesto (ora corrente, ora dell'alba,
389 * ora del tramonto) utilizzate per distinguere scenario
390 * diurno/notturno nella descrizione del codice meteo;
391 * può essere omesso per i campi che non ne fanno uso.
392 *
393 * @return stringa formattata corrispondente al campo estratto; in caso
394 * di marcatore non trovato, una stringa di errore descrittiva
395 * (ad esempio {@code "Field 'UV index' not found in JSON."}).
396 */
397 private String extractData(String marker, String msg, String json, int… hours){
398
399 int idx;
400 String value = "";
401 String tempStr;
402 int stop = 2;
403
404
405 OUTER:
406 for (int i = 0; i < stop; i++) {
407
408 if(i==0) idx = json.lastIndexOf(marker);
409 else idx = json.indexOf(marker);
410
411 if (idx != –1) {
412 int start = idx + marker.length();
413 int end = json.indexOf(",", start); // fine del numero
414
415 tempStr = json.substring(start, end);
416
417 if(i==0) value = Character.toUpperCase(msg.charAt(0)) + msg.substring(1) + ": " + tempStr;
418 else {
419
420 if(tempStr.charAt(1) != '°' && tempStr.charAt(1) != '%') value = value + " ";
421 value = value + tempStr.substring(1, tempStr.length() – 1) + "\000";
422
423 }
424
425 } else
426
427 return "Field '" + msg + "' not found in JSON.";
428
429 if (i==0)
430 switch (marker) {
431 case "\"time\":" :
432 marker = "\"timezone_abbreviation\":";
433 break;
434
435 case "\"weather_code\":" :
436 value = value.replace(tempStr, describeWeatherCode(Integer.parseInt(tempStr), hours));
437 break OUTER;
438
439 case "\"sunrise\":", "\"sunset\":" :
440 i++;
441 value = value.replace("\"", "");
442
443 case "\"temperature_2m_max\":", "\"temperature_2m_min\":" :
444 value = value.replace("[", "");
445
446 }
447 else if(marker.equals("\"timezone_abbreviation\":")) {
448
449 value = value.replace("\"", "")
450 .replace("\000", "")
451 .replace("Time: ", "");
452
453 char c = value.charAt(value.length() – 1);
454 value = value.replace(" GMT+" + c , ":00+0" + c + ":00");
455
456 marker = "\"timezone\":";
457 stop = 3;
458
459 }
460 }
461
462 return value;
463 }
464
465
466 /**
467 * Traduce il codice meteo WMO in una descrizione testuale arricchita
468 * da emoji.
469 *
470 * <p>
471 * In base all'ora corrente rispetto all'orario dell'alba e del tramonto, locali,
472 * il metodo distingue tra contesto "diurno" e "notturno" e seleziona di
473 * conseguenza le emoji correlate (sole, luna, nuvole con sole, ecc.).
474 * </p>
475 *
476 * @param code codice meteo WMO restituito da open-meteo.
477 * @param hours array di tre elementi che rappresentano, nell'ordine:
478 * <ul>
479 * <li><code>hours[0]</code>: ora corrente come intero nell'intervallo 0–23;</li>
480 * <li><code>hours[1]</code>: ora dell'alba come intero nell'intervallo 0–23;</li>
481 * <li><code>hours[2]</code>: ora del tramonto come intero nell'intervallo 0–23.</li>
482 * </ul>
483 * @return descrizione del fenomeno, con emoji coerente con la fascia
484 * oraria e le condizioni meteo (ad esempio {@code "🌤 Mainly clear"}).
485 */
486 private String describeWeatherCode(int code, int[] hours) {
487
488 boolean moon = !(hours[0] >= hours[1] && hours[0] < hours[2]);
489
490
491 return switch (code) {
492 case 0 -> ((moon) ? "🌙":"☀️") + " Clear sky";
493 case 1 -> ((moon) ? "🌑☁️":"🌤") + " Mainly clear";
494 case 2 -> ((moon) ? "🌑💨":"⛅") + " Partly cloudy";
495 case 3 -> "☁️ Overcast";
496 case 45 -> "🌫️ Fog";
497 case 48 -> "🌫️ Depositing rime fog";
498 case 51 -> ((moon) ? "🌑🌧️":"🌦️") + " Light drizzle";
499 case 53 -> ((moon) ? "🌑🌧️":"🌦️") + " Moderate drizzle";
500 case 55 -> ((moon) ? "🌑🌧️":"🌦️") + " Dense drizzle";
501 case 56 -> ((moon) ? "🌑🌧️":"🌦") + " ❄️ Light freezing drizzle";
502 case 57 -> ((moon) ? "🌑🌧️":"🌦") + " ❄️ Dense freezing drizzle";
503 case 61 -> "🌧️ Slight rain";
504 case 63 -> "🌧️ Moderate rain";
505 case 65 -> "🌧️ Heavy rain";
506 case 66 -> "🌧️ ❄️ Light freezing rain";
507 case 67 -> "🌧️ ❄️ Heavy freezing rain";
508 case 71 -> "🌨 Slight snowfall";
509 case 73 -> "🌨 Moderate snowfall";
510 case 75 -> "🌨 Heavy snowfall";
511 case 77 -> "🌨 Snow grains";
512 case 80 -> "🌧️ Slight rain showers";
513 case 81 -> "🌧️ Moderate rain showers";
514 case 82 -> "🌧️ Violent rain showers";
515 case 85 -> "🌨 Slight snow showers";
516 case 86 -> "🌨 Heavy snow showers";
517 case 95 -> "⛈️ Thunderstorm";
518 case 96 -> "⛈️ ❄️ Thunderstorm with slight hail";
519 case 99 -> "⛈️ ❄️ Thunderstorm with heavy hail";
520 default -> "Unknown weather code: " + code;
521 };
522 }
523
524
525 /**
526 * Converte la data/ora ISO-8601 della risposta API in una rappresentazione
527 * leggibile per l'utente.
528 *
529 * <p>
530 * La stringa in ingresso è in un formato compatibile con
531 * {@link java.time.ZonedDateTime#parse(CharSequence)} e contiene
532 * data, ora, offset e ID di fuso orario, ad esempio:
533 * </p>
534 *
535 * <pre>
536 * 2026-01-22T12:30:00+01:00[Europe/Rome]
537 * </pre>
538 *
539 * <p>
540 * Il metodo costruisce una rappresentazione del tipo:
541 * </p>
542 *
543 * <pre>
544 * Thursday 22 January 2026 12:30 (GMT+1, Europe/Rome)
545 * </pre>
546 *
547 * La stringa formattata viene inserita come primo elemento
548 * della {@link #dataList}. Inoltre il metodo restituisce l'ora
549 * corrente come intero 0–23, utile per distinguere giorno/notte.
550 * </p>
551 *
552 * @param dateTime data/ora in formato ISO-8601 compatibile con
553 * {@code ZonedDateTime}, derivata dal JSON.
554 * @return ora corrente 0–23 nel fuso orario locale.
555 */
556 private int formatDateTime(String dateTime){
557
558
559 dateTime = dateTime.replace("\000", "]")
560 .replace(" ", "[");
561
562
563 ZonedDateTime zone = ZonedDateTime.parse(dateTime);
564
565 DateTimeFormatter format = DateTimeFormatter.ofPattern("EEEE dd MMMM YYYY HH:mm (O, VV)", Locale.ENGLISH);
566 String st = zone.format(format);
567
568
569 dataList.add(st.replace(st.charAt(0), Character.toUpperCase(st.charAt(0))));
570
571
572 return Integer.parseInt(zone.format(DateTimeFormatter.ofPattern("HH")));
573
574 }
575
576
577 /**
578 * Estrae e formatta l'orario giornaliero (ad esempio dell'alba o del tramonto)
579 * e ne restituisce l'ora come intero.
580 *
581 * <p>
582 * Il parametro {@code dailyTime} è una stringa composta da una
583 * etichetta testuale e da una data/ora separate da tre spazi,
584 * ad esempio:
585 * </p>
586 *
587 * <pre>
588 * "Sunrise: 2026-01-22T07:31"
589 * </pre>
590 *
591 * <p>
592 * La parte di data/ora non include i secondi; il metodo aggiunge
593 * {@code ":00"} per ottenere una stringa compatibile con
594 * {@link java.time.LocalDateTime#parse(CharSequence)} e quindi
595 * in formato ISO-8601 completo.
596 * </p>
597 *
598 * <p>
599 * In dettaglio, il metodo:
600 * </p>
601 * <ul>
602 * <li>parsa la parte di data/ora in un {@link java.time.LocalDateTime};</li>
603 * <li>formatta l'orario in {@code "HH:mm"};</li>
604 * <li>aggiunge alla {@link #dataList} una riga del tipo
605 * {@code "Sunrise: 07:31"};</li>
606 * <li>restituisce l'ora come intero 0–23 dell'evento.</li>
607 * </ul>
608 *
609 * @param dailyTime stringa contenente etichetta e data/ora separate
610 * da tre spazi; la parte di data/ora viene completata
611 * con i secondi per diventare ISO-8601 compatibile.
612 * @param dailyText descrittore logico dell'evento (ad es. {@code "sunrise"}
613 * o {@code "sunset"}); viene usato come informazione
614 * semantica per distinguere le due tipologie di orario.
615 * @return ora dell'evento 0–23.
616 */
617 private int formatDailyTime(String dailyTime, String dailyText){
618
619 String[] array = dailyTime.split(" ");
620
621 LocalDateTime time = LocalDateTime.parse(array[1] + ":00");
622
623 String hours = time.format(DateTimeFormatter.ofPattern("HH:mm"));
624
625
626 dataList.add(array[0] + " " + hours);
627
628
629 if(dailyText.equals("sunrise"))
630 return Integer.parseInt(time.format(DateTimeFormatter.ofPattern("HH")));
631
632 else
633 return Integer.parseInt(time.format(DateTimeFormatter.ofPattern("HH")));
634
635
636 }
637
638
639 /**
640 * Costruisce il report testuale completo, arricchito da emoji,
641 * a partire dai dati presenti in {@link #dataList}.
642 *
643 * <p>Il report comprende:</p>
644 * <ul>
645 * <li>intestazione dell'applicazione;</li>
646 * <li>informazioni su città, regione, paese, data, ora, fuso
647 * orario e zone ID testuale;</li>
648 * <li>condizioni meteo giornaliere (temperatura massima/minima, orario
649 * dell'alba/tramonto, ecc.);</li>
650 * <li>condizioni meteo relative all'orario corrente (temperatura, pressione,
651 * umidità, vento, visibilità, precipitazioni, UV, ecc.);</li>
652 * <li>riga di chiusura che delimita il bollettino.</li>
653 * </ul>
654 *
655 * <p>
656 * Il metodo assume che {@link #dataList} sia già stata popolata in modo
657 * coerente da {@code getDataList}, con gli elementi nelle posizioni
658 * previste dalla formattazione.
659 * </p>
660 *
661 * @return stringa contenente il bollettino meteo formattato,
662 * pronta per la stampa in console.
663 */
664 public String formatReport(){
665
666
667 String header = "\n\n—————— WEATHER STATION ——————\n\n";
668
669 String dataDateTime = dataList.get(0);
670
671 String info = CITY.getCityName() + ", " + CITY.getRegionName() + " (IT) 🌍\n\n" + dataDateTime;
672
673
674 String formattedReport = info + "\n\n\n\n"
675 + dataList.get(5) + "\n\n"
676 + dataList.get(1) + " • " + dataList.get(2) + "\n\n"
677 + dataList.get(3) + " • " + dataList.get(4) + "\n\n\n"
678 + dataList.get(6).replace(" ", " 🌡️ ") + " " + "(a"+ dataList.get(7)
679 .replace(" temperature: ","")
680 .substring(1) + ")" + "\n\n"
681 + dataList.get(8).replace(" ", " 🌡️ ") + "\n\n"
682 + dataList.get(9).replace(" ", " 🌡️ ") + "\n\n"
683 + dataList.get(10).replace(" speed", "")
684 .replace(" ", " 🌬️ ") + dataList.get(11)
685 .replace("Wind direction: ", " from ") + "\n\n\n"
686 + dataList.get(12) + " • " + dataList.get(13) + "\n\n"
687 + dataList.get(14) + " • " + dataList.get(15) + "\n\n\n"
688 + dataList.get(16).replace(" ", " 🔆 ") + "\n\n";
689
690 String footer = "—————————————————–\n\n";
691
692 return header + formattedReport + footer;
693
694 }
695
696
697 /**
698 * Metodo di alto livello che, con un'unica chiamata,
699 * restituisce il bollettino meteo formattato.
700 *
701 * <p>La sequenza eseguita è:</p>
702 * <ol>
703 * <li>recupero della risposta JSON tramite {@link #connect()};</li>
704 * <li>riempie {@link #dataList} mediante {@link #buildDataList(StringBuilder)};</li>
705 * <li>generazione del report finale tramite {@link #formatReport()}.</li>
706 * </ol>
707 *
708 * @return stringa contenente il report meteorologico completo.
709 */
710 public String buildReport() {
711
712 StringBuilder response = connect();
713
714 buildDataList(response);
715
716 return formatReport();
717
718 }
719
720}
1// © 2025 Alessio Severi — vedi licenza nel file Main.java
2
3
4package weather;
5
6/**
7 * Interfaccia di marker che raggruppa i tipi relativi alle coordinate
8 * geografiche utilizzate dall'applicazione Weather Station.
9 * <p>
10 * Contiene l'enum {@link Coordinates.ItalianCapital}, che
11 * rappresenta i capoluoghi di regione italiani supportati dal sistema.
12 * </p>
13 */
14public interface Coordinates {
15
16 /**
17 * Elenco dei capoluoghi di regione italiani supportati
18 * dall'applicazione.
19 * <p>
20 * Ogni costante incapsula:
21 * </p>
22 * <ul>
23 * <li>il nome della città in inglese;</li>
24 * <li>il nome della regione in inglese;</li>
25 * <li>latitudine in formato decimale (WGS84);</li>
26 * <li>longitudine in formato decimale (WGS84).</li>
27 * </ul>
28 * <p>
29 * Questi valori vengono utilizzati per costruire la richiesta
30 * verso il servizio open-meteo e la visualizzazione del report
31 * meteorologico.
32 * </p>
33 */
34 public enum ItalianCapital {
35
36
37 ROMA ("Rome", "Lazio", "41.9028", "12.4964"),
38 MILANO ("Milan", "Lombardy", "45.4642", "9.1900"),
39 TORINO ("Turin", "Piedmont", "45.0703", "7.6869"),
40 NAPOLI ("Naples", "Campania", "40.8518", "14.2681"),
41 PALERMO ("Palermo", "Sicily", "38.1157", "13.3613"),
42 CAGLIARI ("Cagliari", "Sardinia", "39.2238", "9.1217"),
43 GENOVA ("Genoa", "Liguria", "44.4056", "8.9463"),
44 BOLOGNA ("Bologna", "Emilia-Romagna", "44.4949", "11.3426"),
45 FIRENZE ("Florence", "Tuscany", "43.7696", "11.2558"),
46 VENEZIA ("Venice", "Veneto", "45.4408", "12.3155"),
47 BARI ("Bari", "Apulia", "41.1171", "16.8719"),
48 LAQUILA ("L'Aquila", "Abruzzo", "42.3499", "13.3995"),
49 ANCONA ("Ancona", "Marche", "43.6158", "13.5189"),
50 PERUGIA ("Perugia", "Umbria", "43.1107", "12.3908"),
51 CAMPOBASSO ("Campobasso", "Molise", "41.5600", "14.6640"),
52 POTENZA ("Potenza", "Basilicata", "40.6401", "15.8050"),
53 CATANZARO ("Catanzaro", "Calabria", "38.9098", "16.5877"),
54 AOSTA ("Aosta", "Aosta Valley", "45.7376", "7.3201"),
55 TRENTO ("Trento", "Trentino-South Tyrol", "46.0700", "11.1211"),
56 TRIESTE ("Trieste", "Friuli-Venezia Giulia", "45.6495", "13.7768");
57
58
59
60 private final String cityName;
61 private final String regionName;
62 private final String latitude;
63 private final String longitude;
64
65
66 /**
67 * Costruisce una costante che rappresenta un capoluogo italiano.
68 *
69 * @param cityName nome della città (in inglese)
70 * @param regionName nome della regione (in inglese)
71 * @param latitude latitudine in formato decimale (WGS84)
72 * @param longitude longitudine in formato decimale (WGS84)
73 */
74 ItalianCapital(String cityName,
75 String regionName,
76 String latitude,
77 String longitude) {
78
79 this.cityName = cityName;
80 this.regionName = regionName;
81 this.latitude = latitude;
82 this.longitude = longitude;
83 }
84
85
86 /**
87 * Restituisce il nome della città.
88 *
89 * @return nome della città (in inglese).
90 */
91 public String getCityName() {
92 return cityName;
93 }
94
95 /**
96 * Restituisce il nome della regione.
97 *
98 * @return nome della regione (in inglese).
99 */
100 public String getRegionName() {
101 return regionName;
102 }
103
104 /**
105 * Restituisce la latitudine in formato decimale.
106 *
107 * @return latitudine (WGS84) come stringa.
108 */
109 public String getLatitude() {
110 return latitude;
111 }
112
113 /**
114 * Restituisce la longitudine in formato decimale.
115 *
116 * @return longitudine (WGS84) come stringa.
117 */
118 public String getLongitude() {
119 return longitude;
120 }
121
122
123 /**
124 * Restituisce la rappresentazione testuale della costante.
125 * <p>
126 * Convenzionalmente, la rappresentazione testuale coincide
127 * con il nome della città.
128 * </p>
129 *
130 * @return il nome della città.
131 */
132 @Override
133 public String toString() {
134 return cityName;
135 }
136 }
137
138
139}