1295 lines
47 KiB
C++
1295 lines
47 KiB
C++
#include "MainWindow.h"
|
|
|
|
#include "LocationParser.h"
|
|
#include "GlobeWidget.h"
|
|
#include "MapWidget.h"
|
|
#include "RinexNavigation.h"
|
|
#include "SerialReader.h"
|
|
|
|
#include <QAbstractItemView>
|
|
#include <QAbstractTableModel>
|
|
#include <QCheckBox>
|
|
#include <QComboBox>
|
|
#include <QDateTime>
|
|
#include <QDir>
|
|
#include <QDoubleSpinBox>
|
|
#include <QFormLayout>
|
|
#include <QFile>
|
|
#include <QFileDialog>
|
|
#include <QGridLayout>
|
|
#include <QGroupBox>
|
|
#include <QHeaderView>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QPlainTextEdit>
|
|
#include <QScrollArea>
|
|
#include <QPushButton>
|
|
#include <QRegularExpression>
|
|
#include <QSpinBox>
|
|
#include <QSplitter>
|
|
#include <QStatusBar>
|
|
#include <QStyle>
|
|
#include <QTabWidget>
|
|
#include <QTableView>
|
|
#include <QToolButton>
|
|
#include <QTimer>
|
|
#include <QVBoxLayout>
|
|
#include <QPixmap>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
|
|
constexpr int LocationTimeoutMs = 10000;
|
|
constexpr int UiRefreshIntervalMs = 100;
|
|
constexpr int MaxPendingLogLines = 500;
|
|
constexpr double Pi = 3.14159265358979323846;
|
|
constexpr double EarthRadiusMeters = 6371008.8;
|
|
|
|
struct LocationAnalysis {
|
|
bool valid = false;
|
|
double averageLat = 0.0;
|
|
double averageLon = 0.0;
|
|
double lastDeviationMeters = 0.0;
|
|
double meanDeviationMeters = 0.0;
|
|
double rmsScatterMeters = 0.0;
|
|
double maxDeviationMeters = 0.0;
|
|
};
|
|
|
|
double degreesToRadians(double degrees)
|
|
{
|
|
return degrees * Pi / 180.0;
|
|
}
|
|
|
|
double radiansToDegrees(double radians)
|
|
{
|
|
return radians * 180.0 / Pi;
|
|
}
|
|
|
|
double distanceMeters(double lat1, double lon1, double lat2, double lon2)
|
|
{
|
|
const double phi1 = degreesToRadians(lat1);
|
|
const double phi2 = degreesToRadians(lat2);
|
|
const double dPhi = degreesToRadians(lat2 - lat1);
|
|
const double dLambda = degreesToRadians(lon2 - lon1);
|
|
const double sinHalfPhi = std::sin(dPhi / 2.0);
|
|
const double sinHalfLambda = std::sin(dLambda / 2.0);
|
|
const double a = sinHalfPhi * sinHalfPhi
|
|
+ std::cos(phi1) * std::cos(phi2) * sinHalfLambda * sinHalfLambda;
|
|
const double c = 2.0 * std::atan2(std::sqrt(a), std::sqrt(std::max(0.0, 1.0 - a)));
|
|
return EarthRadiusMeters * c;
|
|
}
|
|
|
|
std::optional<double> parseCoordinateText(QString text, double minimum, double maximum)
|
|
{
|
|
text = text.trimmed();
|
|
text.replace(',', '.');
|
|
bool ok = false;
|
|
const double value = text.toDouble(&ok);
|
|
if (!ok || !std::isfinite(value) || value < minimum || value > maximum) {
|
|
return std::nullopt;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
double calculateCep50(const QVector<TelemetryPoint> &points,
|
|
double referenceLatitude, double referenceLongitude)
|
|
{
|
|
QVector<double> radialErrors;
|
|
radialErrors.reserve(points.size());
|
|
for (const TelemetryPoint &point : points) {
|
|
radialErrors.push_back(distanceMeters(referenceLatitude, referenceLongitude,
|
|
point.latitude, point.longitude));
|
|
}
|
|
std::sort(radialErrors.begin(), radialErrors.end());
|
|
|
|
const int middle = radialErrors.size() / 2;
|
|
if (radialErrors.size() % 2 == 1) {
|
|
return radialErrors[middle];
|
|
}
|
|
return (radialErrors[middle - 1] + radialErrors[middle]) / 2.0;
|
|
}
|
|
|
|
LocationAnalysis analyzeLocations(const QVector<TelemetryPoint> &points)
|
|
{
|
|
LocationAnalysis analysis;
|
|
if (points.isEmpty()) {
|
|
return analysis;
|
|
}
|
|
|
|
double x = 0.0;
|
|
double y = 0.0;
|
|
double z = 0.0;
|
|
for (const TelemetryPoint &point : points) {
|
|
const double lat = degreesToRadians(point.latitude);
|
|
const double lon = degreesToRadians(point.longitude);
|
|
const double cosLat = std::cos(lat);
|
|
x += cosLat * std::cos(lon);
|
|
y += cosLat * std::sin(lon);
|
|
z += std::sin(lat);
|
|
}
|
|
|
|
x /= points.size();
|
|
y /= points.size();
|
|
z /= points.size();
|
|
|
|
const double horizontal = std::sqrt(x * x + y * y);
|
|
analysis.averageLat = radiansToDegrees(std::atan2(z, horizontal));
|
|
analysis.averageLon = radiansToDegrees(std::atan2(y, x));
|
|
analysis.valid = true;
|
|
|
|
double distanceSum = 0.0;
|
|
double squaredDistanceSum = 0.0;
|
|
for (const TelemetryPoint &point : points) {
|
|
const double distance = distanceMeters(point.latitude, point.longitude,
|
|
analysis.averageLat, analysis.averageLon);
|
|
distanceSum += distance;
|
|
squaredDistanceSum += distance * distance;
|
|
analysis.maxDeviationMeters = std::max(analysis.maxDeviationMeters, distance);
|
|
}
|
|
|
|
analysis.meanDeviationMeters = distanceSum / points.size();
|
|
analysis.rmsScatterMeters = std::sqrt(squaredDistanceSum / points.size());
|
|
const TelemetryPoint &last = points.last();
|
|
analysis.lastDeviationMeters = distanceMeters(last.latitude, last.longitude,
|
|
analysis.averageLat, analysis.averageLon);
|
|
return analysis;
|
|
}
|
|
|
|
QVector<TelemetryPoint> preferredAnalysisPoints(const QVector<TelemetryPoint> &points)
|
|
{
|
|
QVector<TelemetryPoint> corrected;
|
|
for (const TelemetryPoint &point : points) {
|
|
if (point.source == LocationSource::ImuCorrected) {
|
|
corrected.push_back(point);
|
|
}
|
|
}
|
|
return corrected.isEmpty() ? points : corrected;
|
|
}
|
|
|
|
QString metersText(double meters)
|
|
{
|
|
if (!std::isfinite(meters)) {
|
|
return QStringLiteral("-");
|
|
}
|
|
|
|
const int precision = meters < 10.0 ? 2 : (meters < 100.0 ? 1 : 0);
|
|
return QStringLiteral("%1 m").arg(meters, 0, 'f', precision);
|
|
}
|
|
|
|
QString optionalDoubleText(const std::optional<double> &value, int precision)
|
|
{
|
|
if (!value.has_value()) {
|
|
return QStringLiteral("-");
|
|
}
|
|
return QString::number(*value, 'f', precision);
|
|
}
|
|
|
|
QString preciseCoordinateText(double value)
|
|
{
|
|
return QString::number(value, 'f', 15);
|
|
}
|
|
|
|
QString coordinateText(double value, const QString &sourceText)
|
|
{
|
|
return sourceText.isEmpty() ? preciseCoordinateText(value) : sourceText;
|
|
}
|
|
|
|
QString optionalIntText(const std::optional<int> &value)
|
|
{
|
|
if (!value.has_value()) {
|
|
return QStringLiteral("-");
|
|
}
|
|
return QString::number(*value);
|
|
}
|
|
|
|
QString optionalDopText(const std::optional<double> &value)
|
|
{
|
|
if (!value.has_value() || !std::isfinite(*value)) {
|
|
return QStringLiteral("-");
|
|
}
|
|
return QString::number(*value, 'f', 3);
|
|
}
|
|
|
|
QStringList detectedSerialPorts()
|
|
{
|
|
const QDir deviceDirectory(QStringLiteral("/dev"));
|
|
QStringList patterns;
|
|
#ifdef Q_OS_MACOS
|
|
// macOS exposes USB UARTs as paired callout (cu.*) and terminal (tty.*)
|
|
// devices. Prefer cu.* because it does not wait for carrier detect.
|
|
patterns = {
|
|
QStringLiteral("cu.usb*"), QStringLiteral("cu.wchusbserial*"),
|
|
QStringLiteral("cu.SLAB_USBtoUART*"), QStringLiteral("cu.*USB*"),
|
|
QStringLiteral("tty.usb*"), QStringLiteral("tty.wchusbserial*"),
|
|
QStringLiteral("tty.SLAB_USBtoUART*"), QStringLiteral("tty.*USB*")};
|
|
#else
|
|
patterns = {QStringLiteral("ttyUSB*"), QStringLiteral("ttyACM*")};
|
|
#endif
|
|
|
|
QStringList ports;
|
|
for (const QString &pattern : patterns) {
|
|
const QStringList names = deviceDirectory.entryList(
|
|
{pattern}, QDir::System | QDir::Files, QDir::Name);
|
|
for (const QString &name : names) {
|
|
const QString path = QStringLiteral("/dev/") + name;
|
|
if (!ports.contains(path)) {
|
|
ports.push_back(path);
|
|
}
|
|
}
|
|
}
|
|
return ports;
|
|
}
|
|
|
|
QString fallbackSerialPort()
|
|
{
|
|
#ifdef Q_OS_MACOS
|
|
return QStringLiteral("/dev/cu.usbserial");
|
|
#else
|
|
return QStringLiteral("/dev/ttyUSB2");
|
|
#endif
|
|
}
|
|
|
|
void mergeDopValues(DopValues &target, const DopValues &source)
|
|
{
|
|
if (source.gdop.has_value()) {
|
|
target.gdop = source.gdop;
|
|
}
|
|
if (source.pdop.has_value()) {
|
|
target.pdop = source.pdop;
|
|
}
|
|
if (source.hdop.has_value()) {
|
|
target.hdop = source.hdop;
|
|
}
|
|
if (source.vdop.has_value()) {
|
|
target.vdop = source.vdop;
|
|
}
|
|
if (source.tdop.has_value()) {
|
|
target.tdop = source.tdop;
|
|
}
|
|
}
|
|
|
|
QString satelliteQualityText(const std::optional<int> &satelliteCount)
|
|
{
|
|
if (!satelliteCount.has_value()) {
|
|
return QStringLiteral("Uydu bekleniyor");
|
|
}
|
|
if (*satelliteCount >= 5) {
|
|
return QStringLiteral("Cozum icin yeterli");
|
|
}
|
|
if (*satelliteCount >= 4) {
|
|
return QStringLiteral("Minimum navigasyon seviyesi");
|
|
}
|
|
return QStringLiteral("Konum cozumu icin yetersiz");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class TelemetryTableModel final : public QAbstractTableModel {
|
|
public:
|
|
explicit TelemetryTableModel(QObject *parent = nullptr)
|
|
: QAbstractTableModel(parent)
|
|
{
|
|
}
|
|
|
|
void setPoints(const QVector<TelemetryPoint> &points)
|
|
{
|
|
beginResetModel();
|
|
m_points = points;
|
|
endResetModel();
|
|
}
|
|
|
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override
|
|
{
|
|
return parent.isValid() ? 0 : m_points.size();
|
|
}
|
|
|
|
int columnCount(const QModelIndex &parent = QModelIndex()) const override
|
|
{
|
|
return parent.isValid() ? 0 : 7;
|
|
}
|
|
|
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
|
|
{
|
|
if (!index.isValid() || index.row() < 0 || index.row() >= m_points.size()) {
|
|
return {};
|
|
}
|
|
if (role == Qt::TextAlignmentRole) {
|
|
return Qt::AlignCenter;
|
|
}
|
|
if (role != Qt::DisplayRole) {
|
|
return {};
|
|
}
|
|
|
|
const TelemetryPoint &point = m_points[index.row()];
|
|
switch (index.column()) {
|
|
case 0:
|
|
return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz"));
|
|
case 1:
|
|
return locationSourceText(point.source);
|
|
case 2:
|
|
return coordinateText(point.latitude, point.latitudeText);
|
|
case 3:
|
|
return coordinateText(point.longitude, point.longitudeText);
|
|
case 4:
|
|
return optionalDoubleText(point.hheight, 2);
|
|
case 5:
|
|
return optionalIntText(point.satellites);
|
|
case 6:
|
|
return optionalDopText(point.dop.pdop);
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
QVariant headerData(int section, Qt::Orientation orientation,
|
|
int role = Qt::DisplayRole) const override
|
|
{
|
|
if (role != Qt::DisplayRole || orientation != Qt::Horizontal) {
|
|
return {};
|
|
}
|
|
static const QStringList headers = {
|
|
QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"),
|
|
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")};
|
|
return section >= 0 && section < headers.size() ? headers[section] : QVariant();
|
|
}
|
|
|
|
private:
|
|
QVector<TelemetryPoint> m_points;
|
|
};
|
|
|
|
MainWindow::MainWindow(QWidget *parent)
|
|
: QMainWindow(parent)
|
|
, m_serial(new SerialReader(this))
|
|
, m_rinex(new RinexNavigation(this))
|
|
{
|
|
buildUi();
|
|
|
|
m_locationTimeoutTimer = new QTimer(this);
|
|
m_locationTimeoutTimer->setSingleShot(true);
|
|
m_locationTimeoutTimer->setInterval(LocationTimeoutMs);
|
|
|
|
m_uiRefreshTimer = new QTimer(this);
|
|
m_uiRefreshTimer->setSingleShot(true);
|
|
m_uiRefreshTimer->setInterval(UiRefreshIntervalMs);
|
|
|
|
connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine);
|
|
connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus);
|
|
connect(m_serial, &SerialReader::errorOccurred, this, &MainWindow::handleSerialError);
|
|
connect(m_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout);
|
|
connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates);
|
|
connect(m_rinex, &RinexNavigation::satelliteMotionUpdated,
|
|
m_globe, &GlobeWidget::setSatelliteMotion);
|
|
connect(m_rinex, &RinexNavigation::statusChanged, this,
|
|
[this](const QString &status, bool error) {
|
|
m_globe->setRinexStatus(status, error);
|
|
statusBar()->showMessage(status, error ? 8000 : 4000);
|
|
});
|
|
m_rinex->refresh();
|
|
auto *rinexRefreshTimer = new QTimer(this);
|
|
rinexRefreshTimer->setInterval(15 * 60 * 1000);
|
|
connect(rinexRefreshTimer, &QTimer::timeout, m_rinex, &RinexNavigation::refresh);
|
|
rinexRefreshTimer->start();
|
|
|
|
resize(1480, 900);
|
|
setWindowTitle(QStringLiteral("Canli Konum Haritasi"));
|
|
}
|
|
|
|
void MainWindow::toggleSerial()
|
|
{
|
|
if (m_serial->isOpen()) {
|
|
m_serial->closePort();
|
|
if (m_locationTimeoutTimer != nullptr) {
|
|
m_locationTimeoutTimer->stop();
|
|
}
|
|
updateConnectionControls();
|
|
return;
|
|
}
|
|
|
|
if (m_serial->openPort(m_portCombo->currentText().trimmed(), selectedBaudRate())) {
|
|
updateConnectionControls();
|
|
}
|
|
}
|
|
|
|
void MainWindow::clearPoints()
|
|
{
|
|
if (m_locationTimeoutTimer != nullptr) {
|
|
m_locationTimeoutTimer->stop();
|
|
}
|
|
resetTelemetryState();
|
|
}
|
|
|
|
bool MainWindow::loadTelemetryLog(const QString &filePath)
|
|
{
|
|
QFile file(filePath);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
const QString message = QStringLiteral("Log acilamadi: %1").arg(file.errorString());
|
|
m_statusLabel->setText(message);
|
|
statusBar()->showMessage(message, 8000);
|
|
return false;
|
|
}
|
|
const QList<QByteArray> lines = file.readAll().split('\n');
|
|
resetTelemetryState();
|
|
const int replayCapacity = static_cast<int>(std::min<qsizetype>(10000, lines.size()));
|
|
m_maxPointsSpin->setValue(std::max(10, replayCapacity));
|
|
int parsedLocations = 0;
|
|
for (const QByteArray &rawLine : lines) {
|
|
const QString line = QString::fromUtf8(rawLine).trimmed();
|
|
if (line.isEmpty() || line.startsWith(QLatin1Char('#'))) {
|
|
continue;
|
|
}
|
|
const auto telemetry = LocationParser::parseLine(line);
|
|
if (telemetry.has_value() && telemetry->hasLocation()) {
|
|
++parsedLocations;
|
|
}
|
|
handleSerialLine(line);
|
|
}
|
|
flushUiUpdates();
|
|
m_map->fitToPoints();
|
|
const QString message = QStringLiteral("Replay yuklendi: %1 konum - %2")
|
|
.arg(parsedLocations).arg(filePath);
|
|
m_statusLabel->setText(message);
|
|
statusBar()->showMessage(message, 8000);
|
|
return parsedLocations > 0;
|
|
}
|
|
|
|
void MainWindow::openTelemetryLog()
|
|
{
|
|
const QString filePath = QFileDialog::getOpenFileName(
|
|
this, QStringLiteral("Sentetik telemetri logunu ac"), QString(),
|
|
QStringLiteral("Telemetry logs (*.log *.txt);;All files (*)"));
|
|
if (!filePath.isEmpty()) {
|
|
loadTelemetryLog(filePath);
|
|
}
|
|
}
|
|
|
|
void MainWindow::refreshSerialPorts()
|
|
{
|
|
const QString previousPort = m_portCombo->currentText().trimmed();
|
|
const QString configuredPort = qEnvironmentVariable("LOCATION_PLOTTER_PORT").trimmed();
|
|
const QStringList detectedPorts = detectedSerialPorts();
|
|
|
|
m_portCombo->clear();
|
|
for (const QString &port : detectedPorts) {
|
|
m_portCombo->addItem(port);
|
|
}
|
|
|
|
QString selectedPort;
|
|
if (!configuredPort.isEmpty()) {
|
|
selectedPort = configuredPort;
|
|
} else if (!previousPort.isEmpty() && detectedPorts.contains(previousPort)) {
|
|
selectedPort = previousPort;
|
|
} else if (!detectedPorts.isEmpty()) {
|
|
selectedPort = detectedPorts.first();
|
|
} else {
|
|
selectedPort = previousPort.isEmpty() ? fallbackSerialPort() : previousPort;
|
|
}
|
|
|
|
if (m_portCombo->findText(selectedPort) < 0) {
|
|
m_portCombo->addItem(selectedPort);
|
|
}
|
|
m_portCombo->setCurrentText(selectedPort);
|
|
}
|
|
|
|
void MainWindow::handleSerialLine(const QString &line)
|
|
{
|
|
static const QRegularExpression solveStartRegex(
|
|
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
|
|
QRegularExpression::CaseInsensitiveOption);
|
|
const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
|
|
if (solveStarted) {
|
|
m_observedPrns.clear();
|
|
}
|
|
const QSet<QString> linePrns = LocationParser::parseGpsPrns(line);
|
|
if (!linePrns.isEmpty()) {
|
|
m_observedPrns.unite(linePrns);
|
|
}
|
|
if (solveStarted || !linePrns.isEmpty()) {
|
|
m_globe->setObservedPrns(m_observedPrns);
|
|
}
|
|
|
|
const std::optional<ParsedTelemetry> telemetry = LocationParser::parseLine(line);
|
|
const bool solveFinished = line.trimmed().startsWith(
|
|
QStringLiteral("NAV solve done"), Qt::CaseInsensitive);
|
|
appendLogLine(line, telemetry.has_value() || solveFinished);
|
|
|
|
if (!telemetry.has_value()) {
|
|
if (solveFinished && m_pendingTelemetry.hasLocation()) {
|
|
commitPendingTelemetry();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (telemetry->satellites.has_value()) {
|
|
m_lastSatelliteCount = telemetry->satellites;
|
|
}
|
|
if (telemetry->dop.hasAnyValue()) {
|
|
mergeDopValues(m_lastDop, telemetry->dop);
|
|
}
|
|
|
|
if (telemetry->hasLocation()) {
|
|
m_pendingTelemetry = {};
|
|
addTelemetryPoint(*telemetry);
|
|
return;
|
|
}
|
|
|
|
const bool hasLocationPart = telemetry->latitude.has_value() || telemetry->longitude.has_value()
|
|
|| telemetry->hheight.has_value();
|
|
if (hasLocationPart) {
|
|
mergePendingTelemetry(*telemetry);
|
|
if (m_pendingTelemetry.hasLocation() && telemetry->hheight.has_value()) {
|
|
commitPendingTelemetry();
|
|
} else {
|
|
scheduleTelemetryRefresh();
|
|
}
|
|
} else if (m_pendingTelemetry.hasLocation() && solveFinished) {
|
|
commitPendingTelemetry();
|
|
} else {
|
|
scheduleTelemetryRefresh();
|
|
}
|
|
}
|
|
|
|
void MainWindow::handleSerialStatus(const QString &status)
|
|
{
|
|
flushPendingLogLines();
|
|
m_statusLabel->setText(status);
|
|
m_log->appendPlainText(QStringLiteral("[STATUS] ") + status);
|
|
statusBar()->showMessage(status, 4000);
|
|
updateConnectionControls();
|
|
}
|
|
|
|
void MainWindow::handleSerialError(const QString &message)
|
|
{
|
|
flushPendingLogLines();
|
|
m_statusLabel->setText(message);
|
|
m_log->appendPlainText(QStringLiteral("[ERROR] ") + message);
|
|
statusBar()->showMessage(message, 8000);
|
|
updateConnectionControls();
|
|
}
|
|
|
|
void MainWindow::handleLocationTimeout()
|
|
{
|
|
resetTelemetryState();
|
|
|
|
const QString message = QStringLiteral("10 sn konum gelmedi, degerler sifirlandi");
|
|
m_statusLabel->setText(message);
|
|
m_log->appendPlainText(QStringLiteral("[STATUS] ") + message);
|
|
statusBar()->showMessage(message, 8000);
|
|
}
|
|
|
|
void MainWindow::trimToMaxPoints()
|
|
{
|
|
const int maxPoints = m_maxPointsSpin->value();
|
|
while (m_points.size() > maxPoints) {
|
|
m_points.removeFirst();
|
|
}
|
|
m_telemetryRefreshPending = false;
|
|
refreshTelemetryViews();
|
|
}
|
|
|
|
void MainWindow::updateReferenceLocation()
|
|
{
|
|
const QString latitudeText = m_referenceLatEdit->text().trimmed();
|
|
const QString longitudeText = m_referenceLonEdit->text().trimmed();
|
|
const std::optional<double> latitude = parseCoordinateText(latitudeText, -90.0, 90.0);
|
|
const std::optional<double> longitude = parseCoordinateText(longitudeText, -180.0, 180.0);
|
|
|
|
const bool latitudeInvalid = !latitudeText.isEmpty() && !latitude.has_value();
|
|
const bool longitudeInvalid = !longitudeText.isEmpty() && !longitude.has_value();
|
|
m_referenceLatEdit->setStyleSheet(latitudeInvalid
|
|
? QStringLiteral("border: 1px solid #b42318;") : QString());
|
|
m_referenceLonEdit->setStyleSheet(longitudeInvalid
|
|
? QStringLiteral("border: 1px solid #b42318;") : QString());
|
|
|
|
if (latitude.has_value() && longitude.has_value()) {
|
|
m_referenceLatitude = latitude;
|
|
m_referenceLongitude = longitude;
|
|
} else {
|
|
m_referenceLatitude.reset();
|
|
m_referenceLongitude.reset();
|
|
}
|
|
updateAnalysis();
|
|
}
|
|
|
|
void MainWindow::flushUiUpdates()
|
|
{
|
|
flushPendingLogLines();
|
|
if (!m_telemetryRefreshPending) {
|
|
return;
|
|
}
|
|
|
|
m_telemetryRefreshPending = false;
|
|
refreshTelemetryViews();
|
|
}
|
|
|
|
void MainWindow::buildUi()
|
|
{
|
|
auto *central = new QWidget(this);
|
|
central->setObjectName(QStringLiteral("centralRoot"));
|
|
central->setStyleSheet(QStringLiteral(R"(
|
|
#centralRoot {
|
|
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
|
stop:0 #08111d, stop:0.5 #101d2b, stop:1 #172737);
|
|
}
|
|
#brandBar, #controlBar {
|
|
background: rgba(18, 31, 45, 248);
|
|
border: 1px solid #30465a;
|
|
border-radius: 10px;
|
|
}
|
|
QLabel#appTitle {
|
|
color: #f2f7fb;
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
}
|
|
QLabel#appSubtitle {
|
|
color: #9fb0c1;
|
|
font-size: 11px;
|
|
}
|
|
QLabel#liveBadge {
|
|
color: #08745f;
|
|
background: #e4f5ef;
|
|
border: 1px solid #b7e4d5;
|
|
border-radius: 10px;
|
|
padding: 4px 10px;
|
|
font-weight: 700;
|
|
}
|
|
QGroupBox {
|
|
background: rgba(20, 34, 48, 248);
|
|
border: 1px solid #344a5f;
|
|
border-radius: 8px;
|
|
margin-top: 10px;
|
|
padding: 10px 8px 8px 8px;
|
|
font-weight: 700;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 4px;
|
|
color: #dce8f2;
|
|
}
|
|
QLabel {
|
|
color: #d5e0e9;
|
|
}
|
|
QLabel#metricValue {
|
|
color: #62b8e8;
|
|
font-size: 30px;
|
|
font-weight: 800;
|
|
}
|
|
QLabel#subtleText {
|
|
color: #94a8b9;
|
|
font-weight: 500;
|
|
}
|
|
QLabel#dopValue {
|
|
color: #eef5fa;
|
|
font-weight: 800;
|
|
}
|
|
QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox, QPlainTextEdit, QTableView {
|
|
color: #e7eef4;
|
|
background: #0d1925;
|
|
border: 1px solid #3b5267;
|
|
border-radius: 6px;
|
|
padding: 4px;
|
|
selection-background-color: #2d6f9f;
|
|
}
|
|
QComboBox QAbstractItemView {
|
|
color: #e7eef4;
|
|
background-color: #132332;
|
|
border: 1px solid #4b657c;
|
|
selection-color: #ffffff;
|
|
selection-background-color: #23658a;
|
|
outline: 0;
|
|
padding: 4px;
|
|
}
|
|
QComboBox QAbstractItemView::item {
|
|
min-height: 26px;
|
|
padding: 3px 8px;
|
|
}
|
|
QPushButton, QToolButton {
|
|
color: #e7eef4;
|
|
background: #182b3d;
|
|
border: 1px solid #456078;
|
|
border-radius: 6px;
|
|
padding: 5px 10px;
|
|
font-weight: 600;
|
|
}
|
|
QPushButton:hover, QToolButton:hover {
|
|
background: #23445d;
|
|
border-color: #62b8e8;
|
|
}
|
|
QPushButton:pressed, QToolButton:pressed { background: #dbeaf6; }
|
|
QCheckBox { spacing: 7px; color: #d5e0e9; }
|
|
QCheckBox::indicator { width: 17px; height: 17px; }
|
|
QCheckBox::indicator:unchecked {
|
|
background: #0d1925; border: 1px solid #6d8294; border-radius: 4px;
|
|
}
|
|
QCheckBox::indicator:checked {
|
|
background: #167b70; border: 1px solid #167b70; border-radius: 4px;
|
|
}
|
|
QHeaderView::section {
|
|
color: #dce8f2;
|
|
background: #1d3143;
|
|
border: 0;
|
|
border-right: 1px solid #d8dee7;
|
|
padding: 5px;
|
|
font-weight: 700;
|
|
}
|
|
QTabWidget::pane {
|
|
border: 1px solid #344a5f;
|
|
background: #081522;
|
|
}
|
|
QTabBar::tab {
|
|
color: #9fb0c1;
|
|
background: #101f2d;
|
|
border: 1px solid #344a5f;
|
|
border-bottom: 0;
|
|
padding: 7px 14px;
|
|
margin-right: 2px;
|
|
}
|
|
QTabBar::tab:selected {
|
|
color: #f1f7fb;
|
|
background: #1d3850;
|
|
border-top: 2px solid #57bceb;
|
|
}
|
|
QSplitter::handle { background: #263c4f; }
|
|
QSplitter::handle:hover { background: #57bceb; }
|
|
QScrollArea, QScrollArea > QWidget > QWidget { background: transparent; }
|
|
QWidget#dashboardPanel { background: #0b1723; }
|
|
)"));
|
|
auto *rootLayout = new QVBoxLayout(central);
|
|
rootLayout->setContentsMargins(10, 10, 10, 10);
|
|
rootLayout->setSpacing(8);
|
|
|
|
auto *brandBar = new QWidget(central);
|
|
brandBar->setObjectName(QStringLiteral("brandBar"));
|
|
auto *brandLayout = new QHBoxLayout(brandBar);
|
|
brandLayout->setContentsMargins(14, 8, 14, 8);
|
|
auto *logoLabel = new QLabel(brandBar);
|
|
const QPixmap logo(QStringLiteral(":/resources/ankasys-logo.png"));
|
|
logoLabel->setPixmap(logo.scaled(124, 52, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
|
logoLabel->setFixedSize(132, 54);
|
|
logoLabel->setAlignment(Qt::AlignCenter);
|
|
logoLabel->setToolTip(QStringLiteral("ANKASYS - Anka Microelectronic Systems"));
|
|
auto *titleLayout = new QVBoxLayout();
|
|
titleLayout->setSpacing(1);
|
|
auto *titleLabel = new QLabel(QStringLiteral("GNSS Konum Izleme Merkezi"), brandBar);
|
|
titleLabel->setObjectName(QStringLiteral("appTitle"));
|
|
auto *subtitleLabel = new QLabel(QStringLiteral("Gercek zamanli telemetri, sapma ve CEP50 analizi"), brandBar);
|
|
subtitleLabel->setObjectName(QStringLiteral("appSubtitle"));
|
|
titleLayout->addWidget(titleLabel);
|
|
titleLayout->addWidget(subtitleLabel);
|
|
auto *liveBadge = new QLabel(QStringLiteral("● SISTEM HAZIR"), brandBar);
|
|
liveBadge->setObjectName(QStringLiteral("liveBadge"));
|
|
brandLayout->addWidget(logoLabel);
|
|
brandLayout->addLayout(titleLayout);
|
|
brandLayout->addStretch(1);
|
|
brandLayout->addWidget(liveBadge);
|
|
rootLayout->addWidget(brandBar);
|
|
|
|
auto *controlBar = new QWidget(central);
|
|
controlBar->setObjectName(QStringLiteral("controlBar"));
|
|
auto *toolbarLayout = new QHBoxLayout(controlBar);
|
|
toolbarLayout->setContentsMargins(12, 7, 12, 7);
|
|
toolbarLayout->setSpacing(8);
|
|
|
|
m_portCombo = new QComboBox(this);
|
|
m_portCombo->setObjectName(QStringLiteral("portSelector"));
|
|
m_portCombo->setMinimumWidth(220);
|
|
m_portCombo->setEditable(true);
|
|
m_portCombo->setToolTip(QStringLiteral(
|
|
"USB UART portu. macOS'ta /dev/cu.usb... tercih edilir; /dev/tty.usb... da desteklenir."));
|
|
refreshSerialPorts();
|
|
|
|
auto *refreshPortsButton = new QToolButton(this);
|
|
refreshPortsButton->setObjectName(QStringLiteral("refreshPortsButton"));
|
|
refreshPortsButton->setText(QStringLiteral("Yenile"));
|
|
refreshPortsButton->setToolTip(QStringLiteral("Seri port listesini yenile"));
|
|
|
|
m_baudCombo = new QComboBox(this);
|
|
for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) {
|
|
m_baudCombo->addItem(QString::number(baud));
|
|
}
|
|
m_baudCombo->setCurrentText(QStringLiteral("115200"));
|
|
|
|
m_maxPointsSpin = new QSpinBox(this);
|
|
m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput"));
|
|
m_maxPointsSpin->setRange(1, 10000);
|
|
m_maxPointsSpin->setValue(10);
|
|
m_maxPointsSpin->setSuffix(QStringLiteral(" nokta"));
|
|
|
|
m_connectButton = new QPushButton(this);
|
|
m_connectButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
|
|
m_clearButton = new QPushButton(QStringLiteral("Temizle"), this);
|
|
m_clearButton->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
|
|
m_openLogButton = new QPushButton(QStringLiteral("Senaryo Logu Ac"), this);
|
|
m_openLogButton->setObjectName(QStringLiteral("openLogButton"));
|
|
m_openLogButton->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton));
|
|
|
|
m_autoFollowCheck = new QCheckBox(QStringLiteral("Kamerayi takip et"), this);
|
|
m_autoFollowCheck->setChecked(true);
|
|
|
|
auto *zoomInButton = new QToolButton(this);
|
|
zoomInButton->setText(QStringLiteral("+"));
|
|
zoomInButton->setToolTip(QStringLiteral("Yaklastir"));
|
|
auto *zoomOutButton = new QToolButton(this);
|
|
zoomOutButton->setText(QStringLiteral("-"));
|
|
zoomOutButton->setToolTip(QStringLiteral("Uzaklastir"));
|
|
|
|
toolbarLayout->addWidget(new QLabel(QStringLiteral("Port"), this));
|
|
toolbarLayout->addWidget(m_portCombo);
|
|
toolbarLayout->addWidget(refreshPortsButton);
|
|
toolbarLayout->addWidget(new QLabel(QStringLiteral("Baud"), this));
|
|
toolbarLayout->addWidget(m_baudCombo);
|
|
toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this));
|
|
toolbarLayout->addWidget(m_maxPointsSpin);
|
|
toolbarLayout->addWidget(m_connectButton);
|
|
toolbarLayout->addWidget(m_openLogButton);
|
|
toolbarLayout->addWidget(m_clearButton);
|
|
toolbarLayout->addWidget(m_autoFollowCheck);
|
|
toolbarLayout->addWidget(zoomOutButton);
|
|
toolbarLayout->addWidget(zoomInButton);
|
|
toolbarLayout->addStretch(1);
|
|
rootLayout->addWidget(controlBar);
|
|
|
|
m_map = new MapWidget(this);
|
|
m_globe = new GlobeWidget(this);
|
|
auto *viewTabs = new QTabWidget(this);
|
|
viewTabs->setObjectName(QStringLiteral("mainViewTabs"));
|
|
viewTabs->addTab(m_map, QStringLiteral("2B Harita"));
|
|
viewTabs->addTab(m_globe, QStringLiteral("3B Dunya · Uydular"));
|
|
|
|
auto *sidePanel = new QWidget(this);
|
|
sidePanel->setMinimumWidth(390);
|
|
auto *sideOuterLayout = new QVBoxLayout(sidePanel);
|
|
sideOuterLayout->setContentsMargins(8, 0, 0, 0);
|
|
sideOuterLayout->setSpacing(0);
|
|
auto *dashboard = new QWidget(sidePanel);
|
|
dashboard->setObjectName(QStringLiteral("dashboardPanel"));
|
|
auto *sideLayout = new QVBoxLayout(dashboard);
|
|
sideLayout->setContentsMargins(4, 4, 4, 4);
|
|
sideLayout->setSpacing(8);
|
|
|
|
auto *statusGroup = new QGroupBox(QStringLiteral("Baglanti"), sidePanel);
|
|
auto *statusLayout = new QGridLayout(statusGroup);
|
|
m_countLabel = new QLabel(QStringLiteral("Konum: 0"), statusGroup);
|
|
m_statusLabel = new QLabel(QStringLiteral("Hazir"), statusGroup);
|
|
m_statusLabel->setObjectName(QStringLiteral("subtleText"));
|
|
m_statusLabel->setWordWrap(true);
|
|
statusLayout->addWidget(new QLabel(QStringLiteral("Durum"), statusGroup), 0, 0);
|
|
statusLayout->addWidget(m_statusLabel, 0, 1);
|
|
statusLayout->addWidget(new QLabel(QStringLiteral("Kayitli nokta"), statusGroup), 1, 0);
|
|
statusLayout->addWidget(m_countLabel, 1, 1);
|
|
statusLayout->setColumnStretch(1, 1);
|
|
sideLayout->addWidget(statusGroup);
|
|
|
|
auto *satelliteGroup = new QGroupBox(QStringLiteral("Uydu durumu"), sidePanel);
|
|
auto *satelliteLayout = new QVBoxLayout(satelliteGroup);
|
|
m_satelliteLabel = new QLabel(QStringLiteral("-"), satelliteGroup);
|
|
m_satelliteLabel->setObjectName(QStringLiteral("metricValue"));
|
|
m_satelliteLabel->setAlignment(Qt::AlignCenter);
|
|
m_satelliteQualityLabel = new QLabel(QStringLiteral("Uydu bekleniyor"), satelliteGroup);
|
|
m_satelliteQualityLabel->setObjectName(QStringLiteral("subtleText"));
|
|
m_satelliteQualityLabel->setAlignment(Qt::AlignCenter);
|
|
m_satelliteQualityLabel->setWordWrap(true);
|
|
satelliteLayout->addWidget(m_satelliteLabel);
|
|
satelliteLayout->addWidget(m_satelliteQualityLabel);
|
|
sideLayout->addWidget(satelliteGroup);
|
|
|
|
auto *dopGroup = new QGroupBox(QStringLiteral("DOP degerleri"), sidePanel);
|
|
auto *dopLayout = new QGridLayout(dopGroup);
|
|
m_gdopLabel = new QLabel(QStringLiteral("-"), dopGroup);
|
|
m_hdopLabel = new QLabel(QStringLiteral("-"), dopGroup);
|
|
m_vdopLabel = new QLabel(QStringLiteral("-"), dopGroup);
|
|
m_tdopLabel = new QLabel(QStringLiteral("-"), dopGroup);
|
|
|
|
const auto addDopRow = [dopLayout](int row, const QString &label, QLabel *value) {
|
|
auto *nameLabel = new QLabel(label);
|
|
nameLabel->setObjectName(QStringLiteral("subtleText"));
|
|
value->setObjectName(QStringLiteral("dopValue"));
|
|
value->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
value->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
dopLayout->addWidget(nameLabel, row, 0);
|
|
dopLayout->addWidget(value, row, 1);
|
|
};
|
|
|
|
addDopRow(0, QStringLiteral("GDOP"), m_gdopLabel);
|
|
addDopRow(1, QStringLiteral("HDOP"), m_hdopLabel);
|
|
addDopRow(2, QStringLiteral("VDOP"), m_vdopLabel);
|
|
addDopRow(3, QStringLiteral("TDOP"), m_tdopLabel);
|
|
dopLayout->setColumnStretch(1, 1);
|
|
sideLayout->addWidget(dopGroup);
|
|
|
|
auto *analysisGroup = new QGroupBox(QStringLiteral("Konum analizi"), sidePanel);
|
|
auto *analysisLayout = new QGridLayout(analysisGroup);
|
|
auto *layersGroup = new QGroupBox(QStringLiteral("Harita katmanlari"), sidePanel);
|
|
auto *layersLayout = new QVBoxLayout(layersGroup);
|
|
m_showTelemetryCheck = new QCheckBox(QStringLiteral("Harita isaretlerini goster"), layersGroup);
|
|
m_showTelemetryCheck->setObjectName(QStringLiteral("showTelemetryCheck"));
|
|
m_showTelemetryCheck->setChecked(true);
|
|
m_showRecentPointsCheck = new QCheckBox(QStringLiteral("Son konumlari goster"), layersGroup);
|
|
m_showRecentPointsCheck->setObjectName(QStringLiteral("showRecentPointsCheck"));
|
|
m_showRecentPointsCheck->setChecked(true);
|
|
m_showAverageCheck = new QCheckBox(QStringLiteral("Ortalamayi goster"), layersGroup);
|
|
m_showAverageCheck->setObjectName(QStringLiteral("showAverageCheck"));
|
|
m_showAverageCheck->setChecked(true);
|
|
auto *elevationMaskSpin = new QDoubleSpinBox(layersGroup);
|
|
elevationMaskSpin->setObjectName(QStringLiteral("elevationMaskSpin"));
|
|
elevationMaskSpin->setRange(0.0, 90.0);
|
|
elevationMaskSpin->setDecimals(1);
|
|
elevationMaskSpin->setSingleStep(0.5);
|
|
elevationMaskSpin->setValue(10.0);
|
|
elevationMaskSpin->setKeyboardTracking(false);
|
|
elevationMaskSpin->setAccelerated(true);
|
|
elevationMaskSpin->setMinimumWidth(86);
|
|
elevationMaskSpin->setSuffix(QStringLiteral("°"));
|
|
elevationMaskSpin->setToolTip(
|
|
QStringLiteral("0-90 arasinda bir deger yazin ve Enter'a basin"));
|
|
auto *elevationMaskLayout = new QHBoxLayout();
|
|
elevationMaskLayout->addWidget(new QLabel(QStringLiteral("Min. elevation"), layersGroup));
|
|
elevationMaskLayout->addStretch(1);
|
|
elevationMaskLayout->addWidget(elevationMaskSpin);
|
|
layersLayout->addWidget(m_showTelemetryCheck);
|
|
layersLayout->addWidget(m_showRecentPointsCheck);
|
|
layersLayout->addWidget(m_showAverageCheck);
|
|
layersLayout->addLayout(elevationMaskLayout);
|
|
sideLayout->addWidget(layersGroup);
|
|
m_referenceLatEdit = new QLineEdit(analysisGroup);
|
|
m_referenceLatEdit->setObjectName(QStringLiteral("referenceLatInput"));
|
|
m_referenceLatEdit->setPlaceholderText(QStringLiteral("Orn. 40.789250342142"));
|
|
m_referenceLatEdit->setClearButtonEnabled(true);
|
|
m_referenceLatEdit->setMaxLength(32);
|
|
m_referenceLatEdit->setToolTip(QStringLiteral("Kullanici referansi; CEP50 hesabini etkilemez"));
|
|
m_referenceLonEdit = new QLineEdit(analysisGroup);
|
|
m_referenceLonEdit->setObjectName(QStringLiteral("referenceLonInput"));
|
|
m_referenceLonEdit->setPlaceholderText(QStringLiteral("Orn. 29.462775308417"));
|
|
m_referenceLonEdit->setClearButtonEnabled(true);
|
|
m_referenceLonEdit->setMaxLength(32);
|
|
m_referenceLonEdit->setToolTip(QStringLiteral("Kullanici referansi; CEP50 hesabini etkilemez"));
|
|
auto *mapSelectionHint = new QLabel(
|
|
QStringLiteral("Sag tik: kullanici referansi. CEP50, olcumlerin %50'sine gore otomatik hesaplanir."), analysisGroup);
|
|
mapSelectionHint->setObjectName(QStringLiteral("subtleText"));
|
|
mapSelectionHint->setWordWrap(true);
|
|
m_cepLabel = new QLabel(QStringLiteral("-"), analysisGroup);
|
|
m_cepLabel->setObjectName(QStringLiteral("cepValue"));
|
|
m_cepLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
m_cepLabel->setToolTip(QStringLiteral("Radyal konum hatalarinin yuzde 50 yuzdeligi"));
|
|
m_meanLocationLabel = new QLabel(QStringLiteral("-"), analysisGroup);
|
|
m_meanLocationLabel->setMinimumHeight(36);
|
|
m_lastDeviationLabel = new QLabel(QStringLiteral("-"), analysisGroup);
|
|
m_meanDeviationLabel = new QLabel(QStringLiteral("-"), analysisGroup);
|
|
m_rmsScatterLabel = new QLabel(QStringLiteral("-"), analysisGroup);
|
|
m_maxScatterLabel = new QLabel(QStringLiteral("-"), analysisGroup);
|
|
|
|
const auto addAnalysisRow = [analysisLayout](int row, const QString &label, QLabel *value) {
|
|
auto *nameLabel = new QLabel(label);
|
|
value->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
value->setWordWrap(true);
|
|
analysisLayout->addWidget(nameLabel, row, 0);
|
|
analysisLayout->addWidget(value, row, 1);
|
|
};
|
|
|
|
analysisLayout->addWidget(mapSelectionHint, 0, 0, 1, 2);
|
|
analysisLayout->addWidget(new QLabel(QStringLiteral("Referans enlem"), analysisGroup), 1, 0);
|
|
analysisLayout->addWidget(m_referenceLatEdit, 1, 1);
|
|
analysisLayout->addWidget(new QLabel(QStringLiteral("Referans boylam"), analysisGroup), 2, 0);
|
|
analysisLayout->addWidget(m_referenceLonEdit, 2, 1);
|
|
analysisLayout->addWidget(new QLabel(QStringLiteral("CEP50"), analysisGroup), 3, 0);
|
|
analysisLayout->addWidget(m_cepLabel, 3, 1);
|
|
addAnalysisRow(4, QStringLiteral("Ortalama"), m_meanLocationLabel);
|
|
addAnalysisRow(5, QStringLiteral("Son sapma"), m_lastDeviationLabel);
|
|
addAnalysisRow(6, QStringLiteral("Ort. sapma"), m_meanDeviationLabel);
|
|
addAnalysisRow(7, QStringLiteral("RMS sacilim"), m_rmsScatterLabel);
|
|
addAnalysisRow(8, QStringLiteral("Maks sapma"), m_maxScatterLabel);
|
|
analysisLayout->setColumnStretch(1, 1);
|
|
sideLayout->addWidget(analysisGroup);
|
|
|
|
auto *telemetryTabs = new QTabWidget(sidePanel);
|
|
|
|
m_table = new QTableView(telemetryTabs);
|
|
m_tableModel = new TelemetryTableModel(m_table);
|
|
m_table->setModel(m_tableModel);
|
|
m_table->verticalHeader()->setVisible(false);
|
|
m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
|
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
m_table->setAlternatingRowColors(true);
|
|
|
|
m_log = new QPlainTextEdit(telemetryTabs);
|
|
m_log->setReadOnly(true);
|
|
m_log->setLineWrapMode(QPlainTextEdit::NoWrap);
|
|
m_log->setMaximumBlockCount(500);
|
|
m_log->setPlaceholderText(QStringLiteral("Ham seri debug satirlari"));
|
|
telemetryTabs->addTab(m_log, QStringLiteral("UART Log"));
|
|
telemetryTabs->addTab(m_table, QStringLiteral("Konumlar"));
|
|
telemetryTabs->setCurrentWidget(m_log);
|
|
telemetryTabs->setMinimumHeight(210);
|
|
|
|
auto *dashboardScroll = new QScrollArea(sidePanel);
|
|
dashboardScroll->setObjectName(QStringLiteral("dashboardScroll"));
|
|
dashboardScroll->setWidgetResizable(true);
|
|
dashboardScroll->setFrameShape(QFrame::NoFrame);
|
|
dashboardScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
dashboardScroll->setWidget(dashboard);
|
|
|
|
auto *sideSplitter = new QSplitter(Qt::Vertical, sidePanel);
|
|
sideSplitter->setObjectName(QStringLiteral("sideSplitter"));
|
|
sideSplitter->setChildrenCollapsible(false);
|
|
sideSplitter->setHandleWidth(7);
|
|
sideSplitter->addWidget(dashboardScroll);
|
|
sideSplitter->addWidget(telemetryTabs);
|
|
sideSplitter->setStretchFactor(0, 2);
|
|
sideSplitter->setStretchFactor(1, 3);
|
|
sideSplitter->setSizes({380, 410});
|
|
sideOuterLayout->addWidget(sideSplitter);
|
|
|
|
auto *splitter = new QSplitter(Qt::Horizontal, central);
|
|
splitter->setObjectName(QStringLiteral("mainSplitter"));
|
|
splitter->setChildrenCollapsible(false);
|
|
splitter->setHandleWidth(7);
|
|
splitter->addWidget(viewTabs);
|
|
splitter->addWidget(sidePanel);
|
|
splitter->setStretchFactor(0, 4);
|
|
splitter->setStretchFactor(1, 1);
|
|
splitter->setSizes({960, 500});
|
|
rootLayout->addWidget(splitter, 1);
|
|
|
|
setCentralWidget(central);
|
|
updateConnectionControls();
|
|
updateSummaryLabels();
|
|
updateAnalysis();
|
|
|
|
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
|
|
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
|
|
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
|
|
connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts);
|
|
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
|
|
&MainWindow::trimToMaxPoints);
|
|
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
|
|
connect(m_showTelemetryCheck, &QCheckBox::toggled,
|
|
m_map, &MapWidget::setTelemetryOverlayEnabled);
|
|
connect(m_showTelemetryCheck, &QCheckBox::toggled, this, [this](bool enabled) {
|
|
m_showRecentPointsCheck->setEnabled(enabled);
|
|
m_showAverageCheck->setEnabled(enabled);
|
|
});
|
|
connect(m_showRecentPointsCheck, &QCheckBox::toggled,
|
|
m_map, &MapWidget::setRecentPointsEnabled);
|
|
connect(m_showAverageCheck, &QCheckBox::toggled, m_map, &MapWidget::setAverageOverlayEnabled);
|
|
connect(elevationMaskSpin, qOverload<double>(&QDoubleSpinBox::valueChanged),
|
|
m_globe, &GlobeWidget::setElevationMask);
|
|
connect(m_referenceLatEdit, &QLineEdit::editingFinished,
|
|
this, &MainWindow::updateReferenceLocation);
|
|
connect(m_referenceLonEdit, &QLineEdit::editingFinished,
|
|
this, &MainWindow::updateReferenceLocation);
|
|
connect(m_map, &MapWidget::autoFollowChanged, m_autoFollowCheck, &QCheckBox::setChecked);
|
|
connect(m_map, &MapWidget::referenceLocationSelected, this,
|
|
[this](double latitude, double longitude) {
|
|
m_referenceLatEdit->setText(preciseCoordinateText(latitude));
|
|
m_referenceLonEdit->setText(preciseCoordinateText(longitude));
|
|
updateReferenceLocation();
|
|
statusBar()->showMessage(QStringLiteral("CEP merkezi haritadan secildi"), 4000);
|
|
});
|
|
connect(zoomInButton, &QToolButton::clicked, m_map, &MapWidget::zoomIn);
|
|
connect(zoomOutButton, &QToolButton::clicked, m_map, &MapWidget::zoomOut);
|
|
}
|
|
|
|
void MainWindow::updateConnectionControls()
|
|
{
|
|
const bool open = m_serial->isOpen();
|
|
m_connectButton->setText(open ? QStringLiteral("Durdur") : QStringLiteral("Dinle"));
|
|
m_connectButton->setIcon(style()->standardIcon(open ? QStyle::SP_MediaStop : QStyle::SP_MediaPlay));
|
|
m_portCombo->setEnabled(!open);
|
|
m_baudCombo->setEnabled(!open);
|
|
}
|
|
|
|
void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
|
|
{
|
|
TelemetryPoint point;
|
|
point.source = telemetry.source;
|
|
point.latitude = *telemetry.latitude;
|
|
point.longitude = *telemetry.longitude;
|
|
point.latitudeText = telemetry.latitudeText;
|
|
point.longitudeText = telemetry.longitudeText;
|
|
point.hheight = telemetry.hheight;
|
|
point.satellites = telemetry.satellites.has_value() ? telemetry.satellites : m_lastSatelliteCount;
|
|
point.dop = m_lastDop;
|
|
mergeDopValues(point.dop, telemetry.dop);
|
|
point.rawLine = telemetry.rawLine;
|
|
point.receivedAt = QDateTime::currentDateTimeUtc();
|
|
|
|
m_points.push_back(point);
|
|
m_globe->setReceiverLocation(point.latitude, point.longitude);
|
|
if (m_locationTimeoutTimer != nullptr && m_serial->isOpen()) {
|
|
m_locationTimeoutTimer->start();
|
|
}
|
|
const int maxPoints = m_maxPointsSpin->value();
|
|
while (m_points.size() > maxPoints) {
|
|
m_points.removeFirst();
|
|
}
|
|
scheduleTelemetryRefresh();
|
|
}
|
|
|
|
void MainWindow::refreshTable()
|
|
{
|
|
m_tableModel->setPoints(m_points);
|
|
if (m_tableModel->rowCount() > 0) {
|
|
m_table->scrollToBottom();
|
|
}
|
|
}
|
|
|
|
void MainWindow::updateSummaryLabels()
|
|
{
|
|
m_satelliteLabel->setText(
|
|
m_lastSatelliteCount.has_value()
|
|
? QString::number(*m_lastSatelliteCount)
|
|
: QStringLiteral("-"));
|
|
m_satelliteQualityLabel->setText(satelliteQualityText(m_lastSatelliteCount));
|
|
m_countLabel->setText(QStringLiteral("Konum: %1 / %2")
|
|
.arg(m_points.size())
|
|
.arg(m_maxPointsSpin->value()));
|
|
updateDopLabels();
|
|
}
|
|
|
|
void MainWindow::updateDopLabels()
|
|
{
|
|
m_gdopLabel->setText(optionalDopText(m_lastDop.gdop));
|
|
m_hdopLabel->setText(optionalDopText(m_lastDop.hdop));
|
|
m_vdopLabel->setText(optionalDopText(m_lastDop.vdop));
|
|
m_tdopLabel->setText(optionalDopText(m_lastDop.tdop));
|
|
}
|
|
|
|
void MainWindow::updateAnalysis()
|
|
{
|
|
const QVector<TelemetryPoint> analysisPoints = preferredAnalysisPoints(m_points);
|
|
const LocationAnalysis analysis = analyzeLocations(analysisPoints);
|
|
if (!analysis.valid) {
|
|
m_cepLabel->setText(QStringLiteral("-"));
|
|
m_meanLocationLabel->setText(QStringLiteral("-"));
|
|
m_lastDeviationLabel->setText(QStringLiteral("-"));
|
|
m_meanDeviationLabel->setText(QStringLiteral("-"));
|
|
m_rmsScatterLabel->setText(QStringLiteral("-"));
|
|
m_maxScatterLabel->setText(QStringLiteral("-"));
|
|
m_map->clearAverageMarker();
|
|
m_map->clearCepMarker();
|
|
return;
|
|
}
|
|
|
|
m_meanLocationLabel->setText(QStringLiteral("%1\n%2")
|
|
.arg(preciseCoordinateText(analysis.averageLat))
|
|
.arg(preciseCoordinateText(analysis.averageLon)));
|
|
m_lastDeviationLabel->setText(metersText(analysis.lastDeviationMeters));
|
|
m_meanDeviationLabel->setText(metersText(analysis.meanDeviationMeters));
|
|
m_rmsScatterLabel->setText(metersText(analysis.rmsScatterMeters));
|
|
m_maxScatterLabel->setText(metersText(analysis.maxDeviationMeters));
|
|
m_map->setAverageMarker(analysis.averageLat, analysis.averageLon,
|
|
analysis.rmsScatterMeters);
|
|
|
|
const double cep50 = calculateCep50(analysisPoints, analysis.averageLat,
|
|
analysis.averageLon);
|
|
m_cepLabel->setText(QStringLiteral("%1 (P50, N=%2)")
|
|
.arg(metersText(cep50))
|
|
.arg(analysisPoints.size()));
|
|
m_map->setCepMarker(analysis.averageLat, analysis.averageLon, cep50);
|
|
}
|
|
|
|
void MainWindow::appendLogLine(const QString &line, bool parsed)
|
|
{
|
|
const QString prefix = parsed ? QStringLiteral("[OK] ") : QStringLiteral("[..] ");
|
|
if (m_pendingLogLines.size() >= MaxPendingLogLines) {
|
|
m_pendingLogLines.removeFirst();
|
|
}
|
|
m_pendingLogLines.push_back(prefix + line);
|
|
if (m_uiRefreshTimer != nullptr && !m_uiRefreshTimer->isActive()) {
|
|
m_uiRefreshTimer->start();
|
|
}
|
|
}
|
|
|
|
void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry)
|
|
{
|
|
if (telemetry.latitude.has_value() && m_pendingTelemetry.hasLocation()) {
|
|
commitPendingTelemetry();
|
|
}
|
|
|
|
if (telemetry.latitude.has_value()) {
|
|
m_pendingTelemetry.source = telemetry.source;
|
|
m_pendingTelemetry.latitude = telemetry.latitude;
|
|
m_pendingTelemetry.latitudeText = telemetry.latitudeText;
|
|
}
|
|
if (telemetry.longitude.has_value()) {
|
|
m_pendingTelemetry.longitude = telemetry.longitude;
|
|
m_pendingTelemetry.longitudeText = telemetry.longitudeText;
|
|
}
|
|
if (telemetry.hheight.has_value()) {
|
|
m_pendingTelemetry.hheight = telemetry.hheight;
|
|
}
|
|
if (telemetry.satellites.has_value()) {
|
|
m_pendingTelemetry.satellites = telemetry.satellites;
|
|
}
|
|
if (telemetry.dop.hasAnyValue()) {
|
|
mergeDopValues(m_pendingTelemetry.dop, telemetry.dop);
|
|
}
|
|
|
|
if (m_pendingTelemetry.rawLine.isEmpty()) {
|
|
m_pendingTelemetry.rawLine = telemetry.rawLine;
|
|
} else if (!telemetry.rawLine.isEmpty()) {
|
|
m_pendingTelemetry.rawLine += QStringLiteral(" | ") + telemetry.rawLine;
|
|
}
|
|
}
|
|
|
|
void MainWindow::commitPendingTelemetry()
|
|
{
|
|
if (!m_pendingTelemetry.hasLocation()) {
|
|
return;
|
|
}
|
|
|
|
addTelemetryPoint(m_pendingTelemetry);
|
|
m_pendingTelemetry = {};
|
|
}
|
|
|
|
void MainWindow::resetTelemetryState()
|
|
{
|
|
m_telemetryRefreshPending = false;
|
|
m_points.clear();
|
|
m_lastSatelliteCount.reset();
|
|
m_observedPrns.clear();
|
|
m_lastDop = {};
|
|
m_pendingTelemetry = {};
|
|
m_map->clearPoints();
|
|
m_globe->clearReceiverLocation();
|
|
m_globe->setObservedPrns({});
|
|
refreshTable();
|
|
updateSummaryLabels();
|
|
updateAnalysis();
|
|
}
|
|
|
|
void MainWindow::scheduleTelemetryRefresh()
|
|
{
|
|
m_telemetryRefreshPending = true;
|
|
if (m_uiRefreshTimer != nullptr && !m_uiRefreshTimer->isActive()) {
|
|
m_uiRefreshTimer->start();
|
|
}
|
|
}
|
|
|
|
void MainWindow::refreshTelemetryViews()
|
|
{
|
|
m_map->setPoints(m_points);
|
|
refreshTable();
|
|
updateSummaryLabels();
|
|
updateAnalysis();
|
|
}
|
|
|
|
void MainWindow::flushPendingLogLines()
|
|
{
|
|
if (m_pendingLogLines.isEmpty()) {
|
|
return;
|
|
}
|
|
m_log->appendPlainText(m_pendingLogLines.join('\n'));
|
|
m_pendingLogLines.clear();
|
|
}
|
|
|
|
int MainWindow::selectedBaudRate() const
|
|
{
|
|
bool ok = false;
|
|
const int baudRate = m_baudCombo->currentText().toInt(&ok);
|
|
return ok ? baudRate : 115200;
|
|
}
|