Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 6 additions & 26 deletions src/main/java/com/example/weatherapp/web/ReportController.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package com.example.weatherapp.web;

import com.example.weatherapp.weather.WeatherService;
import com.example.weatherapp.weather.WeatherService.WeatherReport;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -12,40 +8,24 @@
@Controller
public class ReportController {

private final WeatherService weatherService;
private final ObjectMapper objectMapper;

public ReportController(WeatherService weatherService) {
this.weatherService = weatherService;
this.objectMapper = new ObjectMapper();
public ReportController() {
}

@GetMapping("/report")
public String report(@RequestParam("lat") double lat,
@RequestParam("lon") double lon,
@RequestParam(value = "city", required = false) String city,
Model model) throws JsonProcessingException {
Model model) {
// Basic validation of ranges
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
model.addAttribute("error", "Invalid coordinates.");
return "report";
}

try {
WeatherReport report = weatherService.fetchHourlyReport(lat, lon);
String reportJson = objectMapper.writeValueAsString(report);

model.addAttribute("city", city);
model.addAttribute("lat", lat);
model.addAttribute("lon", lon);
model.addAttribute("timezone", report.getTimezone());
model.addAttribute("reportJson", reportJson);
} catch (Exception ex) {
model.addAttribute("city", city);
model.addAttribute("lat", lat);
model.addAttribute("lon", lon);
model.addAttribute("error", "Failed to fetch weather data. Please try again later.");
}
// Pass only basic info. Weather data will be fetched on the client side.
model.addAttribute("city", city);
model.addAttribute("lat", lat);
model.addAttribute("lon", lon);
return "report";
}
}
138 changes: 90 additions & 48 deletions src/main/resources/templates/report.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,76 +93,118 @@
<h1>
Weather report
<span th:if="${city}">for <span th:text="${city}"></span></span>
(<span th:text="${lat}">lat</span>, <span th:text="${lon}">lon</span>)
(<span id="lat-value" th:text="${lat}">lat</span>, <span id="lon-value" th:text="${lon}">lon</span>)
</h1>
<p class="muted">Timezone: <span th:text="${timezone}">auto</span></p>
<p class="muted">Timezone: <span id="timezone">auto</span></p>

<div th:if="${error}" class="error" th:text="${error}">An error occurred</div>
<div th:if="${error}" id="server-error" class="error" th:text="${error}">An error occurred</div>
<div id="client-error" class="error" style="display:none"></div>

<div id="charts" class="charts" th:if="${reportJson}">
<div id="loading" class="muted">Loading weather data…</div>

<div id="charts" class="charts" style="display:none">
<div id="tempChart" class="chart"></div>
<div id="precipChart" class="chart"></div>
<div id="windChart" class="chart"></div>
<div id="humidityChart" class="chart"></div>
</div>

<script id="report-json" type="application/json" th:utext="${reportJson}">{}</script>

<script>
// Load Google Charts
google.charts.load('current', { packages: ['corechart'] });
google.charts.setOnLoadCallback(drawCharts);
google.charts.setOnLoadCallback(initReport);

// Store chart data/options for redraw
let chartDataCache = {};

function drawCharts() {
const jsonEl = document.getElementById('report-json');
if (!jsonEl) return;
let report;
try {
report = JSON.parse(jsonEl.textContent || '{}');
} catch (e) {
console.error('Failed to parse report JSON', e);
function initReport() {
// If server-side validation failed, don't attempt client fetch
if (document.getElementById('server-error')) {
document.getElementById('loading').style.display = 'none';
return;
}
if (!report || !report.times || !report.temperature_2m) {
console.warn('No report data');
const latText = (document.getElementById('lat-value')?.textContent || '').trim();
const lonText = (document.getElementById('lon-value')?.textContent || '').trim();
const lat = parseFloat(latText);
const lon = parseFloat(lonText);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
showClientError('Invalid coordinates.');
return;
}
fetchAndDraw(lat, lon);
}

async function fetchAndDraw(lat, lon) {
const url = new URL('https://api.open-meteo.com/v1/forecast');
url.searchParams.set('latitude', lat);
url.searchParams.set('longitude', lon);
url.searchParams.set('hourly', [
'temperature_2m',
'precipitation',
'wind_speed_10m',
'relative_humidity_2m'
].join(','));
url.searchParams.set('forecast_days', '3');
url.searchParams.set('timezone', 'auto');

try {
const resp = await fetch(url.toString());
if (!resp.ok) throw new Error('Weather API request failed');
const data = await resp.json();
const hourly = data.hourly || {};
// Update timezone display
const tz = data.timezone || 'auto';
const tzEl = document.getElementById('timezone');
if (tzEl) tzEl.textContent = tz;

// Helper to convert ISO datetime string to JS Date
const toDate = (s) => new Date(String(s).replace(' ', 'T'));

// Prepare data for charts and cache for redraw
chartDataCache = {
tempChart: {
type: 'LineChart',
title: 'Temperature 2m (°C)',
columns: ['Time', 'Temperature (°C)'],
rows: report.times.map((t, i) => [toDate(t), safeNumber(report.temperature_2m[i])])
},
precipChart: {
type: 'ColumnChart',
title: 'Precipitation (mm)',
columns: ['Time', 'Precipitation (mm)'],
rows: report.times.map((t, i) => [toDate(t), safeNumber(report.precipitation?.[i])])
},
windChart: {
type: 'LineChart',
title: 'Wind speed 10m (m/s)',
columns: ['Time', 'Wind speed (m/s)'],
rows: report.times.map((t, i) => [toDate(t), safeNumber(report.wind_speed_10m?.[i])])
},
humidityChart: {
type: 'LineChart',
title: 'Relative humidity 2m (%)',
columns: ['Time', 'Humidity (%)'],
rows: report.times.map((t, i) => [toDate(t), safeNumber(report.relative_humidity_2m?.[i])])
if (!hourly.time || !hourly.temperature_2m) {
throw new Error('Incomplete data from weather API');
}
};

redrawAllCharts();
// Helper to convert ISO datetime string to JS Date
const toDate = (s) => new Date(String(s));

// Prepare data for charts and cache for redraw
chartDataCache = {
tempChart: {
type: 'LineChart',
title: 'Temperature 2m (°C)',
columns: ['Time', 'Temperature (°C)'],
rows: hourly.time.map((t, i) => [toDate(t), safeNumber(hourly.temperature_2m[i])])
},
precipChart: {
type: 'ColumnChart',
title: 'Precipitation (mm)',
columns: ['Time', 'Precipitation (mm)'],
rows: hourly.time.map((t, i) => [toDate(t), safeNumber(hourly.precipitation?.[i])])
},
windChart: {
type: 'LineChart',
title: 'Wind speed 10m (m/s)',
columns: ['Time', 'Wind speed (m/s)'],
rows: hourly.time.map((t, i) => [toDate(t), safeNumber(hourly.wind_speed_10m?.[i])])
},
humidityChart: {
type: 'LineChart',
title: 'Relative humidity 2m (%)',
columns: ['Time', 'Humidity (%)'],
rows: hourly.time.map((t, i) => [toDate(t), safeNumber(hourly.relative_humidity_2m?.[i])])
}
};

document.getElementById('loading').style.display = 'none';
document.getElementById('charts').style.display = '';
redrawAllCharts();
} catch (e) {
console.error(e);
showClientError('Failed to fetch weather data. Please try again later.');
}
}

function showClientError(message) {
document.getElementById('loading').style.display = 'none';
const el = document.getElementById('client-error');
el.textContent = message || 'An error occurred';
el.style.display = '';
}

function safeNumber(v) {
Expand Down