Files
weather-webapp/templates/index.html
2025-07-15 12:24:46 +02:00

406 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Interaktywna mapa pogody kliknij lub wyszukaj miasto, by sprawdzić prognozę.">
<title>Pogoda Web</title>
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" loading="lazy" />
<script src="https://cdn.jsdelivr.net/npm/chart.js" loading="lazy"></script>
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #e0f7fa, #e8eaf6);
color: #333;
}
header {
padding: 20px;
background: #2196f3;
color: white;
text-align: center;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
main {
max-width: 1000px;
margin: auto;
padding: 20px;
}
#controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
}
input, button {
font-size: 1em;
padding: 10px;
border-radius: 6px;
border: 1px solid #ccc;
}
button {
background: #2196f3;
color: white;
border: none;
transition: background 0.3s;
cursor: pointer;
}
button:hover {
background: #1976d2;
}
#map {
height: 400px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
#result {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
white-space: pre-wrap;
margin-bottom: 20px;
}
canvas {
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
margin-bottom: 40px;
}
</style>
</head>
<body>
<header>
<h1>🌦️ Pogoda Web</h1>
<p>Kliknij na mapie lub wyszukaj miasto, aby zobaczyć prognozę pogody</p>
<div id="quote" style="margin-top: 1em; font-style: italic; font-size: 1.1em;"></div>
</header>
<main>
<div id="controls">
<input type="text" id="cityInput" placeholder="Wpisz miasto" autocomplete="off" />
<button onclick="searchCity()">Szukaj</button>
</div>
<div id="map" title="Mapa pogodowa" aria-label="Mapa pogodowa z możliwością kliknięcia"></div>
<div id="result">Kliknij na mapie lub wyszukaj miasto, aby zobaczyć pogodę.</div>
<canvas id="dailyChart"></canvas>
<canvas id="hourlyChart"></canvas>
</main>
<script src="https://unpkg.com/leaflet/dist/leaflet.js" loading="lazy"></script>
<script>
const weatherQuotes = [
"Ale nie do nas należy panowanie nad wszystkimi erami tego świata; my mamy za zadanie zrobić, co w naszej mocy, dla epoki, w której żyjemy, wytrzebić zło ze znanego nam pola, aby przekazać następcom rolę czystą, gotową do uprawy. Jaka im będzie sprzyjała pogoda, to już nie nasza rzecz.",
"Czyli do czego to służy? - nadal nie rozumiałam. - Niszczy wszystko, całe życie - złowieszczy głos Zafkiela potoczył się po pomieszczeniu. - TO KATAKLIZM, KATAKLIZM, KATAKLIZM! I oto dowód na to, co się dzieje, kiedy ktoś przez kilka miliardów lat włącza i wyłącza deszcz. Zdecydowanie nie polecam tego zawodu.",
" Jesteś dobrym człowiekiem. ...na złą pogodę.",
"Nie ma złej pogody, jest tylko złe ubranie!",
"Pomyślmy. Dziś mamy środę, a że to mój dzień, nazywaj mnie Wednesday, Panem Wednesday. Choć, zważywszy na pogodę, równie dobrze mógłbym nazywać się Thursday.",
"Nie jestem ja rządcą pogody i nie jest nim żadna istota dwunożna.",
" (...) Proszę spojrzeć przez okno. I powiedzieć, co pan widzi. Mgłę — odparł najwyższy kapłan. Vetinari westchnął. Czasami pogoda zupełnie nie miała wyczucia konwencji narracyjnej.",
"Czasem kluczem do sukcesu było właśnie cierpliwe wyczekiwanie. Jeśli pogoda ci nie odpowiada, nie pakujesz się w sam środek burzy, tylko czekasz, aż burza minie. Po co niepotrzebnie moknąć?",
"Internetowe wyzwiska znikają jak pogoda. Do nienawiści w małżeństwie trzeba się przyłożyć. Jest szczera.",
"Zima nie umiera. Nie tak jak umierają ludzie. Trzyma się późnych przymrozków, zapachu jesieni w letnie wieczory, a w czasie upałów ucieka w góry.",
"Historia ludzkości to w siedemdziesięciu trzech procentach rozmowy o pogodzie wyrażające prawdziwy strach."
];
function getRandomQuote() {
const index = Math.floor(Math.random() * weatherQuotes.length);
return weatherQuotes[index];
}
const map = L.map('map').setView([52.237, 21.017], 6);
let marker = null;
let currentPlaceName = null;
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }).addTo(map);
const rainLayer = L.tileLayer('', {
opacity: 0.6,
zIndex: 100,
attribution: 'Dane radarowe: RainViewer.com'
});
async function updateRainLayer() {
try {
const res = await fetch('https://api.rainviewer.com/public/weather-maps.json');
const data = await res.json();
const timestamps = data.radar.past;
if (!timestamps.length) return;
const latestTime = timestamps[timestamps.length - 1];
const tileUrl = `https://tilecache.rainviewer.com/v2/radar/${latestTime}/256/{z}/{x}/{y}/2/1_1.png`;
rainLayer.setUrl(tileUrl);
map.addLayer(rainLayer);
} catch (err) {
console.error("Błąd pobierania warstwy deszczu:", err);
}
}
async function fetchWithTimeout(resource, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
options.signal = controller.signal;
try {
const response = await fetch(resource, options);
clearTimeout(timeout);
return response;
} catch (error) {
clearTimeout(timeout);
throw error;
}
}
async function fetchPlaceName(lat, lon) {
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`;
const response = await fetchWithTimeout(url, {
headers: { 'User-Agent': 'PogodaApp/1.0' }
});
if (!response.ok) throw new Error('Błąd podczas pobierania nazwy miejsca.');
const data = await response.json();
return data.display_name || 'Nieznana lokalizacja';
}
function setMarker(lat, lon, name) {
if (marker) map.removeLayer(marker);
marker = L.marker([lat, lon]).addTo(map);
if (name) marker.bindPopup(name).openPopup();
}
map.on('click', async (e) => {
const { lat, lng } = e.latlng;
setMarker(lat, lng, "Ładowanie nazwy...");
map.setView([lat, lng], 10);
updateResult("Ładowanie nazwy lokalizacji...");
try {
const placeName = await fetchPlaceName(lat, lng);
currentPlaceName = placeName;
setMarker(lat, lng, placeName);
updateResult("Ładowanie pogody...");
await fetchWeather(lat, lng);
} catch (err) {
updateResult("Błąd: " + err.message);
}
});
function updateResult(text) {
document.getElementById('result').innerText = text;
}
async function searchCity() {
const city = document.getElementById('cityInput').value.trim();
if (!city) return updateResult("Proszę wpisać nazwę miasta.");
updateResult("Wyszukiwanie miasta...");
try {
const res = await fetchWithTimeout(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(city)}`);
if (!res.ok) throw new Error('Błąd podczas wyszukiwania miasta.');
const data = await res.json();
if (!data.length) return updateResult("Nie znaleziono miasta.");
const { lat, lon, display_name } = data[0];
currentPlaceName = display_name;
map.setView([lat, lon], 10);
setMarker(lat, lon, display_name);
updateResult("Ładowanie pogody...");
await fetchWeather(lat, lon);
} catch (err) {
updateResult("Błąd: " + err.message);
}
}
let dailyChart = null;
let hourlyChart = null;
async function fetchWeather(lat, lon) {
try {
const res = await fetchWithTimeout('/get_weather', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ lat, lon })
});
if (!res.ok) throw new Error(`Błąd serwera: ${res.status}`);
const data = await res.json();
if (data.error) return updateResult("Błąd serwera: " + data.error);
const current = data.current ?? {};
const daily = data.daily?.forecast ?? {};
const hourly = data.hourly?.hourly_forecast ?? {};
let resultText = `Lokalizacja: ${currentPlaceName ?? 'Nieznana lokalizacja'}
Temperatura: ${current.temperature ?? 'brak danych'} °C
Wiatr: ${current.windspeed ?? 'brak danych'} km/h
Czas pomiaru: ${current.time ? new Date(current.time).toLocaleString() : 'brak danych'}`;
if (Array.isArray(daily.time) && daily.time.length > 0) {
resultText += "\n\nPrognoza 7-dniowa:\n";
for (let i = 0; i < daily.time.length; i++) {
resultText += `${daily.time[i]}: ${daily.temperature_2m_min?.[i]}${daily.temperature_2m_max?.[i]} °C, Opady: ${daily.precipitation_sum?.[i]} mm\n`;
}
}
if (Array.isArray(hourly.time) && hourly.time.length > 0) {
resultText += "\n\nNajbliższe godziny:\n";
for (let i = 0; i < Math.min(24, hourly.time.length); i++) {
resultText += `${hourly.time[i]}: ${hourly.temperature_2m?.[i]} °C, Wiatr: ${hourly.windspeed_10m?.[i]} km/h, Opady: ${hourly.precipitation?.[i]} mm\n`;
}
}
updateResult(resultText.trim());
const quote = getRandomQuote();
document.getElementById('quote').innerText = `Cytat dnia:\n"${quote}"`;
drawDailyChart(daily);
drawHourlyChart(hourly);
} catch (err) {
updateResult("Błąd podczas pobierania pogody: " + err.message);
}
}
function drawDailyChart(data) {
const ctx = document.getElementById('dailyChart').getContext('2d');
if (dailyChart) dailyChart.destroy();
dailyChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.time,
datasets: [
{
label: 'Temp. max (°C)',
data: data.temperature_2m_max,
backgroundColor: 'rgba(255, 99, 132, 0.6)'
},
{
label: 'Temp. min (°C)',
data: data.temperature_2m_min,
backgroundColor: 'rgba(54, 162, 235, 0.6)'
},
{
label: 'Opady (mm)',
data: data.precipitation_sum,
type: 'line',
borderColor: 'rgba(0, 128, 0, 0.8)',
fill: false,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
stacked: false,
scales: {
y: {
title: { display: true, text: 'Temperatura (°C)' }
},
y1: {
position: 'right',
title: { display: true, text: 'Opady (mm)' },
grid: { drawOnChartArea: false }
}
}
}
});
}
function drawHourlyChart(data) {
const ctx = document.getElementById('hourlyChart').getContext('2d');
if (hourlyChart) hourlyChart.destroy();
hourlyChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.time,
datasets: [
{
label: 'Temperatura (°C)',
data: data.temperature_2m,
borderColor: 'orange',
fill: false
},
{
label: 'Wiatr (km/h)',
data: data.windspeed_10m,
borderColor: 'blue',
fill: false
},
{
label: 'Opady (mm)',
data: data.precipitation,
borderColor: 'green',
fill: false
}
]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: 'Wartość' }
}
}
}
});
}
function getUserLocation() {
if (navigator.geolocation) {
updateResult("Pobieranie Twojej lokalizacji...");
navigator.geolocation.getCurrentPosition(
async (position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
setMarker(lat, lon, "Ładowanie nazwy...");
map.setView([lat, lon], 10);
try {
const placeName = await fetchPlaceName(lat, lon);
currentPlaceName = placeName;
setMarker(lat, lon, placeName);
updateResult("Ładowanie pogody...");
await fetchWeather(lat, lon);
} catch (err) {
updateResult("Błąd lokalizacji: " + err.message);
}
},
(error) => {
updateResult("Nie udało się uzyskać lokalizacji: " + error.message);
}
);
} else {
updateResult("Geolokalizacja nie jest obsługiwana w tej przeglądarce.");
}
}
window.addEventListener('load', () => {
getUserLocation();
});
updateRainLayer();
</script>
</body>
</html>