Add weather report from Open-Meteo for Passau
This commit is contained in:
1 parent
cc958cb7bb
commit
5ea523e6c4
1 file changed
+246
-1
+246
-1
@@ -71,6 +71,77 @@
|
||||
font-size: 11px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.weather-divider {
|
||||
height: 1px;
|
||||
background: rgba(230, 223, 209, 0.2);
|
||||
margin: 8px 0;
|
||||
}
|
||||
.weather-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 0 8px 0;
|
||||
border-bottom: 1px solid rgba(230, 223, 209, 0.2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.weather-current-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.weather-current-icon svg { width: 100%; height: 100%; }
|
||||
.weather-current-temp {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.weather-current-detail {
|
||||
font-size: 11px;
|
||||
color: #b0a89a;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.weather-day {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.weather-day-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 2px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.weather-day-name {
|
||||
color: #e6dfd1;
|
||||
font-weight: bold;
|
||||
}
|
||||
.weather-day-minmax {
|
||||
color: #999187;
|
||||
font-size: 11px;
|
||||
}
|
||||
.weather-day-slots {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.weather-slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 2px;
|
||||
}
|
||||
.weather-slot-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.weather-slot-icon svg { width: 100%; height: 100%; }
|
||||
.weather-slot-label {
|
||||
color: #999187;
|
||||
font-size: 9px;
|
||||
}
|
||||
.weather-slot-temp {
|
||||
color: #e6dfd1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -160,10 +231,13 @@
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#b0a89a" stroke-width="2"><path d="M3 12 A9 9 0 0 1 21 12 Z"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#b0a89a" stroke-width="2"><path d="M3 12 A9 9 0 0 1 21 22 Z"/></svg>
|
||||
</div>
|
||||
<div class="info-content"><span class="info-label">Mond unter</span><span><span class="info-value" id="moonset-value">--:--</span><span class="info-sub" id="moonset-when"></span></span></div>
|
||||
</div>
|
||||
<div id="weather-divider" class="weather-divider"></div>
|
||||
<div id="weather-current" class="weather-current"></div>
|
||||
<div id="weather-forecast" class="weather-forecast"></div>
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vanta@0.5.21/dist/vanta.clouds.min.js"></script>
|
||||
@@ -602,6 +676,177 @@
|
||||
setTimeout(tick, 1000);
|
||||
}
|
||||
|
||||
// ============ Weather ============
|
||||
var WEATHER_LAT = 48.5667;
|
||||
var WEATHER_LON = 13.4319;
|
||||
var weatherData = null;
|
||||
|
||||
var WMO_ICONS = {
|
||||
0: 'sun',
|
||||
1: 'sun-cloud',
|
||||
2: 'cloud-sun',
|
||||
3: 'cloud',
|
||||
45: 'fog',
|
||||
48: 'fog',
|
||||
51: 'drizzle',
|
||||
53: 'drizzle',
|
||||
55: 'drizzle',
|
||||
56: 'drizzle',
|
||||
57: 'drizzle',
|
||||
61: 'rain',
|
||||
63: 'rain',
|
||||
65: 'rain',
|
||||
66: 'rain',
|
||||
67: 'rain',
|
||||
71: 'snow',
|
||||
73: 'snow',
|
||||
75: 'snow',
|
||||
77: 'snow',
|
||||
80: 'rain',
|
||||
81: 'rain',
|
||||
82: 'rain',
|
||||
85: 'snow',
|
||||
86: 'snow',
|
||||
95: 'thunder',
|
||||
96: 'thunder',
|
||||
99: 'thunder'
|
||||
};
|
||||
|
||||
function weatherSVG(code) {
|
||||
var c = '#e6dfd1';
|
||||
switch (code) {
|
||||
case 0:
|
||||
return '<svg viewBox="0 0 24 24" fill="' + c + '"><circle cx="12" cy="12" r="5"/><g stroke="' + c + '" stroke-width="2" stroke-linecap="round"><line x1="12" y1="2" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="2" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/><line x1="4.9" y1="4.9" x2="6.3" y2="6.3"/><line x1="17.7" y1="17.7" x2="19.1" y2="19.1"/><line x1="4.9" y1="19.1" x2="6.3" y2="17.7"/><line x1="17.7" y1="6.3" x2="19.1" y2="4.9"/></g></svg>';
|
||||
case 1:
|
||||
case 2:
|
||||
return '<svg viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="2"><circle cx="8" cy="8" r="3" fill="' + c + '"/><line x1="8" y1="2" x2="8" y2="3"/><line x1="2" y1="8" x2="3" y2="8"/><line x1="3.5" y1="3.5" x2="4.2" y2="4.2"/><line x1="12.5" y1="3.5" x2="11.8" y2="4.2"/><path d="M10 14 a4 4 0 0 1 4 -4 a4 4 0 0 1 4 4 a3 3 0 0 1 0 6 h-8 a3 3 0 0 1 0 -6 z" fill="#999187" stroke="none"/></svg>';
|
||||
case 3:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 18 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/></svg>';
|
||||
case 45:
|
||||
case 48:
|
||||
return '<svg viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="2"><path d="M6 18 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z" fill="#999187" stroke="' + c + '"/><line x1="8" y1="20" x2="8" y2="22"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="16" y1="20" x2="16" y2="22"/></svg>';
|
||||
case 51:
|
||||
case 55:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 14 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="8" y1="17" x2="7" y2="20"/><line x1="13" y1="17" x2="12" y2="20"/><line x1="17" y1="17" x2="16" y2="20"/></g></svg>';
|
||||
case 53:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 14 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="9" y1="16" x2="8" y2="20"/><line x1="13" y1="16" x2="12" y2="20"/></g></svg>';
|
||||
case 61:
|
||||
case 80:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="8" y1="16" x2="7" y2="21"/><line x1="13" y1="16" x2="12" y2="21"/><line x1="17" y1="16" x2="16" y2="21"/></g></svg>';
|
||||
case 63:
|
||||
case 81:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="9" y1="15" x2="7" y2="21"/><line x1="13" y1="15" x2="11" y2="21"/><line x1="17" y1="15" x2="15" y2="21"/></g></svg>';
|
||||
case 65:
|
||||
case 82:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="7" y1="15" x2="4" y2="22"/><line x1="11" y1="15" x2="8" y2="22"/><line x1="15" y1="15" x2="12" y2="22"/><line x1="19" y1="15" x2="16" y2="22"/></g></svg>';
|
||||
case 71:
|
||||
case 73:
|
||||
case 75:
|
||||
case 77:
|
||||
case 85:
|
||||
case 86:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g fill="' + c + '"><circle cx="8" cy="18" r="1.5"/><circle cx="13" cy="19" r="1.5"/><circle cx="17" cy="18" r="1.5"/></g></svg>';
|
||||
case 95:
|
||||
case 96:
|
||||
case 99:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><path d="M12 2 L8 11 L12 11 L10 18 L16 9 L12 9 Z" fill="#d6b46a"/></svg>';
|
||||
default:
|
||||
return '<svg viewBox="0 0 24 24" fill="#999187"><circle cx="12" cy="12" r="6"/></svg>';
|
||||
}
|
||||
}
|
||||
|
||||
function fetchWeather() {
|
||||
var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + WEATHER_LAT +
|
||||
'&longitude=' + WEATHER_LON +
|
||||
'¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m' +
|
||||
'&hourly=temperature_2m,weather_code' +
|
||||
'&daily=weather_code,temperature_2m_max,temperature_2m_min' +
|
||||
'&timezone=Europe/Berlin&forecast_days=5';
|
||||
|
||||
fetch(url)
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) {
|
||||
weatherData = data;
|
||||
updateWeatherDisplay();
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log('Weather fetch failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function findHourlyIndex(targetDate, hourlyTimeArray) {
|
||||
var bestIdx = 0;
|
||||
var bestDiff = Infinity;
|
||||
for (var i = 0; i < hourlyTimeArray.length; i++) {
|
||||
var t = new Date(hourlyTimeArray[i]);
|
||||
var diff = Math.abs(t.getTime() - targetDate.getTime());
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
return bestIdx;
|
||||
}
|
||||
|
||||
function updateWeatherDisplay() {
|
||||
if (!weatherData) return;
|
||||
|
||||
var now = new Date();
|
||||
var current = weatherData.current;
|
||||
var hourly = weatherData.hourly;
|
||||
var daily = weatherData.daily;
|
||||
|
||||
// Current
|
||||
var currentHtml = '<div class="weather-current-icon">' + weatherSVG(current.weather_code) + '</div>' +
|
||||
'<div><div class="weather-current-temp">' + Math.round(current.temperature_2m) + '°C</div>' +
|
||||
'<div class="weather-current-detail">' +
|
||||
'Feuchte ' + Math.round(current.relative_humidity_2m) + '% · Wind ' + Math.round(current.wind_speed_10m) + ' km/h</div></div>';
|
||||
document.getElementById('weather-current').innerHTML = currentHtml;
|
||||
|
||||
// Forecast
|
||||
var forecastHtml = '';
|
||||
var dayNames = ['Heute', 'Morgen'];
|
||||
for (var d = 0; d < 4; d++) {
|
||||
var dayDate = new Date(daily.time[d]);
|
||||
var dayLabel = d === 0 ? 'Heute' : dayNames[1] || dayDate.toLocaleDateString('de-DE', { weekday: 'short' });
|
||||
if (d > 1) {
|
||||
dayLabel = dayDate.toLocaleDateString('de-DE', { weekday: 'short' }) + ' ' +
|
||||
(dayDate.getDate() < 10 ? '0' : '') + dayDate.getDate() + '.' +
|
||||
(dayDate.getMonth() + 1 < 10 ? '0' : '') + (dayDate.getMonth() + 1) + '.';
|
||||
}
|
||||
|
||||
// Morning (8), Noon (14), Evening (20)
|
||||
var slots = [8, 14, 20];
|
||||
var slotsHtml = '';
|
||||
for (var i = 0; i < slots.length; i++) {
|
||||
var slotDate = new Date(dayDate);
|
||||
slotDate.setHours(slots[i], 0, 0, 0);
|
||||
var slotIdx = findHourlyIndex(slotDate, hourly.time);
|
||||
var slotCode = hourly.weather_code[slotIdx];
|
||||
var slotTemp = hourly.temperature_2m[slotIdx];
|
||||
var slotLabels = ['M', 'Mi', 'A'];
|
||||
slotsHtml += '<div class="weather-slot">' +
|
||||
'<div class="weather-slot-icon">' + weatherSVG(slotCode) + '</div>' +
|
||||
'<div class="weather-slot-label">' + slotLabels[i] + '</div>' +
|
||||
'<div class="weather-slot-temp">' + Math.round(slotTemp) + '°</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
forecastHtml += '<div class="weather-day">' +
|
||||
'<div class="weather-day-header">' +
|
||||
'<span class="weather-day-name">' + dayLabel + '</span>' +
|
||||
'<span class="weather-day-minmax">' + Math.round(daily.temperature_2m_min[d]) + '° / ' + Math.round(daily.temperature_2m_max[d]) + '°</span>' +
|
||||
'</div>' +
|
||||
'<div class="weather-day-slots">' + slotsHtml + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
document.getElementById('weather-forecast').innerHTML = forecastHtml;
|
||||
}
|
||||
|
||||
// Update weather every 15 minutes
|
||||
fetchWeather();
|
||||
setInterval(fetchWeather, 15 * 60 * 1000);
|
||||
|
||||
animate();
|
||||
tick();
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user