135 lines
3.3 KiB
Go
135 lines
3.3 KiB
Go
package server
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/blakeridgway/heloha/internal/radar"
|
|
)
|
|
|
|
const maxFrames = 12
|
|
|
|
type productKey struct{ site, product string }
|
|
|
|
type frameRing struct {
|
|
frames [maxFrames]*radar.RadarProduct
|
|
head int // next write slot
|
|
count int // filled slots (0..maxFrames)
|
|
}
|
|
|
|
func (r *frameRing) push(p *radar.RadarProduct) {
|
|
// Deduplicate: skip if same or older scan time as the newest stored frame.
|
|
if r.count > 0 {
|
|
newest := r.frames[(r.head-1+maxFrames)%maxFrames]
|
|
if !newest.Time.Before(p.Time) {
|
|
return
|
|
}
|
|
}
|
|
r.frames[r.head] = p
|
|
r.head = (r.head + 1) % maxFrames
|
|
if r.count < maxFrames {
|
|
r.count++
|
|
}
|
|
}
|
|
|
|
// get returns the frame at index i where 0=oldest, count-1=newest.
|
|
func (r *frameRing) get(i int) *radar.RadarProduct {
|
|
if i < 0 || i >= r.count {
|
|
return nil
|
|
}
|
|
slot := (r.head - r.count + i + maxFrames) % maxFrames
|
|
return r.frames[slot]
|
|
}
|
|
|
|
// RadarStore holds multi-site, multi-product frame history.
|
|
type RadarStore struct {
|
|
mu sync.RWMutex
|
|
rings map[productKey]*frameRing
|
|
}
|
|
|
|
func NewRadarStore() *RadarStore {
|
|
return &RadarStore{rings: make(map[productKey]*frameRing)}
|
|
}
|
|
|
|
func (s *RadarStore) Set(site, product string, p *radar.RadarProduct) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
k := productKey{site: site, product: product}
|
|
if s.rings[k] == nil {
|
|
s.rings[k] = &frameRing{}
|
|
}
|
|
s.rings[k].push(p)
|
|
}
|
|
|
|
// GetLatest returns the newest frame for a given site and product, or nil.
|
|
func (s *RadarStore) GetLatest(site, product string) *radar.RadarProduct {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
k := productKey{site: site, product: product}
|
|
r := s.rings[k]
|
|
if r == nil || r.count == 0 {
|
|
return nil
|
|
}
|
|
return r.get(r.count - 1)
|
|
}
|
|
|
|
// GetAllLatest returns the newest frame for every site for the given product.
|
|
func (s *RadarStore) GetAllLatest(product string) []*radar.RadarProduct {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
out := make([]*radar.RadarProduct, 0, 16)
|
|
for k, r := range s.rings {
|
|
if k.product == product && r.count > 0 {
|
|
out = append(out, r.get(r.count-1))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GetAllAtFrame returns one frame per site at the given age index (0=oldest, N-1=newest).
|
|
func (s *RadarStore) GetAllAtFrame(product string, frameIdx int) []*radar.RadarProduct {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
out := make([]*radar.RadarProduct, 0, 16)
|
|
for k, r := range s.rings {
|
|
if k.product != product {
|
|
continue
|
|
}
|
|
// Map frameIdx relative to this ring's count.
|
|
// frameIdx 0 = oldest across all rings → use offset from back.
|
|
p := r.get(frameIdx)
|
|
if p != nil {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// FrameInfo is one entry in the frames API response.
|
|
type FrameInfo struct {
|
|
Index int `json:"index"`
|
|
Time time.Time `json:"time"`
|
|
AgeSeconds int `json:"age_seconds"`
|
|
}
|
|
|
|
// FrameMeta returns frame metadata ordered oldest→newest using KTLX as the reference.
|
|
func (s *RadarStore) FrameMeta(product string) (int, []FrameInfo) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
r := s.rings[productKey{site: "ktlx", product: product}]
|
|
if r == nil || r.count == 0 {
|
|
return 0, nil
|
|
}
|
|
now := time.Now()
|
|
out := make([]FrameInfo, r.count)
|
|
for i := 0; i < r.count; i++ {
|
|
p := r.get(i)
|
|
out[i] = FrameInfo{
|
|
Index: i,
|
|
Time: p.Time,
|
|
AgeSeconds: int(now.Sub(p.Time).Seconds()),
|
|
}
|
|
}
|
|
return r.count, out
|
|
}
|