Emit location-filtered SPC outlook v2 runs

This commit is contained in:
2026-06-12 04:24:12 +00:00
parent 435d1ade07
commit 21a35a5205
2 changed files with 191 additions and 29 deletions

View File

@@ -29,10 +29,10 @@ var idTokenRE = regexp.MustCompile(`[^a-z0-9]+`)
// ConvectiveOutlookNormalizer converts:
//
// standards.SchemaRawSPCConvectiveOutlookV1 -> standards.SchemaWeatherOutlookV1
// standards.SchemaRawSPCConvectiveOutlookV1 -> standards.SchemaWeatherOutlookV2
//
// It maps SPC GeoJSON outlook features into canonical outlook polygons and
// enriches each day with the matching required print-page discussion.
// It maps SPC GeoJSON outlook features containing the configured location into
// canonical outlook polygons and adds matching day-level print-page discussions.
type ConvectiveOutlookNormalizer struct{}
func (ConvectiveOutlookNormalizer) Match(e event.Event) bool {
@@ -50,7 +50,7 @@ func (ConvectiveOutlookNormalizer) Normalize(ctx context.Context, in event.Event
return normcommon.NormalizeJSON(
in,
outlookNormalizer,
standards.SchemaWeatherOutlookV1,
standards.SchemaWeatherOutlookV2,
func(parsed spcprovider.RawConvectiveOutlookBundle) (model.WeatherOutlookRun, time.Time, error) {
return buildConvectiveOutlook(parsed, fallbackAsOf)
},
@@ -104,10 +104,18 @@ func buildConvectiveOutlook(bundle spcprovider.RawConvectiveOutlookBundle, fallb
if latestIssue.IsZero() || outlook.IssuedAt.After(latestIssue) {
latestIssue = outlook.IssuedAt
}
if !outlook.ContainsLocation {
continue
}
outlooks = append(outlooks, outlook)
}
}
runDiscussions, err := buildOutlookDiscussions(outlooks, discussions)
if err != nil {
return model.WeatherOutlookRun{}, time.Time{}, err
}
asOf := latestIssue
if asOf.IsZero() {
asOf = latestDiscussionUpdated
@@ -132,6 +140,7 @@ func buildConvectiveOutlook(bundle spcprovider.RawConvectiveOutlookBundle, fallb
AsOf: asOf.UTC(),
IssuedAt: issuedAt,
Outlooks: outlooks,
Discussions: runDiscussions,
}
return run, run.AsOf, nil
}
@@ -174,6 +183,43 @@ func parseDiscussions(pages []spcprovider.RawDiscussionPage) (map[int]parsedDisc
return out, latestUpdated, nil
}
func buildOutlookDiscussions(outlooks []model.WeatherOutlook, discussions map[int]parsedDiscussion) ([]model.WeatherOutlookDiscussion, error) {
daysWithOutlooks := map[int]bool{}
for _, outlook := range outlooks {
daysWithOutlooks[outlook.Day] = true
}
days := make([]int, 0, len(daysWithOutlooks))
for day := range daysWithOutlooks {
days = append(days, day)
}
sort.Ints(days)
out := make([]model.WeatherOutlookDiscussion, 0, len(days))
for _, day := range days {
disc, ok := discussions[day]
if !ok {
return nil, fmt.Errorf("discussion for retained day %d is required", day)
}
out = append(out, model.WeatherOutlookDiscussion{
Day: day,
Headline: disc.Headline,
Summary: disc.Summary,
Discussion: disc.Discussion,
UpdatedAt: utcTimePtr(disc.UpdatedAt),
})
}
return out, nil
}
func utcTimePtr(t *time.Time) *time.Time {
if t == nil {
return nil
}
tt := t.UTC()
return &tt
}
func orderedProducts(products []spcprovider.RawOutlookProduct) []spcprovider.RawOutlookProduct {
out := make([]spcprovider.RawOutlookProduct, len(products))
copy(out, products)