predictor/internal/engine/constraints.go

117 lines
3.5 KiB
Go

package engine
import (
"fmt"
"predictor-refactored/internal/numerics"
)
// Altitude triggers when the balloon altitude satisfies Op against Limit.
//
// Examples:
//
// Altitude{Op: OpGreaterEqual, Limit: 30000} — burst at 30 km
// Altitude{Op: OpLessEqual, Limit: 0} — sea-level descent termination
type Altitude struct {
Op Operator
Limit float64
On Action
}
func (c Altitude) Name() string {
return fmt.Sprintf("altitude %s %g", c.Op, c.Limit)
}
func (c Altitude) Violated(_ float64, s State) bool { return c.Op.Test(s.Altitude, c.Limit) }
func (c Altitude) Action() Action { return c.On }
// Time triggers when the integration time t (UNIX seconds) satisfies Op
// against Limit.
type Time struct {
Op Operator
Limit float64
On Action
}
func (c Time) Name() string { return fmt.Sprintf("time %s %g", c.Op, c.Limit) }
func (c Time) Violated(t float64, _ State) bool { return c.Op.Test(t, c.Limit) }
func (c Time) Action() Action { return c.On }
// TerrainContact triggers when the ground elevation exceeds the balloon's
// altitude — i.e. the balloon has hit the ground.
type TerrainContact struct {
Provider TerrainProvider
On Action
}
func (c TerrainContact) Name() string { return "terrain_contact" }
func (c TerrainContact) Violated(_ float64, s State) bool {
return c.Provider.Elevation(s.Lat, s.Lng) > s.Altitude
}
func (c TerrainContact) Action() Action { return c.On }
// PolygonMode selects whether Polygon fires when the balloon is inside or
// outside the configured polygon.
type PolygonMode int
const (
// PolygonInside fires when (lat, lng) lies inside the polygon — useful
// for "must not enter restricted airspace".
PolygonInside PolygonMode = iota
// PolygonOutside fires when (lat, lng) lies outside the polygon —
// useful for "must remain over the test range".
PolygonOutside
)
// PolygonVertex is one vertex of a geographic polygon. Latitudes are in
// degrees [-90, 90]; longitudes in degrees [0, 360) or [-180, 180]
// (callers normalise — see Polygon.Violated).
type PolygonVertex struct {
Lat float64
Lng float64
}
// Polygon is a constraint over a closed geographic polygon, evaluated in
// plate-carrée coordinates with antimeridian handling (see
// numerics.PointInPolygon). Build one with NewPolygon so the flattened
// vertex slices used by the hot path are precomputed.
type Polygon struct {
Vertices []PolygonVertex
Mode PolygonMode
On Action
// Label, if set, is returned by Name. Defaults to "polygon_inside" or
// "polygon_outside" based on Mode.
Label string
// Precomputed parallel vertex slices for numerics.PointInPolygon.
polyLat, polyLng []float64
}
// NewPolygon builds a Polygon, precomputing the flattened vertex slices.
func NewPolygon(verts []PolygonVertex, mode PolygonMode, on Action, label string) Polygon {
lat := make([]float64, len(verts))
lng := make([]float64, len(verts))
for i, v := range verts {
lat[i], lng[i] = v.Lat, v.Lng
}
return Polygon{Vertices: verts, Mode: mode, On: on, Label: label, polyLat: lat, polyLng: lng}
}
func (c Polygon) Name() string {
if c.Label != "" {
return c.Label
}
if c.Mode == PolygonOutside {
return "polygon_outside"
}
return "polygon_inside"
}
func (c Polygon) Action() Action { return c.On }
// Violated reports whether the state satisfies the polygon-containment rule.
func (c Polygon) Violated(_ float64, s State) bool {
in := numerics.PointInPolygon(s.Lat, s.Lng, c.polyLat, c.polyLng)
if c.Mode == PolygonInside {
return in
}
return !in
}