swift
altitude.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2013 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QStringBuilder>
7 #include <Qt>
8 #include <QtGlobal>
9 
10 #include "misc/comparefunctions.h"
11 #include "misc/iconlist.h"
12 #include "misc/icons.h"
13 #include "misc/math/mathutils.h"
14 #include "misc/pq/constants.h"
16 #include "misc/pq/pqstring.h"
17 #include "misc/stringutils.h"
18 
19 using namespace swift::misc::physical_quantities;
20 using namespace swift::misc::math;
21 
22 namespace swift::misc::aviation
23 {
24 
25  void CAltitude::registerMetadata()
26  {
28  qRegisterMetaType<CAltitude::ReferenceDatum>();
29  qRegisterMetaType<CAltitude::AltitudeType>();
30  }
31 
32  const QVector<CAltitude::MetricTuple> &CAltitude::metricTuples()
33  {
34  static const QVector<MetricTuple> v = {
35  { 300, 1000 }, { 600, 2000 }, { 900, 3000 }, { 1200, 3900 }, { 1200, 4000 }, { 1500, 4900 },
36  { 1500, 5000 }, { 1800, 5900 }, { 1850, 6000 }, { 2100, 6900 }, { 2150, 7000 }, { 2400, 7900 },
37  { 2450, 8000 }, { 2700, 8900 }, { 2750, 9000 }, { 3000, 9800 }, { 3050, 10000 }, { 3300, 10800 },
38  { 3350, 11000 }, { 3600, 11800 }, { 3650, 12000 }, { 3900, 12800 }, { 3950, 13000 }, { 4200, 13800 },
39  { 4250, 14000 }, { 4500, 14800 }, { 4550, 15000 }, { 4800, 15700 }, { 4900, 16000 }, { 5100, 16700 },
40  { 5200, 17000 }, { 5400, 17700 }, { 5500, 18000 }, { 5700, 18700 }, { 5800, 19000 }, { 6000, 19700 },
41  { 6100, 20000 }, { 6300, 20700 }, { 6400, 21000 }, { 6600, 21700 }, { 6700, 22000 }, { 6900, 22600 },
42  { 7000, 23000 }, { 7200, 23600 }, { 7300, 24000 }, { 7500, 24600 }, { 7600, 25000 }, { 7800, 25600 },
43  { 7900, 26000 }, { 8100, 26600 }, { 8250, 27000 }, { 8400, 27600 }, { 8550, 28000 }, { 8600, 28200 },
44  { 8850, 29000 }, { 8900, 29100 }, { 9100, 29900 }, { 9150, 30000 }, { 9200, 30100 }, { 9450, 31000 },
45  { 9500, 31100 }, { 9600, 31500 }, { 9750, 32000 }, { 9800, 32100 }, { 10050, 33000 }, { 10100, 33100 },
46  { 10350, 34000 }, { 10400, 34100 }, { 10600, 34800 }, { 10650, 35000 }, { 10700, 35100 }, { 10950, 36000 },
47  { 11000, 36100 }, { 11100, 36400 }, { 11300, 37000 }, { 11300, 37100 }, { 11600, 38000 }, { 11600, 38100 },
48  { 11900, 39000 }, { 11900, 39100 }, { 12100, 39700 }, { 12200, 40000 }, { 12200, 40100 }, { 12500, 41000 },
49  { 13100, 43000 }, { 13700, 44900 }, { 13700, 45000 }, { 14100, 46300 }, { 14300, 46900 }, { 14350, 47000 },
50  { 14900, 48900 }, { 14950, 49000 }, { 15100, 49500 }, { 15550, 51000 },
51  };
52  return v;
53  }
54 
55  CAltitude::CAltitude(const QString &altitudeAsString, CPqString::SeparatorMode mode)
56  : CLength(0, CLengthUnit::m()), m_datum(MeanSeaLevel)
57  {
58  this->parseFromString(altitudeAsString, mode);
59  }
60 
62  {
63  if (this->isNull()) { return CAltitude::null(); }
64  CAltitude copy(*this);
65  if (!offset.isNull() && !offset.isZeroEpsilonConsidered()) { copy += offset.switchedUnit(this->getUnit()); }
66  return copy;
67  }
68 
69  void CAltitude::addOffset(const CLength &offset) { *this = this->withOffset(offset); }
70 
72  {
73  if (newUnit.isNull() || this->getUnit().isNull() || this->getUnit() == newUnit) { return *this; }
74  CLength::switchUnit(newUnit);
75  return *this;
76  }
77 
79  {
80  if (newUnit.isNull() || this->getUnit().isNull() || this->getUnit() == newUnit) { return *this; }
81  CAltitude copy(*this);
82  copy.switchUnit(newUnit);
83  return copy;
84  }
85 
86  QString CAltitude::convertToQString(bool i18n) const
87  {
88  static const QString n("null");
89  if (this->isNull()) { return n; }
90 
91  if (m_datum == FlightLevel)
92  {
93  const int fl = qRound(this->CLength::value(CLengthUnit::ft()) / 100.0);
94  return QStringLiteral("FL%1").arg(fl);
95  }
96  else { return this->CLength::valueRoundedWithUnit(1, i18n) % (this->isMeanSeaLevel() ? u" MSL" : u" AGL"); }
97  }
98 
99  int CAltitude::comparePropertyByIndex(CPropertyIndexRef index, const CAltitude &compareValue) const
100  {
101  if (index.isMyself()) { return this->compare(compareValue); }
102  return CLength::comparePropertyByIndex(index, compareValue);
103  }
104 
106  {
107  if (m_datum != MeanSeaLevel && m_datum != FlightLevel) { return false; }
108  m_datum = FlightLevel;
109  return true;
110  }
111 
113  {
114  if (m_datum != MeanSeaLevel && m_datum != FlightLevel) { return false; }
115  m_datum = MeanSeaLevel;
116  return true;
117  }
118 
119  void CAltitude::convertToPressureAltitude(const CPressure &seaLevelPressure)
120  {
121  if (m_altitudeType == PressureAltitude) { return; }
122  if (this->isNull()) { return; }
123  const CPressure deltaPressure = standardISASeaLevelPressure() - seaLevelPressure;
124  const double deltaPressureV = deltaPressure.value(CPressureUnit::mbar());
125  const double deltaAltitudeV = deltaPressureV * 30.0; // 30.0 ft per mbar
126  CLength deltaAltitude(deltaAltitudeV, CLengthUnit::ft());
127  *this += deltaAltitude;
128  m_altitudeType = PressureAltitude;
129  }
130 
131  CAltitude CAltitude::toPressureAltitude(const CPressure &seaLevelPressure) const
132  {
133  if (seaLevelPressure.isNull()) { return CAltitude::null(); }
134  if (this->isNull()) { return CAltitude::null(); }
135  CAltitude other(*this);
136  other.convertToPressureAltitude(seaLevelPressure);
137  return other;
138  }
139 
140  void CAltitude::parseFromString(const QString &value)
141  {
142  this->parseFromString(value, CPqString::SeparatorBestGuess);
143  }
144 
145  void CAltitude::parseFromString(const QString &value, CPqString::SeparatorMode mode)
146  {
147  QString v = value.trimmed();
148 
149  // special case FL
150  if (v.contains("FL", Qt::CaseInsensitive) || v.startsWith("F"))
151  {
152  v = char09OnlyString(value);
153  bool ok = false;
154  const int dv = v.toInt(&ok) * 100;
155  const CAltitude a(ok ? dv : 0.0, FlightLevel, ok ? CLengthUnit::ft() : nullptr);
156  *this = a;
157  return;
158  }
159 
160  // special case A (altitude
161  if (v.contains("ALT", Qt::CaseInsensitive) || v.startsWith("A"))
162  {
163  v = char09OnlyString(value);
164  bool ok = false;
165  const int dv = v.toInt(&ok) * 100;
166  const CAltitude a(ok ? dv : 0.0, MeanSeaLevel, ok ? CLengthUnit::ft() : nullptr);
167  *this = a;
168  return;
169  }
170 
171  // normal altitude, AGL/MSL
173  if (v.contains("MSL", Qt::CaseInsensitive))
174  {
175  v = v.replace("MSL", "", Qt::CaseInsensitive).trimmed();
176  rd = MeanSeaLevel;
177  }
178  else if (v.contains("AGL"))
179  {
180  v = v.replace("AGL", "", Qt::CaseInsensitive).trimmed();
181  rd = AboveGround;
182  }
183 
184  const CLength l = CPqString::parse<CLength>(v, mode);
185  *this = CAltitude(l, rd);
186  }
187 
189  {
190  QString v(value.trimmed()); // do not convert case because of units
191  if (v.startsWith("VFR", Qt::CaseInsensitive))
192  {
193  // we set a more or less meaningful value
194  *this = CAltitude(5000, MeanSeaLevel, CLengthUnit::ft());
195  return true;
196  }
197 
198  this->setNull();
199  if (v.length() < 3)
200  {
201  if (msgs) { msgs->push_back(CStatusMessage(this).validationError(u"Altitude empty or too short")); }
202  return false;
203  }
204 
205  if (!fpAltitudeRegExp().globalMatch(v).hasNext())
206  {
207  if (msgs)
208  {
209  msgs->push_back(CStatusMessage(this).validationError(u"Altitude '%1' needs to match format '%2'")
210  << v << fpAltitudeExamples());
211  }
212  return false;
213  }
214 
215  // normalize characters to upper/lower
216  // in same step get numeric value only
217  bool beforeDigit = true;
218  QString numericPart;
219  for (int i = 0; i < v.length(); i++)
220  {
221  const QChar c = v[i];
222  if (c.isDigit())
223  {
224  beforeDigit = false;
225  numericPart.push_back(c);
226  continue;
227  }
228  v[i] = beforeDigit ? c.toUpper() : c.toLower();
229  }
230 
231  if (numericPart.isEmpty())
232  {
233  if (msgs) { msgs->push_back(CStatusMessage(this).validationError(u"Altitude '%1' has no numeric part")); }
234  return false;
235  }
236 
237  bool ok;
238  if (v.startsWith("F", Qt::CaseInsensitive))
239  {
240  this->setUnit(CLengthUnit::ft());
241  this->setValueSameUnit(numericPart.toInt(&ok) * 100);
242  m_datum = FlightLevel;
243  }
244  else if (v.startsWith("S", Qt::CaseInsensitive))
245  {
246  this->setUnit(CLengthUnit::m());
247  this->setValueSameUnit(numericPart.toInt(&ok) * 10);
248  m_datum = FlightLevel;
249  }
250  else if (v.startsWith("A", Qt::CaseInsensitive))
251  {
252  this->setUnit(CLengthUnit::ft());
253  this->setValueSameUnit(numericPart.toInt(&ok) * 100);
254  m_datum = MeanSeaLevel;
255  }
256  else if (v.startsWith("M", Qt::CaseInsensitive))
257  {
258  this->setUnit(CLengthUnit::m());
259  this->setValueSameUnit(numericPart.toInt(&ok) * 10);
260  m_datum = MeanSeaLevel;
261  }
262  else if (v.endsWith(CLengthUnit::m().getSymbol()))
263  {
264  this->setUnit(CLengthUnit::m());
265  this->setValueSameUnit(numericPart.toInt(&ok));
266  m_datum = MeanSeaLevel;
267  }
268  else if (v.endsWith(CLengthUnit::ft().getSymbol()))
269  {
270  this->setUnit(CLengthUnit::ft());
271  this->setValueSameUnit(numericPart.toInt(&ok));
272  m_datum = MeanSeaLevel;
273  }
274  else
275  {
276  if (msgs)
277  {
278  msgs->push_back(CStatusMessage(this).validationError(u"Altitude '%1' needs to match format '%2'")
279  << v << fpAltitudeExamples());
280  }
281  return false;
282  }
283 
284  // further checks
285  const bool valid = this->isValidFpAltitude(msgs);
286  if (!valid) { this->setNull(); }
287  return valid;
288  }
289 
291  {
292  if (this->isNull())
293  {
294  if (msgs) { msgs->push_back(CStatusMessage(this).validationError(u"Altitude NULL value")); }
295  return false;
296  }
297  if (!(this->getReferenceDatum() == FlightLevel || this->getReferenceDatum() == MeanSeaLevel))
298  {
299  if (msgs) { msgs->push_back(CStatusMessage(this).validationError(u"Altitude, must be FL or MSL")); }
300  return false;
301  }
302  if (!(this->getUnit() == CLengthUnit::m() || this->getUnit() == CLengthUnit::ft()))
303  {
304  if (msgs)
305  {
306  msgs->push_back(CStatusMessage(this).validationError(u"Altitude, valid unit must be 'ft' or 'm'"));
307  }
308  return false;
309  }
311  {
312  if (msgs) { msgs->push_back(CStatusMessage(this).validationError(u"Altitude must be positive")); }
313  return false;
314  }
315  if (this->isFlightLevel())
316  {
317  if (!this->isInteger())
318  {
319  if (msgs) { msgs->push_back(CStatusMessage(this).validationError(u"FL needs to be positive integer")); }
320  return false;
321  }
322 
323  if (this->getUnit() == CLengthUnit::ft())
324  {
325  const int flInt = this->valueInteger() / 100; // internally represented as ft: FL10->1000
326  if (flInt < 10 || flInt >= 1000)
327  {
328  if (msgs) { msgs->push_back(CStatusMessage(this).validationError(u"FL range is 10-999")); }
329  return false;
330  }
331  if (!CMathUtils::epsilonZero(fmod(flInt, 5)))
332  {
333  if (msgs) { msgs->push_back(CStatusMessage(this).validationWarning(u"FL should end with 0 or 5")); }
334  }
335  }
336  }
337  return true;
338  }
339 
341  {
342  if (this->isNull()) { return {}; }
343  if (this->isFlightLevel())
344  {
345  if (this->getUnit() == CLengthUnit::m())
346  {
347  int m = this->valueInteger() / 10;
348  return QStringLiteral("S%1").arg(m, 4, 10, QChar('0'));
349  }
350  int ft = this->valueInteger(CLengthUnit::ft()) / 100;
351  return QStringLiteral("FL%1").arg(ft, 3, 10, QChar('0'));
352  }
353 
354  if (this->getUnit() == CLengthUnit::m())
355  {
356  int m = this->valueInteger() / 10;
357  return QStringLiteral("M%1").arg(m, 4, 10, QChar('0'));
358  }
359  int ft = this->valueInteger(CLengthUnit::ft()) / 100;
360  return QStringLiteral("A%1").arg(ft, 3, 10, QChar('0'));
361  }
362 
364  {
365  if (this->isFlightLevel()) { return this->asFpICAOAltitudeString(); }
366  // the M/A formats are not supported by VATSIM, means by other clients
367 
368  // as feed, as none of the other clients
369  const CAltitude a = this->roundedToNearest100ft(false);
370  // return a.valueRoundedWithUnit(CLengthUnit::ft(), 0);
371  return a.valueIntegerAsString(CLengthUnit::ft());
372  }
373 
374  const QRegularExpression &CAltitude::fpAltitudeRegExp()
375  {
376  thread_local const QRegularExpression re(
377  "([Ff][Ll]?\\d{2,3})|([Ss]\\d{2,4})|([Aa]\\d{2,3})|([Mm]\\d{2,4})|(\\d{3,5}(ft|m))");
378  return re;
379  }
380 
381  QString CAltitude::fpAltitudeInfo(const QString &separator)
382  {
383  // remark use arg %01 to avoid clash with numbers, see
384  // https://stackoverflow.com/questions/35517025/qstringarg-with-number-after-placeholder
385  static const QString e(
386  "FL085, F85 flight level in hecto feets%1S0150 metric level in tens of meters%1A055 altitude in hundreds "
387  "of feet%012000ft altitude in ft%1M0610 altitude in tens of meters%016100m altitude in meters");
388  return e.arg(separator);
389  }
390 
392  {
393  static const QString e("FL085, F85, S0150, A055, 1200ft, M0610, 6100m");
394  return e;
395  }
396 
397  CIcons::IconIndex CAltitude::toIcon() const { return CIcons::GeoPosition; }
398 
399  int CAltitude::compare(const CAltitude &otherAltitude) const
400  {
401  if (this->getReferenceDatum() != otherAltitude.getReferenceDatum())
402  {
403  return Compare::compare(static_cast<int>(this->getReferenceDatum()),
404  static_cast<int>(otherAltitude.getReferenceDatum()));
405  }
406  return CLength::compare(*this, otherAltitude);
407  }
408 
410  {
411  // 23453 => 234.53
412  CAltitude a = this->switchedUnit(CLengthUnit::ft());
413  const double ft = a.value(CLengthUnit::ft()) / 100.0;
414  const int ftR = roundDown ? static_cast<int>(floor(ft)) * 100 : qRound(ft) * 100;
415  a.setValueSameUnit(ftR);
416  return a;
417  }
418 
420  {
421  // m/ft/FL
422  for (const MetricTuple &m : metricTuples())
423  {
424  if (std::get<1>(m) == feet) return std::get<0>(m);
425  }
426  return -1;
427  }
428 
430  {
431  // m/ft/FL
432  for (const MetricTuple &m : metricTuples())
433  {
434  if (std::get<0>(m) == metric) return std::get<1>(m);
435  }
436  return -1;
437  }
438 
440  {
441  static const CAltitude null(0, CAltitude::MeanSeaLevel, CLengthUnit::nullUnit());
442  return null;
443  }
444 
445  CLengthUnit CAltitude::defaultUnit() { return CLengthUnit::ft(); }
446 
448  {
449  // Average sea-level pressure is 1013.25mbar or 1013.25hPa
450  static const CPressure standardPressure(CPhysicalQuantitiesConstants::ISASeaLevelPressure());
451  return standardPressure;
452  }
453 } // namespace swift::misc::aviation
IconIndex
Index for each icon, allows to send them via DBus, efficiently store them, etc.
Definition: icons.h:32
Non-owning reference to a CPropertyIndex with a subset of its features.
bool isMyself() const
Myself index, used with nesting.
void push_back(const T &value)
Appends an element at the end of the sequence.
Definition: sequence.h:305
Streamable status message, e.g.
Status messages, e.g. from Core -> GUI.
Altitude as used in aviation, can be AGL or MSL altitude.
Definition: altitude.h:52
QString asFpVatsimAltitudeString() const
As simple VATSIM string, only FLxxx or altitude as ft.
Definition: altitude.cpp:363
CAltitude switchedUnit(const physical_quantities::CLengthUnit &newUnit) const
Value in switched unit.
Definition: altitude.cpp:78
void parseFromString(const QString &value)
Parse value from string.
Definition: altitude.cpp:140
bool isValidFpAltitude(CStatusMessageList *msgs=nullptr) const
Is this a valid FP altitude.
Definition: altitude.cpp:290
static physical_quantities::CLengthUnit defaultUnit()
Default unit for calculations.
Definition: altitude.cpp:445
static QString fpAltitudeExamples()
Examples of FP altitude strings.
Definition: altitude.cpp:391
static int findMetricAltitude(int feet)
Search the corresponding feet <-> metric / metric <-> feet.
Definition: altitude.cpp:419
bool toFlightLevel()
MSL to flightlevel.
Definition: altitude.cpp:105
int comparePropertyByIndex(CPropertyIndexRef index, const CAltitude &compareValue) const
Set property by index.
Definition: altitude.cpp:99
QString asFpICAOAltitudeString() const
Altitude string (official version)
Definition: altitude.cpp:340
bool parseFromFpAltitudeString(const QString &value, CStatusMessageList *msgs=nullptr)
Parse from FP altitude string.
Definition: altitude.cpp:188
bool isMeanSeaLevel() const
MSL Mean sea level?
Definition: altitude.h:139
CAltitude withOffset(const CLength &offset) const
Altitude with offset.
Definition: altitude.cpp:61
static int findAltitudeForMetricAltitude(int metric)
Search the corresponding feet <-> metric / metric <-> feet.
Definition: altitude.cpp:429
QString convertToQString(bool i18n=false) const
Cast as QString.
Definition: altitude.cpp:86
bool isFlightLevel() const
Flight level?
Definition: altitude.h:142
CAltitude & switchUnit(const physical_quantities::CLengthUnit &newUnit)
Value in switched unit.
Definition: altitude.cpp:71
static QString fpAltitudeInfo(const QString &separator=", ")
Info for FP altitude strings.
Definition: altitude.cpp:381
int compare(const CAltitude &otherAltitude) const
Return negative, zero, or positive if a is less than, equal to, or greater than b.
Definition: altitude.cpp:399
CAltitude()
Default constructor: 0m Altitude MSL.
Definition: altitude.h:87
@ PressureAltitude
Altitude above the standard datum plane.
Definition: altitude.h:82
void convertToPressureAltitude(const physical_quantities::CPressure &seaLevelPressure)
Converts this to pressure altitude. Requires the current barometric pressure at MSL.
Definition: altitude.cpp:119
CAltitude roundedToNearest100ft(bool roundDown) const
Round to the nearest 100ft, like needed for China and Russia.
Definition: altitude.cpp:409
void addOffset(const CLength &offset)
Add offset value.
Definition: altitude.cpp:69
ReferenceDatum
Enum type to distinguish between MSL and AGL.
Definition: altitude.h:73
static const QRegularExpression & fpAltitudeRegExp()
Checking FP altitude strings like "A20", "FL100".
Definition: altitude.cpp:374
static const CAltitude & null()
Null altitude (MSL)
Definition: altitude.cpp:439
std::tuple< int, int > MetricTuple
Metric tuple.
Definition: altitude.h:58
swift::misc::CIcons::IconIndex toIcon() const
As icon, not implemented by all classes.
Definition: altitude.cpp:397
ReferenceDatum getReferenceDatum() const
Get reference datum (MSL or AGL)
Definition: altitude.h:145
CAltitude toPressureAltitude(const physical_quantities::CPressure &seaLevelPressure) const
Returns the altitude converted to pressure altitude. Requires the current barometric pressure at MSL.
Definition: altitude.cpp:131
static const physical_quantities::CPressure & standardISASeaLevelPressure()
Standard pressure 1013.25mbar/hPa.
Definition: altitude.cpp:447
bool toMeanSeaLevel()
Flightlevel to MSL.
Definition: altitude.cpp:112
static void registerMetadata()
Register metadata.
Definition: mixinmetatype.h:56
Physical unit length (length)
Definition: length.h:18
Specialized class for distance units (meter, foot, nautical miles).
Definition: units.h:95
void setValueSameUnit(double value)
Change value without changing unit.
PQ switchedUnit(const MU &newUnit) const
Return copy with switched unit.
double value(MU unit) const
Value in given unit.
QString valueIntegerAsString(MU unit) const
As integer value.
void setUnit(const CLengthUnit &unit)
Simply set unit, do no calclulate conversion.
bool isZeroEpsilonConsidered() const
Quantity value <= epsilon.
SeparatorMode
Number separators / group separators.
Definition: pqstring.h:34
QString char09OnlyString(const QString &string)
String only with 0-9.
Definition: stringutils.h:192