mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-03-31 12:15:36 -04:00
Added optional scrollbar to be able to scroll back through waterfall.
When scrolling is enabled:
Can adjust power scale for complete waterfall, not just future spectra.
Waterfall time axis can use local time or UTC.
Waterfall not lost when resizing window.
Can now zoom when device is stopped.
Added Min averaging type.
Added button to load spectrum from .csv file.
Add button to save spectrum/waterfall to .png or jpg.
Changed show all controls button to a combobox with choices of Min/Std/All (Minimum/Standard/All).
Changed some buttons in spectrum GUI from QPushButton to QToolButton so their size matches the others.
Fix spectrum from displaying a mixture of old and new spectrums (m_currentSpectrum was a pointer to SpectrumVis buffer).
Added M1 and M2 memories to allow display of reference spectra.
Added math operations to allow spectrum to be difference of current spectrum and either memory or a moving average.
Fixed measurement counts, so they are performed once per spectrum, not on displayed spectra.
Added spectrum mask measurement, to check when a spectrum exceeds mask held in M1 or M2.
Optionally display power/frequency under cursor in status line.
Optionally display peak power/frequency in status line.
Fix incorrect nyquist sample replication, when zoom used.
Fix cursor not changing from resize to pointer when moving over spectrum measurements window.
Add spectrum colour setting.
545 lines
18 KiB
C++
545 lines
18 KiB
C++
///////////////////////////////////////////////////////////////////////////////////
|
|
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
|
|
// written by Christian Daniel //
|
|
// Copyright (C) 2015-2016, 2018-2019 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
|
|
// Copyright (C) 2020-2026 Jon Beniston, M7RCE <jon@beniston.com> //
|
|
// //
|
|
// This program is free software; you can redistribute it and/or modify //
|
|
// it under the terms of the GNU General Public License as published by //
|
|
// the Free Software Foundation as version 3 of the License, or //
|
|
// (at your option) any later version. //
|
|
// //
|
|
// This program is distributed in the hope that it will be useful, //
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
|
// GNU General Public License V3 for more details. //
|
|
// //
|
|
// You should have received a copy of the GNU General Public License //
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#ifndef INCLUDE_UNITS_H
|
|
#define INCLUDE_UNITS_H
|
|
|
|
#include <cmath>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#include <QRegularExpression>
|
|
#include <QDebug>
|
|
|
|
#include "export.h"
|
|
|
|
// Unit conversions
|
|
class SDRBASE_API Units
|
|
{
|
|
public:
|
|
|
|
static inline float feetToMetres(float feet)
|
|
{
|
|
return feet * 0.3048f;
|
|
}
|
|
|
|
static inline int feetToIntegerMetres(float feet)
|
|
{
|
|
return (int)std::round(feetToMetres(feet));
|
|
}
|
|
|
|
static inline float metresToFeet(float metres)
|
|
{
|
|
return metres * 3.28084f;
|
|
}
|
|
|
|
static inline int metresToIntegerFeet(float metres)
|
|
{
|
|
return (int)std::round(metresToFeet(metres));
|
|
}
|
|
|
|
static inline float nauticalMilesToMetres(float nauticalMiles)
|
|
{
|
|
return nauticalMiles * 1855.0f;
|
|
}
|
|
|
|
static inline int nauticalMilesToIntegerMetres(float nauticalMiles)
|
|
{
|
|
return (int)std::round(nauticalMilesToMetres(nauticalMiles));
|
|
}
|
|
|
|
static float knotsToKPH(float knots)
|
|
{
|
|
return knots * 1.852f;
|
|
}
|
|
|
|
static int knotsToIntegerKPH(float knots)
|
|
{
|
|
return (int)std::round(knotsToKPH(knots));
|
|
}
|
|
|
|
static float knotsToMPH(float knots)
|
|
{
|
|
return knots * 1.15078f;
|
|
}
|
|
|
|
static int knotsToIntegerMPH(float knots)
|
|
{
|
|
return (int)std::round(knotsToMPH(knots));
|
|
}
|
|
|
|
static float knotsToMetresPerSecond(float knots)
|
|
{
|
|
return knots * 0.514444f;
|
|
}
|
|
|
|
static float kmpsToKPH(float kps)
|
|
{
|
|
return kps * (60.0 * 60.0);
|
|
}
|
|
|
|
static int kmpsToIntegerKPH(float kps)
|
|
{
|
|
return (int)std::round(kmpsToKPH(kps));
|
|
}
|
|
|
|
static float feetPerMinToMetresPerSecond(float fpm)
|
|
{
|
|
return fpm * 0.00508f;
|
|
}
|
|
|
|
static int feetPerMinToIntegerMetresPerSecond(float fpm)
|
|
{
|
|
return (int)std::round(feetPerMinToMetresPerSecond(fpm));
|
|
}
|
|
|
|
template <class T>
|
|
static T degreesToRadians(T degrees)
|
|
{
|
|
return degrees * ((T)M_PI) / 180.0f;
|
|
}
|
|
|
|
template <class T>
|
|
static T radiansToDegrees(T radians)
|
|
{
|
|
return radians * 180.0f / ((T)M_PI);
|
|
}
|
|
|
|
static float fahrenheitToCelsius(float fahrenheit)
|
|
{
|
|
return (fahrenheit - 32.0f) * (5.0f/9.0f);
|
|
}
|
|
|
|
static float celsiusToKelvin(float celsius)
|
|
{
|
|
return celsius + 273.15f;
|
|
}
|
|
|
|
static float inchesToMilimetres(float inches)
|
|
{
|
|
return inches * 25.4f;
|
|
}
|
|
|
|
static float milesToKilometres(float miles)
|
|
{
|
|
return miles * 1.60934f;
|
|
}
|
|
|
|
static float degreesMinutesSecondsToDecimal(int degrees, int minutes, float seconds)
|
|
{
|
|
float dec = std::abs(degrees) + minutes * 1.0f/60.0f + seconds * 1.0f/(60.0f*60.0f);
|
|
if (degrees < 0)
|
|
return -dec;
|
|
else
|
|
return dec;
|
|
}
|
|
|
|
static float hoursMinutesSecondsToDecimal(int hours, int minutes, float seconds)
|
|
{
|
|
return hours + minutes * 1.0f/60.0f + seconds * 1.0f/(60.0f*60.0f);
|
|
}
|
|
|
|
// Also supports decimal degrees
|
|
static bool degreeMinuteAndSecondsToDecimalDegrees(const QString& string, float& degrees)
|
|
{
|
|
QRegularExpression decimal(QRegularExpression::anchoredPattern("(-?[0-9]+(\\.[0-9]+)?)"));
|
|
QRegularExpressionMatch match;
|
|
match = decimal.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
degrees = match.capturedTexts()[1].toFloat();
|
|
return true;
|
|
}
|
|
|
|
QRegularExpression dms(QRegularExpression::anchoredPattern(QString("(-)?([0-9]+)[%1d](([0-9]+)['m](([0-9]+(\\.[0-9]+)?)[\"s])?)?").arg(QChar(0xb0))));
|
|
match = dms.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
float d = 0.0f;
|
|
bool neg = false;
|
|
if (dms.captureCount() >= 1) {
|
|
neg = match.capturedTexts()[1] == "-";
|
|
}
|
|
if ((dms.captureCount() >= 3) && (match.capturedTexts().size() >= 3)) {
|
|
d = match.capturedTexts()[2].toFloat();
|
|
}
|
|
float m = 0.0f;
|
|
if ((dms.captureCount() >= 5) && (match.capturedTexts().size() >= 5)) {
|
|
m = match.capturedTexts()[4].toFloat();
|
|
}
|
|
float s = 0.0f;
|
|
if ((dms.captureCount() >= 7) && (match.capturedTexts().size() >= 7)) {
|
|
s = match.capturedTexts()[6].toFloat();
|
|
}
|
|
degrees = d + m/60.0 + s/(60.0*60.0);
|
|
if (neg) {
|
|
degrees = -degrees;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static QString decimalDegreesToDegreeMinutesAndSeconds(float decimal, int secondsFieldWidth=5)
|
|
{
|
|
double v, d, m, s;
|
|
int neg;
|
|
|
|
v = decimal;
|
|
neg = v < 0.0f;
|
|
v = fabs(v);
|
|
d = floor(v);
|
|
v -= d;
|
|
v *= 60.0;
|
|
m = floor(v);
|
|
v -= m;
|
|
v *= 60.0;
|
|
s = v;
|
|
return QString("%1%2%3%4'%5\"").arg(neg ? "-" : "").arg((int)d).arg(QChar(0xb0)).arg((int)m, 2, 10, QChar('0')).arg(s, secondsFieldWidth, 'f', 2, QChar('0'));
|
|
}
|
|
|
|
static QString decimalDegreesToDegreesAndMinutes(float decimal)
|
|
{
|
|
double v, d, m;
|
|
int neg;
|
|
|
|
v = decimal;
|
|
neg = v < 0.0f;
|
|
v = fabs(v);
|
|
d = floor(v);
|
|
v -= d;
|
|
v *= 60.0;
|
|
m = round(v);
|
|
if (m == 60)
|
|
{
|
|
if (neg) {
|
|
d--;
|
|
} else {
|
|
d++;
|
|
}
|
|
m = 0;
|
|
}
|
|
return QString("%1%2%3%4'").arg(neg ? "-" : "").arg((int)d).arg(QChar(0xb0)).arg((int)m, 2, 10, QChar('0'));
|
|
}
|
|
|
|
static QString decimalDegreesToDegrees(float decimal)
|
|
{
|
|
double v, d;
|
|
int neg;
|
|
|
|
v = decimal;
|
|
neg = v < 0.0f;
|
|
v = fabs(v);
|
|
d = round(v);
|
|
return QString("%1%2%3").arg(neg ? "-" : "").arg((int)d).arg(QChar(0xb0));
|
|
}
|
|
|
|
static QString decimalHoursToHoursMinutesAndSeconds(float decimal, int precision=2)
|
|
{
|
|
double v, h, m, s;
|
|
|
|
v = decimal;
|
|
v = fabs(v);
|
|
h = floor(v);
|
|
v -= h;
|
|
v *= 60.0;
|
|
m = floor(v);
|
|
v -= m;
|
|
v *= 60.0;
|
|
s = v;
|
|
return QString("%1h%2m%3s").arg((int)h).arg((int)m, 2, 10, QChar('0')).arg(s, 2, 'f', precision, QChar('0'));
|
|
}
|
|
|
|
// Try to convert a string to latitude and longitude. Returns false if not recognised format.
|
|
// https://en.wikipedia.org/wiki/ISO_6709 specifies a standard syntax
|
|
// We support both decimal and DMS formats
|
|
static bool stringToLatitudeAndLongitude(const QString& string, float& latitude, float& longitude, bool exact=true)
|
|
{
|
|
QRegularExpressionMatch match;
|
|
|
|
QString decimalPattern = "(-?[0-9]+(\\.[0-9]+)?) *[ ,] *(-?[0-9]+(\\.[0-9]+)?)";
|
|
if (exact) {
|
|
decimalPattern = QRegularExpression::anchoredPattern(decimalPattern);
|
|
}
|
|
QRegularExpression decimal(decimalPattern);
|
|
match = decimal.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
latitude = match.capturedTexts()[1].toFloat();
|
|
longitude = match.capturedTexts()[3].toFloat();
|
|
return true;
|
|
}
|
|
|
|
QString dmsPattern = QString("([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([NS]) *,? *([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([EW])").arg(QChar(0xb0));
|
|
if (exact) {
|
|
dmsPattern = QRegularExpression::anchoredPattern(dmsPattern);
|
|
}
|
|
QRegularExpression dms(dmsPattern);
|
|
match = dms.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
float latD = match.capturedTexts()[1].toFloat();
|
|
float latM = match.capturedTexts()[2].toFloat();
|
|
float latS = match.capturedTexts()[3].toFloat();
|
|
bool north = match.capturedTexts()[5] == "N";
|
|
float lonD = match.capturedTexts()[6].toFloat();
|
|
float lonM = match.capturedTexts()[7].toFloat();
|
|
float lonS = match.capturedTexts()[8].toFloat();
|
|
bool east = match.capturedTexts()[10] == "E";
|
|
latitude = latD + latM/60.0 + latS/(60.0*60.0);
|
|
if (!north)
|
|
latitude = -latitude;
|
|
longitude = lonD + lonM/60.0 + lonS/(60.0*60.0);
|
|
if (!east)
|
|
longitude = -longitude;
|
|
return true;
|
|
}
|
|
|
|
QString dms2Pattern = "([0-9]+)([NS])([0-9]{2})([0-9]{2}) *,?([0-9]+)([EW])([0-9]{2})([0-9]{2})";
|
|
if (exact) {
|
|
dms2Pattern = QRegularExpression::anchoredPattern(dms2Pattern);
|
|
}
|
|
QRegularExpression dms2(dms2Pattern);
|
|
match = dms2.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
float latD = match.capturedTexts()[1].toFloat();
|
|
bool north = match.capturedTexts()[2] == "N";
|
|
float latM = match.capturedTexts()[3].toFloat();
|
|
float latS = match.capturedTexts()[4].toFloat();
|
|
float lonD = match.capturedTexts()[5].toFloat();
|
|
bool east = match.capturedTexts()[6] == "E";
|
|
float lonM = match.capturedTexts()[7].toFloat();
|
|
float lonS = match.capturedTexts()[8].toFloat();
|
|
latitude = latD + latM/60.0 + latS/(60.0*60.0);
|
|
if (!north)
|
|
latitude = -latitude;
|
|
longitude = lonD + lonM/60.0 + lonS/(60.0*60.0);
|
|
if (!east)
|
|
longitude = -longitude;
|
|
return true;
|
|
}
|
|
|
|
// 512255.5900N 0024400.6105W as used on aviation charts
|
|
QString dms3Pattern = "(\\d{2})(\\d{2})((\\d{2})(\\.\\d+)?)([NS]) *,?(\\d{3})(\\d{2})((\\d{2})(\\.\\d+)?)([EW])";
|
|
if (exact) {
|
|
dms3Pattern = QRegularExpression::anchoredPattern(dms3Pattern);
|
|
}
|
|
QRegularExpression dms3(dms3Pattern);
|
|
match = dms3.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
float latD = match.capturedTexts()[1].toFloat();
|
|
float latM = match.capturedTexts()[2].toFloat();
|
|
float latS = match.capturedTexts()[3].toFloat();
|
|
bool north = match.capturedTexts()[6] == "N";
|
|
float lonD = match.capturedTexts()[7].toFloat();
|
|
float lonM = match.capturedTexts()[8].toFloat();
|
|
float lonS = match.capturedTexts()[9].toFloat();
|
|
bool east = match.capturedTexts()[12] == "E";
|
|
latitude = latD + latM/60.0 + latS/(60.0*60.0);
|
|
if (!north)
|
|
latitude = -latitude;
|
|
longitude = lonD + lonM/60.0 + lonS/(60.0*60.0);
|
|
if (!east)
|
|
longitude = -longitude;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Try to convert a string to Right Ascension (RA) and Declination. Returns false if not recognised format.
|
|
// This supports HMS/DMS or decimal.
|
|
// E.g.:
|
|
// 12 05 12.23 +17 06 21.0
|
|
// 12:05:12.23 -17:06:21.0
|
|
// 12h05m12.23s +17d06m21.0s
|
|
// 107.1324 -34.233
|
|
static bool stringToRADec(const QString& string, float& ra, float& dec)
|
|
{
|
|
QRegularExpressionMatch match;
|
|
QRegularExpression dms(QRegularExpression::anchoredPattern("([0-9]+)[ :h]([0-9]+)[ :m]([0-9]+(\\.[0-9]+)?)s? *,? *([+-]?[0-9]+)[ :d]([0-9]+)[ :m]([0-9]+(\\.[0-9]+)?)s?"));
|
|
match = dms.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
int raHours = match.capturedTexts()[1].toInt();
|
|
int raMins = match.capturedTexts()[2].toInt();
|
|
float raSecs = match.capturedTexts()[3].toFloat();
|
|
ra = raHours + raMins / 60.0f + raSecs / (60.0f * 60.0f);
|
|
int decDegs = match.capturedTexts()[5].toInt();
|
|
int decMins = match.capturedTexts()[6].toInt();
|
|
float decSecs = match.capturedTexts()[7].toFloat();
|
|
bool neg = decDegs < 0;
|
|
dec = abs(decDegs) + decMins / 60.0f + decSecs / (60.0f * 60.0f);
|
|
if (neg) {
|
|
dec = -dec;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QRegularExpression decimal(QRegularExpression::anchoredPattern("([0-9]+(\\.[0-9]+)?) *,? *([+-]?[0-9]+(\\.[0-9]+)?)"));
|
|
match = decimal.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
ra = match.capturedTexts()[1].toFloat();
|
|
dec = match.capturedTexts()[3].toFloat();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static double raToDecimal(const QString& value)
|
|
{
|
|
QRegularExpression decimal(QRegularExpression::anchoredPattern("^([0-9]+(\\.[0-9]+)?)"));
|
|
QRegularExpression hms(QRegularExpression::anchoredPattern("^([0-9]+)[ h]([0-9]+)[ m]([0-9]+(\\.[0-9]+)?)s?"));
|
|
QRegularExpressionMatch decimalMatch = decimal.match(value);
|
|
QRegularExpressionMatch hmsMatch = hms.match(value);
|
|
|
|
if (decimalMatch.hasMatch())
|
|
return decimalMatch.capturedTexts()[0].toDouble();
|
|
else if (hmsMatch.hasMatch())
|
|
{
|
|
return Units::hoursMinutesSecondsToDecimal(
|
|
hmsMatch.capturedTexts()[1].toDouble(),
|
|
hmsMatch.capturedTexts()[2].toDouble(),
|
|
hmsMatch.capturedTexts()[3].toDouble());
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
static double decToDecimal(const QString& value)
|
|
{
|
|
QRegularExpression decimal(QRegularExpression::anchoredPattern("^(-?[0-9]+(\\.[0-9]+)?)"));
|
|
QRegularExpression dms(QRegularExpression::anchoredPattern(QString("^(-?[0-9]+)[ %1d]([0-9]+)[ 'm]([0-9]+(\\.[0-9]+)?)[\"s]?").arg(QChar(0xb0))));
|
|
QRegularExpressionMatch decimalMatch = decimal.match(value);
|
|
QRegularExpressionMatch dmsMatch = dms.match(value);
|
|
|
|
if (decimalMatch.hasMatch())
|
|
return decimalMatch.capturedTexts()[0].toDouble();
|
|
else if (dmsMatch.hasMatch())
|
|
{
|
|
return Units::degreesMinutesSecondsToDecimal(
|
|
dmsMatch.capturedTexts()[1].toDouble(),
|
|
dmsMatch.capturedTexts()[2].toDouble(),
|
|
dmsMatch.capturedTexts()[3].toDouble());
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
static float solarFluxUnitsToJansky(float sfu)
|
|
{
|
|
return sfu * 10000.0f;
|
|
}
|
|
|
|
template <class T>
|
|
static T solarFluxUnitsToWattsPerMetrePerHertz(T sfu)
|
|
{
|
|
return sfu * 1e-22f;
|
|
}
|
|
|
|
template <class T>
|
|
static T wattsPerMetrePerHertzToSolarFluxUnits(T w)
|
|
{
|
|
return w / 1e-22f;
|
|
}
|
|
|
|
template <class T>
|
|
static T wattsPerMetrePerHertzToJansky(T w)
|
|
{
|
|
return w / 1e-26f;
|
|
}
|
|
|
|
template <class T>
|
|
static T noiseFigureToNoiseTemp(T nfdB, T refTempK=T(290.0))
|
|
{
|
|
return refTempK * (std::pow(T(10.0), nfdB/T(10.0)) - T(1.0));
|
|
}
|
|
|
|
template <class T>
|
|
static T noiseTempToNoiseFigureTo(T tempK, T refTempK=T(290.0))
|
|
{
|
|
return T(10.0) * std::log10(tempK/refTempK+T(1.0));
|
|
}
|
|
|
|
// Convert number using engineering units to double precision. E.g: 10.3M 0.3m -5k 3.4
|
|
static double engUnitsToDouble(const QString& string, double defaultValue = 0.0, bool *ok = nullptr)
|
|
{
|
|
static const QChar micro(0xb5);
|
|
static const QString pattern = QString(R"((-?)([\d]+)?(\.[\d]+)?([n%1umkMG]?))").arg(micro);
|
|
static const QMap<QChar, double> map = {
|
|
{'n', 1e-9},
|
|
{micro, 1e-6},
|
|
{'u', 1e-6},
|
|
{'m', 1e-3},
|
|
{'k', 1e3},
|
|
{'M', 1e6},
|
|
{'G', 1e9}
|
|
};
|
|
|
|
QRegularExpression re(QRegularExpression::anchoredPattern(pattern));
|
|
QRegularExpressionMatch match;
|
|
|
|
match = re.match(string);
|
|
if (match.hasMatch())
|
|
{
|
|
double value = (match.capturedTexts()[1] + match.capturedTexts()[2] + match.capturedTexts()[3]).toDouble();
|
|
QString units = match.capturedTexts()[4];
|
|
if (!units.isEmpty()) {
|
|
value = value * map.value(units[0]);
|
|
}
|
|
if (ok) {
|
|
*ok = true;
|
|
}
|
|
return value;
|
|
}
|
|
else
|
|
{
|
|
if (ok) {
|
|
*ok = false;
|
|
}
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
// Convert number using engineering units to positive integer
|
|
static int engUnitsToPosInt(const QString& string, int defaultValue = 0, bool *ok = nullptr)
|
|
{
|
|
bool doubleOk;
|
|
double doubleValue = Units::engUnitsToDouble(string, 0.0, &doubleOk);
|
|
|
|
if (doubleOk && (doubleValue >= 0.0) && (trunc(doubleValue) == doubleValue))
|
|
{
|
|
if (ok) {
|
|
*ok = true;
|
|
}
|
|
return (int) doubleValue;
|
|
}
|
|
else
|
|
{
|
|
if (ok) {
|
|
*ok = false;
|
|
}
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
#endif // INCLUDE_UNITS_H
|