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" case .hot: return "Heiß" case .danger: return "Gefährlich" } } var advice: String? { switch self { case .safe: return nil case .warm: return "Schatten suchen, Pausen einlegen — kurze Runden sind ok." case .hot: return "Pfoten verbrennen. Gras oder Schatten bevorzugen, Asphalt meiden." case .danger: return "Pfoten verbrennen sofort. Nur früh morgens oder abends rausgehen." } } } 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 (1–10) static func gassiScore( tempMax: Double, precipProb: Int, windKmh: Double, asphalt: Double, thunderstorm: Bool ) -> Int { var score = 10 // Temperatur (ideal 10–20°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 } }