Skip to content

Commit bf6e151

Browse files
committed
Move numerous foundational types from sim to aviation pkg
Infrastructure for moving scenario.go from sim to server.
1 parent 2701362 commit bf6e151

18 files changed

+496
-500
lines changed

pkg/aviation/aviation.go

+368
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525
"github.com/mmp/vice/pkg/renderer"
2626
"github.com/mmp/vice/pkg/util"
2727

28+
"github.com/brunoga/deep"
2829
"github.com/klauspost/compress/zstd"
30+
"github.com/mmp/earcut-go"
2931
)
3032

3133
type ReportingPoint struct {
@@ -1444,3 +1446,369 @@ func (p *SquawkCodePool) NumAvailable() int {
14441446
}
14451447
return n
14461448
}
1449+
1450+
type ControllerAirspaceVolume struct {
1451+
LowerLimit int `json:"lower"`
1452+
UpperLimit int `json:"upper"`
1453+
Boundaries [][]math.Point2LL `json:"boundary_polylines"` // not in JSON
1454+
BoundaryNames []string `json:"boundaries"`
1455+
Label string `json:"label"`
1456+
LabelPosition math.Point2LL `json:"label_position"`
1457+
}
1458+
1459+
///////////////////////////////////////////////////////////////////////////
1460+
// RestrictionArea
1461+
1462+
// This many adapted and then this many user-defined
1463+
const MaxRestrictionAreas = 100
1464+
1465+
type RestrictionArea struct {
1466+
Title string `json:"title"`
1467+
Text [2]string `json:"text"`
1468+
BlinkingText bool `json:"blinking_text"`
1469+
HideId bool `json:"hide_id"`
1470+
TextPosition math.Point2LL `json:"text_position"`
1471+
CircleCenter math.Point2LL `json:"circle_center"`
1472+
CircleRadius float32 `json:"circle_radius"`
1473+
VerticesUser WaypointArray `json:"vertices"`
1474+
Vertices [][]math.Point2LL
1475+
Closed bool `json:"closed"`
1476+
Shaded bool `json:"shade_region"`
1477+
Color int `json:"color"`
1478+
1479+
Tris [][3]math.Point2LL
1480+
Deleted bool
1481+
}
1482+
1483+
type Airspace struct {
1484+
Boundaries map[string][]math.Point2LL `json:"boundaries"`
1485+
Volumes map[string][]ControllerAirspaceVolume `json:"volumes"`
1486+
}
1487+
1488+
func RestrictionAreaFromTFR(tfr TFR) RestrictionArea {
1489+
ra := RestrictionArea{
1490+
Title: tfr.LocalName,
1491+
Vertices: deep.MustCopy(tfr.Points),
1492+
}
1493+
1494+
if len(ra.Title) > 32 {
1495+
ra.Title = ra.Title[:32]
1496+
}
1497+
1498+
ra.HideId = true
1499+
ra.Closed = true
1500+
ra.Shaded = true // ??
1501+
ra.TextPosition = ra.AverageVertexPosition()
1502+
1503+
ra.UpdateTriangles()
1504+
1505+
return ra
1506+
}
1507+
1508+
func (ra *RestrictionArea) AverageVertexPosition() math.Point2LL {
1509+
var c math.Point2LL
1510+
var n float32
1511+
for _, loop := range ra.Vertices {
1512+
n += float32(len(loop))
1513+
for _, v := range loop {
1514+
c = math.Add2f(c, v)
1515+
}
1516+
}
1517+
return math.Scale2f(c, math.Max(1, 1/n)) // avoid 1/0 and return (0,0) if there are no verts.
1518+
}
1519+
1520+
func (ra *RestrictionArea) UpdateTriangles() {
1521+
if !ra.Closed || !ra.Shaded {
1522+
ra.Tris = nil
1523+
return
1524+
}
1525+
1526+
clear(ra.Tris)
1527+
for _, loop := range ra.Vertices {
1528+
if len(loop) < 3 {
1529+
continue
1530+
}
1531+
1532+
vertices := make([]earcut.Vertex, len(loop))
1533+
for i, v := range loop {
1534+
vertices[i].P = [2]float64{float64(v[0]), float64(v[1])}
1535+
}
1536+
1537+
for _, tri := range earcut.Triangulate(earcut.Polygon{Rings: [][]earcut.Vertex{vertices}}) {
1538+
var v32 [3]math.Point2LL
1539+
for i, v64 := range tri.Vertices {
1540+
v32[i] = [2]float32{float32(v64.P[0]), float32(v64.P[1])}
1541+
}
1542+
ra.Tris = append(ra.Tris, v32)
1543+
}
1544+
}
1545+
}
1546+
1547+
func (ra *RestrictionArea) MoveTo(p math.Point2LL) {
1548+
if ra.CircleRadius > 0 {
1549+
// Circle
1550+
delta := math.Sub2f(p, ra.CircleCenter)
1551+
ra.CircleCenter = p
1552+
ra.TextPosition = math.Add2f(ra.TextPosition, delta)
1553+
} else {
1554+
pc := ra.TextPosition
1555+
if pc.IsZero() {
1556+
pc = ra.AverageVertexPosition()
1557+
}
1558+
delta := math.Sub2f(p, pc)
1559+
ra.TextPosition = p
1560+
1561+
for _, loop := range ra.Vertices {
1562+
for i := range loop {
1563+
loop[i] = math.Add2f(loop[i], delta)
1564+
}
1565+
}
1566+
}
1567+
}
1568+
1569+
type STARSFacilityAdaptation struct {
1570+
AirspaceAwareness []AirspaceAwareness `json:"airspace_awareness"`
1571+
ForceQLToSelf bool `json:"force_ql_self"`
1572+
AllowLongScratchpad bool `json:"allow_long_scratchpad"`
1573+
VideoMapNames []string `json:"stars_maps"`
1574+
VideoMapLabels map[string]string `json:"map_labels"`
1575+
ControllerConfigs map[string]*STARSControllerConfig `json:"controller_configs"`
1576+
InhibitCAVolumes []AirspaceVolume `json:"inhibit_ca_volumes"`
1577+
RadarSites map[string]*RadarSite `json:"radar_sites"`
1578+
Center math.Point2LL `json:"-"`
1579+
CenterString string `json:"center"`
1580+
Range float32 `json:"range"`
1581+
Scratchpads map[string]string `json:"scratchpads"`
1582+
SignificantPoints map[string]SignificantPoint `json:"significant_points"`
1583+
Altimeters []string `json:"altimeters"`
1584+
1585+
MonitoredBeaconCodeBlocksString *string `json:"beacon_code_blocks"`
1586+
MonitoredBeaconCodeBlocks []Squawk
1587+
1588+
VideoMapFile string `json:"video_map_file"`
1589+
CoordinationFixes map[string]AdaptationFixes `json:"coordination_fixes"`
1590+
SingleCharAIDs map[string]string `json:"single_char_aids"` // Char to airport
1591+
BeaconBank int `json:"beacon_bank"`
1592+
KeepLDB bool `json:"keep_ldb"`
1593+
FullLDBSeconds int `json:"full_ldb_seconds"`
1594+
1595+
HandoffAcceptFlashDuration int `json:"handoff_acceptance_flash_duration"`
1596+
DisplayHOFacilityOnly bool `json:"display_handoff_facility_only"`
1597+
HOSectorDisplayDuration int `json:"handoff_sector_display_duration"`
1598+
1599+
PDB struct {
1600+
ShowScratchpad2 bool `json:"show_scratchpad2"`
1601+
HideGroundspeed bool `json:"hide_gs"`
1602+
ShowAircraftType bool `json:"show_aircraft_type"`
1603+
SplitGSAndCWT bool `json:"split_gs_and_cwt"`
1604+
DisplayCustomSPCs bool `json:"display_custom_spcs"`
1605+
} `json:"pdb"`
1606+
Scratchpad1 struct {
1607+
DisplayExitFix bool `json:"display_exit_fix"`
1608+
DisplayExitFix1 bool `json:"display_exit_fix_1"`
1609+
DisplayExitGate bool `json:"display_exit_gate"`
1610+
DisplayAltExitGate bool `json:"display_alternate_exit_gate"`
1611+
} `json:"scratchpad1"`
1612+
CustomSPCs []string `json:"custom_spcs"`
1613+
1614+
CoordinationLists []CoordinationList `json:"coordination_lists"`
1615+
RestrictionAreas []RestrictionArea `json:"restriction_areas"`
1616+
UseLegacyFont bool `json:"use_legacy_font"`
1617+
}
1618+
1619+
type STARSControllerConfig struct {
1620+
VideoMapNames []string `json:"video_maps"`
1621+
DefaultMaps []string `json:"default_maps"`
1622+
Center math.Point2LL `json:"-"`
1623+
CenterString string `json:"center"`
1624+
Range float32 `json:"range"`
1625+
MonitoredBeaconCodeBlocksString *string `json:"beacon_code_blocks"`
1626+
MonitoredBeaconCodeBlocks []Squawk
1627+
}
1628+
1629+
type CoordinationList struct {
1630+
Name string `json:"name"`
1631+
Id string `json:"id"`
1632+
Airports []string `json:"airports"`
1633+
YellowEntries bool `json:"yellow_entries"`
1634+
}
1635+
1636+
type SignificantPoint struct {
1637+
Name string // JSON comes in as a map from name to SignificantPoint; we set this.
1638+
ShortName string `json:"short_name"`
1639+
Abbreviation string `json:"abbreviation"`
1640+
Description string `json:"description"`
1641+
Location math.Point2LL `json:"location"`
1642+
}
1643+
1644+
type AirspaceAwareness struct {
1645+
Fix []string `json:"fixes"`
1646+
AltitudeRange [2]int `json:"altitude_range"`
1647+
ReceivingController string `json:"receiving_controller"`
1648+
AircraftType []string `json:"aircraft_type"`
1649+
}
1650+
1651+
type InboundFlow struct {
1652+
Arrivals []Arrival `json:"arrivals"`
1653+
Overflights []Overflight `json:"overflights"`
1654+
}
1655+
1656+
func (fa *STARSFacilityAdaptation) GetCoordinationFix(fp *STARSFlightPlan, acpos math.Point2LL, waypoints []Waypoint) (string, bool) {
1657+
for fix, adaptationFixes := range fa.CoordinationFixes {
1658+
if adaptationFix, err := adaptationFixes.Fix(fp.Altitude); err == nil {
1659+
if adaptationFix.Type == ZoneBasedFix {
1660+
// Exclude zone based fixes for now. They come in after the route-based fix
1661+
continue
1662+
}
1663+
1664+
// FIXME (as elsewhere): make this more robust
1665+
if strings.Contains(fp.Route, fix) {
1666+
return fix, true
1667+
}
1668+
1669+
// FIXME: why both this and checking fp.Route?
1670+
for _, waypoint := range waypoints {
1671+
if waypoint.Fix == fix {
1672+
return fix, true
1673+
}
1674+
}
1675+
}
1676+
1677+
}
1678+
1679+
var closestFix string
1680+
minDist := float32(1e30)
1681+
for fix, adaptationFixes := range fa.CoordinationFixes {
1682+
for _, adaptationFix := range adaptationFixes {
1683+
if adaptationFix.Type == ZoneBasedFix {
1684+
if loc, ok := DB.LookupWaypoint(fix); !ok {
1685+
// FIXME: check this (if it isn't already) at scenario load time.
1686+
panic(fix + ": not found in fixes database")
1687+
} else if dist := math.NMDistance2LL(acpos, loc); dist < minDist {
1688+
minDist = dist
1689+
closestFix = fix
1690+
}
1691+
}
1692+
}
1693+
}
1694+
1695+
return closestFix, closestFix != ""
1696+
}
1697+
1698+
type STARSFlightPlan struct {
1699+
*FlightPlan
1700+
FlightPlanType int
1701+
CoordinationTime CoordinationTime
1702+
CoordinationFix string
1703+
ContainedFacilities []string
1704+
Altitude string
1705+
SP1 string
1706+
SP2 string
1707+
InitialController string // For abbreviated FPs
1708+
}
1709+
1710+
type CoordinationTime struct {
1711+
Time time.Time
1712+
Type string // A for arrivals, P for Departures, E for overflights
1713+
}
1714+
1715+
// Flight plan types (STARS)
1716+
const (
1717+
// Flight plan received from a NAS ARTCC. This is a flight plan that
1718+
// has been sent over by an overlying ERAM facility.
1719+
RemoteEnroute = iota
1720+
1721+
// Flight plan received from an adjacent terminal facility This is a
1722+
// flight plan that has been sent over by another STARS facility.
1723+
RemoteNonEnroute
1724+
1725+
// VFR interfacility flight plan entered locally for which the NAS
1726+
// ARTCC has not returned a flight plan This is a flight plan that is
1727+
// made by a STARS facility that gets a NAS code.
1728+
LocalEnroute
1729+
1730+
// Flight plan entered by TCW or flight plan from an adjacent terminal
1731+
// that has been handed off to this STARS facility This is a flight
1732+
// plan that is made at a STARS facility and gets a local code.
1733+
LocalNonEnroute
1734+
)
1735+
1736+
func MakeSTARSFlightPlan(fp *FlightPlan) *STARSFlightPlan {
1737+
return &STARSFlightPlan{
1738+
FlightPlan: fp,
1739+
Altitude: fmt.Sprint(fp.Altitude),
1740+
}
1741+
}
1742+
1743+
func (fp *STARSFlightPlan) SetCoordinationFix(fa STARSFacilityAdaptation, ac *Aircraft, simTime time.Time) error {
1744+
cf, ok := fa.GetCoordinationFix(fp, ac.Position(), ac.Waypoints())
1745+
if !ok {
1746+
return ErrNoCoordinationFix
1747+
}
1748+
fp.CoordinationFix = cf
1749+
1750+
if dist, err := ac.DistanceAlongRoute(cf); err == nil {
1751+
m := dist / float32(fp.CruiseSpeed) * 60
1752+
fp.CoordinationTime = CoordinationTime{
1753+
Time: simTime.Add(time.Duration(m * float32(time.Minute))),
1754+
}
1755+
} else { // zone based fixes.
1756+
loc, ok := DB.LookupWaypoint(fp.CoordinationFix)
1757+
if !ok {
1758+
return ErrNoCoordinationFix
1759+
}
1760+
1761+
dist := math.NMDistance2LL(ac.Position(), loc)
1762+
m := dist / float32(fp.CruiseSpeed) * 60
1763+
fp.CoordinationTime = CoordinationTime{
1764+
Time: simTime.Add(time.Duration(m * float32(time.Minute))),
1765+
}
1766+
}
1767+
return nil
1768+
}
1769+
1770+
///////////////////////////////////////////////////////////////////////////
1771+
// Airspace
1772+
1773+
func InAirspace(p math.Point2LL, alt float32, volumes []ControllerAirspaceVolume) (bool, [][2]int) {
1774+
var altRanges [][2]int
1775+
for _, v := range volumes {
1776+
inside := false
1777+
for _, pts := range v.Boundaries {
1778+
if math.PointInPolygon2LL(p, pts) {
1779+
inside = !inside
1780+
}
1781+
}
1782+
if inside {
1783+
altRanges = append(altRanges, [2]int{v.LowerLimit, v.UpperLimit})
1784+
}
1785+
}
1786+
1787+
// Sort altitude ranges and then merge ones that have 1000 foot separation
1788+
sort.Slice(altRanges, func(i, j int) bool { return altRanges[i][0] < altRanges[j][0] })
1789+
var mergedAlts [][2]int
1790+
i := 0
1791+
inside := false
1792+
for i < len(altRanges) {
1793+
low := altRanges[i][0]
1794+
high := altRanges[i][1]
1795+
1796+
for i+1 < len(altRanges) {
1797+
if altRanges[i+1][0]-high <= 1000 {
1798+
// merge
1799+
high = altRanges[i+1][1]
1800+
i++
1801+
} else {
1802+
break
1803+
}
1804+
}
1805+
1806+
// 10 feet of slop for rounding error
1807+
inside = inside || (int(alt)+10 >= low && int(alt)-10 <= high)
1808+
1809+
mergedAlts = append(mergedAlts, [2]int{low, high})
1810+
i++
1811+
}
1812+
1813+
return inside, mergedAlts
1814+
}

pkg/aviation/errors.go

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var (
1717
ErrInvalidSquawkCode = errors.New("Invalid squawk code")
1818
ErrNoAircraftForCallsign = errors.New("No aircraft exists with specified callsign")
1919
ErrNoController = errors.New("No controller with that callsign")
20+
ErrNoCoordinationFix = errors.New("No coordination fix found")
2021
ErrNoERAMFacility = errors.New("No ERAM facility exists")
2122
ErrNoFlightPlan = errors.New("No flight plan has been filed for aircraft")
2223
ErrNoMatchingFix = errors.New("No matching fix")

0 commit comments

Comments
 (0)