From db0646c10af757a781c6522ddbebfa97af475a4f Mon Sep 17 00:00:00 2001 From: Adam Baikie Date: Fri, 29 May 2026 02:58:38 +1200 Subject: [PATCH 1/3] Add generic autofill and route matching improvements --- GUI.Shared/RuleCondition.cs | 6 ++ GUI/AutoAssigner.cs | 165 ++++++++++++++++++++++++++++++++---- GUI/GUI.csproj | 3 + GUI/Strip.cs | 89 +++++++++++++++---- 4 files changed, 232 insertions(+), 31 deletions(-) diff --git a/GUI.Shared/RuleCondition.cs b/GUI.Shared/RuleCondition.cs index d00bb0c..a03a3e0 100644 --- a/GUI.Shared/RuleCondition.cs +++ b/GUI.Shared/RuleCondition.cs @@ -83,4 +83,10 @@ public class RuleCondition /// [YamlMember(Alias = "atis_regex")] public string ATISRegex { get; set; } = string.Empty; + + /// + /// Gets or sets the regex to match against the destination airport ICAO. + /// + [YamlMember(Alias = "dest_regex")] + public string DestinationRegex { get; set; } = string.Empty; } diff --git a/GUI/AutoAssigner.cs b/GUI/AutoAssigner.cs index 8f8c50c..d9775cb 100644 --- a/GUI/AutoAssigner.cs +++ b/GUI/AutoAssigner.cs @@ -25,6 +25,8 @@ internal class AutoAssigner private readonly List _assignmentRules = []; private readonly Regex _rwyNameRegex = new(@"^(\d{2}[LRC]?|[LRC])$"); + private readonly Regex _euroScopeApproachRunwayRegex = new(@"\bAPPROACH\s+RUNWAYS?\s+(?.*?)(?=\b(?:DRY|WET|FRICTION|DEPARTURE|TRANSITION|WEATHER|WIND|VISIBILITY|CLOUDS|TEMPERATURE|DEW|QNH|NO\s+SIGNIFICANT|ACKNOWLEDGE)\b|$)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private readonly Regex _runwayTokenRegex = new(@"\b(?[0-3]?\d)\s*(?L|R|C|LEFT|RIGHT|CENTRE|CENTER)?\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private readonly Regex _tempRegex = new(@"\n?\s*\+?\s*\[TMP\] (-?\d{1,2})"); internal AutoAssigner(BayManager bayManager) @@ -110,7 +112,7 @@ internal AssignmentResult DetermineResult(Strip strip) if (!string.IsNullOrEmpty(rule.SID)) { - result.SID = GetSIDName(strip, rule.SID); + result.SID = GetSIDName(strip, rule.SID, result.Runway); } if (rule.Departures.Count > 0) @@ -136,12 +138,11 @@ internal bool CompliesWithCondition(RuleCondition rule, AssignmentResult result, { if (!string.IsNullOrEmpty(rule.IsJet)) { - var type = strip.FDR.AircraftTypeAndWake; + var requestedJet = !bool.TryParse(rule.IsJet, out var parsedJet) || parsedJet; + var isJet = IsJetAircraft(strip); + var matched = isJet == requestedJet; - // if we can't find the type, assume its a prop. - var isJet = type is not null && Performance.GetPerformanceData(type)?.IsJet == true; - - if (isJet != matchAsTrue) + if (matched != matchAsTrue) { return false; } @@ -164,6 +165,17 @@ internal bool CompliesWithCondition(RuleCondition rule, AssignmentResult result, } } + if (!string.IsNullOrEmpty(rule.DestinationRegex)) + { + var regex = new Regex(rule.DestinationRegex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + var matched = regex.IsMatch(strip.FDR.DesAirport ?? string.Empty); + + if (matched != matchAsTrue) + { + return false; + } + } + if (!string.IsNullOrEmpty(rule.TempAbove) && !string.IsNullOrEmpty(_bayManager.AerodromeState.ATIS)) { var reg = _tempRegex.Match(_bayManager.AerodromeState.ATIS); @@ -352,19 +364,73 @@ internal bool CompliesWithCondition(RuleCondition rule, AssignmentResult result, return true; } - internal static string GetSIDName(Strip strip, string shortSID) + internal static string GetSIDName(Strip strip, string shortSID, string preferredRunway = "") { var rwys = Airspace2.GetRunways(strip.FDR.DepAirport); - var foundSIDs = rwys?.Select(x => x.SIDs.FirstOrDefault(x => x.sidStar.Name.Contains(shortSID))); + var runwayHints = new HashSet(StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrWhiteSpace(preferredRunway)) + { + runwayHints.Add(preferredRunway); + } + + if (!string.IsNullOrWhiteSpace(strip.RWY)) + { + runwayHints.Add(strip.RWY); + } + + var assignedRunway = strip.FDR.DepartureRunway?.Name; + if (!string.IsNullOrWhiteSpace(assignedRunway)) + { + runwayHints.Add(assignedRunway!); + } + + var orderedRunways = rwys? + .OrderByDescending(x => runwayHints.Contains(x.Name)) + .ToList() ?? []; + + var routeWaypoints = strip.FDR.ParsedRoute + .Select(x => x.Intersection.Name) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var foundSIDs = new List(); - // Match by regex instead. if (shortSID.StartsWith("#", StringComparison.InvariantCulture)) { - var regex = new Regex(shortSID.Remove(0, 1)); - foundSIDs = rwys?.Select(x => x.SIDs.FirstOrDefault(x => regex.IsMatch(x.sidStar.Name))); + var regex = new Regex(shortSID.Remove(0, 1), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + foreach (var runway in orderedRunways) + { + foundSIDs.AddRange(runway.SIDs + .Select(x => x.sidStar?.Name ?? string.Empty) + .Where(x => !string.IsNullOrWhiteSpace(x) && regex.IsMatch(x))); + } + } + else + { + foreach (var runway in orderedRunways) + { + foundSIDs.AddRange(runway.SIDs + .Select(x => x.sidStar?.Name ?? string.Empty) + .Where(x => !string.IsNullOrWhiteSpace(x) && x.IndexOf(shortSID, StringComparison.OrdinalIgnoreCase) >= 0)); + } } - return foundSIDs?.FirstOrDefault(x => x.sidStar is not null).sidStar?.Name ?? shortSID; + return foundSIDs.FirstOrDefault(x => SIDNameContainsRouteWaypoint(x, routeWaypoints)) ?? + foundSIDs.FirstOrDefault() ?? + shortSID; + } + + private static bool SIDNameContainsRouteWaypoint(string sidName, HashSet routeWaypoints) + { + foreach (var waypoint in routeWaypoints) + { + if (waypoint.Length >= 3 && sidName.IndexOf(waypoint, StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + + return false; } public string[] GetDepartureRunways() @@ -374,14 +440,65 @@ public string[] GetDepartureRunways() return []; } - var runwayLine = _bayManager.AerodromeState.ATIS.ToUpperInvariant().Split('\n').FirstOrDefault(x => x.Contains("[RWY]") || x.Contains("RWY:")) ?? string.Empty; + var atis = _bayManager.AerodromeState.ATIS.ToUpperInvariant(); + var runwayLine = atis.Split('\n').FirstOrDefault(x => x.Contains("[RWY]") || x.Contains("RWY:")) ?? string.Empty; if (!string.IsNullOrEmpty(runwayLine)) { - return GetDepartureRunways(runwayLine); + var taggedRunways = GetTaggedRunways(runwayLine); + if (taggedRunways.Length > 0) + { + return taggedRunways; + } } - return []; + var euroScopeApproachRunways = GetEuroScopeRunways(atis, _euroScopeApproachRunwayRegex); + return euroScopeApproachRunways.Length > 0 ? euroScopeApproachRunways : []; + } + + private string[] GetTaggedRunways(string runwayLine) + { + var compactRunways = GetRunwaysFromText(runwayLine); + return compactRunways.Length > 0 ? compactRunways : GetDepartureRunways(runwayLine); + } + + private string[] GetEuroScopeRunways(string atis, Regex regex) + { + var match = regex.Match(atis); + return match.Success ? GetRunwaysFromText(match.Groups["runways"].Value) : []; + } + + private string[] GetRunwaysFromText(string text) + { + var runways = new List(); + foreach (Match match in _runwayTokenRegex.Matches(text ?? string.Empty)) + { + if (!int.TryParse(match.Groups["number"].Value, out var number) || number is < 1 or > 36) + { + continue; + } + + var runway = number.ToString("00", CultureInfo.InvariantCulture) + RunwaySideToLetter(match.Groups["side"].Value); + if (!runways.Contains(runway)) + { + runways.Add(runway); + } + } + + return [.. runways]; + } + + private static string RunwaySideToLetter(string side) + { + return side.ToUpperInvariant() switch + { + "LEFT" => "L", + "RIGHT" => "R", + "CENTRE" => "C", + "CENTER" => "C", + "L" or "R" or "C" => side.ToUpperInvariant(), + _ => string.Empty, + }; } private string[] GetDepartureRunways(string rwyLine) @@ -480,6 +597,24 @@ public static bool IsDepartureFreqAvailable(string freq) return allFreqs.Contains(freq); } + private static bool IsJetAircraft(Strip strip) + { + var aircraftTypeAndWake = strip.FDR.AircraftTypeAndWake; + var aircraftTypeText = aircraftTypeAndWake.ToString().Trim().ToUpperInvariant(); + + return Performance.GetPerformanceData(aircraftTypeAndWake)?.IsJet == true || + LooksLikeJetType(aircraftTypeText); + } + + private static bool LooksLikeJetType(string aircraftType) + { + aircraftType = (aircraftType ?? string.Empty).Split('/').First().Trim().ToUpperInvariant(); + return Regex.IsMatch( + aircraftType, + @"^(A(20|21|30|31|32|33|34|35|38|3ST)|B(3[789]|7\d{2}|CS)|CRJ|E(135|145|170|175|190|195|290|295)|F28|F70|F100|MD|DC9|GLF|CL(30|35|60)|C17|C5|C25|C56X|LJ|FA|H25|PRM|HDJT)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + } + public static string DetermineDepFreq(List freqs) { foreach (var freq in freqs) diff --git a/GUI/GUI.csproj b/GUI/GUI.csproj index c984393..3d242f8 100644 --- a/GUI/GUI.csproj +++ b/GUI/GUI.csproj @@ -113,6 +113,9 @@ Always + + PreserveNewest + Always diff --git a/GUI/Strip.cs b/GUI/Strip.cs index aa3b35a..ef7b6dd 100644 --- a/GUI/Strip.cs +++ b/GUI/Strip.cs @@ -1024,14 +1024,7 @@ public async void UpdateFDR() if (ValidRoutes is not null) { CondensedRoute = CleanVatsysRoute(FDR.Route); - DodgyRoute = true; - foreach (var validroute in ValidRoutes) - { - if (validroute.RouteText.Contains(CondensedRoute)) - { - DodgyRoute = false; - } - } + DodgyRoute = !MatchesAnyStandardRoute(CondensedRoute, ValidRoutes); // If not departure or FDR active. if (DefaultStripType != StripType.DEPARTURE || (int)FDR.State > 5) @@ -1044,16 +1037,10 @@ public async void UpdateFDR() if (DodgyRoute) { var rte = string.Join(" ", CleanVatsysRoute(FDR.Route).Split(' ').Skip(1).ToArray()); - foreach (var validroute in ValidRoutes) - { - if (validroute.RouteText.Contains(rte)) - { - DodgyRoute = false; - } - } + DodgyRoute = !MatchesAnyStandardRoute(rte, ValidRoutes); } - if (!DodgyRoute && CondensedRoute == "FAIL") + if (!DodgyRoute && (CondensedRoute == "FAIL" || CondensedRoute == "\0")) { DodgyRoute = true; } @@ -1358,6 +1345,76 @@ private static string CleanVatsysRoute(string rawRoute) } } + private static bool MatchesAnyStandardRoute(string filedRoute, IEnumerable validRoutes) + { + return !string.IsNullOrWhiteSpace(filedRoute) && + filedRoute != "\0" && + filedRoute != "FAIL" && + validRoutes.Any(validRoute => RouteTokenSequencesOverlap(filedRoute, validRoute.RouteText)); + } + + private static bool RouteTokenSequencesOverlap(string filedRoute, string standardRoute) + { + var filedTokens = RouteTokens(filedRoute); + var standardTokens = RouteTokens(standardRoute); + + return filedTokens.Length > 0 && + standardTokens.Length > 0 && + (ContainsTokenSequence(standardTokens, filedTokens) || ContainsTokenSequence(filedTokens, standardTokens)); + } + + private static string[] RouteTokens(string route) + { + return (route ?? string.Empty) + .Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(NormaliseRouteToken) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToArray(); + } + + private static string NormaliseRouteToken(string routeElement) + { + routeElement = (routeElement ?? string.Empty).Trim().Trim(',', ';').ToUpperInvariant(); + + if (string.IsNullOrWhiteSpace(routeElement) || + routeElement == "DCT" || + _gpscoordRegex.Match(routeElement).Success || + _sidRouteRegex.Match(routeElement).Success) + { + return string.Empty; + } + + return routeElement.Contains("/") ? routeElement.Split('/').First() : routeElement; + } + + private static bool ContainsTokenSequence(string[] haystack, string[] needle) + { + if (needle.Length == 0 || haystack.Length < needle.Length) + { + return false; + } + + for (var i = 0; i <= haystack.Length - needle.Length; i++) + { + var matched = true; + for (var j = 0; j < needle.Length; j++) + { + if (!string.Equals(haystack[i + j], needle[j], StringComparison.OrdinalIgnoreCase)) + { + matched = false; + break; + } + } + + if (matched) + { + return true; + } + } + + return false; + } + /// /// Disposes of the strip. /// From 4e2d5a8ef874e561245976e6d21532f3c6d3a9bf Mon Sep 17 00:00:00 2001 From: Adam Baikie Date: Fri, 29 May 2026 02:58:38 +1200 Subject: [PATCH 2/3] Add generic autofill and route matching improvements --- GUI.Shared/RuleCondition.cs | 6 ++ GUI/AutoAssigner.cs | 175 +++++++++++++++++++++++++++++++++--- GUI/GUI.csproj | 3 + GUI/Strip.cs | 89 ++++++++++++++---- 4 files changed, 247 insertions(+), 26 deletions(-) diff --git a/GUI.Shared/RuleCondition.cs b/GUI.Shared/RuleCondition.cs index d00bb0c..a03a3e0 100644 --- a/GUI.Shared/RuleCondition.cs +++ b/GUI.Shared/RuleCondition.cs @@ -83,4 +83,10 @@ public class RuleCondition /// [YamlMember(Alias = "atis_regex")] public string ATISRegex { get; set; } = string.Empty; + + /// + /// Gets or sets the regex to match against the destination airport ICAO. + /// + [YamlMember(Alias = "dest_regex")] + public string DestinationRegex { get; set; } = string.Empty; } diff --git a/GUI/AutoAssigner.cs b/GUI/AutoAssigner.cs index 8f8c50c..b7c07fd 100644 --- a/GUI/AutoAssigner.cs +++ b/GUI/AutoAssigner.cs @@ -25,6 +25,8 @@ internal class AutoAssigner private readonly List _assignmentRules = []; private readonly Regex _rwyNameRegex = new(@"^(\d{2}[LRC]?|[LRC])$"); + private readonly Regex _euroScopeApproachRunwayRegex = new(@"\bAPPROACH\s+RUNWAYS?\s+(?.*?)(?=\b(?:DRY|WET|FRICTION|DEPARTURE|TRANSITION|WEATHER|WIND|VISIBILITY|CLOUDS|TEMPERATURE|DEW|QNH|NO\s+SIGNIFICANT|ACKNOWLEDGE)\b|$)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private readonly Regex _runwayTokenRegex = new(@"\b(?[0-3]?\d)\s*(?L|R|C|LEFT|RIGHT|CENTRE|CENTER)?\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private readonly Regex _tempRegex = new(@"\n?\s*\+?\s*\[TMP\] (-?\d{1,2})"); internal AutoAssigner(BayManager bayManager) @@ -110,7 +112,7 @@ internal AssignmentResult DetermineResult(Strip strip) if (!string.IsNullOrEmpty(rule.SID)) { - result.SID = GetSIDName(strip, rule.SID); + result.SID = GetSIDName(strip, rule.SID, result.Runway); } if (rule.Departures.Count > 0) @@ -136,12 +138,11 @@ internal bool CompliesWithCondition(RuleCondition rule, AssignmentResult result, { if (!string.IsNullOrEmpty(rule.IsJet)) { - var type = strip.FDR.AircraftTypeAndWake; + var requestedJet = !bool.TryParse(rule.IsJet, out var parsedJet) || parsedJet; + var isJet = IsJetAircraft(strip); + var matched = isJet == requestedJet; - // if we can't find the type, assume its a prop. - var isJet = type is not null && Performance.GetPerformanceData(type)?.IsJet == true; - - if (isJet != matchAsTrue) + if (matched != matchAsTrue) { return false; } @@ -164,6 +165,17 @@ internal bool CompliesWithCondition(RuleCondition rule, AssignmentResult result, } } + if (!string.IsNullOrEmpty(rule.DestinationRegex)) + { + var regex = new Regex(rule.DestinationRegex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + var matched = regex.IsMatch(strip.FDR.DesAirport ?? string.Empty); + + if (matched != matchAsTrue) + { + return false; + } + } + if (!string.IsNullOrEmpty(rule.TempAbove) && !string.IsNullOrEmpty(_bayManager.AerodromeState.ATIS)) { var reg = _tempRegex.Match(_bayManager.AerodromeState.ATIS); @@ -352,7 +364,68 @@ internal bool CompliesWithCondition(RuleCondition rule, AssignmentResult result, return true; } - internal static string GetSIDName(Strip strip, string shortSID) + internal static string GetSIDName(Strip strip, string shortSID, string preferredRunway = "") + { + if (!strip.FDR.DepAirport.StartsWith("NZ", StringComparison.OrdinalIgnoreCase)) + { + return GetLegacySIDName(strip, shortSID); + } + + var rwys = Airspace2.GetRunways(strip.FDR.DepAirport); + var runwayHints = new HashSet(StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrWhiteSpace(preferredRunway)) + { + runwayHints.Add(preferredRunway); + } + + if (!string.IsNullOrWhiteSpace(strip.RWY)) + { + runwayHints.Add(strip.RWY); + } + + var assignedRunway = strip.FDR.DepartureRunway?.Name; + if (!string.IsNullOrWhiteSpace(assignedRunway)) + { + runwayHints.Add(assignedRunway!); + } + + var orderedRunways = rwys? + .OrderByDescending(x => runwayHints.Contains(x.Name)) + .ToList() ?? []; + + var routeWaypoints = strip.FDR.ParsedRoute + .Select(x => x.Intersection.Name) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var foundSIDs = new List(); + + if (shortSID.StartsWith("#", StringComparison.InvariantCulture)) + { + var regex = new Regex(shortSID.Remove(0, 1), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + foreach (var runway in orderedRunways) + { + foundSIDs.AddRange(runway.SIDs + .Select(x => x.sidStar?.Name ?? string.Empty) + .Where(x => !string.IsNullOrWhiteSpace(x) && regex.IsMatch(x))); + } + } + else + { + foreach (var runway in orderedRunways) + { + foundSIDs.AddRange(runway.SIDs + .Select(x => x.sidStar?.Name ?? string.Empty) + .Where(x => !string.IsNullOrWhiteSpace(x) && x.IndexOf(shortSID, StringComparison.OrdinalIgnoreCase) >= 0)); + } + } + + return foundSIDs.FirstOrDefault(x => SIDNameContainsRouteWaypoint(x, routeWaypoints)) ?? + foundSIDs.FirstOrDefault() ?? + shortSID; + } + + private static string GetLegacySIDName(Strip strip, string shortSID) { var rwys = Airspace2.GetRunways(strip.FDR.DepAirport); var foundSIDs = rwys?.Select(x => x.SIDs.FirstOrDefault(x => x.sidStar.Name.Contains(shortSID))); @@ -367,6 +440,19 @@ internal static string GetSIDName(Strip strip, string shortSID) return foundSIDs?.FirstOrDefault(x => x.sidStar is not null).sidStar?.Name ?? shortSID; } + private static bool SIDNameContainsRouteWaypoint(string sidName, HashSet routeWaypoints) + { + foreach (var waypoint in routeWaypoints) + { + if (waypoint.Length >= 3 && sidName.IndexOf(waypoint, StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + + return false; + } + public string[] GetDepartureRunways() { if (string.IsNullOrEmpty(_bayManager.AerodromeState.ATIS)) @@ -374,14 +460,65 @@ public string[] GetDepartureRunways() return []; } - var runwayLine = _bayManager.AerodromeState.ATIS.ToUpperInvariant().Split('\n').FirstOrDefault(x => x.Contains("[RWY]") || x.Contains("RWY:")) ?? string.Empty; + var atis = _bayManager.AerodromeState.ATIS.ToUpperInvariant(); + var runwayLine = atis.Split('\n').FirstOrDefault(x => x.Contains("[RWY]") || x.Contains("RWY:")) ?? string.Empty; if (!string.IsNullOrEmpty(runwayLine)) { - return GetDepartureRunways(runwayLine); + var taggedRunways = GetTaggedRunways(runwayLine); + if (taggedRunways.Length > 0) + { + return taggedRunways; + } } - return []; + var euroScopeApproachRunways = GetEuroScopeRunways(atis, _euroScopeApproachRunwayRegex); + return euroScopeApproachRunways.Length > 0 ? euroScopeApproachRunways : []; + } + + private string[] GetTaggedRunways(string runwayLine) + { + var compactRunways = GetRunwaysFromText(runwayLine); + return compactRunways.Length > 0 ? compactRunways : GetDepartureRunways(runwayLine); + } + + private string[] GetEuroScopeRunways(string atis, Regex regex) + { + var match = regex.Match(atis); + return match.Success ? GetRunwaysFromText(match.Groups["runways"].Value) : []; + } + + private string[] GetRunwaysFromText(string text) + { + var runways = new List(); + foreach (Match match in _runwayTokenRegex.Matches(text ?? string.Empty)) + { + if (!int.TryParse(match.Groups["number"].Value, out var number) || number is < 1 or > 36) + { + continue; + } + + var runway = number.ToString("00", CultureInfo.InvariantCulture) + RunwaySideToLetter(match.Groups["side"].Value); + if (!runways.Contains(runway)) + { + runways.Add(runway); + } + } + + return [.. runways]; + } + + private static string RunwaySideToLetter(string side) + { + return side.ToUpperInvariant() switch + { + "LEFT" => "L", + "RIGHT" => "R", + "CENTRE" => "C", + "CENTER" => "C", + "L" or "R" or "C" => side.ToUpperInvariant(), + _ => string.Empty, + }; } private string[] GetDepartureRunways(string rwyLine) @@ -480,6 +617,24 @@ public static bool IsDepartureFreqAvailable(string freq) return allFreqs.Contains(freq); } + private static bool IsJetAircraft(Strip strip) + { + var aircraftTypeAndWake = strip.FDR.AircraftTypeAndWake; + var aircraftTypeText = aircraftTypeAndWake.ToString().Trim().ToUpperInvariant(); + + return Performance.GetPerformanceData(aircraftTypeAndWake)?.IsJet == true || + LooksLikeJetType(aircraftTypeText); + } + + private static bool LooksLikeJetType(string aircraftType) + { + aircraftType = (aircraftType ?? string.Empty).Split('/').First().Trim().ToUpperInvariant(); + return Regex.IsMatch( + aircraftType, + @"^(A(20|21|30|31|32|33|34|35|38|3ST)|B(3[789]|7\d{2}|CS)|CRJ|E(135|145|170|175|190|195|290|295)|F28|F70|F100|MD|DC9|GLF|CL(30|35|60)|C17|C5|C25|C56X|LJ|FA|H25|PRM|HDJT)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + } + public static string DetermineDepFreq(List freqs) { foreach (var freq in freqs) diff --git a/GUI/GUI.csproj b/GUI/GUI.csproj index c984393..3d242f8 100644 --- a/GUI/GUI.csproj +++ b/GUI/GUI.csproj @@ -113,6 +113,9 @@ Always + + PreserveNewest + Always diff --git a/GUI/Strip.cs b/GUI/Strip.cs index aa3b35a..ef7b6dd 100644 --- a/GUI/Strip.cs +++ b/GUI/Strip.cs @@ -1024,14 +1024,7 @@ public async void UpdateFDR() if (ValidRoutes is not null) { CondensedRoute = CleanVatsysRoute(FDR.Route); - DodgyRoute = true; - foreach (var validroute in ValidRoutes) - { - if (validroute.RouteText.Contains(CondensedRoute)) - { - DodgyRoute = false; - } - } + DodgyRoute = !MatchesAnyStandardRoute(CondensedRoute, ValidRoutes); // If not departure or FDR active. if (DefaultStripType != StripType.DEPARTURE || (int)FDR.State > 5) @@ -1044,16 +1037,10 @@ public async void UpdateFDR() if (DodgyRoute) { var rte = string.Join(" ", CleanVatsysRoute(FDR.Route).Split(' ').Skip(1).ToArray()); - foreach (var validroute in ValidRoutes) - { - if (validroute.RouteText.Contains(rte)) - { - DodgyRoute = false; - } - } + DodgyRoute = !MatchesAnyStandardRoute(rte, ValidRoutes); } - if (!DodgyRoute && CondensedRoute == "FAIL") + if (!DodgyRoute && (CondensedRoute == "FAIL" || CondensedRoute == "\0")) { DodgyRoute = true; } @@ -1358,6 +1345,76 @@ private static string CleanVatsysRoute(string rawRoute) } } + private static bool MatchesAnyStandardRoute(string filedRoute, IEnumerable validRoutes) + { + return !string.IsNullOrWhiteSpace(filedRoute) && + filedRoute != "\0" && + filedRoute != "FAIL" && + validRoutes.Any(validRoute => RouteTokenSequencesOverlap(filedRoute, validRoute.RouteText)); + } + + private static bool RouteTokenSequencesOverlap(string filedRoute, string standardRoute) + { + var filedTokens = RouteTokens(filedRoute); + var standardTokens = RouteTokens(standardRoute); + + return filedTokens.Length > 0 && + standardTokens.Length > 0 && + (ContainsTokenSequence(standardTokens, filedTokens) || ContainsTokenSequence(filedTokens, standardTokens)); + } + + private static string[] RouteTokens(string route) + { + return (route ?? string.Empty) + .Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(NormaliseRouteToken) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToArray(); + } + + private static string NormaliseRouteToken(string routeElement) + { + routeElement = (routeElement ?? string.Empty).Trim().Trim(',', ';').ToUpperInvariant(); + + if (string.IsNullOrWhiteSpace(routeElement) || + routeElement == "DCT" || + _gpscoordRegex.Match(routeElement).Success || + _sidRouteRegex.Match(routeElement).Success) + { + return string.Empty; + } + + return routeElement.Contains("/") ? routeElement.Split('/').First() : routeElement; + } + + private static bool ContainsTokenSequence(string[] haystack, string[] needle) + { + if (needle.Length == 0 || haystack.Length < needle.Length) + { + return false; + } + + for (var i = 0; i <= haystack.Length - needle.Length; i++) + { + var matched = true; + for (var j = 0; j < needle.Length; j++) + { + if (!string.Equals(haystack[i + j], needle[j], StringComparison.OrdinalIgnoreCase)) + { + matched = false; + break; + } + } + + if (matched) + { + return true; + } + } + + return false; + } + /// /// Disposes of the strip. /// From 3b966f560c225a910c2f4285ea955fdee2062812 Mon Sep 17 00:00:00 2001 From: Adam Baikie Date: Fri, 29 May 2026 03:09:36 +1200 Subject: [PATCH 3/3] Resolve SID autofill merge artifact --- GUI/AutoAssigner.cs | 67 ++++----------------------------------------- 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/GUI/AutoAssigner.cs b/GUI/AutoAssigner.cs index 0381be8..b7c07fd 100644 --- a/GUI/AutoAssigner.cs +++ b/GUI/AutoAssigner.cs @@ -365,7 +365,6 @@ internal bool CompliesWithCondition(RuleCondition rule, AssignmentResult result, } internal static string GetSIDName(Strip strip, string shortSID, string preferredRunway = "") -<<<<<<< HEAD { if (!strip.FDR.DepAirport.StartsWith("NZ", StringComparison.OrdinalIgnoreCase)) { @@ -427,74 +426,18 @@ internal static string GetSIDName(Strip strip, string shortSID, string preferred } private static string GetLegacySIDName(Strip strip, string shortSID) -======= ->>>>>>> db0646c10af757a781c6522ddbebfa97af475a4f { var rwys = Airspace2.GetRunways(strip.FDR.DepAirport); - var runwayHints = new HashSet(StringComparer.OrdinalIgnoreCase); - if (!string.IsNullOrWhiteSpace(preferredRunway)) - { - runwayHints.Add(preferredRunway); - } - - if (!string.IsNullOrWhiteSpace(strip.RWY)) - { - runwayHints.Add(strip.RWY); - } - - var assignedRunway = strip.FDR.DepartureRunway?.Name; - if (!string.IsNullOrWhiteSpace(assignedRunway)) - { - runwayHints.Add(assignedRunway!); - } - - var orderedRunways = rwys? - .OrderByDescending(x => runwayHints.Contains(x.Name)) - .ToList() ?? []; - - var routeWaypoints = strip.FDR.ParsedRoute - .Select(x => x.Intersection.Name) - .Where(x => !string.IsNullOrWhiteSpace(x)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - var foundSIDs = new List(); + var foundSIDs = rwys?.Select(x => x.SIDs.FirstOrDefault(x => x.sidStar.Name.Contains(shortSID))); + // Match by regex instead. if (shortSID.StartsWith("#", StringComparison.InvariantCulture)) { - var regex = new Regex(shortSID.Remove(0, 1), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - foreach (var runway in orderedRunways) - { - foundSIDs.AddRange(runway.SIDs - .Select(x => x.sidStar?.Name ?? string.Empty) - .Where(x => !string.IsNullOrWhiteSpace(x) && regex.IsMatch(x))); - } - } - else - { - foreach (var runway in orderedRunways) - { - foundSIDs.AddRange(runway.SIDs - .Select(x => x.sidStar?.Name ?? string.Empty) - .Where(x => !string.IsNullOrWhiteSpace(x) && x.IndexOf(shortSID, StringComparison.OrdinalIgnoreCase) >= 0)); - } + var regex = new Regex(shortSID.Remove(0, 1)); + foundSIDs = rwys?.Select(x => x.SIDs.FirstOrDefault(x => regex.IsMatch(x.sidStar.Name))); } - return foundSIDs.FirstOrDefault(x => SIDNameContainsRouteWaypoint(x, routeWaypoints)) ?? - foundSIDs.FirstOrDefault() ?? - shortSID; - } - - private static bool SIDNameContainsRouteWaypoint(string sidName, HashSet routeWaypoints) - { - foreach (var waypoint in routeWaypoints) - { - if (waypoint.Length >= 3 && sidName.IndexOf(waypoint, StringComparison.OrdinalIgnoreCase) >= 0) - { - return true; - } - } - - return false; + return foundSIDs?.FirstOrDefault(x => x.sidStar is not null).sidStar?.Name ?? shortSID; } private static bool SIDNameContainsRouteWaypoint(string sidName, HashSet routeWaypoints)