morz-infoboard/server/backend/internal/campaigns/messagewall/resolver.go

131 lines
3.3 KiB
Go

package messagewall
import (
"fmt"
"sort"
)
const (
versionV1 = 1
unitGrid = "grid"
fitModeCover = "cover"
fitModeContain = "contain"
defaultDuration = 20
defaultLoadTime = 15
defaultOnError = "skip"
)
func Resolve(request ResolveRequest) (ResolveResult, error) {
if err := Validate(request.Layout); err != nil {
return ResolveResult{}, err
}
result := ResolveResult{
Version: request.Layout.Version,
CoordinateSpace: request.Layout.CoordinateSpace,
FitMode: request.Layout.FitMode,
Scenes: make([]ResolvedScene, 0, len(request.Layout.Slots)),
}
duration := request.DurationSeconds
if duration <= 0 {
duration = defaultDuration
}
loadTimeout := request.LoadTimeoutSeconds
if loadTimeout <= 0 {
loadTimeout = defaultLoadTime
}
onError := request.OnError
if onError == "" {
onError = defaultOnError
}
for _, slot := range request.Layout.Slots {
result.Scenes = append(result.Scenes, ResolvedScene{
SlotID: slot.SlotID,
Source: request.Source,
DurationSeconds: duration,
LoadTimeoutSeconds: loadTimeout,
OnError: onError,
Crop: Crop{
X: slot.X,
Y: slot.Y,
Width: slot.Width,
Height: slot.Height,
Unit: request.Layout.CoordinateSpace.Unit,
},
})
}
sort.Slice(result.Scenes, func(i, j int) bool {
return result.Scenes[i].SlotID < result.Scenes[j].SlotID
})
return result, nil
}
func Validate(layout Layout) error {
if layout.Version != versionV1 {
return fmt.Errorf("unsupported layout version: %d", layout.Version)
}
if layout.CoordinateSpace.Width <= 0 || layout.CoordinateSpace.Height <= 0 {
return fmt.Errorf("coordinate_space width and height must be positive")
}
if layout.CoordinateSpace.Unit != unitGrid {
return fmt.Errorf("unsupported coordinate_space unit: %s", layout.CoordinateSpace.Unit)
}
if layout.FitMode != fitModeCover && layout.FitMode != fitModeContain {
return fmt.Errorf("unsupported fit_mode: %s", layout.FitMode)
}
if len(layout.Slots) == 0 {
return fmt.Errorf("layout must contain at least one slot")
}
seen := make(map[string]struct{}, len(layout.Slots))
for i, slot := range layout.Slots {
if slot.SlotID == "" {
return fmt.Errorf("slot %d has empty slot_id", i)
}
if _, ok := seen[slot.SlotID]; ok {
return fmt.Errorf("duplicate slot_id: %s", slot.SlotID)
}
seen[slot.SlotID] = struct{}{}
if slot.Width <= 0 || slot.Height <= 0 {
return fmt.Errorf("slot %s must have positive width and height", slot.SlotID)
}
if slot.X < 0 || slot.Y < 0 {
return fmt.Errorf("slot %s must not start outside coordinate space", slot.SlotID)
}
if slot.X+slot.Width > layout.CoordinateSpace.Width {
return fmt.Errorf("slot %s exceeds coordinate_space width", slot.SlotID)
}
if slot.Y+slot.Height > layout.CoordinateSpace.Height {
return fmt.Errorf("slot %s exceeds coordinate_space height", slot.SlotID)
}
}
for i := range layout.Slots {
for j := i + 1; j < len(layout.Slots); j++ {
if overlaps(layout.Slots[i], layout.Slots[j]) {
return fmt.Errorf("slots %s and %s overlap", layout.Slots[i].SlotID, layout.Slots[j].SlotID)
}
}
}
return nil
}
func overlaps(a, b Slot) bool {
return a.X < b.X+b.Width && a.X+a.Width > b.X && a.Y < b.Y+b.Height && a.Y+a.Height > b.Y
}