Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TREND parsing #123

Merged
merged 7 commits into from
Feb 6, 2025
Merged
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
4 changes: 2 additions & 2 deletions vATIS.Desktop/Atis/AtisBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,9 @@ private async Task<List<AtisVariable>> ParseNodesFromMetar(AtisStation station,
var windshear = NodeParser.Parse<WindShearNode, string>(metar, station);

var completeWxStringVoice =
$"{surfaceWind.VoiceAtis} {visibility.VoiceAtis} {rvr.VoiceAtis} {presentWeather.VoiceAtis} {clouds.VoiceAtis} {temp.VoiceAtis} {dew.VoiceAtis} {pressure.VoiceAtis} {recentWeather.VoiceAtis} {windshear.VoiceAtis}";
$"{surfaceWind.VoiceAtis} {visibility.VoiceAtis} {rvr.VoiceAtis} {presentWeather.VoiceAtis} {clouds.VoiceAtis} {temp.VoiceAtis} {dew.VoiceAtis} {pressure.VoiceAtis} {recentWeather.VoiceAtis} {windshear.VoiceAtis} {trends.VoiceAtis}";
var completeWxStringAcars =
$"{surfaceWind.TextAtis} {visibility.TextAtis} {rvr.TextAtis} {presentWeather.TextAtis} {clouds.TextAtis} {temp.TextAtis}{(!string.IsNullOrEmpty(temp.TextAtis) || !string.IsNullOrEmpty(dew.TextAtis) ? "/" : "")}{dew.TextAtis} {pressure.TextAtis} {recentWeather.TextAtis} {windshear.TextAtis}";
$"{surfaceWind.TextAtis} {visibility.TextAtis} {rvr.TextAtis} {presentWeather.TextAtis} {clouds.TextAtis} {temp.TextAtis}{(!string.IsNullOrEmpty(temp.TextAtis) || !string.IsNullOrEmpty(dew.TextAtis) ? "/" : "")}{dew.TextAtis} {pressure.TextAtis} {recentWeather.TextAtis} {windshear.TextAtis} {trends.TextAtis}";

var airportConditions = "";
if (!string.IsNullOrEmpty(preset.AirportConditions) || station.AirportConditionDefinitions.Any(x => x.Enabled))
Expand Down
174 changes: 151 additions & 23 deletions vATIS.Desktop/Atis/Nodes/TrendNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

using System;
using System.Collections.Generic;
using Vatsim.Vatis.Atis.Extensions;
using Vatsim.Vatis.Weather.Decoder.ChunkDecoder;
using Vatsim.Vatis.Weather.Decoder.Entity;

namespace Vatsim.Vatis.Atis.Nodes;
Expand All @@ -17,36 +19,23 @@ public class TrendNode : BaseNode<TrendForecast>
/// <inheritdoc/>
public override void Parse(DecodedMetar metar)
{
if (metar.TrendForecast == null)
if (Station == null || metar.TrendForecast == null)
return;

var tts = new List<string>();
var acars = new List<string>();
var voiceAtis = new List<string>();
var textAtis = new List<string>();

tts.Add("TREND");
switch (metar.TrendForecast.ChangeIndicator)
{
case TrendForecastType.Becoming:
tts.Add("BECOMING");
acars.Add("BECMG");
break;
case TrendForecastType.Temporary:
tts.Add("TEMPORARY");
acars.Add("TEMPO");
break;
case TrendForecastType.NoSignificantChanges:
tts.Add("NO SIGNIFICANT CHANGES");
acars.Add("NOSIG");
break;
}
var decodedTrend = new DecodedMetar();
ProcessTrendForecast(metar.TrendForecast, decodedTrend, voiceAtis, textAtis);

if (metar.TrendForecast.Forecast != null)
if (metar.TrendForecastFuture != null)
{
// TODO: Implement trend forecast parsing
var futureDecodedTrend = new DecodedMetar();
ProcessTrendForecast(metar.TrendForecastFuture, futureDecodedTrend, voiceAtis, textAtis, isFuture: true);
}

VoiceAtis = string.Join(". ", tts);
TextAtis = string.Join(" ", acars);
VoiceAtis = string.Join(" ", voiceAtis);
TextAtis = string.Join(" ", textAtis);
}

/// <inheritdoc/>
Expand All @@ -60,4 +49,143 @@ public override string ParseVoiceVariables(TrendForecast node, string? format)
{
throw new NotImplementedException();
}

private static void GetChunkResult(Dictionary<string, object> chunkResult, DecodedMetar decodedMetar)
{
if (chunkResult.TryGetValue("Result", out var value) && value is Dictionary<string, object>)
{
if (value is Dictionary<string, object> result)
{
foreach (var obj in result)
{
typeof(DecodedMetar).GetProperty(obj.Key)?.SetValue(decodedMetar, obj.Value, null);
}
}
}
}

private void ProcessTrendForecast(TrendForecast? forecast, DecodedMetar decodedTrend, List<string> voiceAtis,
List<string> textAtis, bool isFuture = false)
{
if (forecast == null || Station == null)
return;

if (!isFuture)
{
voiceAtis.Add("TREND,");
}

switch (forecast.ChangeIndicator)
{
case TrendForecastType.Becoming:
voiceAtis.Add("BECOMING");
textAtis.Add("BECMG");
break;
case TrendForecastType.Temporary:
voiceAtis.Add("TEMPORARY");
textAtis.Add("TEMPO");
break;
case TrendForecastType.NoSignificantChanges:
voiceAtis.Add("NO SIGNIFICANT CHANGES");
textAtis.Add("NOSIG");
break;
}

if (forecast.AtTime != null)
{
if (int.TryParse(forecast.AtTime, out var time))
{
voiceAtis.Add($"AT {time.ToSerialFormat()}.");
}

textAtis.Add($"AT{forecast.AtTime}");
}

if (forecast.FromTime != null)
{
if (int.TryParse(forecast.FromTime, out var time))
{
voiceAtis.Add($"FROM {time.ToSerialFormat()}.");
}

textAtis.Add($"FM{forecast.FromTime}");
}

if (forecast.UntilTime != null)
{
if (int.TryParse(forecast.UntilTime, out var time))
{
voiceAtis.Add($"UNTIL {time.ToSerialFormat()}.");
}

textAtis.Add($"TL{forecast.UntilTime}");
}

if (forecast.SurfaceWind != null)
{
var chunkResult = new SurfaceWindChunkDecoder().Parse(forecast.SurfaceWind);
GetChunkResult(chunkResult, decodedTrend);
}

if (forecast.PrevailingVisibility != null)
{
if (forecast.PrevailingVisibility.Trim() == "CAVOK")
{
decodedTrend.Cavok = true;
}
else
{
var chunk = new VisibilityChunkDecoder();
var chunkResult = chunk.Parse(forecast.PrevailingVisibility);
GetChunkResult(chunkResult, decodedTrend);
}
}

if (forecast.WeatherCodes != null)
{
var chunk = new PresentWeatherChunkDecoder();
var chunkResult = chunk.Parse(forecast.WeatherCodes);
GetChunkResult(chunkResult, decodedTrend);
}

if (forecast.Clouds?.Length > 0)
{
var chunk = new CloudChunkDecoder();
var chunkResult = chunk.Parse(forecast.Clouds);
GetChunkResult(chunkResult, decodedTrend);
}

if (decodedTrend.SurfaceWind != null)
{
var node = NodeParser.Parse<SurfaceWindNode, SurfaceWind>(decodedTrend, Station);
voiceAtis.Add(node.VoiceAtis);
textAtis.Add(node.TextAtis);
}

if (decodedTrend.Cavok)
{
voiceAtis.Add("CAV-OK.");
textAtis.Add("CAVOK");
}
else if (decodedTrend.Visibility != null)
{
var node = NodeParser.Parse<PrevailingVisibilityNode, Visibility>(decodedTrend, Station);
voiceAtis.Add(node.VoiceAtis);
textAtis.Add(node.TextAtis);
}

if (decodedTrend.PresentWeather.Count > 0)
{
var node = NodeParser.Parse<PresentWeatherNode, WeatherPhenomenon>(decodedTrend, Station);
voiceAtis.Add(node.VoiceAtis);
textAtis.Add(node.TextAtis);
}

if (decodedTrend.Clouds.Count > 0)
{
var node = NodeParser.Parse<CloudNode, CloudLayer>(decodedTrend, Station);
voiceAtis.Add(node.VoiceAtis);
textAtis.Add(node.TextAtis);
}
}
}
103 changes: 76 additions & 27 deletions vATIS.Desktop/Weather/Decoder/ChunkDecoder/TrendChunkDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Vatsim.Vatis.Weather.Decoder.ChunkDecoder.Abstract;
using Vatsim.Vatis.Weather.Decoder.Entity;

Expand All @@ -20,10 +21,31 @@ namespace Vatsim.Vatis.Weather.Decoder.ChunkDecoder;
/// </remarks>
public sealed class TrendChunkDecoder : MetarChunkDecoder
{
// Surface Wind
private const string WindDirectionRegexPattern = "(?:[0-9]{3}|VRB|///)";
private const string WindSpeedRegexPattern = "P?(?:[/0-9]{2,3}|//)";
private const string WindSpeedVariationsRegexPattern = "(?:GP?(?:[0-9]{2,3}))?";
private const string WindUnitRegexPattern = "(?:KT|MPS|KPH)";

// Prevailing Visibility
private const string VisibilityRegexPattern = "(?:[0-9]{4})?";

// Present Weather
private const string CaracRegexPattern = "TS|FZ|SH|BL|DR|MI|BC|PR";
private const string TypeRegexPattern = "DZ|RA|SN|SG|PL|DS|GR|GS|UP|IC|FG|BR|SA|DU|HZ|FU|VA|PY|DU|PO|SQ|FC|DS|SS|//";

// Clouds
private const string NoCloudRegexPattern = "(?:NSC|NCD|CLR|SKC)";
private const string LayerRegexPattern = "(?:VV|FEW|SCT|BKN|OVC|///)(?:[0-9]{3}|///)(?:CB|TCU|///)?";

/// <inheritdoc/>
public override string GetRegex()
{
return @"(TREND|NOSIG|BECMG|TEMPO)\s*(?:(FM(\d{4}))?\s*(TL(\d{4}))?\s*(AT(\d{4}))?)?\s*([\w\d\/\s]+)?=";
var windRegex = $"{WindDirectionRegexPattern}{WindSpeedRegexPattern}{WindSpeedVariationsRegexPattern}{WindUnitRegexPattern}";
var visibilityRegex = $"{VisibilityRegexPattern}|CAVOK";
var presentWeatherRegex = $@"(?:[-+]|VC)?(?:{CaracRegexPattern})?(?:{TypeRegexPattern})?(?:{TypeRegexPattern})?(?:{TypeRegexPattern})?";
var cloudRegex = $@"(?:{NoCloudRegexPattern}|(?:{LayerRegexPattern})(?: {LayerRegexPattern})?(?: {LayerRegexPattern})?(?: {LayerRegexPattern})?)";
return $@"TREND (TEMPO|BECMG|NOSIG)\s*(?:AT(\d{{4}}))?\s*(?:FM(\d{{4}}))?\s*(?:TL(\d{{4}}))?\s*({windRegex})?\s*({visibilityRegex})?\s*({presentWeatherRegex})?\s*({cloudRegex})?\s*((?=\s*(?:TEMPO|BECMG|NOSIG|$))(?:\s*(TEMPO|BECMG|NOSIG)\s*(?:AT(\d{{4}}))?\s*(?:FM(\d{{4}}))?\s*(?:TL(\d{{4}}))?\s*({windRegex})?\s*({visibilityRegex})?\s*({presentWeatherRegex})?\s*({cloudRegex})?)?)";
}

/// <inheritdoc/>
Expand All @@ -36,40 +58,67 @@ public override Dictionary<string, object> Parse(string remainingMetar, bool wit

if (found.Count > 1)
{
var trend = new TrendForecast
{
ChangeIndicator = found[1].Value switch
{
"NOSIG" => TrendForecastType.NoSignificantChanges,
"BECMG" => TrendForecastType.Becoming,
"TEMPO" => TrendForecastType.Temporary,
_ => throw new ArgumentException("Invalid ChangeIndicator"),
},
};

if (!string.IsNullOrEmpty(found[2].Value) && found[2].Value.StartsWith("FM"))
{
trend.FromTime = found[3].Value;
}
var firstTrend = ParseTrendForecast(found, 1);
result.Add("TrendForecast", firstTrend);

if (!string.IsNullOrEmpty(found[4].Value) && found[4].Value.StartsWith("TL"))
if (!string.IsNullOrEmpty(found[9].Value))
{
trend.UntilTime = found[5].Value;
var futureTrend = ParseTrendForecast(found, 10);
result.Add("TrendForecastFuture", futureTrend);
}
}

if (!string.IsNullOrEmpty(found[6].Value) && found[6].Value.StartsWith("AT"))
{
trend.AtTime = found[7].Value;
}
return GetResults(newRemainingMetar, result);
}

if (!string.IsNullOrEmpty(found[8].Value))
private TrendForecast ParseTrendForecast(List<Group> found, int startIndex)
{
var trend = new TrendForecast
{
ChangeIndicator = found[startIndex].Value switch
{
trend.Forecast = found[8].Value;
}
"NOSIG" => TrendForecastType.NoSignificantChanges,
"BECMG" => TrendForecastType.Becoming,
"TEMPO" => TrendForecastType.Temporary,
_ => throw new ArgumentException("Invalid ChangeIndicator"),
},
};

result.Add(newRemainingMetar, trend);
if (!string.IsNullOrEmpty(found[startIndex + 1].Value))
{
trend.AtTime = found[startIndex + 1].Value;
}

return GetResults(newRemainingMetar, result);
if (!string.IsNullOrEmpty(found[startIndex + 2].Value))
{
trend.FromTime = found[startIndex + 2].Value;
}

if (!string.IsNullOrEmpty(found[startIndex + 3].Value))
{
trend.UntilTime = found[startIndex + 3].Value;
}

if (!string.IsNullOrEmpty(found[startIndex + 4].Value))
{
trend.SurfaceWind = found[startIndex + 4].Value + " ";
}

if (!string.IsNullOrEmpty(found[startIndex + 5].Value))
{
trend.PrevailingVisibility = found[startIndex + 5].Value + " ";
}

if (!string.IsNullOrEmpty(found[startIndex + 6].Value))
{
trend.WeatherCodes = found[startIndex + 6].Value + " ";
}

if (!string.IsNullOrEmpty(found[startIndex + 7].Value))
{
trend.Clouds = found[startIndex + 7].Value + " ";
}

return trend;
}
}
5 changes: 5 additions & 0 deletions vATIS.Desktop/Weather/Decoder/Entity/DecodedMetar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ public string? RawMetar
/// </summary>
public TrendForecast? TrendForecast { get; set; }

/// <summary>
/// Gets or sets the additional TREND forecast information.
/// </summary>
public TrendForecast? TrendForecastFuture { get; set; }

/// <summary>
/// Gets a value indicating whether the decoded METAR is valid, determined by the absence of decoding exceptions.
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions vATIS.Desktop/Weather/Decoder/Entity/TrendForecast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,24 @@ public sealed class TrendForecast
/// This property contains the specific forecast information parsed from the METAR trend data.
/// </remarks>
public string? Forecast { get; set; }

/// <summary>
/// Gets or sets the surface wind information in the TREND forecast.
/// </summary>
public string? SurfaceWind { get; set; }

/// <summary>
/// Gets or sets the prevailing visibility information in the TREND forecast.
/// </summary>
public string? PrevailingVisibility { get; set; }

/// <summary>
/// Gets or sets the weather phenomenon information in the TREND forecast.
/// </summary>
public string? WeatherCodes { get; set; }

/// <summary>
/// Gets or sets the cloud layer information in the TREND forecast.
/// </summary>
public string? Clouds { get; set; }
}
Loading