461 lines
15 KiB
HTML
461 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="pl">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="description" content="Interaktywna mapa pogody – kliknij lub wyszukaj miasto, by sprawdzić prognozę.">
|
||
<title>Pogoda Web</title>
|
||
<link rel="icon" href="/favicon.ico" />
|
||
|
||
<!-- Bootstrap CSS -->
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||
|
||
<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>
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #e0f7fa, #e8eaf6);
|
||
color: #333;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
header {
|
||
background: linear-gradient(45deg, #2196f3, #0d47a1);
|
||
color: white;
|
||
padding: 2rem 1rem;
|
||
text-align: center;
|
||
box-shadow: 0 4px 10px rgba(33, 150, 243, 0.5);
|
||
border-radius: 0 0 1rem 1rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
header h1 {
|
||
font-weight: 900;
|
||
font-size: 2.8rem;
|
||
letter-spacing: 0.05em;
|
||
text-shadow: 1px 1px 4px rgba(0,0,0,0.3);
|
||
margin-bottom: 0.3rem;
|
||
}
|
||
|
||
header p {
|
||
font-size: 1.25rem;
|
||
font-weight: 500;
|
||
opacity: 0.85;
|
||
margin-bottom: 1rem;
|
||
text-shadow: 1px 1px 3px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
#quote {
|
||
font-style: italic;
|
||
font-size: 1.15rem;
|
||
margin-top: 0.5rem;
|
||
color: #e3f2fd;
|
||
text-shadow: 1px 1px 2px #0d47a1;
|
||
}
|
||
|
||
main.container {
|
||
flex-grow: 1;
|
||
max-width: 1100px;
|
||
}
|
||
|
||
#map {
|
||
height: 450px;
|
||
border-radius: 1rem;
|
||
box-shadow: 0 6px 15px rgba(33, 150, 243, 0.3);
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
#result {
|
||
background: white;
|
||
padding: 1.5rem;
|
||
border-radius: 1rem;
|
||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||
white-space: pre-wrap;
|
||
font-size: 1rem;
|
||
min-height: 150px;
|
||
margin-bottom: 2rem;
|
||
color: #1a237e;
|
||
font-weight: 600;
|
||
}
|
||
|
||
canvas {
|
||
background: white;
|
||
padding: 1rem;
|
||
border-radius: 1rem;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
margin-bottom: 3rem;
|
||
max-height: 350px;
|
||
width: 100% !important;
|
||
}
|
||
|
||
/* Dostosowanie input i button w kontrolkach */
|
||
#controls .form-control {
|
||
font-size: 1.1rem;
|
||
min-width: 200px;
|
||
border-radius: 0.75rem;
|
||
box-shadow: inset 0 2px 6px rgba(33, 150, 243, 0.15);
|
||
border: 1.5px solid #2196f3;
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
|
||
#controls .form-control:focus {
|
||
border-color: #0d47a1;
|
||
box-shadow: 0 0 8px #0d47a1;
|
||
}
|
||
|
||
#controls .btn-primary {
|
||
font-size: 1.1rem;
|
||
padding: 0.65rem 1.5rem;
|
||
border-radius: 0.75rem;
|
||
box-shadow: 0 6px 15px rgba(33, 150, 243, 0.4);
|
||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||
}
|
||
|
||
#controls .btn-primary:hover,
|
||
#controls .btn-primary:focus {
|
||
background-color: #0d47a1;
|
||
box-shadow: 0 8px 20px rgba(13, 71, 161, 0.7);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>🌦️ Pogoda Web</h1>
|
||
<p>Kliknij na mapie lub wyszukaj miasto, aby zobaczyć prognozę pogody</p>
|
||
<div id="quote"></div>
|
||
</header>
|
||
|
||
<main class="container">
|
||
<div id="controls" class="d-flex flex-wrap gap-3 mb-4">
|
||
<input
|
||
type="text"
|
||
id="cityInput"
|
||
class="form-control flex-grow-1"
|
||
placeholder="Wpisz miasto"
|
||
autocomplete="off"
|
||
aria-label="Wpisz nazwę miasta do wyszukania"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onclick="searchCity()"
|
||
class="btn btn-primary"
|
||
aria-label="Szukaj miasta"
|
||
>
|
||
Szukaj
|
||
</button>
|
||
</div>
|
||
|
||
<div id="map" title="Mapa pogodowa" aria-label="Mapa pogodowa z możliwością kliknięcia"></div>
|
||
|
||
<div id="result" aria-live="polite" aria-atomic="true">Kliknij na mapie lub wyszukaj miasto, aby zobaczyć pogodę.</div>
|
||
|
||
<canvas id="dailyChart" aria-label="Wykres prognozy dziennej"></canvas>
|
||
<canvas id="hourlyChart" aria-label="Wykres prognozy godzinowej"></canvas>
|
||
</main>
|
||
|
||
<script src="https://unpkg.com/leaflet/dist/leaflet.js" loading="lazy"></script>
|
||
<!-- Bootstrap Bundle JS -->
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></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>
|
||
|