diff --git a/.gitignore b/.gitignore index 4ce6fdd..339ce9e 100644 --- a/.gitignore +++ b/.gitignore @@ -337,4 +337,7 @@ ASALocalRun/ .localhistory/ # BeatPulse healthcheck temp database -healthchecksdb \ No newline at end of file +healthchecksdb + +obj/ +bin/ \ No newline at end of file diff --git a/AtopPlugin.cs b/AtopPlugin.cs new file mode 100644 index 0000000..988ac8a --- /dev/null +++ b/AtopPlugin.cs @@ -0,0 +1,70 @@ +using System.ComponentModel.Composition; +using AtopPlugin.Display; +using AtopPlugin.State; +using AtopPlugin.UI; +using vatsys; +using vatsys.Plugin; + +namespace AtopPlugin; + +[Export(typeof(IPlugin))] +public class AtopPlugin : ILabelPlugin, IStripPlugin +{ + public string Name => "ATOP Plugin"; + + public AtopPlugin() + { + RegisterEventHandlers(); + AtopMenu.Initialize(); + TempActivationMessagePopup.PopUpActivationMessageIfFirstTime(); + } + + private static void RegisterEventHandlers() + { + Network.PrivateMessagesChanged += PrivateMessagesChangedHandler.Handle; + Network.Disconnected += DisconnectHandler.Handle; + + // changes to cleared flight level do not register an FDR update + // we need to create custom handlers to be able to update the label/strip + FdrPropertyChangesListener.RegisterAllHandlers(); + } + + public void OnFDRUpdate(FDP2.FDR updated) + { + AtopPluginStateManager.ProcessFdrUpdate(updated); + AtopPluginStateManager.ProcessDisplayUpdate(updated); + AtopPluginStateManager.RunConflictProbe(updated); + FdrPropertyChangesListener.RegisterHandler(updated); + + // don't manage jurisdiction if not connected as ATC + if (Network.Me.IsRealATC) JurisdictionManager.HandleFdrUpdate(updated); + } + + public void OnRadarTrackUpdate(RDP.RadarTrack updated) + { + // don't manage jurisdiction if not connected as ATC + if (Network.Me.IsRealATC) JurisdictionManager.HandleRadarTrackUpdate(updated); + } + + public CustomStripItem? GetCustomStripItem(string itemType, Track track, FDP2.FDR flightDataRecord, + RDP.RadarTrack radarTrack) + { + return StripItemRenderer.RenderStripItem(itemType, track, flightDataRecord, radarTrack); + } + + public CustomLabelItem? GetCustomLabelItem(string itemType, Track track, FDP2.FDR flightDataRecord, + RDP.RadarTrack radarTrack) + { + return LabelItemRenderer.RenderLabelItem(itemType, track, flightDataRecord, radarTrack); + } + + public CustomColour? SelectASDTrackColour(Track track) + { + return TrackColorRenderer.GetAsdColor(track); + } + + public CustomColour? SelectGroundTrackColour(Track track) + { + return null; + } +} \ No newline at end of file diff --git a/AtopPlugin.csproj b/AtopPlugin.csproj new file mode 100644 index 0000000..59e41d0 --- /dev/null +++ b/AtopPlugin.csproj @@ -0,0 +1,111 @@ + + + + + Debug + AnyCPU + {59170BAC-129C-4AB8-884D-F583E460AA63} + Library + Properties + AtopPlugin + AtopPlugin + v4.8 + 512 + true + 11 + enable + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x86 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + x86 + + + + + + + + packages\Microsoft.NETFramework.ReferenceAssemblies.net48.1.0.3\build\.NETFramework\v4.8\System.Windows.Forms.dll + + + + + + + + + E:\vatsys\bin\vatSys.exe + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Form + + + SettingsWindow.cs + + + + + + + + + SettingsWindow.cs + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + \ No newline at end of file diff --git a/AuroraLabelItemsPlugin.sln b/AtopPlugin.sln similarity index 86% rename from AuroraLabelItemsPlugin.sln rename to AtopPlugin.sln index 97c859f..f982761 100644 --- a/AuroraLabelItemsPlugin.sln +++ b/AtopPlugin.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30711.63 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuroraLabelItemsPlugin", "AuroraLabelItemsPlugin.csproj", "{59170BAC-129C-4AB8-884D-F583E460AA63}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AtopPlugin", "AtopPlugin.csproj", "{59170BAC-129C-4AB8-884D-F583E460AA63}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/AuroraLabelItemsPlugin.cs b/AuroraLabelItemsPlugin.cs deleted file mode 100644 index 4fda7be..0000000 --- a/AuroraLabelItemsPlugin.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using vatsys; -using vatsys.Plugin; -using System.Collections.Concurrent; -using System.ComponentModel.Composition; //<--Need to add a reference to System.ComponentModel.Composition - -//Note the reference to vatsys (set Copy Local to false) -----> - -namespace AuroraLabelItemsPlugin -{ - [Export(typeof(IPlugin))] - public class AuroraLabelItemsPlugin : ILabelPlugin - { - /// The name of the custom label item we've added to Labels.xml in the Profile - const string LABEL_ITEM_LEVEL = "AURORA_LEVEL"; - const string LABEL_ITEM_3DIGIT_GROUNDSPEED = "AURORA_GROUNDSPEED"; - readonly static CustomColour EastboundColour = new CustomColour(255, 125, 0); - readonly static CustomColour WestboundColour = new CustomColour(0, 125, 255); - readonly ConcurrentDictionary eastboundCallsigns = new ConcurrentDictionary(); - - /// Plugin Name - public string Name { get => "Aurora Label Items"; } - - /// This is called each time a flight data record is updated - /// Here we are updating the eastbound callsigns dictionary with each flight data record - public void OnFDRUpdate(FDP2.FDR updated) - { - if (FDP2.GetFDRIndex(updated.Callsign) == -1) //FDR was removed (that's what triggered the update) - eastboundCallsigns.TryRemove(updated.Callsign, out _); - else - { - if(updated.ParsedRoute.Count > 1) - { - //calculate track from first route point to last (Departure point to destination point) - var rte = updated.ParsedRoute; - double trk = Conversions.CalculateTrack(rte.First().Intersection.LatLong, rte.Last().Intersection.LatLong); - bool east = trk >= 0 && trk < 180; - eastboundCallsigns.AddOrUpdate(updated.Callsign, east, (c,e) => east); - } - } - } - - /// This is called each time a radar track is updated - public void OnRadarTrackUpdate(RDP.RadarTrack updated) - { - - } - - /// vatSys calls this function when it encounters a custom label item (defined in Labels.xml) during the label rendering. - /// itemType is the value of the Type attribute in Labels.xml - /// If it's not our item being called (another plugins, for example), return null. - /// As a general rule, don't do processing in here as you'll slow down the ASD refresh. In the case of parsing a level to a string though, that's fine. - public CustomLabelItem GetCustomLabelItem(string itemType, Track track, FDP2.FDR flightDataRecord, RDP.RadarTrack radarTrack) - { - if (flightDataRecord == null) - return null; - - switch (itemType) - { - case LABEL_ITEM_LEVEL: - int level = radarTrack == null ? flightDataRecord.PRL / 100 : radarTrack.CorrectedAltitude / 100; - string sLevel = level.ToString("D3"); - if (level > RDP.TRANSITION_ALTITUDE)//then flight level - sLevel = "F" + sLevel; - else - sLevel = "A" + sLevel; - - return new CustomLabelItem() - { - Text = sLevel - }; - case LABEL_ITEM_3DIGIT_GROUNDSPEED: - //get groundspeed value from either FDR or radarTrack if coupled - var gs = radarTrack == null ? flightDataRecord.PredictedPosition.Groundspeed : radarTrack.GroundSpeed; - return new CustomLabelItem() - { - Text = gs.ToString("000")//format as 3 digits (with leading zeros) - }; - default: - return null; - } - } - - public CustomColour SelectASDTrackColour(Track track) - { - //only apply East/West colour to jurisdiction state - if (track.State != MMI.HMIStates.Jurisdiction) - return null; - - var fdr = track.GetFDR(); - //if track doesn't have an FDR coupled do nothing - if (fdr == null) - return null; - - //read our dictionary of stored bools (true means is easterly) and return the correct colour - if (eastboundCallsigns.TryGetValue(fdr.Callsign, out bool east)) - { - if (east) - return EastboundColour; - else - return WestboundColour; - } - - return null; - } - - public CustomColour SelectGroundTrackColour(Track track) - { - return null; - } - } -} diff --git a/AuroraLabelItemsPlugin.csproj b/AuroraLabelItemsPlugin.csproj deleted file mode 100644 index 2a68913..0000000 --- a/AuroraLabelItemsPlugin.csproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - - Debug - AnyCPU - {59170BAC-129C-4AB8-884D-F583E460AA63} - Library - Properties - AuroraLabelItemsPlugin - AuroraLabelItemsPlugin - v4.7.2 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - x86 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - x86 - - - - - - - - - - - - - ..\..\..\..\..\Program Files (x86)\vatSys\bin\vatSys.exe - False - - - - - - - - \ No newline at end of file diff --git a/Conflict/ConflictAreaCalculator.cs b/Conflict/ConflictAreaCalculator.cs new file mode 100644 index 0000000..308cd9d --- /dev/null +++ b/Conflict/ConflictAreaCalculator.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using vatsys; + +namespace AtopPlugin.Conflict; + +public static class ConflictAreaCalculator +{ + public static List CalculateAreaOfConflict(FDP2.FDR fdr1, FDP2.FDR fdr2, int value) + { + var fdr2SegmentsConflictingWithFdr1 = new List(); + var route1Waypoints = fdr1.ParsedRoute.ToList() + .Where(s => s.Type == FDP2.FDR.ExtractedRoute.Segment.SegmentTypes.WAYPOINT).ToList(); + var route2Waypoints = fdr2.ParsedRoute.ToList() + .Where(s => s.Type == FDP2.FDR.ExtractedRoute.Segment.SegmentTypes.WAYPOINT).ToList(); + for (var wp1Index = 1; wp1Index < route1Waypoints.Count; ++wp1Index) + { + var route1ProtectedAirspace = CreatePolygon(route1Waypoints[wp1Index - 1].Intersection.LatLong, + route1Waypoints[wp1Index].Intersection.LatLong, value); + for (var wp2Index = 1; wp2Index < route2Waypoints.Count; wp2Index++) + { + var prevRoute2LatLong = route2Waypoints[wp2Index - 1].Intersection.LatLong; + var currRoute2LatLong = route2Waypoints[wp2Index].Intersection.LatLong; + var protectedAirspacePenetrations = CalculatePolygonIntersections(route1ProtectedAirspace, + prevRoute2LatLong, + currRoute2LatLong); + var intersectionPoints = new List(); + var num1 = 0; + var num2 = 0; + foreach (var penetration in protectedAirspacePenetrations) + if (Conversions.IsLatLonOnGC(prevRoute2LatLong, currRoute2LatLong, penetration)) + { + intersectionPoints.Add(penetration); + } + else + { + var route2Track = Conversions.CalculateTrack(prevRoute2LatLong, currRoute2LatLong); + if (Math.Abs(route2Track - Conversions.CalculateTrack(prevRoute2LatLong, penetration)) > 90.0) + ++num1; + if (Math.Abs(route2Track - Conversions.CalculateTrack(penetration, currRoute2LatLong)) > 90.0) + ++num2; + } + + if (num1 % 2 != 0 && num2 % 2 != 0) + { + intersectionPoints.Clear(); + intersectionPoints.Add(prevRoute2LatLong); + intersectionPoints.Add(currRoute2LatLong); + } + else if (num2 % 2 != 0) + { + intersectionPoints.Add(currRoute2LatLong); + } + else if (num1 % 2 != 0) + { + intersectionPoints.Add(prevRoute2LatLong); + } + + intersectionPoints.Sort((x, y) => + Conversions.CalculateDistance(prevRoute2LatLong, x) + .CompareTo(Conversions.CalculateDistance(currRoute2LatLong, y))); + for (var ipIndex = 1; ipIndex < intersectionPoints.Count; ipIndex += 2) + { + var seg = new ConflictSegment(); + seg.StartLatlong = intersectionPoints[ipIndex - 1]; + seg.EndLatlong = intersectionPoints[ipIndex]; + var conflictSegments = fdr2SegmentsConflictingWithFdr1 + .Where(s => s.RouteSegment == route2Waypoints[wp2Index]).Where(s => + (Conversions.CalculateDistance(s.StartLatlong, + prevRoute2LatLong) < + Conversions.CalculateDistance(seg.StartLatlong, + prevRoute2LatLong) && + Conversions.CalculateDistance(s.EndLatlong, + prevRoute2LatLong) > + Conversions.CalculateDistance(seg.StartLatlong, + prevRoute2LatLong)) || + (Conversions.CalculateDistance(s.EndLatlong, + prevRoute2LatLong) > + Conversions.CalculateDistance(seg.EndLatlong, + prevRoute2LatLong) && + Conversions.CalculateDistance(s.StartLatlong, + prevRoute2LatLong) < + Conversions.CalculateDistance(seg.EndLatlong, + prevRoute2LatLong)) || + (Conversions.CalculateDistance(s.StartLatlong, + prevRoute2LatLong) > + Conversions.CalculateDistance(seg.StartLatlong, + prevRoute2LatLong) && + Conversions.CalculateDistance(s.EndLatlong, + prevRoute2LatLong) < + Conversions.CalculateDistance(seg.EndLatlong, + prevRoute2LatLong)) || + Conversions.CalculateDistance(s.StartLatlong, seg.StartLatlong) < 0.01 || + Conversions.CalculateDistance(s.EndLatlong, seg.EndLatlong) < 0.01).ToList(); + if (conflictSegments.Count > 0) + { + foreach (var segment in conflictSegments) + { + if (Conversions.CalculateDistance(segment.EndLatlong, + prevRoute2LatLong) < + Conversions.CalculateDistance(seg.EndLatlong, + prevRoute2LatLong)) + segment.EndLatlong = seg.EndLatlong; + if (Conversions.CalculateDistance(seg.StartLatlong, + prevRoute2LatLong) < + Conversions.CalculateDistance(segment.StartLatlong, + prevRoute2LatLong)) + segment.StartLatlong = seg.StartLatlong; + } + } + else + { + seg.Callsign = fdr2.Callsign; + seg.RouteSegment = route2Waypoints[wp2Index]; + fdr2SegmentsConflictingWithFdr1.Add(seg); + } + } + } + } + + foreach (var segment in fdr2SegmentsConflictingWithFdr1) + { + if (!fdr2SegmentsConflictingWithFdr1.Exists(s => + Conversions.CalculateDistance(segment.StartLatlong, s.EndLatlong) < 0.01)) + segment.StartTime = + FDP2.GetSystemEstimateAtPosition(fdr2, segment.StartLatlong, segment.RouteSegment); + if (!fdr2SegmentsConflictingWithFdr1.Exists(s => + Conversions.CalculateDistance(segment.EndLatlong, s.StartLatlong) < 0.01)) + segment.EndTime = FDP2.GetSystemEstimateAtPosition(fdr2, segment.EndLatlong, segment.RouteSegment); + } + + return fdr2SegmentsConflictingWithFdr1; + } + + + private static List CreatePolygon(Coordinate point1, Coordinate point2, int value) + { + var polygon = new List(); + var track = Conversions.CalculateTrack(point1, point2); + var num1 = track - 90.0; + for (var index = 0; index <= 180; index += 10) + { + var heading = num1 - index; + var fromBearingRange = Conversions.CalculateLLFromBearingRange(point1, value, heading); + polygon.Add(fromBearingRange); + } + + var num2 = track + 90.0; + for (var index = 0; index <= 180; index += 10) + { + var heading = num2 - index; + var fromBearingRange = Conversions.CalculateLLFromBearingRange(point2, value, heading); + polygon.Add(fromBearingRange); + } + + polygon.Add(polygon[0]); + return polygon; + } + + private static List CalculatePolygonIntersections( + List polygon, + Coordinate point1, + Coordinate point2) + { + var polygonIntersections = new List(); + for (var index = 1; index < polygon.Count; ++index) + { + var gcIntersectionLl = + Conversions.CalculateAllGCIntersectionLL(polygon[index - 1], polygon[index], point1, point2); + if (gcIntersectionLl != null) + polygonIntersections.AddRange(gcIntersectionLl); + } + + for (var index = 0; index < polygonIntersections.Count; ++index) + { + var intsect = polygonIntersections[index]; + polygonIntersections.RemoveAll(c => + !Equals(c, intsect) && Conversions.CalculateDistance(intsect, c) < 0.01); + } + + return polygonIntersections; + } +} + +public class ConflictSegment +{ + public string Callsign; + public Coordinate EndLatlong; + public DateTime EndTime = DateTime.MaxValue; + public FDP2.FDR.ExtractedRoute.Segment RouteSegment; + public Coordinate StartLatlong; + public DateTime StartTime = DateTime.MaxValue; +} \ No newline at end of file diff --git a/Conflict/ConflictProbe.cs b/Conflict/ConflictProbe.cs new file mode 100644 index 0000000..fba8923 --- /dev/null +++ b/Conflict/ConflictProbe.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AtopPlugin.Models; +using vatsys; + +namespace AtopPlugin.Conflict; + +public static class ConflictProbe +{ + public static Conflicts Probe(FDP2.FDR fdr) + { + if (!MMI.IsMySectorConcerned(fdr)) return EmptyConflicts(); + + var discoveredConflicts = new List(); + + var block1 = AltitudeBlock.ExtractAltitudeBlock(fdr); + foreach (var fdr2 in FDP2.GetFDRs.Where(fdr2 => + fdr2 != null && fdr.Callsign != fdr2.Callsign && MMI.IsMySectorConcerned(fdr2))) + { + var data = new ConflictData + { + Fdr2 = fdr2 + }; + var rte = fdr.ParsedRoute; + var rte2 = fdr2.ParsedRoute; + var trk = Conversions.CalculateTrack(rte.First().Intersection.LatLong, + rte.Last().Intersection.LatLong); + var trk2 = Conversions.CalculateTrack(rte2.First().Intersection.LatLong, + rte2.Last().Intersection.LatLong); + data.TrkAngle = Math.Abs(trk2 - trk); + var sameDir = data.TrkAngle < 45; + var crossing = (data.TrkAngle >= 45 && data.TrkAngle <= 135) || + (data.TrkAngle >= 315 && data.TrkAngle <= 225); + var oppoDir = data.TrkAngle > 135 && data.TrkAngle < 225; + var block2 = AltitudeBlock.ExtractAltitudeBlock(fdr2); + + data.VerticalAct = AltitudeBlock.Difference(block1, block2); + data.VerticalSep = MinimaCalculator.GetVerticalMinima(fdr, fdr2); + + if (data.VerticalAct >= data.VerticalSep) continue; + + data.LatSep = MinimaCalculator.GetLateralMinima(fdr, fdr2); + + // TODO(msalikhov): figure out what this was trying to do - it had no effect + // else if (data.latSep != 100 && data.latSep == 100) ; + // { + // data.latSep = (50 + 100) / 2; + // } + + var conflictSegments1 = ConflictAreaCalculator.CalculateAreaOfConflict(fdr, fdr2, data.LatSep); + var conflictSegments2 = ConflictAreaCalculator.CalculateAreaOfConflict(fdr2, fdr, data.LatSep); + + conflictSegments1.Sort((s, t) => s.StartTime.CompareTo(t.StartTime)); //sort by first conflict time + conflictSegments2.Sort((s, t) => s.StartTime.CompareTo(t.StartTime)); //sort by first conflict time + + var firstConflictTime = conflictSegments1.FirstOrDefault(); + var firstConflictTime2 = conflictSegments2.FirstOrDefault(); + var failedLateral = conflictSegments1.Count > 0; + if (firstConflictTime == null || firstConflictTime2 == null) continue; + + data.LongTimesep = MinimaCalculator.GetLongitudinalTime(fdr, fdr2); + data.LongTimeact = (firstConflictTime2.StartTime - firstConflictTime.StartTime).Duration(); + data.LongDistsep = MinimaCalculator.GetLongitudinalDistance(fdr, fdr2); + data.LongDistact = Conversions.CalculateDistance(firstConflictTime.StartLatlong, + firstConflictTime2.StartLatlong); + data.TimeLongsame = sameDir && failedLateral && firstConflictTime.EndTime > DateTime.UtcNow + && data.LongTimeact < + data.LongTimesep; //check time based longitudinal for same direction + data.TimeLongcross = crossing && failedLateral && firstConflictTime.EndTime > DateTime.UtcNow + && (firstConflictTime2.StartTime - firstConflictTime.StartTime) + .Duration() < new TimeSpan(0, 0, 15, 0); + data.DistLongsame = sameDir && failedLateral && firstConflictTime.EndTime > DateTime.UtcNow + && data.LongDistact < data.LongDistsep; + + data.TimeLongopposite = false; + + if (failedLateral && oppoDir) + try + { + data.Top = new TimeOfPassing(fdr, fdr2); + data.TimeLongopposite = data.Top.Time > DateTime.UtcNow + && data.Top.Time.Add(data.LongTimesep) > DateTime.UtcNow && + data.Top.Time.Subtract(data.LongTimesep) < DateTime.UtcNow; + } + catch (Exception) + { + // ignored - we were unable to calculate time of passing for some reason + } + + data.LongType = data.LongDistsep == null + ? data.TimeLongsame + : data.DistLongsame; + + var lossOfSep = data.LongType || data.TimeLongcross || data.TimeLongopposite; + data.ConflictType = lossOfSep && sameDir ? ConflictType.SameDirection : + lossOfSep && crossing ? ConflictType.Crossing : + lossOfSep && oppoDir ? ConflictType.OppositeDirection : null; + data.EarliestLos = failedLateral && oppoDir + ? data.Top?.Time.Subtract(new TimeSpan(0, 0, 10, 0)) ?? DateTime.MaxValue + : DateTime.Compare(firstConflictTime.StartTime, firstConflictTime2.StartTime) < 0 + ? firstConflictTime.StartTime + : firstConflictTime2.StartTime; + + var actual = failedLateral && oppoDir && data.VerticalAct < data.VerticalSep + ? new TimeSpan(0, 0, 1, 0, 0) >= data.EarliestLos.Subtract(DateTime.UtcNow).Duration() + : (lossOfSep && new TimeSpan(0, 0, 1, 0, 0) >= + data.EarliestLos.Subtract(DateTime.UtcNow).Duration()) || + data.EarliestLos < DateTime.UtcNow; + + var imminent = failedLateral && oppoDir && data.VerticalAct < data.VerticalSep + ? new TimeSpan(0, 0, 30, 0, 0) >= data.EarliestLos.Subtract(DateTime.UtcNow).Duration() + : lossOfSep && new TimeSpan(0, 0, 30, 0, 0) >= + data.EarliestLos.Subtract(DateTime.UtcNow).Duration(); //check if timediff < 30 min + + var advisory = failedLateral && oppoDir && data.VerticalAct < data.VerticalSep + ? new TimeSpan(0, 2, 0, 0, 0) > data.EarliestLos.Subtract(DateTime.UtcNow).Duration() + && data.EarliestLos.Subtract(DateTime.UtcNow).Duration() >= new TimeSpan(0, 0, 30, 0, 0) + : lossOfSep && new TimeSpan(0, 2, 0, 0, 0) > + data.EarliestLos.Subtract(DateTime.UtcNow).Duration() + && data.EarliestLos.Subtract(DateTime.UtcNow).Duration() >= + new TimeSpan(0, 0, 30, 0, 0); //check if 2 hours > timediff > 30 mins + + data.ConflictStatus = ConflictStatusUtils.From(actual, imminent, advisory); + + if (data.ConflictStatus != ConflictStatus.None) discoveredConflicts.Add(data); + } + + return GroupConflicts(discoveredConflicts); + } + + private static Conflicts EmptyConflicts() + { + return new Conflicts(new List(), new List(), new List()); + } + + private static Conflicts GroupConflicts(List allConflicts) + { + var actual = new List(); + var imminent = new List(); + var advisory = new List(); + + foreach (var conflict in allConflicts) + { + if (conflict.ConflictStatus == ConflictStatus.Actual) actual.Add(conflict); + if (conflict.ConflictStatus == ConflictStatus.Imminent) imminent.Add(conflict); + if (conflict.ConflictStatus == ConflictStatus.Advisory) advisory.Add(conflict); + } + + return new Conflicts(actual, imminent, advisory); + } + + public record struct Conflicts( + List ActualConflicts, + List ImminentConflicts, + List AdvisoryConflicts); + + public record ConflictData + { + public ConflictStatus ConflictStatus; + public ConflictType? ConflictType; + public bool DistLongsame; + public DateTime EarliestLos; + public FDP2.FDR Fdr2; + public int LatSep; + public double LongDistact; + public int? LongDistsep; + public TimeSpan LongTimeact; + public TimeSpan LongTimesep; + public bool LongType; + public bool TimeLongcross; + public bool TimeLongopposite; + public bool TimeLongsame; + public TimeOfPassing? Top; + public double TrkAngle; + public int VerticalAct; + public int VerticalSep; + } +} \ No newline at end of file diff --git a/Conflict/MinimaCalculator.cs b/Conflict/MinimaCalculator.cs new file mode 100644 index 0000000..e4c380c --- /dev/null +++ b/Conflict/MinimaCalculator.cs @@ -0,0 +1,80 @@ +using System; +using AtopPlugin.Models; +using vatsys; + +namespace AtopPlugin.Conflict; + +public static class MinimaCalculator +{ + private const int Rnp4Lateral = 23; + private const int Rnp10Lateral = 50; + private const int StandardLateral = 100; + + private const int StandardVertical = 1000; + private const int NonRvsmVertical = 2000; + private const int SupersonicVertical = 4000; + private const int Above600Vertical = 5000; + + private const int TimeLongitudinal = 15; + private const int JetLongitudinal = 10; + private const int DistanceLongitudinal = 50; + private const int Rnp4Longitudinal = 30; + + public static int GetLateralMinima(FDP2.FDR fdr1, FDP2.FDR fdr2) + { + if (CanApplyRnp4(fdr1) && CanApplyRnp4(fdr2)) return Rnp4Lateral; + + if (CanApplyRnp10(fdr1) && CanApplyRnp10(fdr2)) return Rnp10Lateral; + + return StandardLateral; + } + + public static int GetVerticalMinima(FDP2.FDR fdr1, FDP2.FDR fdr2) + { + var block1 = AltitudeBlock.ExtractAltitudeBlock(fdr1); + var block2 = AltitudeBlock.ExtractAltitudeBlock(fdr2); + + if (block1.IsAbove600() || block2.IsAbove600()) // technically this only applies if they are military + return Above600Vertical; + + if (block1.IsAbove450() || block2.IsAbove450()) // technically this only applies if one is supersonic + return SupersonicVertical; + + if (!(block1.IsBelowRvsm() || block2.IsBelowRvsm()) && (block1.IsAboveRvsm() || block2.IsAboveRvsm())) + return NonRvsmVertical; + + return StandardVertical; + } + + public static TimeSpan GetLongitudinalTime(FDP2.FDR fdr1, FDP2.FDR fdr2) + { + return fdr1.IsJet() && fdr2.IsJet() + ? new TimeSpan(0, JetLongitudinal, 0) + : new TimeSpan(0, TimeLongitudinal, 0); + } + + public static int? GetLongitudinalDistance(FDP2.FDR fdr1, FDP2.FDR fdr2) + { + if (CanApplyRnp4(fdr1) && CanApplyRnp4(fdr2)) return Rnp4Longitudinal; + + if (HasDatalink(fdr1) && HasDatalink(fdr2) && CanApplyRnp10(fdr1) && CanApplyRnp10(fdr2)) + return DistanceLongitudinal; + + return null; + } + + private static bool HasDatalink(FDP2.FDR fdr) + { + return fdr.GetAtopState()?.CalculatedFlightData is { Pbcs: true, Adsc: true, Cpdlc: true }; + } + + private static bool CanApplyRnp4(FDP2.FDR fdr) + { + return HasDatalink(fdr) && fdr.GetAtopState()?.CalculatedFlightData is { Rnp4: true }; + } + + private static bool CanApplyRnp10(FDP2.FDR fdr) + { + return fdr.GetAtopState()?.CalculatedFlightData is { Rnp10: true }; + } +} \ No newline at end of file diff --git a/Constants.cs b/Constants.cs new file mode 100644 index 0000000..317e531 --- /dev/null +++ b/Constants.cs @@ -0,0 +1,120 @@ +using vatsys.Plugin; + +namespace AtopPlugin; + +public static class CustomColors +{ + public static readonly CustomColour SepFlags = new(0, 196, 253); + public static readonly CustomColour Pending = new(46, 139, 87); + public static readonly CustomColour EastboundColour = new(240, 255, 255); + public static readonly CustomColour WestboundColour = new(240, 231, 140); + public static readonly CustomColour NonRvsm = new(242, 133, 0); + public static readonly CustomColour Probe = new(0, 255, 0); + public static readonly CustomColour NotCda = new(211, 28, 111); + public static readonly CustomColour Advisory = new(255, 165, 0); + public static readonly CustomColour Imminent = new(255, 0, 0); + public static readonly CustomColour SpecialConditionCode = new(255, 255, 0); + public static readonly CustomColour ApsBlue = new(141, 182, 205); +} + +public static class Symbols +{ + public const string Empty = ""; + + public const string NoCpdlcNoAdsb = "⎕"; + public const string CpdlcNoAdsb = "⧆"; + public const string CpdlcAndAdsb = "*"; //✱ + + public const string D30 = "3"; + public const string D50 = "D"; + public const string T10 = "M"; + public const string Mnt = "R"; + public const string Rvsm = "W"; + public const string L50 = "R"; + public const string L23 = "4"; + + public const string CommDownlink = "▼"; + public const string CommEmpty = "⬜"; + + public const string RadarFlag = "★"; + public const string UntoggledFlag = "◦"; + public const string ScratchpadFlag = "&"; + public const string MntFlag = "M"; + public const string ComplexFlag = "*"; + public const string RestrictionsFlag = "x"; + + public const string Climbing = "↑"; + public const string Descending = "↓"; + public const string DeviatingAbove = "+"; + public const string DeviatingBelow = "-"; + + public const string Inhibited = "^"; + public const string EmptyAnnotations = "."; + + public const string StripRouteItem = "F"; + public const string StripRadarIndicator = "A"; +} + +public static class StripConstants +{ + public const string LabelItemAdsbCpdlc = "AURORA_ADSB_CPDLC"; //field c(4) + public const string StripItemCallsign = "AURORA_CALLSIGN"; + public const string StripItemCtlsector = "AURORA_CDA"; + public const string StripItemNxtsector = "AURORA_NDA"; + public const string StripItemT10Flag = "AURORA_T10_FLAG"; + public const string StripItemMntFlag = "AURORA_MNT_FLAG"; + public const string StripItemDistFlag = "AURORA_DIST_FLAG"; + public const string StripItemRvsmFlag = "AURORA_RVSM_FLAG"; + public const string StripItemVmi = "AURORA_STRIP_VMI"; + public const string StripItemComplex = "AURORA_STRIP_COMPLEX"; + public const string StripItemClearedLevel = "AURORA_CLEARED_LEVEL"; + public const string StripItemRequestedLevel = "AURORA_REQUESTED_LEVEL"; + public const string StripItemManEst = "AURORA_MAN_EST"; + public const string StripItemPoint = "AURORA_POINT"; + public const string StripItemRoute = "AURORA_ROUTE_STRIP"; + public const string StripItemRadarInd = "AURORA_RADAR_IND"; + public const string StripItemAnnotInd = "AURORA_ANNOT_STRIP"; + public const string StripItemLateralFlag = "AURORA_LATERAL_FLAG"; + public const string StripItemRestr = "AURORA_RESTR_STRIP"; + public const string StripItemClrdRte = "AURORA_CLRD_RTE"; + public const string CparItemType = "CPAR_TYP"; + public const string CparItemRequired = "CPAR_REQUIRED"; + public const string CparItemIntruder = "CPAR_INT"; + public const string CparItemLos = "CPAR_LOS"; + public const string CparItemActual = "CPAR_ACTUAL"; + public const string CparItemPassing = "CPAR_PASSING"; + public const string CparItemConfSegStart1 = "CPAR_CONF_SEG_START_1"; + public const string CparItemConfSegStart2 = "CPAR_CONF_SEG_START_2"; + public const string CparItemConfSegEnd1 = "CPAR_CONF_SEG_END_1"; + public const string CparItemConfSegEnd2 = "CPAR_CONF_SEG_END_2"; + public const string CparItemStartime1 = "CPAR_START_TIME_1"; + public const string CparItemStartime2 = "CPAR_START_TIME_2"; + public const string CparItemEndtime1 = "CPAR_END_TIME_1"; + public const string CparItemEndtime2 = "CPAR_END_TIME_2"; + public const string CparItemAid2 = "CPAR_AID_2"; + public const string CparItemTyp2 = "CPAR_TYP_2"; + public const string CparItemSpd2 = "CPAR_SPD_2"; + public const string CparItemAlt2 = "CPAR_ALT_2"; +} + +public static class LabelConstants +{ + public const string LabelItemSelectHori = "SELECT_HORI"; + public const string LabelItemSelectVert = "SELECT_VERT"; + public const string LabelItemCommIcon = "AURORA_COMM_ICON"; //field a(2) + public const string LabelItemAdsbCpdlc = "AURORA_ADSB_CPDLC"; //field c(4) + public const string LabelItemAdsFlags = "AURORA_ADS_FLAGS"; //field c(4) + public const string LabelItemMntFlags = "AURORA_MNT_FLAGS"; //field c(4) + public const string LabelItemScc = "AURORA_SCC"; //field d(5) + public const string LabelItemAnnotInd = "AURORA_ANNOT_IND"; //field e(1) + public const string LabelItemRestr = "AURORA_RESTR"; //field f(1) + public const string LabelItemLevel = "AURORA_LEVEL"; //field g(3) + public const string LabelItemVmi = "AURORA_VMI"; //field h(1) + public const string LabelItemClearedLevel = "AURORA_CLEARED_LEVEL"; //field i(7) + public const string LabelItemHandoffInd = "AURORA_HO_IND"; //field j(4) + public const string LabelItemRadarInd = "AURORA_RADAR_IND"; //field k(1) + public const string LabelItemInhibitInd = "AURORA_INHIBIT_IND"; //field l(1) + public const string LabelItemFiledSpeed = "AURORA_FILEDSPEED"; //field m(4) + public const string LabelItem3DigitGroundspeed = "AURORA_GROUNDSPEED"; //field n(5) + public const string LabelItemDestination = "AURORA_DESTINATION"; //field o(4) +} \ No newline at end of file diff --git a/Display/LabelItemRenderer.cs b/Display/LabelItemRenderer.cs new file mode 100644 index 0000000..de0b489 --- /dev/null +++ b/Display/LabelItemRenderer.cs @@ -0,0 +1,150 @@ +using AtopPlugin.State; +using vatsys; +using vatsys.Plugin; + +namespace AtopPlugin.Display; + +public static class LabelItemRenderer +{ + public static CustomLabelItem? RenderLabelItem(string itemType, Track track, FDP2.FDR? fdr, + RDP.RadarTrack _) + { + var renderedItem = RenderLabelItemDelegate(itemType, track, fdr); + return renderedItem != null ? ExcludeConflictColor(fdr!, track, renderedItem) : null; + } + + private static CustomLabelItem ExcludeConflictColor(FDP2.FDR fdr, Track track, CustomLabelItem customLabelItem) + { + // If we already overrode it, keep it that way + if (customLabelItem.ForeColourIdentity == Colours.Identities.Custom) return customLabelItem; + + customLabelItem.ForeColourIdentity = Colours.Identities.Custom; + customLabelItem.CustomForeColour = TrackColorRenderer.GetDirectionColour(fdr, track) ?? CustomColors.ApsBlue; + + return customLabelItem; + } + + private static CustomLabelItem? RenderLabelItemDelegate(string itemType, Track track, FDP2.FDR? fdr) + { + if (fdr?.GetAtopState() == null || fdr.GetDisplayState() == null) return null; + + var atopState = fdr.GetAtopState()!; + var displayState = fdr.GetDisplayState()!; + + return itemType switch + { + LabelConstants.LabelItemSelectHori => track.IsSelected() + ? new CustomLabelItem { Text = Symbols.Empty, Border = BorderFlags.Bottom } + : null, + + LabelConstants.LabelItemSelectVert => track.IsSelected() + ? new CustomLabelItem { Text = Symbols.Empty, Border = BorderFlags.Left } + : null, + + LabelConstants.LabelItemCommIcon => atopState.DownlinkIndicator + ? new CustomLabelItem { Text = Symbols.CommDownlink, Border = BorderFlags.All } + : new CustomLabelItem { Text = Symbols.CommEmpty }, + + LabelConstants.LabelItemAdsbCpdlc => fdr.State is FDP2.FDR.FDRStates.STATE_PREACTIVE or + FDP2.FDR.FDRStates.STATE_COORDINATED + ? new CustomLabelItem + { + Text = displayState.CpdlcAdsbSymbol, ForeColourIdentity = Colours.Identities.Custom, + CustomForeColour = CustomColors.NotCda + } + : new CustomLabelItem { Text = displayState.CpdlcAdsbSymbol }, + + LabelConstants.LabelItemAdsFlags => new CustomLabelItem { Text = displayState.AdsFlag }, + + LabelConstants.LabelItemMntFlags => displayState.IsMntFlagToggled + ? new CustomLabelItem { Text = Symbols.MntFlag } + : null, + + LabelConstants.LabelItemScc => atopState.HighestSccFlag != null + ? new CustomLabelItem + { + Text = atopState.HighestSccFlag!.Value, ForeColourIdentity = Colours.Identities.Custom, + CustomForeColour = CustomColors.SpecialConditionCode + } + : null, + + LabelConstants.LabelItemAnnotInd => displayState.HasAnnotations + ? new CustomLabelItem { Text = Symbols.ScratchpadFlag } + : new CustomLabelItem { Text = Symbols.UntoggledFlag }, + + LabelConstants.LabelItemRestr => displayState.IsRestrictionsIndicatorToggled + ? new CustomLabelItem { Text = Symbols.RestrictionsFlag } + : null, + + LabelConstants.LabelItemLevel => displayState.AltitudeColor == null + ? new CustomLabelItem + { + Text = displayState.CurrentLevel.PadLeft(3), + Border = displayState.AltitudeBorderFlags, + BorderColourIdentity = Colours.Identities.Custom, + CustomBorderColour = CustomColors.NotCda + } + : new CustomLabelItem + { + Text = displayState.CurrentLevel.PadLeft(3), + Border = displayState.AltitudeBorderFlags, + BorderColourIdentity = Colours.Identities.Custom, + CustomBorderColour = CustomColors.NotCda, + ForeColourIdentity = Colours.Identities.Custom, + CustomForeColour = displayState.AltitudeColor + }, + + LabelConstants.LabelItemVmi => displayState.AltitudeColor == null + ? new CustomLabelItem + { + Text = (atopState.AltitudeFlag?.Value ?? "").PadLeft(1), + Border = displayState.AltitudeBorderFlags, + BorderColourIdentity = Colours.Identities.Custom, + CustomBorderColour = CustomColors.NotCda + } + : new CustomLabelItem + { + Text = (atopState.AltitudeFlag?.Value ?? "").PadLeft(1), + Border = displayState.AltitudeBorderFlags, + BorderColourIdentity = Colours.Identities.Custom, + CustomBorderColour = CustomColors.NotCda, + ForeColourIdentity = Colours.Identities.Custom, + CustomForeColour = displayState.AltitudeColor + }, + + LabelConstants.LabelItemClearedLevel => displayState.AltitudeColor == null + ? new CustomLabelItem + { + Text = displayState.ClearedLevel.PadLeft(3), + Border = displayState.AltitudeBorderFlags, + BorderColourIdentity = Colours.Identities.Custom, + CustomBorderColour = CustomColors.NotCda + } + : new CustomLabelItem + { + Text = displayState.ClearedLevel.PadLeft(3), + Border = displayState.AltitudeBorderFlags, + BorderColourIdentity = Colours.Identities.Custom, + CustomBorderColour = CustomColors.NotCda, + ForeColourIdentity = Colours.Identities.Custom, + CustomForeColour = displayState.AltitudeColor + }, + + LabelConstants.LabelItemRadarInd => atopState.RadarToggleIndicator + ? new CustomLabelItem { Text = Symbols.RadarFlag, OnMouseClick = RadarFlagToggleHandler.Handle } + : new CustomLabelItem { Text = Symbols.UntoggledFlag, OnMouseClick = RadarFlagToggleHandler.Handle }, + + LabelConstants.LabelItemInhibitInd => fdr.State == FDP2.FDR.FDRStates.STATE_INHIBITED + ? new CustomLabelItem { Text = Symbols.Inhibited } + : null, + + LabelConstants.LabelItemFiledSpeed => new CustomLabelItem { Text = displayState.FiledSpeed }, + + LabelConstants.LabelItem3DigitGroundspeed => new CustomLabelItem { Text = displayState.GroundSpeed }, + + LabelConstants.LabelItemDestination => new CustomLabelItem { Text = fdr.DesAirport }, + + _ => null + }; + } +} \ No newline at end of file diff --git a/Display/StripItemRenderer.cs b/Display/StripItemRenderer.cs new file mode 100644 index 0000000..ee2b90e --- /dev/null +++ b/Display/StripItemRenderer.cs @@ -0,0 +1,133 @@ +using AtopPlugin.Models; +using vatsys; +using vatsys.Plugin; + +namespace AtopPlugin.Display; + +public static class StripItemRenderer +{ + public static CustomStripItem? RenderStripItem(string itemType, Track track, FDP2.FDR? fdr, + RDP.RadarTrack radarTrack) + { + if (fdr?.GetAtopState() == null || fdr.GetDisplayState() == null) return null; + + var atopState = fdr.GetAtopState()!; + var displayState = fdr.GetDisplayState()!; + + return itemType switch + { + StripConstants.StripItemCallsign => RenderCallsignStripItem(fdr), + + StripConstants.StripItemCtlsector => RenderCtlSectorStripItem(fdr), + + StripConstants.StripItemNxtsector => new CustomStripItem { Text = atopState.NextSector?.Name ?? "" }, + + StripConstants.LabelItemAdsbCpdlc => RenderAdsbCpdlcStripItem(fdr), + + StripConstants.StripItemT10Flag => fdr.IsJet() + ? new CustomStripItem { Text = Symbols.T10 } + : null, + + StripConstants.StripItemMntFlag => fdr.IsJet() + ? new CustomStripItem { Text = Symbols.Mnt } + : null, + + StripConstants.StripItemDistFlag => !string.IsNullOrEmpty(displayState.AdsFlag) + ? new CustomStripItem + { + Text = displayState.AdsFlag, BackColourIdentity = Colours.Identities.Custom, + CustomBackColour = CustomColors.SepFlags + } + : null, + + StripConstants.StripItemRvsmFlag => fdr.RVSM + ? new CustomStripItem + { + Text = Symbols.Rvsm, BackColourIdentity = Colours.Identities.Custom, + CustomBackColour = CustomColors.SepFlags + } + : null, + + StripConstants.StripItemVmi => new CustomStripItem { Text = atopState.AltitudeFlag?.Value ?? "" }, + + StripConstants.StripItemComplex => displayState.IsRestrictionsIndicatorToggled + ? new CustomStripItem() { Text = Symbols.ComplexFlag } + : null, + + StripConstants.StripItemClearedLevel => new CustomStripItem { Text = displayState.ClearedLevel }, + + StripConstants.StripItemRequestedLevel => new CustomStripItem { Text = displayState.RequestedLevel }, + + StripConstants.StripItemRoute => new CustomStripItem { Text = Symbols.StripRouteItem }, + + StripConstants.StripItemRadarInd => new CustomStripItem { Text = Symbols.StripRadarIndicator }, + + StripConstants.StripItemAnnotInd => displayState.HasAnnotations + ? new CustomStripItem { Text = Symbols.ScratchpadFlag } + : new CustomStripItem { Text = Symbols.EmptyAnnotations }, + + StripConstants.StripItemLateralFlag => !string.IsNullOrEmpty(displayState.LateralFlag) + ? new CustomStripItem + { + Text = displayState.LateralFlag, BackColourIdentity = Colours.Identities.Custom, + CustomBackColour = CustomColors.SepFlags + } + : null, + + StripConstants.StripItemRestr => displayState.IsRestrictionsIndicatorToggled + ? new CustomStripItem { Text = Symbols.RestrictionsFlag } + : null, + + _ => null + }; + } + + private static CustomStripItem RenderCallsignStripItem(FDP2.FDR fdr) + { + var stripItem = GetStripItemWithColorsForDirection(fdr.GetAtopState()?.DirectionOfFlight); + stripItem.Text = fdr.Callsign; + return stripItem; + } + + private static CustomStripItem RenderCtlSectorStripItem(FDP2.FDR fdr) + { + var pendingCoordination = + fdr.State is FDP2.FDR.FDRStates.STATE_PREACTIVE or FDP2.FDR.FDRStates.STATE_COORDINATED; + var sectorName = fdr.ControllingSector?.Name ?? Symbols.Empty; + + var stripItem = new CustomStripItem { Text = sectorName }; + + if (pendingCoordination) + { + stripItem.ForeColourIdentity = Colours.Identities.Custom; + stripItem.CustomForeColour = CustomColors.Pending; + } + + return stripItem; + } + + private static CustomStripItem? RenderAdsbCpdlcStripItem(FDP2.FDR fdr) + { + var text = fdr.GetDisplayState()!.CpdlcAdsbSymbol; + if (string.IsNullOrEmpty(text)) return null; + + var stripItem = GetStripItemWithColorsForDirection(fdr.GetAtopState()?.DirectionOfFlight); + stripItem.Text = text; + return stripItem; + } + + private static CustomStripItem GetStripItemWithColorsForDirection(DirectionOfFlight? directionOfFlight) + { + var foreColorIdentity = directionOfFlight == DirectionOfFlight.Eastbound + ? Colours.Identities.StripBackground + : Colours.Identities.StripText; + var backColorIdentity = directionOfFlight == DirectionOfFlight.Eastbound + ? Colours.Identities.StripText + : Colours.Identities.StripBackground; + return new CustomStripItem + { + ForeColourIdentity = foreColorIdentity, + BackColourIdentity = backColorIdentity + }; + } +} \ No newline at end of file diff --git a/Display/TrackColorRenderer.cs b/Display/TrackColorRenderer.cs new file mode 100644 index 0000000..005804d --- /dev/null +++ b/Display/TrackColorRenderer.cs @@ -0,0 +1,47 @@ +using AtopPlugin.Models; +using vatsys; +using vatsys.Plugin; + +namespace AtopPlugin.Display; + +public static class TrackColorRenderer +{ + public static CustomColour? GetAsdColor(Track track) + { + var fdr = track.GetFDR(); + + if (fdr == null) return null; + + return fdr.State switch + { + FDP2.FDR.FDRStates.STATE_PREACTIVE or FDP2.FDR.FDRStates.STATE_COORDINATED => GetConflictColour(fdr), + _ => GetConflictColour(fdr) ?? GetDirectionColour(fdr, track) + }; + } + + public static CustomColour? GetDirectionColour(FDP2.FDR fdr, Track track) + { + if (!IsInJurisdiction(track)) return null; + return fdr.GetAtopState()?.DirectionOfFlight switch + { + DirectionOfFlight.Eastbound => CustomColors.EastboundColour, + DirectionOfFlight.Westbound => CustomColors.WestboundColour, + _ => null + }; + } + + private static CustomColour? GetConflictColour(FDP2.FDR fdr) + { + return fdr.GetConflicts() switch + { + { ActualConflicts.Count: > 0 } or { ImminentConflicts.Count: > 0 } => CustomColors.Imminent, + { AdvisoryConflicts.Count: > 0 } => CustomColors.Advisory, + _ => null + }; + } + + private static bool IsInJurisdiction(Track track) + { + return track.State == MMI.HMIStates.Jurisdiction; + } +} \ No newline at end of file diff --git a/Extensions.cs b/Extensions.cs new file mode 100644 index 0000000..39bd82d --- /dev/null +++ b/Extensions.cs @@ -0,0 +1,44 @@ +using AtopPlugin.Conflict; +using AtopPlugin.State; +using vatsys; + +namespace AtopPlugin; + +public static class Extensions +{ + public static AtopAircraftState? GetAtopState(this FDP2.FDR fdr) + { + return AtopPluginStateManager.GetAircraftState(fdr.Callsign); + } + + public static AtopAircraftDisplayState? GetDisplayState(this FDP2.FDR fdr) + { + return AtopPluginStateManager.GetDisplayState(fdr.Callsign); + } + + public static ConflictProbe.Conflicts? GetConflicts(this FDP2.FDR fdr) + { + return AtopPluginStateManager.GetConflicts(fdr.Callsign); + } + + public static int? GetTransponderCode(this FDP2.FDR fdr) + { + return fdr.CoupledTrack?.ActualAircraft.TransponderCode; + } + + public static bool IsJet(this FDP2.FDR fdr) + { + return fdr.PerformanceData?.IsJet ?? false; + } + + public static bool IsConnected(this FDP2.FDR fdr) + { + return fdr.CoupledTrack?.ActualAircraft != null || + Network.GetOnlinePilots.Exists(pilot => pilot.Callsign == fdr.Callsign); + } + + public static bool IsSelected(this Track track) + { + return MMI.SelectedTrack == track; + } +} \ No newline at end of file diff --git a/Logic/AltitudeCalculator.cs b/Logic/AltitudeCalculator.cs new file mode 100644 index 0000000..09ba2ef --- /dev/null +++ b/Logic/AltitudeCalculator.cs @@ -0,0 +1,45 @@ +using AtopPlugin.Models; +using vatsys; + +namespace AtopPlugin.Logic; + +public static class AltitudeCalculator +{ + private const int LevelFlightThreshold = 300; + + public static bool CalculateAltitudeChangePending(FDP2.FDR updatedFdr, AltitudeBlock previousBlock, + bool wasPreviouslyPending) + { + var newBlock = AltitudeBlock.ExtractAltitudeBlock(updatedFdr); + return (newBlock != previousBlock || wasPreviouslyPending) + && !IsWithinThreshold(updatedFdr.PRL, newBlock); + } + + public static AltitudeFlag? CalculateAltitudeFlag(FDP2.FDR fdr, bool pendingAltitudeChange) + { + var altitudeBlock = AltitudeBlock.ExtractAltitudeBlock(fdr); + var (altitudeLower, altitudeUpper) = altitudeBlock; + var isOutsideThresholdAndNotBlank = !IsWithinThreshold(fdr.PRL, altitudeBlock) && !IsBlank(fdr.PRL); + + return isOutsideThresholdAndNotBlank switch + { + true when pendingAltitudeChange && fdr.PRL < altitudeLower => AltitudeFlag.Climbing, + true when pendingAltitudeChange && fdr.PRL > altitudeUpper => AltitudeFlag.Descending, + true when !pendingAltitudeChange && fdr.PRL < altitudeLower => AltitudeFlag.DeviatingBelow, + true when !pendingAltitudeChange && fdr.PRL > altitudeUpper => AltitudeFlag.DeviatingAbove, + _ => null + }; + } + + public static bool IsWithinThreshold(int pilotReportedAltitude, AltitudeBlock altitudeBlock) + { + var lowerWithThreshold = altitudeBlock.LowerAltitude - LevelFlightThreshold; + var upperWithThreshold = altitudeBlock.UpperAltitude + LevelFlightThreshold; + return pilotReportedAltitude > lowerWithThreshold && pilotReportedAltitude < upperWithThreshold; + } + + private static bool IsBlank(int altitude) + { + return altitude < 100; + } +} \ No newline at end of file diff --git a/Logic/FlightDataCalculator.cs b/Logic/FlightDataCalculator.cs new file mode 100644 index 0000000..2138b5c --- /dev/null +++ b/Logic/FlightDataCalculator.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Text.RegularExpressions; +using AtopPlugin.Models; +using vatsys; + +namespace AtopPlugin.Logic; + +public static class FlightDataCalculator +{ + private const string Rnp10Pbn = "A1"; + private const string Rnp4Pbn = "L1"; + + private const string Rsp180Sur = "RSP180"; + + private const string AdscSurvEquip = "D1"; + private const string Rcp240SurvEquip = "P2"; + + private const string PbnEquip = "R"; + + private static readonly string[] CpdlcEquip = + { + "J5", "J7" + }; + + public static CalculatedFlightData GetCalculatedFlightData(FDP2.FDR fdr) + { + // TODO(msalikhov): can we pull from fdr.PBNCapability + var pbn = Regex.Match(fdr.Remarks, @"PBN\/\w+\s").Value; + var sur = Regex.Match(fdr.Remarks, @"SUR\/\w+\s").Value; + + var isPbn = fdr.AircraftEquip.Contains(PbnEquip); + + return new CalculatedFlightData( + isPbn && pbn.Contains(Rnp4Pbn), + isPbn && pbn.Contains(Rnp10Pbn), + CpdlcEquip.Any(cpdlcVal => fdr.AircraftEquip.Contains(cpdlcVal)), + fdr.AircraftSurvEquip.Contains(AdscSurvEquip), + sur.Contains(Rsp180Sur) && fdr.AircraftSurvEquip.Contains(Rcp240SurvEquip) + ); + } +} \ No newline at end of file diff --git a/Logic/NextSectorCalculator.cs b/Logic/NextSectorCalculator.cs new file mode 100644 index 0000000..c1c5270 --- /dev/null +++ b/Logic/NextSectorCalculator.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using vatsys; + +namespace AtopPlugin.Logic; + +public static class NextSectorCalculator +{ + public static SectorsVolumes.Sector? GetNextSector(FDP2.FDR fdr) + { + var segment = (from s in fdr.ParsedRoute.ToList() + where s.Type == FDP2.FDR.ExtractedRoute.Segment.SegmentTypes.ZPOINT && + fdr.ControllingSector != SectorsVolumes.FindSector((SectorsVolumes.Volume)s.Tag) + select s).FirstOrDefault(s => s.ETO > DateTime.UtcNow); + + SectorsVolumes.Volume? volume = null; + if (segment != null) volume = (SectorsVolumes.Volume)segment.Tag; + + var nextSector = volume != null ? SectorsVolumes.FindSector(volume) : null; + + if (nextSector == null) return nextSector; + + SectorsVolumes.Sector? sector = null; + foreach (var s2 in SectorsVolumes.SectorGroupings.Keys) + if (s2.SubSectors.Contains(nextSector) && + (sector == null || sector.SubSectors.Count > s2.SubSectors.Count)) + sector = s2; + if (sector != null) nextSector = sector; + + return nextSector; + } +} \ No newline at end of file diff --git a/Logic/SccFlagCalculator.cs b/Logic/SccFlagCalculator.cs new file mode 100644 index 0000000..2fba290 --- /dev/null +++ b/Logic/SccFlagCalculator.cs @@ -0,0 +1,25 @@ +using AtopPlugin.Models; +using vatsys; + +namespace AtopPlugin.Logic; + +public static class SccFlagCalculator +{ + private const int RadioFailureCode = 7600; + private const int EmergencyCode = 7700; + private const int MilitaryInterceptCode = 7777; + + public static SccFlag? CalculateHighestPriorityFlag(FDP2.FDR fdr, CalculatedFlightData calculatedFlightData) + { + if (!fdr.IsConnected()) return SccFlag.Or; + + var transponderCode = fdr.GetTransponderCode(); + return transponderCode switch + { + EmergencyCode => SccFlag.Emg, + RadioFailureCode => SccFlag.Rcf, + MilitaryInterceptCode => SccFlag.Mti, + _ => calculatedFlightData is { Rnp4: false, Rnp10: false } ? SccFlag.Rnp : null + }; + } +} \ No newline at end of file diff --git a/Models/AltitudeBlock.cs b/Models/AltitudeBlock.cs new file mode 100644 index 0000000..88a5f74 --- /dev/null +++ b/Models/AltitudeBlock.cs @@ -0,0 +1,62 @@ +using vatsys; + +namespace AtopPlugin.Models; + +public record struct AltitudeBlock(int LowerAltitude, int UpperAltitude) +{ + private const int Fl450 = 45000; + private const int Fl600 = 60000; + + public bool IsBelowRvsm() + { + return UpperAltitude <= FDP2.RVSM_BAND_LOWER; + } + + public bool IsAbove450() + { + return UpperAltitude > Fl450 || LowerAltitude > Fl450; + } + + public bool IsAbove600() + { + return UpperAltitude > Fl600 || LowerAltitude > Fl600; + } + + public bool IsAboveRvsm() + { + return UpperAltitude > FDP2.RVSM_BAND_UPPER || LowerAltitude > FDP2.RVSM_BAND_UPPER; + } + + public static int Difference(AltitudeBlock block1, AltitudeBlock block2) + { + // check for intersection + if (block1.LowerAltitude <= block2.UpperAltitude && block2.LowerAltitude <= block1.UpperAltitude) return 0; + + var isBlock1Lower = block1.UpperAltitude < block2.LowerAltitude; + + return isBlock1Lower + ? block2.LowerAltitude - block1.UpperAltitude + : block1.LowerAltitude - block2.UpperAltitude; + } + + public static AltitudeBlock ExtractAltitudeBlock(FDP2.FDR fdr) + { + var cflUpperAsNull = AsNullWhenNegative(fdr.CFLUpper); + var cflLowerAsNull = AsNullWhenNegative(fdr.CFLLower); + var altitudeUpper = cflUpperAsNull ?? fdr.RFL; + var altitudeLower = cflLowerAsNull ?? (cflUpperAsNull ?? fdr.RFL); + return new AltitudeBlock(altitudeLower, altitudeUpper); + } + + private static int? AsNullWhenNegative(int number) + { + return number >= 0 ? number : null; + } + + public override string ToString() + { + if (LowerAltitude == UpperAltitude) return (UpperAltitude / 100).ToString(); + + return LowerAltitude / 100 + "B" + UpperAltitude / 100; + } +} \ No newline at end of file diff --git a/Models/AltitudeFlag.cs b/Models/AltitudeFlag.cs new file mode 100644 index 0000000..949d5fd --- /dev/null +++ b/Models/AltitudeFlag.cs @@ -0,0 +1,16 @@ +namespace AtopPlugin.Models; + +public class AltitudeFlag +{ + private AltitudeFlag(string value) + { + Value = value; + } + + public string Value { get; private set; } + + public static AltitudeFlag Climbing => new(Symbols.Climbing); + public static AltitudeFlag Descending => new(Symbols.Descending); + public static AltitudeFlag DeviatingAbove => new(Symbols.DeviatingAbove); + public static AltitudeFlag DeviatingBelow => new(Symbols.DeviatingBelow); +} \ No newline at end of file diff --git a/Models/CalculatedFlightData.cs b/Models/CalculatedFlightData.cs new file mode 100644 index 0000000..32d88aa --- /dev/null +++ b/Models/CalculatedFlightData.cs @@ -0,0 +1,9 @@ +namespace AtopPlugin.Models; + +public record struct CalculatedFlightData( + bool Rnp4, + bool Rnp10, + bool Cpdlc, + bool Adsc, + bool Pbcs +); \ No newline at end of file diff --git a/Models/ConflictStatus.cs b/Models/ConflictStatus.cs new file mode 100644 index 0000000..1978337 --- /dev/null +++ b/Models/ConflictStatus.cs @@ -0,0 +1,23 @@ +namespace AtopPlugin.Models; + +public enum ConflictStatus +{ + Actual, + Imminent, + Advisory, + None +} + +public static class ConflictStatusUtils +{ + public static ConflictStatus From(bool actual, bool imminent, bool advisory) + { + return (actual, imminent, advisory) switch + { + { actual: true } => ConflictStatus.Actual, + { imminent: true } => ConflictStatus.Imminent, + { advisory: true } => ConflictStatus.Advisory, + _ => ConflictStatus.None + }; + } +} \ No newline at end of file diff --git a/Models/ConflictType.cs b/Models/ConflictType.cs new file mode 100644 index 0000000..6e108ea --- /dev/null +++ b/Models/ConflictType.cs @@ -0,0 +1,8 @@ +namespace AtopPlugin.Models; + +public enum ConflictType +{ + SameDirection, + OppositeDirection, + Crossing +} \ No newline at end of file diff --git a/Models/DirectionOfFlight.cs b/Models/DirectionOfFlight.cs new file mode 100644 index 0000000..7178f50 --- /dev/null +++ b/Models/DirectionOfFlight.cs @@ -0,0 +1,24 @@ +using vatsys; + +namespace AtopPlugin.Models; + +public enum DirectionOfFlight +{ + Eastbound, + Westbound, + Undetermined +} + +public static class DirectionOfFlightCalculator +{ + public static DirectionOfFlight GetDirectionOfFlight(FDP2.FDR fdr) + { + // TODO(msalikhov): handle case where aircraft go the "long way around" + if (fdr.ParsedRoute.Count <= 1) return DirectionOfFlight.Undetermined; + + var track = Conversions.CalculateTrack(fdr.ParsedRoute.First().Intersection.LatLong, + fdr.ParsedRoute.Last().Intersection.LatLong); + + return track is >= 0.0 and < 180.0 ? DirectionOfFlight.Eastbound : DirectionOfFlight.Westbound; + } +} \ No newline at end of file diff --git a/Models/SccFlag.cs b/Models/SccFlag.cs new file mode 100644 index 0000000..591d753 --- /dev/null +++ b/Models/SccFlag.cs @@ -0,0 +1,17 @@ +namespace AtopPlugin.Models; + +public class SccFlag +{ + private SccFlag(string value) + { + Value = value; + } + + public string Value { get; private set; } + + public static SccFlag Rnp => new("RNP"); + public static SccFlag Emg => new("EMG"); + public static SccFlag Rcf => new("RCF"); + public static SccFlag Mti => new("MTI"); + public static SccFlag Or => new("OR"); // out of range flag to represent pilot has has disconnected +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index cad189d..97e0d2e 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -33,4 +32,4 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/State/AtopAircraftDisplayState.cs b/State/AtopAircraftDisplayState.cs new file mode 100644 index 0000000..f89a550 --- /dev/null +++ b/State/AtopAircraftDisplayState.cs @@ -0,0 +1,140 @@ +using System; +using System.Linq; +using AtopPlugin.Logic; +using AtopPlugin.Models; +using vatsys; +using vatsys.Plugin; + +namespace AtopPlugin.State; + +public class AtopAircraftDisplayState +{ + private static readonly string[] RestrictionLabels = { "AT ", " BY ", "CLEARED TO " }; + + public AtopAircraftDisplayState(AtopAircraftState atopAircraftState) + { + UpdateFromAtopState(atopAircraftState); + } + + public string CpdlcAdsbSymbol { get; private set; } + public string AdsFlag { get; private set; } + public string LateralFlag { get; private set; } + public bool IsMntFlagToggled { get; private set; } + public bool HasAnnotations { get; private set; } + public bool IsRestrictionsIndicatorToggled { get; private set; } + public string CurrentLevel { get; private set; } + public string ClearedLevel { get; private set; } + public string RequestedLevel { get; private set; } + public BorderFlags AltitudeBorderFlags { get; private set; } + public CustomColour? AltitudeColor { get; private set; } + public string FiledSpeed { get; private set; } + public string GroundSpeed { get; private set; } + + public void UpdateFromAtopState(AtopAircraftState atopAircraftState) + { + CpdlcAdsbSymbol = GetCpdlcAdsbSymbol(atopAircraftState); + AdsFlag = GetAdsFlag(atopAircraftState); + LateralFlag = GetLateralFlag(atopAircraftState); + IsMntFlagToggled = atopAircraftState.Fdr.IsJet(); + HasAnnotations = !string.IsNullOrEmpty(atopAircraftState.Fdr.LabelOpData); + IsRestrictionsIndicatorToggled = GetRestrictionsIndicatorToggled(atopAircraftState); + CurrentLevel = GetCurrentLevel(atopAircraftState); + ClearedLevel = GetClearedLevel(atopAircraftState); + RequestedLevel = GetRequestedLevel(atopAircraftState); + AltitudeBorderFlags = GetAltitudeBorderFlags(atopAircraftState); + AltitudeColor = atopAircraftState.Fdr.RVSM ? null : CustomColors.NonRvsm; + FiledSpeed = GetFiledSpeed(atopAircraftState); + GroundSpeed = GetGroundSpeed(atopAircraftState); + } + + private static string GetCpdlcAdsbSymbol(AtopAircraftState atopAircraftState) + { + var adsb = atopAircraftState.Fdr.ADSB; + var cpdlc = atopAircraftState.CalculatedFlightData.Cpdlc; + return (adsb, cpdlc) switch + { + { adsb: true, cpdlc: true } => Symbols.CpdlcAndAdsb, + { adsb: true, cpdlc: false } => Symbols.Empty, + { adsb: false, cpdlc: true } => Symbols.CpdlcNoAdsb, + { adsb: false, cpdlc: false } => Symbols.NoCpdlcNoAdsb + }; + } + + private static string GetAdsFlag(AtopAircraftState atopAircraftState) + { + return atopAircraftState.CalculatedFlightData switch + { + { Adsc: true, Cpdlc: true, Rnp4: true } => Symbols.D30, + { Adsc: true, Cpdlc: true, Rnp10: true } => Symbols.D50, + _ => Symbols.Empty + }; + } + + private static string GetLateralFlag(AtopAircraftState atopAircraftState) + { + return atopAircraftState.CalculatedFlightData switch + { + { Adsc: true, Cpdlc: true, Rnp4: true } => Symbols.L23, + { Adsc: true, Cpdlc: true, Rnp10: true } => Symbols.L50, + _ => Symbols.Empty + }; + } + + private static bool GetRestrictionsIndicatorToggled(AtopAircraftState atopAircraftState) + { + return RestrictionLabels.Any(label => atopAircraftState.Fdr.LabelOpData.Contains(label)); + } + + private static string GetCurrentLevel(AtopAircraftState atopAircraftState) + { + var fdr = atopAircraftState.Fdr; + var altitudeBlock = AltitudeBlock.ExtractAltitudeBlock(fdr); + + var prlHundreds = (int)Math.Round(atopAircraftState.Fdr.PRL / 100.0); + if (!atopAircraftState.PendingAltitudeChange || AltitudeCalculator.IsWithinThreshold(fdr.PRL, altitudeBlock)) + return prlHundreds == 0 ? "" : (int)Math.Round(prlHundreds / 10.0) + "0"; + + return prlHundreds == 0 ? "" : prlHundreds.ToString(); + } + + private static string GetClearedLevel(AtopAircraftState atopAircraftState) + { + var fdr = atopAircraftState.Fdr; + var altitudeBlock = AltitudeBlock.ExtractAltitudeBlock(fdr); + + if (!atopAircraftState.PendingAltitudeChange || + AltitudeCalculator.IsWithinThreshold(fdr.PRL, altitudeBlock)) + return Symbols.Empty; + + return altitudeBlock.ToString(); + } + + private static string GetRequestedLevel(AtopAircraftState atopAircraftState) + { + return atopAircraftState.Fdr.State == FDP2.FDR.FDRStates.STATE_INACTIVE + ? (atopAircraftState.Fdr.RFL / 100).ToString() + : Symbols.Empty; + } + + private static BorderFlags GetAltitudeBorderFlags(AtopAircraftState atopAircraftState) + { + return atopAircraftState.Fdr.State switch + { + FDP2.FDR.FDRStates.STATE_PREACTIVE or FDP2.FDR.FDRStates.STATE_COORDINATED => BorderFlags.All, + _ => BorderFlags.None + }; + } + + private static string GetFiledSpeed(AtopAircraftState atopAircraftState) + { + var temperature = GRIB.FindTemperature(atopAircraftState.Fdr.PRL, atopAircraftState.Fdr.GetLocation(), true); + var mach = Conversions.CalculateMach(atopAircraftState.Fdr.TAS, temperature); + return "M" + mach.ToString("F2").Replace(".", ""); + } + + private static string GetGroundSpeed(AtopAircraftState atopAircraftState) + { + var gs = atopAircraftState.Fdr.PredictedPosition.Groundspeed; + return "N" + gs.ToString("000"); + } +} \ No newline at end of file diff --git a/State/AtopAircraftState.cs b/State/AtopAircraftState.cs new file mode 100644 index 0000000..300ff07 --- /dev/null +++ b/State/AtopAircraftState.cs @@ -0,0 +1,50 @@ +using AtopPlugin.Logic; +using AtopPlugin.Models; +using vatsys; + +namespace AtopPlugin.State; + +public class AtopAircraftState +{ + public AtopAircraftState(FDP2.FDR fdr) + { + Fdr = fdr; + UpdateFromFdr(fdr); + DownlinkIndicator = false; + RadarToggleIndicator = false; + WasHandedOff = false; + } + + public FDP2.FDR Fdr { get; private set; } + public CalculatedFlightData CalculatedFlightData { get; private set; } + public DirectionOfFlight DirectionOfFlight { get; private set; } + public SccFlag? HighestSccFlag { get; private set; } + public AltitudeFlag? AltitudeFlag { get; private set; } + public SectorsVolumes.Sector? NextSector { get; private set; } + public bool DownlinkIndicator { get; set; } + public bool RadarToggleIndicator { get; set; } + public bool WasHandedOff { get; private set; } + public bool PendingAltitudeChange { get; private set; } + + private AltitudeBlock PreviousAltitudeBlock { get; set; } + + public void UpdateFromFdr(FDP2.FDR updatedFdr) + { + Fdr = updatedFdr; + + CalculatedFlightData = FlightDataCalculator.GetCalculatedFlightData(updatedFdr); + DirectionOfFlight = DirectionOfFlightCalculator.GetDirectionOfFlight(updatedFdr); + WasHandedOff = !WasHandedOff && (updatedFdr.IsHandoff || updatedFdr.ControllingSector == null); + HighestSccFlag = SccFlagCalculator.CalculateHighestPriorityFlag(updatedFdr, CalculatedFlightData); + + // ensure the bool for altitude change is calculated first since it is used in the altitude flag calculation + PendingAltitudeChange = + AltitudeCalculator.CalculateAltitudeChangePending(updatedFdr, PreviousAltitudeBlock, PendingAltitudeChange); + AltitudeFlag = AltitudeCalculator.CalculateAltitudeFlag(updatedFdr, PendingAltitudeChange); + + NextSector = NextSectorCalculator.GetNextSector(updatedFdr); + + // update this last since so we have the previous value for the next update + PreviousAltitudeBlock = AltitudeBlock.ExtractAltitudeBlock(updatedFdr); + } +} \ No newline at end of file diff --git a/State/AtopPluginStateManager.cs b/State/AtopPluginStateManager.cs new file mode 100644 index 0000000..ca03517 --- /dev/null +++ b/State/AtopPluginStateManager.cs @@ -0,0 +1,136 @@ +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Windows.Forms; +using AtopPlugin.Conflict; +using AtopPlugin.UI; +using vatsys; + +namespace AtopPlugin.State; + +public static class AtopPluginStateManager +{ + private const int MissingFromFdpState = -1; + + private static readonly ConcurrentDictionary AircraftStates = new(); + private static readonly ConcurrentDictionary DisplayStates = new(); + private static readonly ConcurrentDictionary Conflicts = new(); + private static bool _probeEnabled = true; + private static bool _activated = false; + + public static bool Activated + { + get => _activated; + set + { + _activated = value; + AtopMenu.SetActivationState(value); + } + } + + public static AtopAircraftState? GetAircraftState(string callsign) + { + var found = AircraftStates.TryGetValue(callsign, out var state); + return found ? state : null; + } + + public static AtopAircraftDisplayState? GetDisplayState(string callsign) + { + var found = DisplayStates.TryGetValue(callsign, out var state); + return found ? state : null; + } + + public static ConflictProbe.Conflicts? GetConflicts(string callsign) + { + var found = Conflicts.TryGetValue(callsign, out var state); + return found ? state : null; + } + + public static async Task ProcessFdrUpdate(FDP2.FDR updated) + { + var callsign = updated.Callsign; + + if (FDP2.GetFDRIndex(callsign) == MissingFromFdpState) + { + AircraftStates.TryRemove(callsign, out _); + DisplayStates.TryRemove(callsign, out _); + return; + } + + var aircraftState = GetAircraftState(callsign); + if (aircraftState == null) + { + aircraftState = await Task.Run(() => new AtopAircraftState(updated)); + AircraftStates.TryAdd(callsign, aircraftState); + } + else + { + await Task.Run(() => aircraftState.UpdateFromFdr(updated)); + } + } + + public static async Task ProcessDisplayUpdate(FDP2.FDR fdr) + { + var callsign = fdr.Callsign; + var atopState = fdr.GetAtopState(); + if (atopState == null) return; + + var displayState = GetDisplayState(callsign); + if (displayState == null) + { + displayState = await Task.Run(() => new AtopAircraftDisplayState(atopState)); + DisplayStates.TryAdd(callsign, displayState); + } + else + { + await Task.Run(() => displayState.UpdateFromAtopState(atopState)); + } + } + + public static async Task RunConflictProbe(FDP2.FDR fdr) + { + if (_probeEnabled) + { + var newConflicts = await Task.Run(() => ConflictProbe.Probe(fdr)); + Conflicts.AddOrUpdate(fdr.Callsign, newConflicts, (_, _) => newConflicts); + } + } + + public static bool IsConflictProbeEnabled() + { + return _probeEnabled; + } + + public static void SetConflictProbe(bool conflictProbeEnabled) + { + _probeEnabled = conflictProbeEnabled; + if (!_probeEnabled) Conflicts.Clear(); + } + + public static void ToggleActivated() + { + var newActivationState = !Activated; + + switch (newActivationState) + { + case true when !Network.IsConnected: + MessageBox.Show(@"Please connect to the network before activating"); + return; + case true: + MessageBox.Show(@"Session activated"); + break; + case false: + MessageBox.Show(@"Session deactivated"); + break; + } + + Activated = newActivationState; + } + + public static void Reset() + { + AircraftStates.Clear(); + DisplayStates.Clear(); + Conflicts.Clear(); + Activated = false; + } +} \ No newline at end of file diff --git a/State/DisconnectHandler.cs b/State/DisconnectHandler.cs new file mode 100644 index 0000000..fd7e6b7 --- /dev/null +++ b/State/DisconnectHandler.cs @@ -0,0 +1,11 @@ +using System; + +namespace AtopPlugin.State; + +public static class DisconnectHandler +{ + public static void Handle(object sender, EventArgs eventArgs) + { + AtopPluginStateManager.Reset(); + } +} \ No newline at end of file diff --git a/State/FdrPropertyChangesListener.cs b/State/FdrPropertyChangesListener.cs new file mode 100644 index 0000000..ed81788 --- /dev/null +++ b/State/FdrPropertyChangesListener.cs @@ -0,0 +1,34 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using vatsys; + +namespace AtopPlugin.State; + +public static class FdrPropertyChangesListener +{ + private static readonly string[] RelevantProperties = { "CFLUpper", "CFLLower" }; + private static readonly HashSet RegisteredFdrs = new(); + + public static void RegisterAllHandlers() + { + FDP2.GetFDRs.ForEach(RegisterHandler); + } + + public static void RegisterHandler(FDP2.FDR fdr) + { + var newEntry = RegisteredFdrs.Add(fdr.Callsign); + if (!newEntry) return; + fdr.PropertyChanged += Handle; + } + + private static async void Handle(object sender, PropertyChangedEventArgs eventArgs) + { + if (sender is not FDP2.FDR fdr) return; + if (!RelevantProperties.Contains(eventArgs.PropertyName)) return; + await AtopPluginStateManager.ProcessFdrUpdate(fdr); + await AtopPluginStateManager.ProcessDisplayUpdate(fdr); + await AtopPluginStateManager.RunConflictProbe(fdr); + } +} \ No newline at end of file diff --git a/State/JurisdictionManager.cs b/State/JurisdictionManager.cs new file mode 100644 index 0000000..8a6bcb8 --- /dev/null +++ b/State/JurisdictionManager.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using vatsys; + +namespace AtopPlugin.State; + +public static class JurisdictionManager +{ + public static async Task HandleFdrUpdate(FDP2.FDR fdr) + { + if (!fdr.ESTed && MMI.IsMySectorConcerned(fdr)) MMI.EstFDR(fdr); + + var isInControlledSector = await IsInControlledSector(fdr.GetLocation(), fdr.PRL); + var atopState = fdr.GetAtopState(); + + // check if aircraft previously tracked to avoid re-tracking manually dropped/handed off tracks + if (AtopPluginStateManager.Activated && isInControlledSector && !fdr.IsTracked && + atopState is { WasHandedOff: false }) + MMI.AcceptJurisdiction(fdr); + + // if they're outside sector, currently tracked, and not going to re-enter, drop them + // also drop them if we are not activated + if ((!isInControlledSector && fdr.IsTrackedByMe && !await WillEnter(fdr)) || + (fdr.IsTrackedByMe && !AtopPluginStateManager.Activated)) + MMI.HandoffToNone(fdr); + } + + public static async Task HandleRadarTrackUpdate(RDP.RadarTrack rt) + { + if (rt.CoupledFDR == null) return; + await HandleFdrUpdate(rt.CoupledFDR); + } + + private static async Task WillEnter(FDP2.FDR fdr) + { + return await Task.Run(() => MMI.GetSectorEntryTime(fdr) != DateTime.MaxValue); + } + + private static async Task IsInControlledSector(Coordinate? location, int altitude) + { + if (location == null || double.IsNaN(location.Latitude) || double.IsNaN(location.Longitude)) return false; + return await Task.Run(() => + MMI.SectorsControlled.ToList().Exists(sector => sector.IsInSector(location, altitude))); + } +} \ No newline at end of file diff --git a/State/PrivateMessagesChangedHandler.cs b/State/PrivateMessagesChangedHandler.cs new file mode 100644 index 0000000..99d02eb --- /dev/null +++ b/State/PrivateMessagesChangedHandler.cs @@ -0,0 +1,12 @@ +using vatsys; + +namespace AtopPlugin.State; + +public static class PrivateMessagesChangedHandler +{ + public static void Handle(object sender, Network.GenericMessageEventArgs eventArgs) + { + var extendedFdrState = AtopPluginStateManager.GetAircraftState(eventArgs.Message.Address); + if (extendedFdrState != null) extendedFdrState.DownlinkIndicator = !eventArgs.Message.Sent; + } +} \ No newline at end of file diff --git a/State/RadarFlagToggleHandler.cs b/State/RadarFlagToggleHandler.cs new file mode 100644 index 0000000..7145916 --- /dev/null +++ b/State/RadarFlagToggleHandler.cs @@ -0,0 +1,13 @@ +using vatsys.Plugin; + +namespace AtopPlugin.State; + +public static class RadarFlagToggleHandler +{ + public static void Handle(CustomLabelItemMouseClickEventArgs eventArgs) + { + var atopState = eventArgs.Track.GetFDR().GetAtopState(); + if (atopState != null) atopState.RadarToggleIndicator = !atopState.RadarToggleIndicator; + eventArgs.Handled = true; + } +} \ No newline at end of file diff --git a/UI/AtopMenu.cs b/UI/AtopMenu.cs new file mode 100644 index 0000000..935240e --- /dev/null +++ b/UI/AtopMenu.cs @@ -0,0 +1,52 @@ +using System.Windows.Forms; +using AtopPlugin.State; +using vatsys; +using vatsys.Plugin; + +namespace AtopPlugin.UI; + +public static class AtopMenu +{ + private const string CategoryName = "ATOP"; + + private static readonly SettingsWindow SettingsWindow = new(); + private static readonly ToolStripMenuItem ActivationToggle = new("Activate"); + + static AtopMenu() + { + InitializeSettingsMenu(); + InitializeActivationToggle(); + } + + // empty method to force static class initialization to happen + public static void Initialize() + { + } + + private static void InitializeSettingsMenu() + { + var settingsMenuItem = new CustomToolStripMenuItem(CustomToolStripMenuItemWindowType.Main, + CustomToolStripMenuItemCategory.Custom, new ToolStripMenuItem("Settings")) + { + CustomCategoryName = CategoryName + }; + settingsMenuItem.Item.Click += (_, _) => MMI.InvokeOnGUI(SettingsWindow.Show); + MMI.AddCustomMenuItem(settingsMenuItem); + } + + private static void InitializeActivationToggle() + { + var activationMenuItem = new CustomToolStripMenuItem(CustomToolStripMenuItemWindowType.Main, + CustomToolStripMenuItemCategory.Custom, ActivationToggle) + { + CustomCategoryName = CategoryName + }; + activationMenuItem.Item.Click += (_, _) => MMI.InvokeOnGUI(AtopPluginStateManager.ToggleActivated); + MMI.AddCustomMenuItem(activationMenuItem); + } + + public static void SetActivationState(bool state) + { + ActivationToggle.Checked = state; + } +} \ No newline at end of file diff --git a/UI/SettingsWindow.Designer.cs b/UI/SettingsWindow.Designer.cs new file mode 100644 index 0000000..0ab4fa0 --- /dev/null +++ b/UI/SettingsWindow.Designer.cs @@ -0,0 +1,68 @@ +using System.ComponentModel; + +namespace AtopPlugin.UI; + +partial class SettingsWindow +{ + /// + /// Required designer variable. + /// + private IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.probe = new System.Windows.Forms.CheckBox(); + this.SuspendLayout(); + // + // probe + // + this.probe.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.probe.Appearance = System.Windows.Forms.Appearance.Button; + this.probe.CheckAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.probe.Font = new System.Drawing.Font("Terminus (TTF)", 18F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.probe.Location = new System.Drawing.Point(25, 12); + this.probe.Name = "probe"; + this.probe.Size = new System.Drawing.Size(100, 50); + this.probe.TabIndex = 1; + this.probe.Text = "PROBE"; + this.probe.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.probe.UseVisualStyleBackColor = true; + this.probe.CheckedChanged += new System.EventHandler(this.probe_CheckedChanged); + // + // SettingsWindow + // + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 17F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(146, 72); + this.Controls.Add(this.probe); + this.HideOnClose = true; + this.Name = "SettingsWindow"; + this.Text = "ATOP Settings"; + this.TopMost = true; + this.ResumeLayout(false); + } + + private System.Windows.Forms.CheckBox probe; + + #endregion +} \ No newline at end of file diff --git a/UI/SettingsWindow.cs b/UI/SettingsWindow.cs new file mode 100644 index 0000000..e91a4da --- /dev/null +++ b/UI/SettingsWindow.cs @@ -0,0 +1,19 @@ +using System; +using AtopPlugin.State; +using vatsys; + +namespace AtopPlugin.UI; + +public partial class SettingsWindow : BaseForm +{ + public SettingsWindow() + { + InitializeComponent(); + probe.Checked = AtopPluginStateManager.IsConflictProbeEnabled(); + } + + private void probe_CheckedChanged(object sender, EventArgs e) + { + AtopPluginStateManager.SetConflictProbe(probe.Checked); + } +} \ No newline at end of file diff --git a/UI/SettingsWindow.resx b/UI/SettingsWindow.resx new file mode 100644 index 0000000..f8317aa --- /dev/null +++ b/UI/SettingsWindow.resx @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + \ No newline at end of file diff --git a/UI/TempActivationMessagePopup.cs b/UI/TempActivationMessagePopup.cs new file mode 100644 index 0000000..7055320 --- /dev/null +++ b/UI/TempActivationMessagePopup.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AtopPlugin.UI; + +public static class TempActivationMessagePopup +{ + private const string AcknowledgeFileName = ".atop_activation_ack"; + + public static void PopUpActivationMessageIfFirstTime() + { + if (ActivationAckFileExists()) return; + Task.Run(() => MessageBox.Show( + """Starting with this version of the ATOP vatSys plugin, you will have to activate your session by clicking the "Activate" button under the "ATOP" menu in order to use the full controlling functionalities.""", + @"vatSys ATOP Plugin" + )); + WriteActivationAckFile(); + } + + private static bool ActivationAckFileExists() + { + return File.Exists(GetAckFilePath()); + } + + private static void WriteActivationAckFile() + { + File.WriteAllBytes(GetAckFilePath(), new byte[] { }); + } + + private static string GetAckFilePath() + { + return Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, + AcknowledgeFileName); + } +} \ No newline at end of file diff --git a/bin/Release/AuroraLabelItemsPlugin.dll b/bin/Release/AuroraLabelItemsPlugin.dll deleted file mode 100644 index 65015bb..0000000 Binary files a/bin/Release/AuroraLabelItemsPlugin.dll and /dev/null differ diff --git a/bin/Release/AuroraLabelItemsPlugin.pdb b/bin/Release/AuroraLabelItemsPlugin.pdb deleted file mode 100644 index c4db67b..0000000 Binary files a/bin/Release/AuroraLabelItemsPlugin.pdb and /dev/null differ diff --git a/obj/Debug/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs b/obj/Debug/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs deleted file mode 100644 index 3871b18..0000000 --- a/obj/Debug/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] diff --git a/obj/Debug/AuroraLabelItemsPlugin.csprojAssemblyReference.cache b/obj/Debug/AuroraLabelItemsPlugin.csprojAssemblyReference.cache deleted file mode 100644 index 09f263a..0000000 Binary files a/obj/Debug/AuroraLabelItemsPlugin.csprojAssemblyReference.cache and /dev/null differ diff --git a/obj/Debug/DesignTimeResolveAssemblyReferencesInput.cache b/obj/Debug/DesignTimeResolveAssemblyReferencesInput.cache deleted file mode 100644 index c900b45..0000000 Binary files a/obj/Debug/DesignTimeResolveAssemblyReferencesInput.cache and /dev/null differ diff --git a/obj/Release/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs b/obj/Release/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs deleted file mode 100644 index 3871b18..0000000 --- a/obj/Release/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] diff --git a/obj/Release/AuroraLabelItemsPlugin.csproj.CoreCompileInputs.cache b/obj/Release/AuroraLabelItemsPlugin.csproj.CoreCompileInputs.cache deleted file mode 100644 index 4f47018..0000000 --- a/obj/Release/AuroraLabelItemsPlugin.csproj.CoreCompileInputs.cache +++ /dev/null @@ -1 +0,0 @@ -84fb5613f117b97f8daa32f662648a5b7ac99dc4 diff --git a/obj/Release/AuroraLabelItemsPlugin.csproj.FileListAbsolute.txt b/obj/Release/AuroraLabelItemsPlugin.csproj.FileListAbsolute.txt deleted file mode 100644 index 6bfcf0f..0000000 --- a/obj/Release/AuroraLabelItemsPlugin.csproj.FileListAbsolute.txt +++ /dev/null @@ -1,6 +0,0 @@ -C:\Users\jakes\source\repos\AuroraLabelItemsPlugin\bin\Release\AuroraLabelItemsPlugin.dll -C:\Users\jakes\source\repos\AuroraLabelItemsPlugin\bin\Release\AuroraLabelItemsPlugin.pdb -C:\Users\jakes\source\repos\AuroraLabelItemsPlugin\obj\Release\AuroraLabelItemsPlugin.csproj.CoreCompileInputs.cache -C:\Users\jakes\source\repos\AuroraLabelItemsPlugin\obj\Release\AuroraLabelItemsPlugin.dll -C:\Users\jakes\source\repos\AuroraLabelItemsPlugin\obj\Release\AuroraLabelItemsPlugin.pdb -C:\Users\jakes\source\repos\AuroraLabelItemsPlugin\obj\Release\AuroraLabelItemsPlugin.csproj.AssemblyReference.cache diff --git a/obj/Release/AuroraLabelItemsPlugin.dll b/obj/Release/AuroraLabelItemsPlugin.dll deleted file mode 100644 index 65015bb..0000000 Binary files a/obj/Release/AuroraLabelItemsPlugin.dll and /dev/null differ diff --git a/obj/Release/AuroraLabelItemsPlugin.pdb b/obj/Release/AuroraLabelItemsPlugin.pdb deleted file mode 100644 index c4db67b..0000000 Binary files a/obj/Release/AuroraLabelItemsPlugin.pdb and /dev/null differ diff --git a/obj/Release/DesignTimeResolveAssemblyReferencesInput.cache b/obj/Release/DesignTimeResolveAssemblyReferencesInput.cache deleted file mode 100644 index beb90fe..0000000 Binary files a/obj/Release/DesignTimeResolveAssemblyReferencesInput.cache and /dev/null differ diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..98ccd98 --- /dev/null +++ b/packages.config @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md index a202cf9..a47ac32 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,3 @@ -## Example Custom Label Item Plugin +# This repository is deprecated -TODO: Explain \ No newline at end of file +The code for the plugin will be updated here in the future: https://github.com/vzoa/vatsys-atop-plugin