149 lines
4.2 KiB
Go
149 lines
4.2 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// 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 geographic polygon. The polygon is
|
|
// considered closed (last vertex connects to the first) and is interpreted
|
|
// in plate-carrée (rectangular lat/lng) coordinates with longitude
|
|
// wrap-around handling.
|
|
//
|
|
// Edges crossing the 180/-180 antimeridian are split via longitude
|
|
// normalisation against the polygon's centroid: callers that need
|
|
// great-circle accuracy should clip their polygon along the antimeridian
|
|
// before submitting.
|
|
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
|
|
}
|
|
|
|
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 {
|
|
if len(c.Vertices) < 3 {
|
|
return false
|
|
}
|
|
in := pointInPolygon(s.Lat, s.Lng, c.Vertices)
|
|
if c.Mode == PolygonInside {
|
|
return in
|
|
}
|
|
return !in
|
|
}
|
|
|
|
// pointInPolygon implements the ray-casting algorithm in lat/lng space.
|
|
//
|
|
// All vertices and the query point are normalised to within 180° of
|
|
// verts[0] before testing, so a polygon spanning the antimeridian is
|
|
// handled correctly as long as the polygon itself spans no more than 180°
|
|
// in longitude.
|
|
func pointInPolygon(lat, lng float64, verts []PolygonVertex) bool {
|
|
if len(verts) == 0 {
|
|
return false
|
|
}
|
|
ref := verts[0].Lng
|
|
qx := normLng(lng, ref)
|
|
|
|
inside := false
|
|
n := len(verts)
|
|
for i, j := 0, n-1; i < n; j, i = i, i+1 {
|
|
yi, yj := verts[i].Lat, verts[j].Lat
|
|
xi := normLng(verts[i].Lng, ref)
|
|
xj := normLng(verts[j].Lng, ref)
|
|
|
|
if (yi > lat) != (yj > lat) {
|
|
xIntersect := (xj-xi)*(lat-yi)/(yj-yi) + xi
|
|
if qx < xIntersect {
|
|
inside = !inside
|
|
}
|
|
}
|
|
}
|
|
return inside
|
|
}
|
|
|
|
// normLng rewrites v so that it lies within 180° of ref. With ref=10 and
|
|
// v=350, normLng returns -10.
|
|
func normLng(v, ref float64) float64 {
|
|
diff := math.Mod(v-ref+540, 360) - 180
|
|
return ref + diff
|
|
}
|