first commit
This commit is contained in:
44
main.py
Normal file
44
main.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
import requests
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
BASE_URL = "https://meteo.cbpio.pl"
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
@app.route("/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)
|
||||||
|
|
316
templates/index.html
Normal file
316
templates/index.html
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Pogoda Web</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></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>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div id="controls">
|
||||||
|
<input type="text" id="cityInput" placeholder="Wpisz miasto" />
|
||||||
|
<button onclick="searchCity()">Szukaj</button>
|
||||||
|
</div>
|
||||||
|
<div id="map"></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"></script>
|
||||||
|
<script>
|
||||||
|
const map = L.map('map').setView([52.237, 21.017], 6);
|
||||||
|
let marker = null;
|
||||||
|
let currentPlaceName = '';
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }).addTo(map);
|
||||||
|
|
||||||
|
async function fetchPlaceName(lat, lon) {
|
||||||
|
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`;
|
||||||
|
const response = await fetch(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...");
|
||||||
|
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) {
|
||||||
|
updateResult("Proszę wpisać nazwę miasta.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateResult("Wyszukiwanie miasta...");
|
||||||
|
try {
|
||||||
|
const res = await fetch(`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 === 0) {
|
||||||
|
updateResult("Nie znaleziono miasta.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 fetch('/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) {
|
||||||
|
updateResult("Błąd serwera: " + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = data.current || {};
|
||||||
|
const daily = data.daily?.forecast || {};
|
||||||
|
const hourly = data.hourly?.hourly_forecast || {};
|
||||||
|
|
||||||
|
let resultText = `Lokalizacja: ${currentPlaceName}
|
||||||
|
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 (daily.time?.length) {
|
||||||
|
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 (hourly.time?.length) {
|
||||||
|
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());
|
||||||
|
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: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
title: { display: true, text: 'Temperatura (°C)' }
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
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ść' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
Reference in New Issue
Block a user