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.
This commit is contained in:
parent
0867a2171f
commit
357c57e880
4 changed files with 502 additions and 125 deletions
20
BanYaroGo/Support/ColorHex.swift
Normal file
20
BanYaroGo/Support/ColorHex.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
/// Creates a Color from a 6-digit hex string (e.g. "10B981" or "#10B981").
|
||||
/// Falls back to system gray on bad input.
|
||||
init(hex: String) {
|
||||
let cleaned = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#"))
|
||||
var value: UInt64 = 0
|
||||
Scanner(string: cleaned).scanHexInt64(&value)
|
||||
|
||||
guard cleaned.count == 6 else {
|
||||
self.init(.sRGB, red: 0.5, green: 0.5, blue: 0.5)
|
||||
return
|
||||
}
|
||||
let r = Double((value >> 16) & 0xFF) / 255
|
||||
let g = Double((value >> 8) & 0xFF) / 255
|
||||
let b = Double(value & 0xFF) / 255
|
||||
self.init(.sRGB, red: r, green: g, blue: b)
|
||||
}
|
||||
}
|
||||
182
BanYaroGo/Support/GassiWetter.swift
Normal file
182
BanYaroGo/Support/GassiWetter.swift
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
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 (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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue