/* ============================================================ BAN YARO — Wetter (7-Tage-Wettervorhersage) Seiten-Modul: Hunde-optimierte Wettervorhersage mit GPS. ============================================================ */ window.Page_wetter = (() => { // ---------------------------------------------------------- // KONSTANTEN // ---------------------------------------------------------- // WMO-Code → Phosphor-Icon-Name (aus Sprite) const WMO_ICON = { 0:'sun', 1:'sun-dim', 2:'cloud-sun', 3:'cloud', 45:'cloud-fog', 48:'cloud-fog', 51:'cloud-rain', 53:'cloud-rain', 55:'cloud-rain', 61:'cloud-rain', 63:'cloud-rain', 65:'cloud-rain', 71:'cloud-snow', 73:'cloud-snow', 75:'cloud-snow', 77:'snowflake', 80:'rainbow-cloud', 81:'cloud-rain', 82:'cloud-rain', 85:'cloud-snow', 86:'cloud-snow', 95:'cloud-lightning', 96:'cloud-lightning', 99:'cloud-lightning', }; // Farben passend zum Wetter (für Icon-Tinting) const WMO_COLOR = { 0:'#F59E0B', 1:'#F59E0B', 2:'#94A3B8', 3:'#64748B', 45:'#94A3B8', 48:'#94A3B8', 51:'#60A5FA', 53:'#3B82F6', 55:'#2563EB', 61:'#3B82F6', 63:'#2563EB', 65:'#1D4ED8', 71:'#BAE6FD', 73:'#7DD3FC', 75:'#38BDF8', 77:'#BAE6FD', 80:'#60A5FA', 81:'#3B82F6', 82:'#2563EB', 85:'#7DD3FC', 86:'#38BDF8', 95:'#7C3AED', 96:'#6D28D9', 99:'#5B21B6', }; function _wmoIcon(code, size = '2rem', extraStyle = '') { const name = WMO_ICON[code] || 'cloud'; const color = WMO_COLOR[code] || 'var(--c-text-secondary)'; return ``; } const WMO_DESC = { 0:'Klarer Himmel', 1:'Überwiegend klar', 2:'Teilweise bewölkt', 3:'Bedeckt', 45:'Nebel', 48:'Gefrierender Nebel', 51:'Leichter Sprühregen', 53:'Mäßiger Sprühregen', 55:'Starker Sprühregen', 61:'Leichter Regen', 63:'Mäßiger Regen', 65:'Starker Regen', 71:'Leichter Schneefall', 73:'Mäßiger Schneefall', 75:'Starker Schneefall', 77:'Schneekörner', 80:'Leichte Regenschauer', 81:'Mäßige Regenschauer', 82:'Starke Regenschauer', 85:'Leichte Schneeschauer', 86:'Starke Schneeschauer', 95:'Gewitter', 96:'Gewitter mit leichtem Hagel', 99:'Gewitter mit starkem Hagel' }; const DAY_NAMES = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- let _container = null; let _appState = null; let _data = null; let _selDay = 0; let _loading = false; // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; _selDay = 0; _renderShell(); _tryAutoLocate(); } // ---------------------------------------------------------- // REFRESH // ---------------------------------------------------------- async function refresh() { _selDay = 0; _renderShell(); _tryAutoLocate(); } // ---------------------------------------------------------- // RENDER — Grundstruktur // ---------------------------------------------------------- function _renderShell() { _container.innerHTML = `
${_wmoIcon(2, '2.5rem')}

Standort wird ermittelt…

`; } // ---------------------------------------------------------- // STANDORT AUTOMATISCH ERMITTELN // ---------------------------------------------------------- async function _tryAutoLocate() { try { const pos = await API.getLocation({ timeout: 8000, maximumAge: 300_000 }); await _loadData(pos.lat, pos.lon); } catch { _showLocationError(); } } function _showLocationError() { const body = _container.querySelector('#wttr-body'); if (!body) return; body.innerHTML = `
📍

Standort nicht verfügbar

Bitte erlaube den Zugriff auf deinen Standort, um die Wettervorhersage zu laden.

`; body.querySelector('#wttr-btn-retry')?.addEventListener('click', () => { _renderShell(); _tryAutoLocate(); }); } // ---------------------------------------------------------- // DATEN LADEN // ---------------------------------------------------------- async function _loadData(lat, lon) { if (_loading) return; _loading = true; try { _data = await API.weather.forecast(lat, lon); _selDay = 0; _renderWeather(); } catch { const body = _container.querySelector('#wttr-body'); if (body) body.innerHTML = `
⚠️

Wetter nicht verfügbar

Die Wetterdaten konnten nicht geladen werden.

`; body?.querySelector('#wttr-btn-reload')?.addEventListener('click', () => { refresh(); }); } finally { _loading = false; } } // ---------------------------------------------------------- // HAUPT-RENDER // ---------------------------------------------------------- function _renderWeather() { const body = _container.querySelector('#wttr-body'); if (!body || !_data) return; const days = _data.days || []; if (!days.length) return; body.innerHTML = `
${days.map((d, i) => _dayCard(d, i)).join('')}
`; // Strip-Klick-Events body.querySelectorAll('[data-wttr-day]').forEach(card => { card.addEventListener('click', () => { _selDay = parseInt(card.dataset.wttrDay); _updateStrip(); _renderDetail(); _renderDog(); }); }); _renderDetail(); _renderDog(); } // ---------------------------------------------------------- // STRIP AKTUALISIEREN (aktiver Tag) // ---------------------------------------------------------- function _updateStrip() { const body = _container.querySelector('#wttr-body'); if (!body) return; const days = _data?.days || []; body.querySelectorAll('[data-wttr-day]').forEach((card, i) => { const active = i === _selDay; card.style.background = active ? 'var(--c-primary)' : 'var(--c-bg-card)'; card.style.color = active ? '#fff' : 'var(--c-text)'; card.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)'; card.style.transform = active ? 'translateY(-2px)' : ''; card.style.boxShadow = active ? '0 4px 12px rgba(196,132,58,0.3)' : '0 1px 3px rgba(0,0,0,0.07)'; // Temperatur-Farbe im aktiven Zustand const tempEl = card.querySelector('.wttr-temp'); if (tempEl) tempEl.style.color = active ? 'rgba(255,255,255,0.85)' : 'var(--c-text-secondary)'; const precipEl = card.querySelector('.wttr-precip'); if (precipEl) precipEl.style.color = active ? 'rgba(255,255,255,0.75)' : 'var(--c-text-secondary)'; }); } // ---------------------------------------------------------- // TAG-KARTE (Strip) // ---------------------------------------------------------- function _dayCard(d, i) { const active = i === _selDay; const dateObj = new Date(d.date); const dayName = i === 0 ? 'Heute' : DAY_NAMES[dateObj.getDay()]; const bg = active ? 'var(--c-primary)' : 'var(--c-bg-card)'; const col = active ? '#fff' : 'var(--c-text)'; const shadow = active ? '0 4px 12px rgba(196,132,58,0.3)' : '0 1px 3px rgba(0,0,0,0.07)'; const border = active ? 'var(--c-primary)' : 'var(--c-border)'; const transform = active ? 'translateY(-2px)' : ''; const textSec = active ? 'rgba(255,255,255,0.85)' : 'var(--c-text-secondary)'; const textMut = active ? 'rgba(255,255,255,0.75)' : 'var(--c-text-secondary)'; return `
${_esc(dayName)}
${_wmoIcon(d.weathercode, '1.5rem', active ? 'filter:brightness(0) invert(1)' : '')}
${Math.round(d.temp_max)}°/${Math.round(d.temp_min)}° ${d.precip_prob ?? 0}%
`; } // ---------------------------------------------------------- // DETAIL-CARD // ---------------------------------------------------------- function _renderDetail() { const el = _container.querySelector('#wttr-detail'); if (!el || !_data) return; const d = (_data.days || [])[_selDay]; if (!d) return; const desc = WMO_DESC[d.weathercode] || ''; const [uvLabel, uvColor] = _uvLabel(d.uv_index ?? 0); const uvPct = Math.min(100, ((d.uv_index ?? 0) / 11) * 100); const bft = _beaufort(d.wind_kmh ?? 0); const windDir = d.wind_dir_deg ?? 0; const compass = d.wind_dir ?? _compass(windDir); // Sunrise/Sunset Balken const now = new Date(); const sunriseStr = d.sunrise || ''; const sunsetStr = d.sunset || ''; let sunPct = 0; if (sunriseStr && sunsetStr) { const [rH, rM] = sunriseStr.split(':').map(Number); const [sH, sM] = sunsetStr.split(':').map(Number); const riseMin = rH * 60 + rM; const setMin = sH * 60 + sM; const curMin = now.getHours() * 60 + now.getMinutes(); sunPct = _selDay === 0 ? Math.min(100, Math.max(0, ((curMin - riseMin) / (setMin - riseMin)) * 100)) : 0; } el.innerHTML = `
${_wmoIcon(d.weathercode, '3.5rem')}
${_esc(desc)}
${Math.round(d.temp_max)}° / ${Math.round(d.temp_min)}°
${d.feels_max != null ? `
Gefühlt ${Math.round(d.feels_max)}° / ${Math.round(d.feels_min ?? d.feels_max)}°
` : ''}
${sunriseStr && sunsetStr ? `
${_esc(sunriseStr)} ${_esc(sunsetStr)}
` : ''}
${UI.icon('arrow-up')}
${_esc(compass)} · ${Math.round(d.windspeed_max ?? 0)} km/h
${_esc(bft)}
${d.precip_sum != null ? `
${d.precip_sum} mm
Niederschlag
` : ''}
UV-Index ${d.uv_index ?? 0} — ${_esc(uvLabel)}
`; } // ---------------------------------------------------------- // HUNDE-WETTER // ---------------------------------------------------------- function _renderDog() { const el = _container.querySelector('#wttr-dog'); if (!el || !_data) return; const d = (_data.days || [])[_selDay]; if (!d) return; const _POLLEN_NAMES = { erle:'Erle', birke:'Birke', graeser:'Gräser', beifuss:'Beifuß', ambrosia:'Ambrosia' }; let html = `

Hunde-Wetter

`; // Asphalt-Temperatur if (d.asphalt_temp != null) { const [aspText, aspColor, aspAdvice] = _asphaltLevel(d.asphalt_temp); html += `
Asphalt ~${Math.round(d.asphalt_temp)}°C — ${_esc(aspText)}
${aspAdvice ? `
${_esc(aspAdvice)}
` : ''}
`; } // Pfoten-Kälteschutz if (d.paw_cold) { html += `
Kälteschutz für Pfoten: Eis und Streusalz können die Pfoten reizen. Pfotenpflege empfohlen.
`; } // Gewitter if (d.thunderstorm) { html += `
Gewitter erwartet: Hunde können auf Gewitter sensibel reagieren. Sichere Umgebung schaffen.
`; } // Pollenflug const pollen = d.pollen; if (pollen && typeof pollen === 'object' && Object.keys(pollen).length) { const pollenEntries = Object.entries(pollen) .filter(([, v]) => v != null && v.level > 0); if (pollenEntries.length) { html += `
Pollenflug
${pollenEntries.map(([key, lvlObj]) => { const col = _pollenColor(lvlObj?.level ?? 0); const name = _POLLEN_NAMES[key] || key; const lbl = lvlObj?.label || ''; return ` ${_esc(name)}: ${_esc(lbl)} `; }).join('')}
`; } } // Zecken if (d.zecken != null) { const [tickLabel, tickColor] = _tickLevel(d.zecken); html += `
Zecken-Risiko: ${_esc(tickLabel)}
`; } // Wenn keine Hunde-Daten vorhanden if (!d.asphalt_temp && !d.paw_cold && !d.thunderstorm && !d.zecken && !(pollen && Object.keys(pollen).length)) { html += `

Keine besonderen Hinweise für heute.

`; } el.innerHTML = html; } // ---------------------------------------------------------- // HILFSFUNKTIONEN — Wetter // ---------------------------------------------------------- function _beaufort(kmh) { if (kmh < 2) return 'Windstille'; if (kmh < 12) return 'leicht'; if (kmh < 29) return 'mäßig'; if (kmh < 50) return 'frisch'; if (kmh < 62) return 'stark'; if (kmh < 75) return 'stürmisch'; return 'Sturm'; } function _uvLabel(uv) { if (uv <= 2) return ['niedrig', '#4CAF50']; if (uv <= 5) return ['mittel', '#FFC107']; if (uv <= 7) return ['hoch', '#FF9800']; if (uv <= 10) return ['sehr hoch', '#F44336']; return ['extrem', '#9C27B0']; } function _compass(deg) { const dirs = ['N','NO','O','SO','S','SW','W','NW']; return dirs[Math.round(deg / 45) % 8]; } function _asphaltLevel(temp) { if (temp < 40) return ['Pfoten sicher', '#4CAF50', '']; if (temp < 50) return ['leicht erwärmt', '#FFC107', 'Kurze Kontaktzeiten sind unbedenklich.']; if (temp < 60) return ['Vorsicht — Pfoten schützen!', '#FF9800', 'Heiße Oberfläche! Auf Gras ausweichen oder Hundeschuhe verwenden.']; return ['GEFAHR — Verbrennungsgefahr!', '#F44336', 'Asphalt kann Pfoten in Sekunden verbrennen. Spaziergang vermeiden!']; } function _pollenColor(level) { if (level === 0) return '#9E9E9E'; if (level === 1) return '#4CAF50'; if (level === 2) return '#FFC107'; if (level === 3) return '#FF9800'; return '#F44336'; // level 4+ } function _tickLevel(risk) { const r = (risk || '').toLowerCase(); if (r === 'niedrig') return ['niedrig', '#4CAF50']; if (r === 'mittel') return ['mittel', '#FF9800']; return ['hoch', '#F44336']; } function _esc(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ---------------------------------------------------------- // PUBLIC API // ---------------------------------------------------------- return { init, refresh }; })();