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 + "¤t=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}