193 lines
5.5 KiB
Go
193 lines
5.5 KiB
Go
// service.go defines application read ports and use-case orchestration.
|
|
// Layer: internal/app core business-facing API.
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
|
)
|
|
|
|
// Repository defines outbound data access used by weatherapi use cases.
|
|
type Repository interface {
|
|
LatestObservation(ctx context.Context) (*model.WeatherObservation, error)
|
|
LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error)
|
|
LatestNarrativeForecast(ctx context.Context) (*model.WeatherForecastRun, error)
|
|
LatestForecastDiscussion(ctx context.Context) (*model.WeatherForecastDiscussion, error)
|
|
LatestWeatherStoryRun(ctx context.Context) (*model.WeatherStoryRun, error)
|
|
LatestWeatherStory(ctx context.Context) (*model.WeatherStory, error)
|
|
LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error)
|
|
LatestConvectiveOutlookRun(ctx context.Context) (*model.WeatherOutlookRun, error)
|
|
CurrentConditions(ctx context.Context, observationWindowMinutes int) (*CurrentConditions, error)
|
|
}
|
|
|
|
// OutlookFilter selects outlook entries from the latest convective outlook run.
|
|
type OutlookFilter struct {
|
|
Day *int
|
|
OutlookType string
|
|
ContainsLocation *bool
|
|
ActiveAt *time.Time
|
|
}
|
|
|
|
// Service provides weather read use-cases.
|
|
type Service struct {
|
|
repo Repository
|
|
}
|
|
|
|
func NewService(repo Repository) *Service {
|
|
return &Service{repo: repo}
|
|
}
|
|
|
|
func (s *Service) LatestObservation(ctx context.Context) (*model.WeatherObservation, error) {
|
|
return s.repo.LatestObservation(ctx)
|
|
}
|
|
|
|
func (s *Service) LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error) {
|
|
return s.repo.LatestHourlyForecast(ctx)
|
|
}
|
|
|
|
func (s *Service) LatestNarrativeForecast(ctx context.Context) (*model.WeatherForecastRun, error) {
|
|
return s.repo.LatestNarrativeForecast(ctx)
|
|
}
|
|
|
|
func (s *Service) LatestForecastDiscussion(ctx context.Context) (*model.WeatherForecastDiscussion, error) {
|
|
return s.repo.LatestForecastDiscussion(ctx)
|
|
}
|
|
|
|
func (s *Service) LatestWeatherStoryRun(ctx context.Context) (*model.WeatherStoryRun, error) {
|
|
return s.repo.LatestWeatherStoryRun(ctx)
|
|
}
|
|
|
|
func (s *Service) LatestWeatherStory(ctx context.Context) (*model.WeatherStory, error) {
|
|
return s.repo.LatestWeatherStory(ctx)
|
|
}
|
|
|
|
func (s *Service) LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error) {
|
|
return s.repo.LatestAlertRun(ctx)
|
|
}
|
|
|
|
func (s *Service) LatestConvectiveOutlook(ctx context.Context, filter OutlookFilter) (*model.WeatherOutlookRun, error) {
|
|
run, err := s.repo.LatestConvectiveOutlookRun(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if run == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
out := cloneOutlookRun(run)
|
|
outlooks := out.Outlooks[:0]
|
|
for _, outlook := range out.Outlooks {
|
|
if matchesOutlookFilter(outlook, filter) {
|
|
outlooks = append(outlooks, outlook)
|
|
}
|
|
}
|
|
out.Outlooks = outlooks
|
|
out.Discussions = filterOutlookDiscussions(out.Discussions, out.Outlooks)
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Service) CurrentConditions(ctx context.Context) (*CurrentConditions, error) {
|
|
return s.repo.CurrentConditions(ctx, ObservationWindowMinutesDefault)
|
|
}
|
|
|
|
func matchesOutlookFilter(outlook model.WeatherOutlook, filter OutlookFilter) bool {
|
|
if filter.Day != nil && outlook.Day != *filter.Day {
|
|
return false
|
|
}
|
|
if filter.OutlookType != "" && outlook.OutlookType != normalizeOutlookType(filter.OutlookType) {
|
|
return false
|
|
}
|
|
if filter.ContainsLocation != nil && outlook.ContainsLocation != *filter.ContainsLocation {
|
|
return false
|
|
}
|
|
if filter.ActiveAt != nil && (filter.ActiveAt.Before(outlook.ValidFrom) || !filter.ActiveAt.Before(outlook.ValidTo)) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func normalizeOutlookType(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func cloneOutlookRun(run *model.WeatherOutlookRun) *model.WeatherOutlookRun {
|
|
out := *run
|
|
out.Latitude = copyFloat64(run.Latitude)
|
|
out.Longitude = copyFloat64(run.Longitude)
|
|
out.IssuedAt = copyTime(run.IssuedAt)
|
|
if run.Outlooks != nil {
|
|
out.Outlooks = make([]model.WeatherOutlook, len(run.Outlooks))
|
|
for i := range run.Outlooks {
|
|
out.Outlooks[i] = cloneOutlook(run.Outlooks[i])
|
|
}
|
|
}
|
|
if run.Discussions != nil {
|
|
out.Discussions = make([]model.WeatherOutlookDiscussion, len(run.Discussions))
|
|
for i := range run.Discussions {
|
|
out.Discussions[i] = cloneOutlookDiscussion(run.Discussions[i])
|
|
}
|
|
}
|
|
return &out
|
|
}
|
|
|
|
func cloneOutlook(outlook model.WeatherOutlook) model.WeatherOutlook {
|
|
out := outlook
|
|
out.SeverityRank = copyInt(outlook.SeverityRank)
|
|
if outlook.Geometry != nil {
|
|
out.Geometry = append([]byte(nil), outlook.Geometry...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneOutlookDiscussion(discussion model.WeatherOutlookDiscussion) model.WeatherOutlookDiscussion {
|
|
out := discussion
|
|
out.UpdatedAt = copyTime(discussion.UpdatedAt)
|
|
return out
|
|
}
|
|
|
|
func filterOutlookDiscussions(discussions []model.WeatherOutlookDiscussion, outlooks []model.WeatherOutlook) []model.WeatherOutlookDiscussion {
|
|
if len(outlooks) == 0 {
|
|
return []model.WeatherOutlookDiscussion{}
|
|
}
|
|
|
|
retainedDays := make(map[int]struct{}, len(outlooks))
|
|
for _, outlook := range outlooks {
|
|
retainedDays[outlook.Day] = struct{}{}
|
|
}
|
|
|
|
out := discussions[:0]
|
|
for _, discussion := range discussions {
|
|
if _, ok := retainedDays[discussion.Day]; ok {
|
|
out = append(out, discussion)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func copyFloat64(value *float64) *float64 {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
out := *value
|
|
return &out
|
|
}
|
|
|
|
func copyInt(value *int) *int {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
out := *value
|
|
return &out
|
|
}
|
|
|
|
func copyTime(value *time.Time) *time.Time {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
out := *value
|
|
return &out
|
|
}
|