|
93 | 93 | <h1> |
94 | 94 | Weather report |
95 | 95 | <span th:if="${city}">for <span th:text="${city}"></span></span> |
96 | | - (<span th:text="${lat}">lat</span>, <span th:text="${lon}">lon</span>) |
| 96 | + (<span id="lat-value" th:text="${lat}">lat</span>, <span id="lon-value" th:text="${lon}">lon</span>) |
97 | 97 | </h1> |
98 | | - <p class="muted">Timezone: <span th:text="${timezone}">auto</span></p> |
| 98 | + <p class="muted">Timezone: <span id="timezone">auto</span></p> |
99 | 99 |
|
100 | | - <div th:if="${error}" class="error" th:text="${error}">An error occurred</div> |
| 100 | + <div th:if="${error}" id="server-error" class="error" th:text="${error}">An error occurred</div> |
| 101 | + <div id="client-error" class="error" style="display:none"></div> |
101 | 102 |
|
102 | | - <div id="charts" class="charts" th:if="${reportJson}"> |
| 103 | + <div id="loading" class="muted">Loading weather data…</div> |
| 104 | + |
| 105 | + <div id="charts" class="charts" style="display:none"> |
103 | 106 | <div id="tempChart" class="chart"></div> |
104 | 107 | <div id="precipChart" class="chart"></div> |
105 | 108 | <div id="windChart" class="chart"></div> |
106 | 109 | <div id="humidityChart" class="chart"></div> |
107 | 110 | </div> |
108 | 111 |
|
109 | | - <script id="report-json" type="application/json" th:utext="${reportJson}">{}</script> |
110 | | - |
111 | 112 | <script> |
112 | 113 | // Load Google Charts |
113 | 114 | google.charts.load('current', { packages: ['corechart'] }); |
114 | | - google.charts.setOnLoadCallback(drawCharts); |
| 115 | + google.charts.setOnLoadCallback(initReport); |
115 | 116 |
|
116 | | - // Store chart data/options for redraw |
117 | 117 | let chartDataCache = {}; |
118 | 118 |
|
119 | | - function drawCharts() { |
120 | | - const jsonEl = document.getElementById('report-json'); |
121 | | - if (!jsonEl) return; |
122 | | - let report; |
123 | | - try { |
124 | | - report = JSON.parse(jsonEl.textContent || '{}'); |
125 | | - } catch (e) { |
126 | | - console.error('Failed to parse report JSON', e); |
| 119 | + function initReport() { |
| 120 | + // If server-side validation failed, don't attempt client fetch |
| 121 | + if (document.getElementById('server-error')) { |
| 122 | + document.getElementById('loading').style.display = 'none'; |
127 | 123 | return; |
128 | 124 | } |
129 | | - if (!report || !report.times || !report.temperature_2m) { |
130 | | - console.warn('No report data'); |
| 125 | + const latText = (document.getElementById('lat-value')?.textContent || '').trim(); |
| 126 | + const lonText = (document.getElementById('lon-value')?.textContent || '').trim(); |
| 127 | + const lat = parseFloat(latText); |
| 128 | + const lon = parseFloat(lonText); |
| 129 | + if (!Number.isFinite(lat) || !Number.isFinite(lon)) { |
| 130 | + showClientError('Invalid coordinates.'); |
131 | 131 | return; |
132 | 132 | } |
| 133 | + fetchAndDraw(lat, lon); |
| 134 | + } |
| 135 | + |
| 136 | + async function fetchAndDraw(lat, lon) { |
| 137 | + const url = new URL('https://api.open-meteo.com/v1/forecast'); |
| 138 | + url.searchParams.set('latitude', lat); |
| 139 | + url.searchParams.set('longitude', lon); |
| 140 | + url.searchParams.set('hourly', [ |
| 141 | + 'temperature_2m', |
| 142 | + 'precipitation', |
| 143 | + 'wind_speed_10m', |
| 144 | + 'relative_humidity_2m' |
| 145 | + ].join(',')); |
| 146 | + url.searchParams.set('forecast_days', '3'); |
| 147 | + url.searchParams.set('timezone', 'auto'); |
| 148 | + |
| 149 | + try { |
| 150 | + const resp = await fetch(url.toString()); |
| 151 | + if (!resp.ok) throw new Error('Weather API request failed'); |
| 152 | + const data = await resp.json(); |
| 153 | + const hourly = data.hourly || {}; |
| 154 | + // Update timezone display |
| 155 | + const tz = data.timezone || 'auto'; |
| 156 | + const tzEl = document.getElementById('timezone'); |
| 157 | + if (tzEl) tzEl.textContent = tz; |
133 | 158 |
|
134 | | - // Helper to convert ISO datetime string to JS Date |
135 | | - const toDate = (s) => new Date(String(s).replace(' ', 'T')); |
136 | | - |
137 | | - // Prepare data for charts and cache for redraw |
138 | | - chartDataCache = { |
139 | | - tempChart: { |
140 | | - type: 'LineChart', |
141 | | - title: 'Temperature 2m (°C)', |
142 | | - columns: ['Time', 'Temperature (°C)'], |
143 | | - rows: report.times.map((t, i) => [toDate(t), safeNumber(report.temperature_2m[i])]) |
144 | | - }, |
145 | | - precipChart: { |
146 | | - type: 'ColumnChart', |
147 | | - title: 'Precipitation (mm)', |
148 | | - columns: ['Time', 'Precipitation (mm)'], |
149 | | - rows: report.times.map((t, i) => [toDate(t), safeNumber(report.precipitation?.[i])]) |
150 | | - }, |
151 | | - windChart: { |
152 | | - type: 'LineChart', |
153 | | - title: 'Wind speed 10m (m/s)', |
154 | | - columns: ['Time', 'Wind speed (m/s)'], |
155 | | - rows: report.times.map((t, i) => [toDate(t), safeNumber(report.wind_speed_10m?.[i])]) |
156 | | - }, |
157 | | - humidityChart: { |
158 | | - type: 'LineChart', |
159 | | - title: 'Relative humidity 2m (%)', |
160 | | - columns: ['Time', 'Humidity (%)'], |
161 | | - rows: report.times.map((t, i) => [toDate(t), safeNumber(report.relative_humidity_2m?.[i])]) |
| 159 | + if (!hourly.time || !hourly.temperature_2m) { |
| 160 | + throw new Error('Incomplete data from weather API'); |
162 | 161 | } |
163 | | - }; |
164 | 162 |
|
165 | | - redrawAllCharts(); |
| 163 | + // Helper to convert ISO datetime string to JS Date |
| 164 | + const toDate = (s) => new Date(String(s)); |
| 165 | + |
| 166 | + // Prepare data for charts and cache for redraw |
| 167 | + chartDataCache = { |
| 168 | + tempChart: { |
| 169 | + type: 'LineChart', |
| 170 | + title: 'Temperature 2m (°C)', |
| 171 | + columns: ['Time', 'Temperature (°C)'], |
| 172 | + rows: hourly.time.map((t, i) => [toDate(t), safeNumber(hourly.temperature_2m[i])]) |
| 173 | + }, |
| 174 | + precipChart: { |
| 175 | + type: 'ColumnChart', |
| 176 | + title: 'Precipitation (mm)', |
| 177 | + columns: ['Time', 'Precipitation (mm)'], |
| 178 | + rows: hourly.time.map((t, i) => [toDate(t), safeNumber(hourly.precipitation?.[i])]) |
| 179 | + }, |
| 180 | + windChart: { |
| 181 | + type: 'LineChart', |
| 182 | + title: 'Wind speed 10m (m/s)', |
| 183 | + columns: ['Time', 'Wind speed (m/s)'], |
| 184 | + rows: hourly.time.map((t, i) => [toDate(t), safeNumber(hourly.wind_speed_10m?.[i])]) |
| 185 | + }, |
| 186 | + humidityChart: { |
| 187 | + type: 'LineChart', |
| 188 | + title: 'Relative humidity 2m (%)', |
| 189 | + columns: ['Time', 'Humidity (%)'], |
| 190 | + rows: hourly.time.map((t, i) => [toDate(t), safeNumber(hourly.relative_humidity_2m?.[i])]) |
| 191 | + } |
| 192 | + }; |
| 193 | + |
| 194 | + document.getElementById('loading').style.display = 'none'; |
| 195 | + document.getElementById('charts').style.display = ''; |
| 196 | + redrawAllCharts(); |
| 197 | + } catch (e) { |
| 198 | + console.error(e); |
| 199 | + showClientError('Failed to fetch weather data. Please try again later.'); |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + function showClientError(message) { |
| 204 | + document.getElementById('loading').style.display = 'none'; |
| 205 | + const el = document.getElementById('client-error'); |
| 206 | + el.textContent = message || 'An error occurred'; |
| 207 | + el.style.display = ''; |
166 | 208 | } |
167 | 209 |
|
168 | 210 | function safeNumber(v) { |
|
0 commit comments