@@ -25,7 +25,9 @@ import (
25
25
"github.com/mmp/vice/pkg/renderer"
26
26
"github.com/mmp/vice/pkg/util"
27
27
28
+ "github.com/brunoga/deep"
28
29
"github.com/klauspost/compress/zstd"
30
+ "github.com/mmp/earcut-go"
29
31
)
30
32
31
33
type ReportingPoint struct {
@@ -1444,3 +1446,369 @@ func (p *SquawkCodePool) NumAvailable() int {
1444
1446
}
1445
1447
return n
1446
1448
}
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
+ }
0 commit comments