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 }