#include "MainWindow.h" #include "LocationParser.h" #include "GlobeWidget.h" #include "MapWidget.h" #include "RinexNavigation.h" #include "SerialReader.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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 &points, double referenceLatitude, double referenceLongitude) { QVector 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 &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 preferredAnalysisPoints(const QVector &points) { QVector 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 &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 &value) { if (!value.has_value()) { return QStringLiteral("-"); } return QString::number(*value); } QString optionalDopText(const std::optional &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 &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 &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 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 lines = file.readAll().split('\n'); resetTelemetryState(); const int replayCapacity = static_cast(std::min(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 linePrns = LocationParser::parseGpsPrns(line); if (!linePrns.isEmpty()) { m_observedPrns.unite(linePrns); } if (solveStarted || !linePrns.isEmpty()) { m_globe->setObservedPrns(m_observedPrns); } const std::optional 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 latitude = parseCoordinateText(latitudeText, -90.0, 90.0); const std::optional 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(&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(&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 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; }