first commit
This commit is contained in:
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
weather-web:
|
||||||
|
build:
|
||||||
|
context: ./weather-webapp
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
networks:
|
||||||
|
- web-net
|
||||||
|
networks:
|
||||||
|
web-net:
|
||||||
|
external: true
|
||||||
|
name: shared-net
|
||||||
|
|
||||||
|
|
||||||
|
|
21
weather-webapp/Dockerfile
Normal file
21
weather-webapp/Dockerfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN update-ca-certificates
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "main:app"]
|
||||||
|
|
||||||
|
#CMD ["flask", "run", "--host=0.0.0.0", "--port=6000"]
|
||||||
|
|
||||||
|
#CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "6000"]
|
186
weather-webapp/main.py
Normal file
186
weather-webapp/main.py
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
import requests
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
BASE_URL = "https://meteo.cbpio.pl"
|
||||||
|
|
||||||
|
#app.route("/gui/weather")
|
||||||
|
#def simple_weather():
|
||||||
|
# return render_template("weather.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/gui/weather")
|
||||||
|
def simple_weather():
|
||||||
|
city = request.args.get("city", "Warszawa")
|
||||||
|
|
||||||
|
# Sprawdź, czy klient akceptuje JSON (np. curl)
|
||||||
|
if "application/json" in request.headers.get("Accept", "") or "curl" in request.headers.get("User-Agent", ""):
|
||||||
|
try:
|
||||||
|
response = requests.get(f"https://meteo.cbpio.pl/weather", params={"city": city})
|
||||||
|
return jsonify(response.json())
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
# Dla przeglądarki - HTML
|
||||||
|
return render_template("weather.html")
|
||||||
|
|
||||||
|
@app.route("/gui/weather/geo")
|
||||||
|
def geo_weather_page():
|
||||||
|
lat = request.args.get("lat")
|
||||||
|
lon = request.args.get("lon")
|
||||||
|
|
||||||
|
if not lat or not lon:
|
||||||
|
msg = {"error": "Podaj współrzędne, np. ?lat=52.4&lon=16.9"}
|
||||||
|
if "application/json" in request.headers.get("Accept", "") or "curl" in request.headers.get("User-Agent", ""):
|
||||||
|
return jsonify(msg), 400
|
||||||
|
return msg["error"], 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get("https://meteo.cbpio.pl/weather/geo", params={"latitude": lat, "longitude": lon})
|
||||||
|
data = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = {"error": str(e)}
|
||||||
|
if "application/json" in request.headers.get("Accept", "") or "curl" in request.headers.get("User-Agent", ""):
|
||||||
|
return jsonify(error_msg), 500
|
||||||
|
return f"Błąd: {e}", 500
|
||||||
|
|
||||||
|
if "application/json" in request.headers.get("Accept", "") or "curl" in request.headers.get("User-Agent", ""):
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
return render_template("geo_weather.html", weather=data)
|
||||||
|
|
||||||
|
@app.route("/gui/forecast")
|
||||||
|
def forecast():
|
||||||
|
city = request.args.get("city", "").strip()
|
||||||
|
if not city:
|
||||||
|
return render_template("forecast.html", city="", error="Nie podano miasta")
|
||||||
|
|
||||||
|
try:
|
||||||
|
forecast_data = requests.get(f"{BASE_URL}/forecast", params={"city": city}).json()
|
||||||
|
except Exception as e:
|
||||||
|
return render_template("forecast.html", city=city, error="Błąd pobierania danych")
|
||||||
|
|
||||||
|
accept = request.headers.get("Accept", "")
|
||||||
|
if "application/json" in accept:
|
||||||
|
return jsonify(forecast_data)
|
||||||
|
|
||||||
|
return render_template("forecast.html", city=forecast_data["city"], forecast=forecast_data["forecast"])
|
||||||
|
|
||||||
|
@app.route("/gui/forecast/hourly")
|
||||||
|
def hourly_forecast():
|
||||||
|
city = request.args.get("city", "").lower()
|
||||||
|
hours = request.args.get("hours", 24, type=int)
|
||||||
|
|
||||||
|
if not city:
|
||||||
|
return "Brak parametru 'city'", 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.get("https://meteo.cbpio.pl/forecast/hourly", params={"city": city, "hours": hours})
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Błąd pobierania danych: {str(e)}"
|
||||||
|
if "text/html" in request.headers.get("Accept", ""):
|
||||||
|
return render_template("hourly_forecast.html", city=city.capitalize(), hourly_forecast=None, error=error_msg)
|
||||||
|
return jsonify({"error": error_msg}), 500
|
||||||
|
|
||||||
|
hourly = data.get("hourly_forecast", {})
|
||||||
|
|
||||||
|
if "text/html" in request.headers.get("Accept", ""):
|
||||||
|
return render_template(
|
||||||
|
"hourly_forecast.html",
|
||||||
|
city=city.capitalize(),
|
||||||
|
hourly_forecast=hourly,
|
||||||
|
error=None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/gui/history/range")
|
||||||
|
def history_range():
|
||||||
|
city = request.args.get("city", "").lower()
|
||||||
|
start_date = request.args.get("start_date")
|
||||||
|
end_date = request.args.get("end_date")
|
||||||
|
accept = request.headers.get("Accept", "")
|
||||||
|
|
||||||
|
if not city or not start_date or not end_date:
|
||||||
|
error_msg = "Brak wymaganych parametrów"
|
||||||
|
if "text/html" in accept:
|
||||||
|
return render_template("history_range.html", city=city.capitalize(),
|
||||||
|
start_date=start_date, end_date=end_date,
|
||||||
|
history=None, error=error_msg)
|
||||||
|
else:
|
||||||
|
return jsonify({"error": error_msg}), 400
|
||||||
|
|
||||||
|
url = (
|
||||||
|
f"https://meteo.cbpio.pl/history-range"
|
||||||
|
f"?city={city}&start_date={start_date}&end_date={end_date}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = requests.get(url)
|
||||||
|
res.raise_for_status()
|
||||||
|
data = res.json()
|
||||||
|
except Exception as e:
|
||||||
|
if "text/html" in accept:
|
||||||
|
return render_template("history_range.html", city=city.capitalize(),
|
||||||
|
start_date=start_date, end_date=end_date,
|
||||||
|
history=None, error=str(e))
|
||||||
|
else:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
history = data.get("history") # ✅ WYCIĄGAMY KLUCZ "history" z JSON-a
|
||||||
|
|
||||||
|
if "text/html" in accept:
|
||||||
|
return render_template("history_range.html", city=city.capitalize(),
|
||||||
|
start_date=start_date, end_date=end_date,
|
||||||
|
history=history, error=None) # ✅ przekazujemy history, nie cały JSON
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"city": city.capitalize(),
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
"history": history
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/app/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
@app.route("/app/get_weather", methods=["POST"])
|
||||||
|
def get_weather():
|
||||||
|
data = request.get_json()
|
||||||
|
lat = data.get("lat")
|
||||||
|
lon = data.get("lon")
|
||||||
|
|
||||||
|
if lat is None or lon is None:
|
||||||
|
return jsonify({"error": "Missing coordinates"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
current = requests.get(f"{BASE_URL}/weather/geo", params={"latitude": lat, "longitude": lon}).json()
|
||||||
|
forecast = requests.get(f"{BASE_URL}/forecast/geo", params={
|
||||||
|
"latitude": lat,
|
||||||
|
"longitude": lon,
|
||||||
|
"days": 7
|
||||||
|
}).json()
|
||||||
|
hourly = requests.get(f"{BASE_URL}/forecast/hourly/geo", params={
|
||||||
|
"latitude": lat,
|
||||||
|
"longitude": lon,
|
||||||
|
"hours": 24
|
||||||
|
}).json()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"current": current,
|
||||||
|
"daily": forecast,
|
||||||
|
"hourly": hourly
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print("❌ Error fetching data:", e)
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
#app.run(debug=True)
|
||||||
|
app.run(debug=True,host='0.0.0.0', port=8000)
|
4
weather-webapp/requirements.txt
Normal file
4
weather-webapp/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
flask
|
||||||
|
requests
|
||||||
|
gunicorn
|
||||||
|
uvicorn
|
251
weather-webapp/templates/forecast.html
Normal file
251
weather-webapp/templates/forecast.html
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Prognoza pogody - {{ city }}</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
||||||
|
|
||||||
|
/* Reset i podstawy */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #74ebd5 0%, #ACB6E5 100%);
|
||||||
|
color: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #0d3b66;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-shadow: 1px 1px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #ff4c4c;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #ffe6e6;
|
||||||
|
padding: 15px 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 6px rgba(255,0,0,0.15);
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
thead tr {
|
||||||
|
background: linear-gradient(90deg, #0d3b66, #3f72af);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 14px 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
th:last-child, td:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
tbody tr:nth-child(even) {
|
||||||
|
background-color: #f7faff;
|
||||||
|
}
|
||||||
|
tbody tr:hover {
|
||||||
|
background-color: #dbe9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Canvas kontener */
|
||||||
|
#chartContainer {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 25px 30px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Prognoza pogody dla {{ city }}</h1>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="error">{{ error }}</p>
|
||||||
|
{% elif forecast and forecast.time %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Temperatura maks. (°C)</th>
|
||||||
|
<th>Temperatura min. (°C)</th>
|
||||||
|
<th>Opady (mm)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in range(forecast.time|length) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ forecast.time[i] }}</td>
|
||||||
|
<td>{{ forecast.temperature_2m_max[i] }}</td>
|
||||||
|
<td>{{ forecast.temperature_2m_min[i] }}</td>
|
||||||
|
<td>{{ forecast.precipitation_sum[i] }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div id="chartContainer">
|
||||||
|
<canvas id="weatherChart" width="800" height="400"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const labels = {{ forecast.time|tojson }};
|
||||||
|
const tempMax = {{ forecast.temperature_2m_max|tojson }};
|
||||||
|
const tempMin = {{ forecast.temperature_2m_min|tojson }};
|
||||||
|
const precipitation = {{ forecast.precipitation_sum|tojson }};
|
||||||
|
|
||||||
|
const ctx = document.getElementById('weatherChart').getContext('2d');
|
||||||
|
|
||||||
|
const weatherChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Temperatura maksymalna (°C)',
|
||||||
|
data: tempMax,
|
||||||
|
borderColor: 'rgba(255, 99, 132, 1)',
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.3)',
|
||||||
|
fill: false,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 5,
|
||||||
|
pointHoverRadius: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Temperatura minimalna (°C)',
|
||||||
|
data: tempMin,
|
||||||
|
borderColor: 'rgba(54, 162, 235, 1)',
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.3)',
|
||||||
|
fill: false,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 5,
|
||||||
|
pointHoverRadius: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Opady (mm)',
|
||||||
|
data: precipitation,
|
||||||
|
borderColor: 'rgba(75, 192, 192, 1)',
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.3)',
|
||||||
|
fill: true,
|
||||||
|
yAxisID: 'y1',
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 5,
|
||||||
|
pointHoverRadius: 7,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
interaction: {
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Temperatura (°C)',
|
||||||
|
font: { weight: 'bold' }
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0,0,0,0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'right',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Opady (mm)',
|
||||||
|
font: { weight: 'bold' }
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
font: { size: 14, weight: '600' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Prognoza pogody',
|
||||||
|
font: { size: 20, weight: '700' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||||
|
titleFont: { size: 16, weight: 'bold' },
|
||||||
|
bodyFont: { size: 14 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'easeOutQuart'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<p>Brak danych pogodowych.</p>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
27
weather-webapp/templates/geo_weather.html
Normal file
27
weather-webapp/templates/geo_weather.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Pogoda z koordynatów</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; background: #f0f0f0; padding: 2rem; }
|
||||||
|
.card {
|
||||||
|
background: white; padding: 2rem; border-radius: 10px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 500px; margin: auto;
|
||||||
|
}
|
||||||
|
.label { font-weight: bold; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Pogoda z Open-Meteo</h2>
|
||||||
|
<p><span class="label">Miasto:</span> {{ weather.city }}</p>
|
||||||
|
<p><span class="label">Szerokość:</span> {{ weather.latitude }}</p>
|
||||||
|
<p><span class="label">Długość:</span> {{ weather.longitude }}</p>
|
||||||
|
<p><span class="label">Temperatura:</span> {{ weather.temperature }} °C</p>
|
||||||
|
<p><span class="label">Wiatr:</span> {{ weather.windspeed }} m/s</p>
|
||||||
|
<p><span class="label">Czas:</span> {{ weather.time }}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
209
weather-webapp/templates/history_range.html
Normal file
209
weather-webapp/templates/history_range.html
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Historia pogody - {{ city }}</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #FDFCFB 0%, #E2D1C3 100%);
|
||||||
|
color: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #2a2a72;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff4c4c;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #ffe6e6;
|
||||||
|
padding: 15px 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 6px rgba(255,0,0,0.15);
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr {
|
||||||
|
background: linear-gradient(90deg, #2a2a72, #009ffd);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 14px 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
th:last-child, td:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) {
|
||||||
|
background-color: #f7faff;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background-color: #dbe9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-width: 95%;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Historia pogody dla {{ city }}<br>od {{ start_date }} do {{ end_date }}</h1>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="error">{{ error }}</p>
|
||||||
|
{% elif history %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Temp. maks. (°C)</th>
|
||||||
|
<th>Temp. min. (°C)</th>
|
||||||
|
<th>Opady (mm)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in range(history["time"]|length) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ history["time"][i] }}</td>
|
||||||
|
<td>{{ history["temperature_2m_max"][i] }}</td>
|
||||||
|
<td>{{ history["temperature_2m_min"][i] }}</td>
|
||||||
|
<td>{{ history["precipitation_sum"][i] }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Wykres -->
|
||||||
|
<canvas id="historyChart" width="800" height="400"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const labels = {{ history["time"]|tojson }};
|
||||||
|
const tempMax = {{ history["temperature_2m_max"]|tojson }};
|
||||||
|
const tempMin = {{ history["temperature_2m_min"]|tojson }};
|
||||||
|
const precipitation = {{ history["precipitation_sum"]|tojson }};
|
||||||
|
|
||||||
|
const ctx = document.getElementById('historyChart').getContext('2d');
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Temperatura maksymalna (°C)',
|
||||||
|
data: tempMax,
|
||||||
|
borderColor: 'rgba(255, 99, 132, 1)',
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||||
|
fill: false,
|
||||||
|
tension: 0.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Temperatura minimalna (°C)',
|
||||||
|
data: tempMin,
|
||||||
|
borderColor: 'rgba(54, 162, 235, 1)',
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||||
|
fill: false,
|
||||||
|
tension: 0.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Opady (mm)',
|
||||||
|
data: precipitation,
|
||||||
|
borderColor: 'rgba(75, 192, 192, 1)',
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||||
|
fill: true,
|
||||||
|
yAxisID: 'y1',
|
||||||
|
tension: 0.3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Temperatura (°C)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'right',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Opady (mm)'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Historia pogody'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
221
weather-webapp/templates/hourly_forecast.html
Normal file
221
weather-webapp/templates/hourly_forecast.html
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Prognoza godzinowa - {{ city }}</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
background-color: #eef2f3;
|
||||||
|
color: #222;
|
||||||
|
padding: 30px;
|
||||||
|
transition: background 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
body.dark {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2a2a72;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.dark h1 {
|
||||||
|
color: #90caf9;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.controls input, .controls button {
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
margin: 20px auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #2a6ebb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.dark table {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
.dark th {
|
||||||
|
background-color: #37474f;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
margin: 40px auto;
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Prognoza godzinowa dla {{ city }}</h1>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<label>Od godziny: <input type="time" id="startHour"></label>
|
||||||
|
<label>Do godziny: <input type="time" id="endHour"></label>
|
||||||
|
<button onclick="filterByHour()">Filtruj</button>
|
||||||
|
<button onclick="toggleDarkMode()">🌜 Przełącz tryb nocny</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="error">{{ error }}</p>
|
||||||
|
{% elif hourly_forecast %}
|
||||||
|
<table id="hourlyTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Godzina</th><th>Temp. (°C)</th><th>Wiatr (km/h)</th><th>Opady (mm)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in range(hourly_forecast["time"]|length) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ hourly_forecast["time"][i] }}</td>
|
||||||
|
<td>{{ hourly_forecast["temperature_2m"][i] }}</td>
|
||||||
|
<td>{{ hourly_forecast["windspeed_10m"][i] }}</td>
|
||||||
|
<td>{{ hourly_forecast["precipitation"][i] }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<canvas id="hourlyChart" width="800" height="400"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const originalData = {
|
||||||
|
labels: {{ hourly_forecast["time"]|tojson }},
|
||||||
|
temperature: {{ hourly_forecast["temperature_2m"]|tojson }},
|
||||||
|
wind: {{ hourly_forecast["windspeed_10m"]|tojson }},
|
||||||
|
precipitation: {{ hourly_forecast["precipitation"]|tojson }}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = document.getElementById('hourlyChart').getContext('2d');
|
||||||
|
let chart;
|
||||||
|
|
||||||
|
function renderChart(data) {
|
||||||
|
if (chart) chart.destroy();
|
||||||
|
chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: data.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Temperatura (°C)',
|
||||||
|
data: data.temperature,
|
||||||
|
borderColor: 'rgba(255,99,132,1)',
|
||||||
|
backgroundColor: 'rgba(255,99,132,0.2)',
|
||||||
|
tension: 0.3,
|
||||||
|
yAxisID: 'y'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Wiatr (km/h)',
|
||||||
|
data: data.wind,
|
||||||
|
borderColor: 'rgba(255,159,64,1)',
|
||||||
|
backgroundColor: 'rgba(255,159,64,0.2)',
|
||||||
|
tension: 0.3,
|
||||||
|
yAxisID: 'y'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Opady (mm)',
|
||||||
|
data: data.precipitation,
|
||||||
|
borderColor: 'rgba(75,192,192,1)',
|
||||||
|
backgroundColor: 'rgba(75,192,192,0.2)',
|
||||||
|
fill: true,
|
||||||
|
yAxisID: 'y1',
|
||||||
|
tension: 0.3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
title: { display: true, text: 'Prognoza godzinowa' },
|
||||||
|
zoom: {
|
||||||
|
pan: {
|
||||||
|
enabled: true,
|
||||||
|
mode: 'x',
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
wheel: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
pinch: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
mode: 'x'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
position: 'left',
|
||||||
|
title: { display: true, text: 'Temperatura / Wiatr' }
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
position: 'right',
|
||||||
|
title: { display: true, text: 'Opady (mm)' },
|
||||||
|
grid: { drawOnChartArea: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByHour() {
|
||||||
|
const start = document.getElementById('startHour').value;
|
||||||
|
const end = document.getElementById('endHour').value;
|
||||||
|
if (!start || !end) return;
|
||||||
|
|
||||||
|
const filtered = {
|
||||||
|
labels: [],
|
||||||
|
temperature: [],
|
||||||
|
wind: [],
|
||||||
|
precipitation: []
|
||||||
|
};
|
||||||
|
|
||||||
|
originalData.labels.forEach((time, i) => {
|
||||||
|
const hour = time.slice(11, 16);
|
||||||
|
if (hour >= start && hour <= end) {
|
||||||
|
filtered.labels.push(time);
|
||||||
|
filtered.temperature.push(originalData.temperature[i]);
|
||||||
|
filtered.wind.push(originalData.wind[i]);
|
||||||
|
filtered.precipitation.push(originalData.precipitation[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderChart(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDarkMode() {
|
||||||
|
document.body.classList.toggle('dark');
|
||||||
|
renderChart(chart.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChart(originalData);
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
408
weather-webapp/templates/index.html
Normal file
408
weather-webapp/templates/index.html
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
<!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>
|
||||||
|
//if (window.location.pathname === "/ping") {
|
||||||
|
// window.location.href = "/ping.html";
|
||||||
|
//}
|
||||||
|
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('/app/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>
|
||||||
|
|
||||||
|
|
55
weather-webapp/templates/ping.html
Normal file
55
weather-webapp/templates/ping.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Status serwera</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
background: #e0f2f1;
|
||||||
|
color: #004d40;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.box {
|
||||||
|
background: white;
|
||||||
|
padding: 2em;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="box" id="statusBox">
|
||||||
|
<h1>🟡 Ładowanie...</h1>
|
||||||
|
<p>Trwa sprawdzanie statusu serwera...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
fetch("/ping", { headers: { Accept: "application/json" } })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const box = document.getElementById("statusBox");
|
||||||
|
box.innerHTML = `
|
||||||
|
<h1>✅ Serwer działa</h1>
|
||||||
|
<p>Status: <strong>${data.status}</strong></p>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
const box = document.getElementById("statusBox");
|
||||||
|
box.innerHTML = `
|
||||||
|
<h1 class="error">❌ Błąd</h1>
|
||||||
|
<p class="error">Nie udało się połączyć z serwerem:<br>${err.message}</p>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
460
weather-webapp/templates/tmp/bootstrap_index.html
Normal file
460
weather-webapp/templates/tmp/bootstrap_index.html
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
<!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>
|
||||||
|
|
67
weather-webapp/templates/weather.html
Normal file
67
weather-webapp/templates/weather.html
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Pogoda</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
background: #e8f0fe;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Pogoda w <span id="city">...</span></h1>
|
||||||
|
<div class="info"><span class="label">Temperatura:</span> <span id="temp">...</span> °C</div>
|
||||||
|
<div class="info"><span class="label">Wiatr:</span> <span id="wind">...</span> m/s</div>
|
||||||
|
<div class="info"><span class="label">Czas pomiaru:</span> <span id="time">...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Pobierz ?city=poznan z adresu URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const city = urlParams.get("city") || "Warszawa";
|
||||||
|
|
||||||
|
fetch(`https://meteo.cbpio.pl/weather?city=${encodeURIComponent(city)}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById("city").textContent = data.city;
|
||||||
|
document.getElementById("temp").textContent = data.temperature.toFixed(1);
|
||||||
|
document.getElementById("wind").textContent = data.windspeed.toFixed(1);
|
||||||
|
document.getElementById("time").textContent = new Date(data.time).toLocaleString("pl-PL");
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
document.querySelector(".card").innerHTML = "<p style='color: red;'>Błąd ładowania danych pogodowych.</p>";
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
1
weather-webapp/templates/wzory
Symbolic link
1
weather-webapp/templates/wzory
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/home/jakub/services/meteo/server/templates/
|
Reference in New Issue
Block a user