154 lines
5.0 KiB
Go
154 lines
5.0 KiB
Go
package briefing
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
|
)
|
|
|
|
type DerivedDailySummaryModule struct {
|
|
Date string `json:"date,omitempty"`
|
|
HighTempF *int `json:"high_temp_f,omitempty"`
|
|
LowTempF *int `json:"low_temp_f,omitempty"`
|
|
DailyPrecipitationProbability *int `json:"daily_precipitation_probability,omitempty"`
|
|
MostLikelyPrecipitationHour string `json:"most_likely_precipitation_hour,omitempty"`
|
|
ThunderMentioned bool `json:"thunder_mentioned"`
|
|
MaxWindGustMph *int `json:"max_wind_gust_mph,omitempty"`
|
|
ApparentTemperatureMaxF *int `json:"apparent_temperature_max_f,omitempty"`
|
|
DominantConditions []string `json:"dominant_conditions,omitempty"`
|
|
Hazards []string `json:"hazards,omitempty"`
|
|
}
|
|
|
|
func buildDerivedDailySummaryModule(ctx ModuleContext, _ any) (*module.Output, error) {
|
|
summary := ctx.Derived.FirstDailySummary()
|
|
if summary == nil {
|
|
return nil, fmt.Errorf("daily summary facts are required")
|
|
}
|
|
value, err := derivedDailySummaryValue(*summary, ctx.Derived.PrecipTiming, ctx.Timezone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: value}, nil
|
|
}
|
|
|
|
func derivedDailySummaryValue(summary forecast.DailySummary, timing forecast.PrecipTiming, timezone string) (DerivedDailySummaryModule, error) {
|
|
value := DerivedDailySummaryModule{
|
|
Date: friendlyDateLabel(summary.Date, timezone),
|
|
ThunderMentioned: timing.ThunderMentioned,
|
|
}
|
|
conditions := map[string]struct{}{}
|
|
hazards := map[string]struct{}{}
|
|
var temperature forecast.Range
|
|
var apparent forecast.Range
|
|
var maxPop *forecast.TimedValue
|
|
var maxGust *forecast.TimedValue
|
|
for _, daypart := range summary.Dayparts {
|
|
addRange(&temperature, daypart.Temperature)
|
|
addRange(&apparent, daypart.ApparentTemperature)
|
|
maxTimedValue(&maxPop, daypart.MaxPrecipitationProbability)
|
|
maxTimedValue(&maxGust, daypart.PeakWindGust)
|
|
if daypart.DominantCondition != "" {
|
|
conditions[daypart.DominantCondition] = struct{}{}
|
|
}
|
|
for _, hazard := range hazardsForIndicators(daypart.Indicators) {
|
|
hazards[hazard] = struct{}{}
|
|
}
|
|
}
|
|
for _, alert := range summary.AlertOverlaps {
|
|
if alert.Event != "" {
|
|
hazards[alert.Event] = struct{}{}
|
|
}
|
|
}
|
|
narrativeTemperature := narrativeTemperatureRange(summary.NarrativePeriods)
|
|
if narrativeTemperature.Max != nil {
|
|
value.HighTempF = roundedInt(narrativeTemperature.Max)
|
|
} else {
|
|
value.HighTempF = roundedInt(temperature.Max)
|
|
}
|
|
if narrativeTemperature.Min != nil {
|
|
value.LowTempF = roundedInt(narrativeTemperature.Min)
|
|
} else {
|
|
value.LowTempF = roundedInt(temperature.Min)
|
|
}
|
|
value.ApparentTemperatureMaxF = roundedInt(apparent.Max)
|
|
narrativePrecipitation := narrativeMaxPrecipitation(summary.NarrativePeriods)
|
|
if narrativePrecipitation != nil {
|
|
value.DailyPrecipitationProbability = roundedInt(&narrativePrecipitation.Value)
|
|
} else if maxPop != nil {
|
|
value.DailyPrecipitationProbability = roundedInt(&maxPop.Value)
|
|
}
|
|
if maxPop != nil {
|
|
value.MostLikelyPrecipitationHour = mostLikelyPrecipitationHour(maxPop, timezone)
|
|
}
|
|
if maxGust != nil {
|
|
value.MaxWindGustMph = roundedInt(&maxGust.Value)
|
|
}
|
|
value.DominantConditions = sortedSet(conditions)
|
|
value.Hazards = sortedSet(hazards)
|
|
return value, nil
|
|
}
|
|
|
|
func narrativeTemperatureRange(periods []weatherdata.ForecastPeriod) forecast.Range {
|
|
var out forecast.Range
|
|
for _, period := range periods {
|
|
addNarrativeHigh(&out, period.TemperatureFMax)
|
|
addNarrativeLow(&out, period.TemperatureFMin)
|
|
if period.TemperatureF != nil && period.IsDay != nil {
|
|
if *period.IsDay {
|
|
addNarrativeHigh(&out, period.TemperatureF)
|
|
} else {
|
|
addNarrativeLow(&out, period.TemperatureF)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func addNarrativeHigh(target *forecast.Range, value *float64) {
|
|
if value == nil {
|
|
return
|
|
}
|
|
if target.Max == nil || *value > *target.Max {
|
|
copied := *value
|
|
target.Max = &copied
|
|
}
|
|
}
|
|
|
|
func addNarrativeLow(target *forecast.Range, value *float64) {
|
|
if value == nil {
|
|
return
|
|
}
|
|
if target.Min == nil || *value < *target.Min {
|
|
copied := *value
|
|
target.Min = &copied
|
|
}
|
|
}
|
|
|
|
func narrativeMaxPrecipitation(periods []weatherdata.ForecastPeriod) *forecast.TimedValue {
|
|
var maxPop *forecast.TimedValue
|
|
for _, period := range periods {
|
|
if period.ProbabilityOfPrecipitationPercent == nil {
|
|
continue
|
|
}
|
|
value := forecast.TimedValue{
|
|
Value: *period.ProbabilityOfPrecipitationPercent,
|
|
Time: period.StartTime,
|
|
}
|
|
maxTimedValue(&maxPop, &value)
|
|
}
|
|
return maxPop
|
|
}
|
|
|
|
func mostLikelyPrecipitationHour(maxPop *forecast.TimedValue, timezone string) string {
|
|
if maxPop == nil || maxPop.Value <= 0 {
|
|
return ""
|
|
}
|
|
percent := roundedInt(&maxPop.Value)
|
|
if percent == nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%d%% at %s", *percent, clockLabel(maxPop.Time, timezone))
|
|
}
|