Files
LehrerApp/LehrerApp.Desktop/ViewModels/Planning/SchoolDayWeatherSummary.cs
T

155 lines
7.0 KiB
C#
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.
using LehrerApp.Core.Models;
namespace LehrerApp.Desktop.ViewModels.Planning;
public enum SchoolDayWeatherKind { Sunny, Cloudy, Rain, Storm, Thunderstorm }
/// <summary>Verdichtet die stündliche MOSMIX-Prognose auf ein planungsrelevantes Schultagsymbol.
/// Extremere Bedingungen haben bewusst Vorrang vor Mittelwerten.</summary>
public sealed class SchoolDayWeatherSummary
{
public DateOnly Date { get; init; }
public SchoolDayWeatherKind Kind { get; init; }
public double? MinTemperatureC { get; init; }
public double? MaxTemperatureC { get; init; }
public double PrecipitationMm { get; init; }
public double? MaxWindGustKmh { get; init; }
public IReadOnlyList<WeatherWarning> Warnings { get; init; } = [];
public string Symbol => Kind switch
{
SchoolDayWeatherKind.Thunderstorm => "⛈️",
SchoolDayWeatherKind.Storm => "💨",
SchoolDayWeatherKind.Rain => "🌧️",
SchoolDayWeatherKind.Cloudy => "☁️",
_ => "☀️",
};
public string Label => Kind switch
{
SchoolDayWeatherKind.Thunderstorm => "Gewitter",
SchoolDayWeatherKind.Storm => "Sturm",
SchoolDayWeatherKind.Rain => "Regen",
SchoolDayWeatherKind.Cloudy => "Bewölkt",
_ => "Sonnig",
};
public string Tooltip
{
get
{
var lines = new List<string> { Label };
if (MinTemperatureC is { } min && MaxTemperatureC is { } max)
lines.Add(Math.Abs(max - min) < 0.05 ? $"{min:0.#} °C" : $"{min:0.#}{max:0.#} °C");
if (PrecipitationMm >= 0.05) lines.Add($"Niederschlag: {PrecipitationMm:0.#} mm");
if (MaxWindGustKmh is { } gust) lines.Add($"Max. Böen: {gust:0.#} km/h");
foreach (var warning in Warnings)
{
var period = WarningPeriod(warning);
lines.Add($"⚠ {warning.Headline}{(period.Length > 0 ? $" ({period})" : "")}");
if (!string.IsNullOrWhiteSpace(warning.Instruction)) lines.Add(warning.Instruction.Trim());
}
return string.Join("\n", lines);
}
}
public static SchoolDayWeatherSummary? From(WeatherSnapshot snapshot, DateOnly date)
{
var allHours = snapshot.Forecast
.Where(x => DateOnly.FromDateTime(Local(x.ValidAt)) == date)
.ToList();
// Für die Unterrichtsplanung zählt vor allem der Zeitraum, in dem Schule stattfindet.
// Falls MOSMIX für diesen Zeitraum noch keine Werte liefert, bleibt der Tagesrest nutzbar.
var schoolHours = allHours.Where(x =>
{
var hour = Local(x.ValidAt).Hour;
return hour >= 6 && hour <= 18;
}).ToList();
var hours = schoolHours.Count > 0 ? schoolHours : allHours;
if (hours.Count == 0) return null;
var warnings = snapshot.Warnings.Where(x => WarningTouchesDate(x, date)).ToList();
var codes = hours.Where(x => x.WeatherCode.HasValue).Select(x => x.WeatherCode!.Value).ToList();
var precipitation = hours.Sum(x => x.PrecipitationMm ?? 0);
var gust = hours.Where(x => x.WindGustKmh.HasValue).Select(x => x.WindGustKmh!.Value).DefaultIfEmpty().Max();
var hasGust = hours.Any(x => x.WindGustKmh.HasValue);
var warningText = string.Join(" ", warnings.SelectMany(x => new[] { x.Event, x.Headline }));
var kind = codes.Any(x => x is >= 95 and <= 99) ||
warningText.Contains("GEWITTER", StringComparison.OrdinalIgnoreCase)
? SchoolDayWeatherKind.Thunderstorm
: (hasGust && gust >= 62) || ContainsStormWarning(warningText)
? SchoolDayWeatherKind.Storm
: precipitation >= 0.2 || codes.Any(IsRainCode)
? SchoolDayWeatherKind.Rain
: IsCloudy(hours, codes)
? SchoolDayWeatherKind.Cloudy
: SchoolDayWeatherKind.Sunny;
var temperatures = hours.Where(x => x.TemperatureC.HasValue)
.Select(x => x.TemperatureC!.Value).ToList();
return new SchoolDayWeatherSummary
{
Date = date,
Kind = kind,
MinTemperatureC = temperatures.Count > 0 ? temperatures.Min() : null,
MaxTemperatureC = temperatures.Count > 0 ? temperatures.Max() : null,
PrecipitationMm = Math.Round(precipitation, 1, MidpointRounding.AwayFromZero),
MaxWindGustKmh = hasGust ? Math.Round(gust, 1, MidpointRounding.AwayFromZero) : null,
Warnings = warnings,
};
}
public static bool WarningTouchesDate(WeatherWarning warning, DateOnly date)
{
if (warning.Onset is null && warning.Expires is null) return false;
var start = warning.Onset is { } onset ? Local(onset) : Local(warning.Expires!.Value).AddHours(-12);
var end = warning.Expires is { } expires ? Local(expires) : start.AddHours(12);
var dayStart = date.ToDateTime(TimeOnly.MinValue);
var dayEnd = date.ToDateTime(TimeOnly.MaxValue);
return start <= dayEnd && end >= dayStart;
}
public static string WarningPeriod(WeatherWarning warning)
{
var onset = warning.Onset is { } start ? Local(start) : (DateTime?)null;
var expires = warning.Expires is { } end ? Local(end) : (DateTime?)null;
return (onset, expires) switch
{
({ } a, { } b) when DateOnly.FromDateTime(a) == DateOnly.FromDateTime(b) =>
$"{a:dd.MM., HH:mm}{b:HH:mm} Uhr",
({ } a, { } b) => $"{a:dd.MM., HH:mm}{b:dd.MM., HH:mm} Uhr",
({ } a, null) => $"ab {a:dd.MM., HH:mm} Uhr",
(null, { } b) => $"bis {b:dd.MM., HH:mm} Uhr",
_ => "",
};
}
private static bool IsRainCode(int code) => code is >= 51 and <= 67 or >= 80 and <= 82;
private static bool IsCloudy(IEnumerable<WeatherForecastHour> hours, IEnumerable<int> codes)
{
var cloudValues = hours.Where(x => x.CloudCoverPercent.HasValue)
.Select(x => x.CloudCoverPercent!.Value).ToList();
return codes.Any(x => x is 2 or 3 or 45 or 48) ||
cloudValues.Count > 0 && cloudValues.Average() >= 60;
}
private static bool ContainsStormWarning(string text) =>
text.Contains("STURM", StringComparison.OrdinalIgnoreCase) ||
text.Contains("ORKAN", StringComparison.OrdinalIgnoreCase) ||
text.Contains("WIND", StringComparison.OrdinalIgnoreCase) ||
text.Contains("BÖEN", StringComparison.OrdinalIgnoreCase);
private static DateTime Local(DateTime value) => value.Kind == DateTimeKind.Utc
? value.ToLocalTime() : value;
}
public sealed class WeekWeatherWarningItem(WeatherWarning warning)
{
public string Headline { get; } = string.IsNullOrWhiteSpace(warning.Headline) ? warning.Event : warning.Headline;
public string PeriodDisplay { get; } = SchoolDayWeatherSummary.WarningPeriod(warning);
public string Tooltip { get; } = string.Join("\n", new[] { warning.Description, warning.Instruction }
.Where(x => !string.IsNullOrWhiteSpace(x)));
}