Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions GUI.Shared/RuleCondition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,10 @@ public class RuleCondition
/// </summary>
[YamlMember(Alias = "atis_regex")]
public string ATISRegex { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the regex to match against the destination airport ICAO.
/// </summary>
[YamlMember(Alias = "dest_regex")]
public string DestinationRegex { get; set; } = string.Empty;
}
175 changes: 165 additions & 10 deletions GUI/AutoAssigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ internal class AutoAssigner
private readonly List<AssignmentRule> _assignmentRules = [];

private readonly Regex _rwyNameRegex = new(@"^(\d{2}[LRC]?|[LRC])$");
private readonly Regex _euroScopeApproachRunwayRegex = new(@"\bAPPROACH\s+RUNWAYS?\s+(?<runways>.*?)(?=\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(?<number>[0-3]?\d)\s*(?<side>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)
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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<string>(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<string>();

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)));
Expand All @@ -367,21 +440,85 @@ 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<string> 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))
{
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<string>();
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)
Expand Down Expand Up @@ -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<string> freqs)
{
foreach (var freq in freqs)
Expand Down
3 changes: 3 additions & 0 deletions GUI/GUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@
<None Update="AerodromeSettings.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="AutoFill\**\*.yml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="libSkiaSharp.adll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down
89 changes: 73 additions & 16 deletions GUI/Strip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
Expand Down Expand Up @@ -1358,6 +1345,76 @@ private static string CleanVatsysRoute(string rawRoute)
}
}

private static bool MatchesAnyStandardRoute(string filedRoute, IEnumerable<RouteDTO> 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;
}

/// <summary>
/// Disposes of the strip.
/// </summary>
Expand Down