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