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.
///