package briefing import ( "strings" "time" "unicode" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) const defaultSPCRiskDigestOutlookType = "categorical" const defaultSPCRiskDigestMinimumSeverityRank = 3 type SPCConvectiveOutlooksModule struct { Checked bool `json:"checked"` AsOf string `json:"as_of,omitempty"` IssuedAt string `json:"issued_at,omitempty"` LocationID string `json:"location_id,omitempty"` LocationName string `json:"location_name,omitempty"` OutlookCount int `json:"outlook_count"` Outlooks []SPCConvectiveOutlookRecord `json:"outlooks,omitempty"` RiskDigest []SPCConvectiveOutlookDigest `json:"risk_digest,omitempty"` } type SPCConvectiveOutlookRecord struct { Day int `json:"day,omitempty"` OutlookType string `json:"outlook_type,omitempty"` Label string `json:"label,omitempty"` LabelText string `json:"label_text,omitempty"` PeriodBegins string `json:"period_begins,omitempty"` PeriodEnds string `json:"period_ends,omitempty"` IssuedAt string `json:"issued_at,omitempty"` ContainsLocation bool `json:"contains_location"` ImageURL string `json:"image_url,omitempty"` } type SPCConvectiveOutlookDigest struct { LabelText string `json:"label_text,omitempty"` RiskLabel string `json:"risk_label,omitempty"` PeriodBegins string `json:"period_begins,omitempty"` PeriodEnds string `json:"period_ends,omitempty"` } func buildSPCConvectiveOutlooksModule(ctx ModuleContext, _ any) (*module.Output, error) { value := SPCConvectiveOutlooksModule{} run := ctx.Collected.SPCConvectiveOutlooks if run != nil { value.Checked = true value.AsOf = friendlyOptionalTime(run.AsOf, ctx.Timezone) value.IssuedAt = friendlyOptionalTime(run.IssuedAt, ctx.Timezone) value.LocationID = run.LocationID value.LocationName = run.LocationName } source, ok := sourceByName(ctx.Collected.SourceProvenance, string(module.SPCConvectiveOutlooks)) if value.Checked && value.AsOf == "" && ok && !source.FetchedAt.IsZero() { value.AsOf = friendlyDateTimeLabel(source.FetchedAt, ctx.Timezone) } if value.Checked && value.IssuedAt == "" && ok { value.IssuedAt = friendlyOptionalTime(source.IssuedAt, ctx.Timezone) } value.Outlooks = spcConvectiveOutlookRecords(ctx.Derived.SPCConvectiveOutlooks, ctx.Resolved.ValidPeriod, ctx.Timezone) value.RiskDigest = spcConvectiveOutlookRiskDigest(ctx.Derived.SPCConvectiveOutlooks, ctx.Resolved.ValidPeriod, ctx.Timezone, defaultSPCRiskDigestPolicy()) value.OutlookCount = len(value.Outlooks) return &module.Output{ID: module.SPCConvectiveOutlooks, StanzaName: string(module.SPCConvectiveOutlooks), Value: value}, nil } type spcRiskDigestPolicy struct { OutlookType string MinimumSeverityRank int } func defaultSPCRiskDigestPolicy() spcRiskDigestPolicy { return spcRiskDigestPolicy{ OutlookType: defaultSPCRiskDigestOutlookType, MinimumSeverityRank: defaultSPCRiskDigestMinimumSeverityRank, } } func spcConvectiveOutlookRecords(outlooks []weatherdata.ConvectiveOutlook, reportPeriod timeutil.Period, timezone string) []SPCConvectiveOutlookRecord { records := make([]SPCConvectiveOutlookRecord, 0, len(outlooks)) for _, outlook := range outlooks { outlookPeriod := timeutil.Period{Start: outlook.ValidFrom, End: outlook.ValidTo} if !outlookPeriod.IsValid() || !outlookPeriod.Overlaps(reportPeriod) { continue } records = append(records, SPCConvectiveOutlookRecord{ Day: outlook.Day, OutlookType: outlook.OutlookType, Label: outlook.Label, LabelText: outlook.LabelText, PeriodBegins: friendlyPeriodBeginsLabel(outlookPeriod, timezone), PeriodEnds: friendlyPeriodEndsLabel(outlookPeriod, timezone), IssuedAt: friendlyOptionalTime(outlook.IssuedAt, timezone), ContainsLocation: outlook.ContainsLocation, ImageURL: outlook.ImageURL, }) } return records } func spcConvectiveOutlookRiskDigest(outlooks []weatherdata.ConvectiveOutlook, reportPeriod timeutil.Period, timezone string, policy spcRiskDigestPolicy) []SPCConvectiveOutlookDigest { records := make([]SPCConvectiveOutlookDigest, 0, len(outlooks)) for _, outlook := range outlooks { if outlook.OutlookType != policy.OutlookType { continue } if outlook.SeverityRank == nil || *outlook.SeverityRank < policy.MinimumSeverityRank { continue } if !outlook.ContainsLocation { continue } outlookPeriod := timeutil.Period{Start: outlook.ValidFrom, End: outlook.ValidTo} if !outlookPeriod.IsValid() || !outlookPeriod.Overlaps(reportPeriod) { continue } records = append(records, SPCConvectiveOutlookDigest{ LabelText: outlook.LabelText, RiskLabel: spcRiskDigestLabel(outlook.LabelText), PeriodBegins: friendlyMonthDayTimeLabel(outlookPeriod.Start, timezone), PeriodEnds: friendlyMonthDayTimeLabel(outlookPeriod.End, timezone), }) } return records } func spcRiskDigestLabel(labelText string) string { label := strings.TrimSpace(labelText) if label == "" { return "" } runes := []rune(strings.ToLower(label)) runes[0] = unicode.ToUpper(runes[0]) return string(runes) } func friendlyOptionalTime(value *time.Time, timezone string) string { if value == nil { return "" } return friendlyDateTimeLabel(*value, timezone) } func sourceByName(sources []weatherdata.Source, name string) (weatherdata.Source, bool) { for _, source := range sources { if source.Name == name { return source, true } } return weatherdata.Source{}, false }