banyaro-ios/BanYaroGo/Support/GassiWetter.swift
rene 357c57e880 Gassi-Wetter mit WeatherKit + banyaro-Logik
WeatherKit als Datenquelle (statt OpenMeteo-Proxy via banyaro-Backend):
- BanYaroGo.entitlements: com.apple.developer.weatherkit
- WetterView komplett neu mit WeatherService.shared.weather(for:)
- DayWeather.symbolName als SF-Symbol direkt, kein WMO-Mapping nötig

GassiWetter-Logik (1:1-Port aus banyaro PWA wetter.js):
- gassiScore(...) 1-10 mit Temp/Regen/Wind/Asphalt/Gewitter
- asphaltTemp(airMax, uvMax) — gleiche Formel mit t_factor und UV-Bonus
- asphaltLevel safe/warm/hot/danger mit Advice-Texten
- schnueffelIndex aus Feuchte (precipProb-derived) und Temperatur
- tickRisk March-Oktober, Schwellen 7/12/20°C
- pawColdProtection bei tempMin <= 0

UI:
- Horizontaler Tag-Picker (Heute/Morgen + EEE) mit Mini-Stats
- Großer Gassi-Score-Badge in Empfehlungs-Farbe (grün/amber/rot)
- Stats-Grid 2x2: Niederschlag, Wind, UV, Asphalt
- Hunde-Hinweise als farbige Boxen (Asphalt, Pfoten, Gewitter, Zecken)
- Schnüffel-Index als kompakte Karte mit Emoji

Color(hex:)-Extension für die HEX-Werte aus dem PWA übernommen.
2026-05-30 13:16:48 +02:00

182 lines
5.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
/// Gassi-Wetter-Bewertung 1:1-Port der Logik aus banyaro PWA (wetter.js).
/// Asphalt-Formel, Gassi-Score-Komposition, Schnüffel-Index und Zecken-
/// Heuristik identisch, damit PWA und Go App dieselbe Empfehlung geben.
enum GassiWetter {
// MARK: - Asphalt-Temperatur (aus Lufttemp + UV-Index)
static func asphaltTemp(airMax: Double, uvMax: Double) -> Double {
// UV-Bonus skaliert mit Temperatur: unter 5°C kaum Aufheizung, ab 30°C voll
let tFactor = max(0.0, min(1.0, (airMax - 5) / 25))
let bonus = min(uvMax * 3.0 * tFactor, 30.0)
return (airMax + bonus).rounded(toPlaces: 1)
}
enum AsphaltLevel {
case safe, warm, hot, danger
var label: String {
switch self {
case .safe: return "Unbedenklich"
case .warm: return "Warm — kurze Runden ok"
case .hot: return "Heiß — Pfoten verbrennen"
case .danger: return "Gefährlich — gar nicht laufen"
}
}
var advice: String? {
switch self {
case .safe: return nil
case .warm: return "Schatten suchen, Pausen einlegen."
case .hot: return "Gras oder Schatten bevorzugen — Asphalt vermeiden."
case .danger: return "Nur Morgens/Abends rausgehen, mittags drinnen."
}
}
}
static func asphaltLevel(for asphalt: Double) -> AsphaltLevel {
switch asphalt {
case ..<30: return .safe
case ..<40: return .warm
case ..<55: return .hot
default: return .danger
}
}
// MARK: - Gassi-Score (110)
static func gassiScore(
tempMax: Double,
precipProb: Int,
windKmh: Double,
asphalt: Double,
thunderstorm: Bool
) -> Int {
var score = 10
// Temperatur (ideal 1020°C)
if tempMax > 30 { score -= 3 }
else if tempMax > 25 { score -= 1 }
else if tempMax < 0 { score -= 3 }
else if tempMax < 5 { score -= 1 }
// Regen
if precipProb > 70 { score -= 3 }
else if precipProb > 40 { score -= 2 }
else if precipProb > 20 { score -= 1 }
// Wind
if windKmh > 60 { score -= 2 }
else if windKmh > 40 { score -= 1 }
// Asphalt
if asphalt > 55 { score -= 2 }
else if asphalt > 45 { score -= 1 }
// Gewitter
if thunderstorm { score -= 3 }
return max(1, min(10, score))
}
struct GassiScoreBadge {
let score: Int
let label: String
let colorHex: String
init(score: Int) {
self.score = score
if score >= 8 {
label = "Toller Gassi-Tag!"
colorHex = "10B981"
} else if score >= 5 {
label = "Geht so"
colorHex = "F59E0B"
} else {
label = "Lieber drinbleiben"
colorHex = "EF4444"
}
}
}
// MARK: - Schnüffel-Index
struct SchnueffelIndex {
let label: String
let emoji: String
let colorHex: String
}
static func schnueffelIndex(tempMax: Double, precipProb: Int) -> SchnueffelIndex {
let feucht: Feuchte = precipProb > 60 ? .feucht
: precipProb > 30 ? .leicht
: .trocken
if feucht == .feucht, tempMax >= 10, tempMax <= 18 {
return SchnueffelIndex(label: "Exzellent", emoji: "👃", colorHex: "10B981")
}
if feucht == .feucht, tempMax > 10, tempMax <= 22 {
return SchnueffelIndex(label: "Sehr gut", emoji: "👃", colorHex: "34D399")
}
if tempMax < 5 {
return SchnueffelIndex(label: "Gut (kalte Luft trägt Gerüche)", emoji: "🌬️", colorHex: "60A5FA")
}
if feucht == .leicht, tempMax >= 10, tempMax <= 22 {
return SchnueffelIndex(label: "Gut", emoji: "👃", colorHex: "4CAF50")
}
if feucht == .trocken, tempMax > 25 {
return SchnueffelIndex(label: "Schwach", emoji: "💨", colorHex: "94A3B8")
}
return SchnueffelIndex(label: "Mittel", emoji: "🌫", colorHex: "F59E0B")
}
private enum Feuchte { case feucht, leicht, trocken }
// MARK: - Zecken-Risiko
enum TickRisk {
case none, low, medium, high
var label: String {
switch self {
case .none: return "Keine"
case .low: return "Niedrig"
case .medium: return "Mittel"
case .high: return "Hoch"
}
}
var colorHex: String {
switch self {
case .none: return "94A3B8"
case .low: return "10B981"
case .medium: return "F59E0B"
case .high: return "EF4444"
}
}
}
static func tickRisk(tempMax: Double, month: Int) -> TickRisk {
// Wie in banyaro: nur März-Oktober relevant
guard (3...10).contains(month) else { return .none }
if tempMax <= 7 { return .none }
if tempMax > 20 { return .high }
if tempMax > 12 { return .medium }
return .low
}
// MARK: - Pfoten-Kälteschutz
static func pawColdProtection(tempMin: Double) -> Bool {
tempMin <= 0
}
}
private extension Double {
func rounded(toPlaces places: Int) -> Double {
let multiplier = pow(10.0, Double(places))
return (self * multiplier).rounded() / multiplier
}
}