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;
|
font-size: 11px;
|
||||||
margin-left: 4px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -160,10 +231,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<div class="info-icon">
|
<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>
|
||||||
<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 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>
|
||||||
|
<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>
|
</div>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
<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>
|
<script src="https://cdn.jsdelivr.net/npm/vanta@0.5.21/dist/vanta.clouds.min.js"></script>
|
||||||
@@ -602,6 +676,177 @@
|
|||||||
setTimeout(tick, 1000);
|
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();
|
animate();
|
||||||
tick();
|
tick();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in new issue
Block a user