swift
metardecoder.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2015 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
7 
8 #include <QHash>
9 #include <QRegularExpression>
10 #include <QRegularExpressionMatch>
11 #include <QStringList>
12 #include <QtGlobal>
13 
15 #include "misc/aviation/altitude.h"
16 #include "misc/logmessage.h"
17 #include "misc/pq/angle.h"
18 #include "misc/pq/length.h"
20 #include "misc/pq/pressure.h"
21 #include "misc/pq/speed.h"
22 #include "misc/pq/temperature.h"
23 #include "misc/pq/time.h"
24 #include "misc/pq/units.h"
25 #include "misc/statusmessage.h"
28 #include "misc/weather/windlayer.h"
29 
30 using namespace swift::misc::physical_quantities;
31 using namespace swift::misc::aviation;
32 
33 namespace swift::misc::weather
34 {
35 
36  // Implementation based on the following websites:
37  // http://meteocentre.com/doc/metar.html
38  // http://www.sigmet.de/key.php
39  // http://wx.erau.edu/reference/text/metar_code_format.pdf
40 
41  class IMetarDecoderPart
42  {
43  public:
45  virtual ~IMetarDecoderPart() = default;
46 
48  virtual QString getDecoderType() const = 0;
49 
50  protected:
52  virtual const QRegularExpression &getRegExp() const = 0;
53 
54  virtual bool isRepeatable() const { return false; }
55  virtual bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const = 0;
56  virtual bool isMandatory() const = 0;
57 
58  public:
60  bool parse(QString &metarString, CMetar &metar)
61  {
62  bool isValid = false;
63  const QRegularExpression re(getRegExp());
64  Q_ASSERT(re.isValid());
65  // Loop stop condition:
66  // - Invalid data
67  // - One match found and token not repeatable
68  do {
69  QRegularExpressionMatch match = re.match(metarString);
70  if (match.hasMatch())
71  {
72  // If invalid data, we return straight away
73  if (!validateAndSet(match, metar)) { return false; }
74 
75  // Remove the captured part
76  metarString.replace(re, QString());
77  isValid = true;
78  }
79  else
80  {
81  // No (more) match found.
82  if (!isMandatory()) { isValid = true; }
83  break;
84  }
85  }
86  while (isRepeatable());
87 
88  if (!isValid)
89  {
90  CLogMessage(static_cast<CMetarDecoder *>(nullptr)).debug()
91  << "Failed to match" << getDecoderType() << "in remaining METAR:" << metarString;
92  }
93  return isValid;
94  }
95  };
96 
98  class CMetarDecoderReportType : public IMetarDecoderPart
99  {
100  public:
101  QString getDecoderType() const override { return "ReportType"; }
102 
103  protected:
104  const QRegularExpression &getRegExp() const override
105  {
106  static const QRegularExpression re(QStringLiteral("^(?<reporttype>METAR|SPECI)? "));
107  return re;
108  }
109 
110  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
111  {
112  QString reportTypeAsString = match.captured("reporttype");
113  if (reportTypeAsString.isEmpty() || !getReportTypeHash().contains(reportTypeAsString)) { return false; }
114 
115  metar.setReportType(getReportTypeHash().value(reportTypeAsString));
116  return true;
117  }
118 
119  bool isMandatory() const override { return false; }
120 
121  private:
122  const QHash<QString, CMetar::ReportType> &getReportTypeHash() const
123  {
124  static const QHash<QString, CMetar::ReportType> hash = { { "METAR", CMetar::METAR },
125  { "SPECI", CMetar::SPECI } };
126  return hash;
127  }
128  };
129 
130  class CMetarDecoderAirport : public IMetarDecoderPart
131  {
132  public:
133  QString getDecoderType() const override { return "Airport"; }
134 
135  protected:
136  const QRegularExpression &getRegExp() const override
137  {
138  static const QRegularExpression re(QStringLiteral("^(?<airport>\\w{4}) "));
139  return re;
140  }
141 
142  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
143  {
144  QString airportAsString = match.captured("airport");
145  Q_ASSERT(!airportAsString.isEmpty());
146  metar.setAirportIcaoCode(CAirportIcaoCode(airportAsString));
147  return true;
148  }
149 
150  bool isMandatory() const override { return true; }
151  };
152 
153  class CMetarDecoderDayTime : public IMetarDecoderPart
154  {
155  public:
156  QString getDecoderType() const override { return "DayTime"; }
157 
158  protected:
159  const QRegularExpression &getRegExp() const override
160  {
161  static const QRegularExpression re(QStringLiteral("^(?<day>\\d{2})(?<hour>\\d{2})(?<minute>\\d{2})Z "));
162  return re;
163  }
164 
165  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
166  {
167  bool ok = false;
168  int day = match.captured("day").toInt(&ok);
169  int hour = match.captured("hour").toInt(&ok);
170  int minute = match.captured("minute").toInt(&ok);
171  if (!ok) return false;
172 
173  if (day < 1 || day > 31) return false;
174  if (hour < 0 || hour > 23) return false;
175  if (minute < 0 || minute > 59) return false;
176 
177  physical_quantities::CTime time(hour, minute, 0);
178  metar.setDayTime(day, time);
179  return true;
180  }
181 
182  bool isMandatory() const override { return true; }
183  };
184 
185  class CMetarDecoderStatus : public IMetarDecoderPart
186  {
187  public:
188  QString getDecoderType() const override { return "Status"; }
189 
190  protected:
191  // Possible matches:
192  // * (AUTO) - Automatic Station Indicator
193  // * (NIL) - NO METAR
194  // * (BBB) - Correction Indicator
195  const QRegularExpression &getRegExp() const override
196  {
197  static const QRegularExpression re(QStringLiteral("^([A-Z]+) "));
198  return re;
199  }
200 
201  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
202  {
203  if (match.captured(1) == "AUTO")
204  {
205  metar.setAutomated(true);
206  return true;
207  }
208  else if (match.captured(1) == "NIL") { return true; }
209  else if (match.captured(1).size() == 3) { return true; }
210  else { return false; }
211  }
212 
213  bool isMandatory() const override { return false; }
214  };
215 
216  class CMetarDecoderWind : public IMetarDecoderPart
217  {
218  public:
219  QString getDecoderType() const override { return "Wind"; }
220 
221  protected:
222  const QHash<QString, CSpeedUnit> &getWindUnitHash() const
223  {
224  static const QHash<QString, CSpeedUnit> hash = { { "KT", CSpeedUnit::kts() },
225  { "MPS", CSpeedUnit::m_s() },
226  { "KPH", CSpeedUnit::km_h() },
227  { "KMH", CSpeedUnit::km_h() } };
228  return hash;
229  }
230 
231  const QRegularExpression &getRegExp() const override
232  {
233  static const QRegularExpression re(getRegExpImpl());
234  return re;
235  }
236 
237  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
238  {
239  bool ok = false;
240  QString directionAsString = match.captured("direction");
241  if (directionAsString == "///") return true;
242  int direction = 0;
243  bool directionVariable = false;
244  if (directionAsString == "VRB") { directionVariable = true; }
245  else
246  {
247  direction = directionAsString.toInt(&ok);
248  if (!ok) return false;
249  }
250 
251  QString speedAsString = match.captured("speed");
252  if (speedAsString == "//") return true;
253  int speed = speedAsString.toInt(&ok);
254  if (!ok) return false;
255  QString gustAsString = match.captured("gustSpeed");
256  int gustSpeed = 0;
257  if (!gustAsString.isEmpty())
258  {
259  gustSpeed = gustAsString.toInt(&ok);
260  if (!ok) return false;
261  }
262  QString unitAsString = match.captured("unit");
263  if (!getWindUnitHash().contains(unitAsString)) return false;
264 
265  CWindLayer windLayer(CAltitude(0, CAltitude::AboveGround, CLengthUnit::ft()),
266  CAngle(direction, CAngleUnit::deg()),
267  CSpeed(speed, getWindUnitHash().value(unitAsString)),
268  CSpeed(gustSpeed, getWindUnitHash().value(unitAsString)));
269  windLayer.setDirectionVariable(directionVariable);
270  metar.setWindLayer(windLayer);
271  return true;
272  }
273 
274  bool isMandatory() const override { return false; }
275 
276  private:
277  QString getRegExpImpl() const
278  {
279  // Wind direction in three digits, 'VRB' or /// if no info available
280  const QString direction = QStringLiteral("(?<direction>\\d{3}|VRB|/{3})");
281  // Wind speed in two digits (or three digits if required)
282  const QString speed = QStringLiteral("(?<speed>\\d{2,3}|/{2})");
283  // Optional: Gust in two digits (or three digits if required)
284  const QString gustSpeed = QStringLiteral("(G(?<gustSpeed>\\d{2,3}))?");
285  // Unit
286  const QString unit = QStringLiteral("(?<unit>") + QStringList(getWindUnitHash().keys()).join('|') + ")";
287  // Regexp
288  const QString regexp = "^" + direction + speed + gustSpeed + unit + " ?";
289  return regexp;
290  }
291  };
292 
293  class CMetarDecoderVariationsWindDirection : public IMetarDecoderPart
294  {
295  public:
296  QString getDecoderType() const override { return "WindDirection"; }
297 
298  protected:
299  const QRegularExpression &getRegExp() const override
300  {
301  static const QRegularExpression re(getRegExpImpl());
302  return re;
303  }
304 
305  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
306  {
307  QString directionFromAsString = match.captured("direction_from");
308  QString directionToAsString = match.captured("direction_to");
309 
310  int directionFrom = 0;
311  int directionTo = 0;
312 
313  bool ok = false;
314  directionFrom = directionFromAsString.toInt(&ok);
315  directionTo = directionToAsString.toInt(&ok);
316  if (!ok) return false;
317 
318  auto windLayer = metar.getWindLayer();
319  windLayer.setDirection(CAngle(directionFrom, CAngleUnit::deg()), CAngle(directionTo, CAngleUnit::deg()));
320  metar.setWindLayer(windLayer);
321  return true;
322  }
323 
324  bool isMandatory() const override { return false; }
325 
326  private:
327  QString getRegExpImpl() const
328  {
329  // <from>V in degrees
330  const QString directionFrom("(?<direction_from>\\d{3})V");
331  // <to> in degrees
332  const QString directionTo("(?<direction_to>\\d{3})");
333  // Add space at the end
334  const QString regexp = "^" + directionFrom + directionTo + " ";
335  return regexp;
336  }
337  };
338 
339  class CMetarDecoderVisibility : public IMetarDecoderPart
340  {
341  public:
342  QString getDecoderType() const override { return "Visibility"; }
343 
344  protected:
345  const QHash<QString, QString> &getCardinalDirections() const
346  {
347  static const QHash<QString, QString> hash = {
348  { "N", "north" }, { "NE", "north-east" }, { "E", "east" }, { "SE", "south-east" },
349  { "S", "south" }, { "SW", "south-west" }, { "W", "west" }, { "NW", "north-west" },
350  };
351  return hash;
352  }
353 
354  const QRegularExpression &getRegExp() const override
355  {
356  static const QRegularExpression re(getRegExpImpl());
357  return re;
358  }
359 
360  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
361  {
362  bool ok = false;
363  if (!match.captured("cavok").isEmpty())
364  {
365  metar.setCavok();
366  return true;
367  }
368  QString visibilityAsString = match.captured("visibility");
369  if (visibilityAsString == "////") return true;
370 
371  double visibility = 0;
372  if (!visibilityAsString.isEmpty())
373  {
374  visibility = visibilityAsString.toDouble(&ok);
375  if (!ok) return false;
376  metar.setVisibility(CLength(visibility, CLengthUnit::m()));
377  return true;
378  }
379 
380  QString distanceAsString = match.captured("distance");
381  if (!distanceAsString.isEmpty())
382  {
383  visibility += distanceAsString.toDouble(&ok);
384  if (!ok) return false;
385  }
386  QString numeratorAsString = match.captured("numerator");
387  QString denominatorAsString = match.captured("denominator");
388  if (!numeratorAsString.isEmpty() && !denominatorAsString.isEmpty())
389  {
390 
391  double numerator = numeratorAsString.toDouble(&ok);
392  if (!ok) return false;
393 
394  double denominator = denominatorAsString.toDouble(&ok);
395  if (!ok) return false;
396  if (denominator < 1 || numerator < 1) return false;
397  visibility += (numerator / denominator);
398  }
399 
400  QString unitAsString = match.captured("unit");
401  CLengthUnit unit = CLengthUnit::SM();
402  if (unitAsString == "KM") unit = CLengthUnit::km();
403 
404  metar.setVisibility(CLength(visibility, unit));
405 
406  return true;
407  }
408 
409  bool isMandatory() const override { return false; }
410 
411  private:
412  QString getRegExpImpl() const
413  {
414  // CAVOK
415  const QString cavok = QStringLiteral("(?<cavok>CAVOK)");
416  // European version:
417  // Visibility of 4 digits in meter
418  // Cardinal directions N, NE etc.
419  // "////" in case no info is available
420  // NDV = No Directional Variation
421  const QString visibility_eu = QStringLiteral("(?<visibility>\\d{4}|/{4})(NDV)?") + "(" +
422  QStringList(getCardinalDirections().keys()).join('|') + ")?";
423  // US/Canada version:
424  // Surface visibility reported in statute miles.
425  // A space divides whole miles and fractions.
426  // Group ends with SM to indicate statute miles. For example,
427  // 1 1/2SM.
428  // Auto only: M prefixed to value < 1/4 mile, e.g., M1/4S
429  const QString visibility_us =
430  QStringLiteral("(?<distance>\\d{0,2}) ?M?((?<numerator>\\d)/(?<denominator>\\d))?(?<unit>SM|KM)");
431  const QString regexp = "^(" + cavok + "|" + visibility_eu + "|" + visibility_us + ") ";
432  return regexp;
433  }
434  };
435 
436  class CMetarDecoderRunwayVisualRange : public IMetarDecoderPart
437  {
438  public:
439  QString getDecoderType() const override { return "RunwayVisualRange"; }
440 
441  protected:
442  const QRegularExpression &getRegExp() const override
443  {
444 
445  static const QRegularExpression re(getRegExpImpl());
446  return re;
447  }
448 
449  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
450  {
451  QString runway = match.captured("runway");
452  QString runwayVisibilityAsString = match.captured("rwy_visibility");
453  Q_ASSERT(!runway.isEmpty() && !runwayVisibilityAsString.isEmpty());
454 
455  bool ok = false;
456  double runwayVisibility = runwayVisibilityAsString.toDouble(&ok);
457  if (!ok) return false;
458  CLengthUnit lengthUnit = CLengthUnit::m();
459  if (match.captured("unit") == "FT") lengthUnit = CLengthUnit::ft();
460  // Ignore for now until we make use of it.
461  Q_UNUSED(metar)
462  Q_UNUSED(runwayVisibility)
463  Q_UNUSED(lengthUnit)
464  return true;
465  }
466 
467  bool isMandatory() const override { return false; }
468 
469  private:
470  QString getRegExpImpl() const
471  {
472  // 10 Minute RVR value: Reported in hundreds of feet if visibility is ≤ one statute mile
473  // or RVR is ≤ 6000 feet. Group ends with FT to indicate feet. For example, R06L/2000FT.
474  // The RVR value is prefixed with either M or P to indicate the value is lower or higher
475  // than the RVR reportable values, e.g., R06L/P6000FT. If the RVR is variable during the 10
476  // minute evaluation period, the variability is reported, e.g., R06L/2000V4000FT
477 
478  // Runway
479  const QString runway = QStringLiteral("R(?<runway>\\d{2}[LCR]*)");
480  // Visibility
481  const QString visibility = QStringLiteral("/[PM]?(?<rwy_visibility>\\d{4})");
482  // Variability
483  static const QString variability = QStringLiteral("V?(?<variability>\\d{4})?");
484  // Unit
485  const QString unit = QStringLiteral("(?<unit>FT)?");
486  // Trend
487  const QString trend = QStringLiteral("/?(?<trend>[DNU])?");
488  const QString regexp = "^" + runway + visibility + variability + unit + trend + " ";
489  return regexp;
490  }
491  };
492 
493  class CMetarDecoderPresentWeather : public IMetarDecoderPart
494  {
495  public:
496  QString getDecoderType() const override { return "PresentWeather"; }
497 
498  protected:
499  const QHash<QString, CPresentWeather::Intensity> &getIntensityHash() const
500  {
501  static const QHash<QString, CPresentWeather::Intensity> hash = { { "-", CPresentWeather::Light },
502  { "+", CPresentWeather::Heavy },
503  { "VC", CPresentWeather::InVincinity } };
504  return hash;
505  }
506 
507  const QHash<QString, CPresentWeather::Descriptor> &getDescriptorHash() const
508  {
509  static const QHash<QString, CPresentWeather::Descriptor> hash = {
510  { "MI", CPresentWeather::Shallow }, { "BC", CPresentWeather::Patches },
511  { "PR", CPresentWeather::Partial }, { "DR", CPresentWeather::Drifting },
512  { "BL", CPresentWeather::Blowing }, { "SH", CPresentWeather::Showers },
513  { "TS", CPresentWeather::Thunderstorm }, { "FR", CPresentWeather::Freezing },
514  };
515  return hash;
516  }
517 
518  const QHash<QString, CPresentWeather::WeatherPhenomenon> &getWeatherPhenomenaHash() const
519  {
521  { "DZ", CPresentWeather::Drizzle },
522  { "RA", CPresentWeather::Rain },
523  { "SN", CPresentWeather::Snow },
524  { "SG", CPresentWeather::SnowGrains },
525  { "IC", CPresentWeather::IceCrystals },
526  { "PC", CPresentWeather::IcePellets },
527  { "GR", CPresentWeather::Hail },
528  { "GS", CPresentWeather::SnowPellets },
529  { "UP", CPresentWeather::Unknown },
530  { "BR", CPresentWeather::Mist },
531  { "FG", CPresentWeather::Fog },
532  { "FU", CPresentWeather::Smoke },
533  { "VA", CPresentWeather::VolcanicAsh },
534  { "DU", CPresentWeather::Dust },
535  { "SA", CPresentWeather::Sand },
536  { "HZ", CPresentWeather::Haze },
537  { "PO", CPresentWeather::DustSandWhirls },
538  { "SQ", CPresentWeather::Squalls },
539  { "FC", CPresentWeather::TornadoOrWaterspout },
540  { "FC", CPresentWeather::FunnelCloud },
541  { "SS", CPresentWeather::Sandstorm },
542  { "DS", CPresentWeather::Duststorm },
543  { "//", {} },
544  };
545  return hash;
546  }
547 
548  bool isRepeatable() const override { return true; }
549 
550  const QRegularExpression &getRegExp() const override
551  {
552  static const QRegularExpression re(getRegExpImpl());
553  return re;
554  }
555 
556  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
557  {
558  QString intensityAsString = match.captured("intensity");
559  CPresentWeather::Intensity itensity = CPresentWeather::Moderate;
560  if (!intensityAsString.isEmpty()) { itensity = getIntensityHash().value(intensityAsString); }
561 
562  QString descriptorAsString = match.captured("descriptor");
563  CPresentWeather::Descriptor descriptor = CPresentWeather::None;
564  if (!descriptorAsString.isEmpty()) { descriptor = getDescriptorHash().value(descriptorAsString); }
565 
566  int weatherPhenomena = 0;
567  QString wp1AsString = match.captured("wp1");
568  if (!wp1AsString.isEmpty()) { weatherPhenomena |= getWeatherPhenomenaHash().value(wp1AsString); }
569 
570  QString wp2AsString = match.captured("wp2");
571  if (!wp2AsString.isEmpty()) { weatherPhenomena |= getWeatherPhenomenaHash().value(wp2AsString); }
572 
573  CPresentWeather presentWeather(itensity, descriptor, weatherPhenomena);
574  metar.addPresentWeather(presentWeather);
575  return true;
576  }
577 
578  bool isMandatory() const override { return false; }
579 
580  private:
581  QString getRegExpImpl() const
582  {
583  // w'w' represents present weather, coded in accordance with WMO Code Table 4678.
584  // As many groups as necessary are included, with each group containing from 2 to 9 characters.
585  // * Weather phenomena are preceded by one or two qualifiers
586  // * No w'w' group has more than one descriptor.
587 
588  // Qualifier intensity. (-) light (no sign) moderate (+) heavy or VC
589  const QString qualifier_intensity("(?<intensity>[-+]|VC)?");
590  // Descriptor, if any
591  const QString qualifier_descriptor =
592  "(?<descriptor>" + QStringList(getDescriptorHash().keys()).join('|') + ")?";
593  const QString weatherPhenomenaJoined = QStringList(getWeatherPhenomenaHash().keys()).join('|');
594  const QString weather_phenomina1 = "(?<wp1>" + weatherPhenomenaJoined + ")?";
595  const QString weather_phenomina2 = "(?<wp2>" + weatherPhenomenaJoined + ")?";
596  const QString weather_phenomina3 = "(?<wp3>" + weatherPhenomenaJoined + ")?";
597  const QString weather_phenomina4 = "(?<wp4>" + weatherPhenomenaJoined + ")?";
598  const QString regexp = "^(" + qualifier_intensity + qualifier_descriptor + weather_phenomina1 +
599  weather_phenomina2 + weather_phenomina3 + weather_phenomina4 + ") ";
600  return regexp;
601  }
602  };
603 
604  class CMetarDecoderCloud : public IMetarDecoderPart
605  {
606  public:
607  QString getDecoderType() const override { return "Cloud"; }
608 
609  protected:
610  const QStringList &getClearSkyTokens() const
611  {
612  static const QStringList list = { "SKC", "NSC", "CLR", "NCD" };
613  return list;
614  }
615 
616  const QHash<QString, CCloudLayer::Coverage> &getCoverage() const
617  {
618  static const QHash<QString, CCloudLayer::Coverage> hash = { { "///", CCloudLayer::None },
619  { "FEW", CCloudLayer::Few },
620  { "SCT", CCloudLayer::Scattered },
621  { "BKN", CCloudLayer::Broken },
622  { "OVC", CCloudLayer::Overcast } };
623  return hash;
624  }
625 
626  bool isRepeatable() const override { return true; }
627 
628  const QRegularExpression &getRegExp() const override
629  {
630  static const QRegularExpression re(getRegExpImpl());
631  return re;
632  }
633 
634  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
635  {
636  QString noClouds = match.captured("clear_sky");
637  if (!noClouds.isEmpty())
638  {
639  metar.removeAllClouds();
640  return true;
641  }
642 
643  QString coverageAsString = match.captured("coverage");
644  QString baseAsString = match.captured("base");
645  Q_ASSERT(!coverageAsString.isEmpty() && !baseAsString.isEmpty());
646  Q_ASSERT(getCoverage().contains(coverageAsString));
647  if (baseAsString == "///") return true;
648 
649  bool ok = false;
650  int base = baseAsString.toInt(&ok);
651  // Factor 100
652  base *= 100;
653  if (!ok) return false;
654 
655  CCloudLayer cloudLayer(CAltitude(base, CAltitude::AboveGround, CLengthUnit::ft()), {},
656  getCoverage().value(coverageAsString));
657  metar.addCloudLayer(cloudLayer);
658  QString cb_tcu = match.captured("cb_tcu");
659  if (!cb_tcu.isEmpty()) {}
660  return true;
661  }
662 
663  bool isMandatory() const override { return false; }
664 
665  private:
666  QString getRegExpImpl() const
667  {
668  // Clear sky
669  const QString clearSky = QString("(?<clear_sky>") + getClearSkyTokens().join('|') + QString(")");
670  // Cloud coverage.
671  const QString coverage =
672  QString("(?<coverage>") + QStringList(getCoverage().keys()).join('|') + QString(")");
673  // Cloud base
674  const QString base = QStringLiteral("(?<base>\\d{3}|///)");
675  // CB (Cumulonimbus) or TCU (Towering Cumulus) are appended to the cloud group without a space
676  const QString extra = QStringLiteral("(?<cb_tcu>CB|TCU|///)?");
677  // Add space at the end
678  const QString regexp = QString("^(") + clearSky + '|' + coverage + base + extra + QString(") ");
679  return regexp;
680  }
681  };
682 
683  class CMetarDecoderVerticalVisibility : public IMetarDecoderPart
684  {
685  public:
686  QString getDecoderType() const override { return "VerticalVisibility"; }
687 
688  protected:
689  const QRegularExpression &getRegExp() const override
690  {
691  static const QRegularExpression re(getRegExpImpl());
692  return re;
693  }
694 
695  bool validateAndSet(const QRegularExpressionMatch & /* match */, CMetar & /* metar */) const override
696  {
697  // todo
698  return true;
699  }
700 
701  bool isMandatory() const override { return false; }
702 
703  private:
704  QString getRegExpImpl() const
705  {
706  // Vertical visibility
707  const QString verticalVisibility = QStringLiteral("VV(?<vertical_visibility>\\d{3}|///)");
708  const QString regexp = "^" + verticalVisibility + " ";
709  return regexp;
710  }
711  };
712 
713  class CMetarDecoderTemperature : public IMetarDecoderPart
714  {
715  public:
716  QString getDecoderType() const override { return "Temperature"; }
717 
718  protected:
719  const QRegularExpression &getRegExp() const override
720  {
721  static const QRegularExpression re(getRegExpImpl());
722  return re;
723  }
724 
725  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
726  {
727  QString temperatureAsString = match.captured("temperature");
728  if (temperatureAsString.isEmpty()) return false;
729  QString dewPointAsString = match.captured("dew_point");
730  if (dewPointAsString.isEmpty()) return false;
731 
732  if (temperatureAsString == "//" || dewPointAsString == "//") return true;
733 
734  bool temperatureNegative = false;
735  if (temperatureAsString.startsWith('M'))
736  {
737  temperatureNegative = true;
738  temperatureAsString.remove('M');
739  }
740 
741  bool dewPointNegative = false;
742  if (dewPointAsString.startsWith('M'))
743  {
744  dewPointNegative = true;
745  dewPointAsString.remove('M');
746  }
747 
748  int temperature = temperatureAsString.toInt();
749  if (temperatureNegative) { temperature *= -1; }
750  metar.setTemperature(CTemperature(temperature, CTemperatureUnit::C()));
751 
752  int dewPoint = dewPointAsString.toInt();
753  if (dewPointNegative) { dewPoint *= -1; }
754  metar.setDewPoint(CTemperature(dewPoint, CTemperatureUnit::C()));
755 
756  return true;
757  }
758 
759  bool isMandatory() const override { return false; }
760 
761  private:
762  QString getRegExpImpl() const
763  {
764  // Tmperature
765  const QString temperature = QStringLiteral("(?<temperature>M?\\d{2}|//)");
766  // Separator
767  const QString separator = "/";
768  // Dew point
769  const QString dewPoint = QStringLiteral("(?<dew_point>M?\\d{2}|//)");
770  // Add space at the end
771  const QString regexp = "^" + temperature + separator + dewPoint + " ?";
772  return regexp;
773  }
774  };
775 
776  class CMetarDecoderPressure : public IMetarDecoderPart
777  {
778  public:
779  QString getDecoderType() const override { return "Pressure"; }
780 
781  protected:
782  const QHash<QString, CPressureUnit> &getPressureUnits() const
783  {
784  static const QHash<QString, CPressureUnit> hash = { { "Q", CPressureUnit::hPa() },
785  { "A", CPressureUnit::inHg() } };
786  return hash;
787  }
788 
789  const QRegularExpression &getRegExp() const override
790  {
791  static const QRegularExpression re(getRegExpImpl());
792  return re;
793  }
794 
795  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
796  {
797  QString unitAsString = match.captured("unit");
798  QString pressureAsString = match.captured("pressure");
799  QString qfeAsString = match.captured("qfe");
800  if ((unitAsString.isEmpty() || pressureAsString.isEmpty()) && qfeAsString.isEmpty()) return false;
801 
802  // In case no value is defined
803  if (pressureAsString == "////") return true;
804 
805  if (!unitAsString.isEmpty() && !pressureAsString.isEmpty())
806  {
807  Q_ASSERT(getPressureUnits().contains(unitAsString));
808  bool ok = false;
809  double pressure = pressureAsString.toDouble(&ok);
810  if (!ok) return false;
811  CPressureUnit pressureUnit = getPressureUnits().value(unitAsString);
812  if (pressureUnit == CPressureUnit::inHg()) pressure /= 100;
813  metar.setAltimeter(CPressure(pressure, pressureUnit));
814  return true;
815  }
816 
817  if (!qfeAsString.isEmpty())
818  {
819  // todo QFE
820  return true;
821  }
822  return false;
823  }
824 
825  bool isMandatory() const override { return false; }
826 
827  private:
828  QString getRegExpImpl() const
829  {
830  // Q => QNH comes in hPa
831  // A => QNH comes in inches of Mercury
832  const QString unit = QStringLiteral("((?<unit>Q|A)");
833  // Pressure
834  const QString pressure = QStringLiteral("(?<pressure>\\d{4}|////) ?)");
835  // QFE
836  const QString qfe = QStringLiteral("(QFE (?<qfe>\\d+).\\d");
837  const QString regexp = "^" + unit + pressure + "|" + qfe + " ?)";
838  return regexp;
839  }
840  };
841 
842  class CMetarDecoderRecentWeather : public IMetarDecoderPart
843  {
844  public:
845  QString getDecoderType() const override { return "RecentWeather"; }
846 
847  protected:
848  const QRegularExpression &getRegExp() const override
849  {
850  static const QRegularExpression re(getRegExpImpl());
851  return re;
852  }
853 
854  bool validateAndSet(const QRegularExpressionMatch & , CMetar & ) const override
855  {
856  // Ignore for now
857  return true;
858  }
859 
860  bool isMandatory() const override { return false; }
861 
862  private:
863  QString getRegExpImpl() const
864  {
865  // Qualifier intensity. (-) light (no sign) moderate (+) heavy or VC
866  const QString qualifier_intensity("(?<intensity>[-+]|VC)?");
867  // Descriptor, if any
868  const QString qualifier_descriptor = "(?<descriptor>" + m_descriptor.join('|') + ")?";
869  const QString weather_phenomina1 = "(?<wp1>" + m_phenomina.join('|') + ")?";
870  const QString weather_phenomina2 = "(?<wp2>" + m_phenomina.join('|') + ")?";
871  const QString weather_phenomina3 = "(?<wp3>" + m_phenomina.join('|') + ")?";
872  const QString weather_phenomina4 = "(?<wp4>" + m_phenomina.join('|') + ")?";
873  const QString regexp = "^RE" + qualifier_intensity + qualifier_descriptor + weather_phenomina1 +
874  weather_phenomina2 + weather_phenomina3 + weather_phenomina4 + " ";
875  return regexp;
876  }
877 
878  const QStringList m_descriptor = QStringList { "MI", "BC", "PR", "DR", "BL", "SH", "TS", "FZ" };
879  const QStringList m_phenomina =
880  QStringList { "DZ", "RA", "SN", "SG", "IC", "PE", "GR", "GS", "BR", "FG", "FU",
881  "VA", "IC", "DU", "SA", "HZ", "PY", "PO", "SQ", "FC", "SS", "DS" };
882  };
883 
884  class CMetarDecoderWindShear : public IMetarDecoderPart
885  {
886  public:
887  QString getDecoderType() const override { return "WindShear"; }
888 
889  protected:
890  const QRegularExpression &getRegExp() const override
891  {
892  static const QRegularExpression re(getRegExpImpl());
893  return re;
894  }
895 
896  bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override
897  {
898  QString runwayAsString = match.captured("runway");
899  if (!runwayAsString.isEmpty())
900  {
901  // Ignore for now until we make use of it.
902  Q_UNUSED(runwayAsString)
903  Q_UNUSED(metar)
904  }
905 
906  return true;
907  }
908 
909  bool isMandatory() const override { return false; }
910 
911  private:
912  QString getRegExpImpl() const
913  {
914  // Wind shear on all runways
915  const QString wsAllRwy = QStringLiteral("WS ALL RWY");
916  // RWY designator
917  const QString runway = QStringLiteral("RW?Y?(?<runway>\\d{2}[LCR]*)");
918  const QString regexp = "^WS (" + wsAllRwy + "|" + runway + ") ";
919  return regexp;
920  }
921  };
922 
923  CMetarDecoder::CMetarDecoder() { allocateDecoders(); }
924 
925  CMetarDecoder::~CMetarDecoder() {}
926 
927  CMetar CMetarDecoder::decode(const QString &metarString) const
928  {
929  CMetar metar;
930  QString metarStringCopy = metarString.simplified();
931 
932  for (const auto &decoder : m_decoders)
933  {
934  if (!decoder->parse(metarStringCopy, metar))
935  {
936  const QString type = decoder->getDecoderType();
937  CLogMessage(this).debug() << "Invalid METAR:" << metarString << type;
938  return {};
939  }
940  }
941 
942  metar.setMessage(metarString);
943  return metar;
944  }
945 
946  void CMetarDecoder::allocateDecoders()
947  {
948  m_decoders.clear();
949  m_decoders.push_back(std::make_unique<CMetarDecoderReportType>());
950  m_decoders.push_back(std::make_unique<CMetarDecoderAirport>());
951  m_decoders.push_back(std::make_unique<CMetarDecoderDayTime>());
952  m_decoders.push_back(std::make_unique<CMetarDecoderStatus>());
953  m_decoders.push_back(std::make_unique<CMetarDecoderWind>());
954  m_decoders.push_back(std::make_unique<CMetarDecoderVariationsWindDirection>());
955  m_decoders.push_back(std::make_unique<CMetarDecoderVisibility>());
956  m_decoders.push_back(std::make_unique<CMetarDecoderRunwayVisualRange>());
957  m_decoders.push_back(std::make_unique<CMetarDecoderPresentWeather>());
958  m_decoders.push_back(std::make_unique<CMetarDecoderCloud>());
959  m_decoders.push_back(std::make_unique<CMetarDecoderVerticalVisibility>());
960  m_decoders.push_back(std::make_unique<CMetarDecoderTemperature>());
961  m_decoders.push_back(std::make_unique<CMetarDecoderPressure>());
962  m_decoders.push_back(std::make_unique<CMetarDecoderRecentWeather>());
963  m_decoders.push_back(std::make_unique<CMetarDecoderWindShear>());
964  }
965 
966 } // namespace swift::misc::weather
967 
Value object encapsulating information of airport ICAO data.
Altitude as used in aviation, can be AGL or MSL altitude.
Definition: altitude.h:52
Physical unit angle (radians, degrees)
Definition: angle.h:23
static CAngleUnit deg()
Degrees.
Definition: units.h:278
Physical unit length (length)
Definition: length.h:18
Specialized class for distance units (meter, foot, nautical miles).
Definition: units.h:95
static CLengthUnit km()
Kilometer km.
Definition: units.h:167
static CLengthUnit m()
Meter m.
Definition: units.h:142
static CLengthUnit SM()
Statute mile.
Definition: units.h:192
static CLengthUnit ft()
Foot ft.
Definition: units.h:159
double value(MU unit) const
Value in given unit.
Specialized class for pressure (psi, hPa, bar).
Definition: units.h:544
static CPressureUnit inHg()
Inch of mercury at 0°C.
Definition: units.h:630
static CPressureUnit hPa()
Hectopascal.
Definition: units.h:595
static CSpeedUnit km_h()
Kilometer/hour km/h.
Definition: units.h:868
static CSpeedUnit m_s()
Meter/second m/s.
Definition: units.h:824
static CSpeedUnit kts()
Knots.
Definition: units.h:833
static CTemperatureUnit C()
Centigrade C.
Definition: units.h:729