banyaro-ios/BanYaroGo/Support/GassiWetter.swift
rene 08069d6ea4 Wetter: deutsche Conditions + Asphalt-Label entrümpelt
- WeatherCondition deutsch: 'Mostly Clear' → 'Überwiegend klar' etc.
- AsphaltLevel.label nur noch ein Wort (Heiß/Warm/Gefährlich),
  die safety-Info wandert in den advice-Text → Title bleibt einzeilig
2026-05-30 13:30:01 +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"
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 (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
}
}