diff --git a/doc/img/APTDemod_plugin_map.png b/doc/img/APTDemod_plugin_map.png new file mode 100644 index 000000000..b4eb60ff8 Binary files /dev/null and b/doc/img/APTDemod_plugin_map.png differ diff --git a/doc/img/APTDemod_plugin_temperature.png b/doc/img/APTDemod_plugin_temperature.png new file mode 100644 index 000000000..c834186a5 Binary files /dev/null and b/doc/img/APTDemod_plugin_temperature.png differ diff --git a/plugins/channelrx/demodapt/CMakeLists.txt b/plugins/channelrx/demodapt/CMakeLists.txt index a097d181b..da90ac59a 100644 --- a/plugins/channelrx/demodapt/CMakeLists.txt +++ b/plugins/channelrx/demodapt/CMakeLists.txt @@ -23,6 +23,7 @@ set(demodapt_HEADERS include_directories( ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client ${APT_INCLUDE_DIR} + ${SGP4_INCLUDE_DIR} ) if(NOT SERVER_MODE) @@ -32,12 +33,15 @@ if(NOT SERVER_MODE) aptdemodgui.ui aptdemodsettingsdialog.cpp aptdemodsettingsdialog.ui + aptdemodselectdialog.cpp + aptdemodselectdialog.ui icons.qrc ) set(demodapt_HEADERS ${demodapt_HEADERS} aptdemodgui.h aptdemodsettingsdialog.h + aptdemodselectdialog.h ) set(TARGET_NAME demodapt) @@ -59,12 +63,22 @@ if(APT_EXTERNAL) add_dependencies(${TARGET_NAME} apt) endif() +if(SGP4_EXTERNAL) + add_dependencies(${TARGET_NAME} sgp4) +endif() + target_link_libraries(${TARGET_NAME} Qt5::Core ${TARGET_LIB} sdrbase ${TARGET_LIB_GUI} ${APT_LIBRARIES} + ${SGP4_LIBRARIES} ) install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/channelrx/demodapt/aptdemod.cpp b/plugins/channelrx/demodapt/aptdemod.cpp index 19a11857c..7ba83b91a 100644 --- a/plugins/channelrx/demodapt/aptdemod.cpp +++ b/plugins/channelrx/demodapt/aptdemod.cpp @@ -47,6 +47,7 @@ MESSAGE_CLASS_DEFINITION(APTDemod::MsgConfigureAPTDemod, Message) MESSAGE_CLASS_DEFINITION(APTDemod::MsgPixels, Message) MESSAGE_CLASS_DEFINITION(APTDemod::MsgImage, Message) MESSAGE_CLASS_DEFINITION(APTDemod::MsgLine, Message) +MESSAGE_CLASS_DEFINITION(APTDemod::MsgMapImageName, Message) MESSAGE_CLASS_DEFINITION(APTDemod::MsgResetDecoder, Message) const char * const APTDemod::m_channelIdURI = "sdrangel.channel.aptdemod"; @@ -62,7 +63,7 @@ APTDemod::APTDemod(DeviceAPI *deviceAPI) : m_basebandSink = new APTDemodBaseband(this); m_basebandSink->moveToThread(&m_thread); - m_imageWorker = new APTDemodImageWorker(); + m_imageWorker = new APTDemodImageWorker(this); m_basebandSink->setImagWorkerMessageQueue(m_imageWorker->getInputMessageQueue()); m_imageWorker->moveToThread(&m_imageThread); @@ -286,6 +287,42 @@ void APTDemod::applySettings(const APTDemodSettings& settings, bool force) if ((settings.m_autoSaveMinScanLines != m_settings.m_autoSaveMinScanLines) || force) { reverseAPIKeys.append("autoSaveMinScanLines"); } + if ((settings.m_saveCombined != m_settings.m_saveCombined) || force) { + reverseAPIKeys.append("saveCombined"); + } + if ((settings.m_saveSeparate != m_settings.m_saveSeparate) || force) { + reverseAPIKeys.append("saveSeparate"); + } + if ((settings.m_saveProjection != m_settings.m_saveProjection) || force) { + reverseAPIKeys.append("saveProjection"); + } + if ((settings.m_scanlinesPerImageUpdate != m_settings.m_scanlinesPerImageUpdate) || force) { + reverseAPIKeys.append("scanlinesPerImageUpdate"); + } + if ((settings.m_transparencyThreshold != m_settings.m_transparencyThreshold) || force) { + reverseAPIKeys.append("transparencyThreshold"); + } + if ((settings.m_opacityThreshold != m_settings.m_opacityThreshold) || force) { + reverseAPIKeys.append("opacityThreshold"); + } + if ((settings.m_palettes != m_settings.m_palettes) || force) { + reverseAPIKeys.append("palettes"); + } + if ((settings.m_palette != m_settings.m_palette) || force) { + reverseAPIKeys.append("palette"); + } + if ((settings.m_horizontalPixelsPerDegree != m_settings.m_horizontalPixelsPerDegree) || force) { + reverseAPIKeys.append("horizontalPixelsPerDegree"); + } + if ((settings.m_verticalPixelsPerDegree != m_settings.m_verticalPixelsPerDegree) || force) { + reverseAPIKeys.append("verticalPixelsPerDegree"); + } + if ((settings.m_satTimeOffset != m_settings.m_satTimeOffset) || force) { + reverseAPIKeys.append("satTimeOffset"); + } + if ((settings.m_satYaw != m_settings.m_satYaw) || force) { + reverseAPIKeys.append("satYaw"); + } if (m_settings.m_streamIndex != settings.m_streamIndex) { @@ -426,6 +463,42 @@ void APTDemod::webapiUpdateChannelSettings( if (channelSettingsKeys.contains("autoSaveMinScanLines")) { settings.m_autoSaveMinScanLines = response.getAptDemodSettings()->getAutoSaveMinScanLines(); } + if (channelSettingsKeys.contains("saveCombined")) { + settings.m_saveCombined = response.getAptDemodSettings()->getSaveCombined(); + } + if (channelSettingsKeys.contains("saveSeparate")) { + settings.m_saveSeparate = response.getAptDemodSettings()->getSaveSeparate(); + } + if (channelSettingsKeys.contains("saveProjection")) { + settings.m_saveProjection = response.getAptDemodSettings()->getSaveProjection(); + } + if (channelSettingsKeys.contains("scanlinesPerImageUpdate")) { + settings.m_scanlinesPerImageUpdate = response.getAptDemodSettings()->getScanlinesPerImageUpdate(); + } + if (channelSettingsKeys.contains("transparencyThreshold")) { + settings.m_transparencyThreshold = response.getAptDemodSettings()->getTransparencyThreshold(); + } + if (channelSettingsKeys.contains("m_opacityThreshold")) { + settings.m_opacityThreshold = response.getAptDemodSettings()->getOpacityThreshold(); + } + if (channelSettingsKeys.contains("palettes")) { + settings.m_palettes = (*response.getAptDemodSettings()->getPalettes()).split(";"); + } + if (channelSettingsKeys.contains("palette")) { + settings.m_palette = response.getAptDemodSettings()->getPalette(); + } + if (channelSettingsKeys.contains("horizontalPixelsPerDegree")) { + settings.m_horizontalPixelsPerDegree = response.getAptDemodSettings()->getHorizontalPixelsPerDegree(); + } + if (channelSettingsKeys.contains("verticalPixelsPerDegree")) { + settings.m_verticalPixelsPerDegree = response.getAptDemodSettings()->getVerticalPixelsPerDegree(); + } + if (channelSettingsKeys.contains("satTimeOffset")) { + settings.m_satTimeOffset = response.getAptDemodSettings()->getSatTimeOffset(); + } + if (channelSettingsKeys.contains("satYaw")) { + settings.m_satYaw = response.getAptDemodSettings()->getSatYaw(); + } if (channelSettingsKeys.contains("rgbColor")) { settings.m_rgbColor = response.getAptDemodSettings()->getRgbColor(); } @@ -474,6 +547,18 @@ void APTDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& resp response.getAptDemodSettings()->setAutoSave(settings.m_autoSave); response.getAptDemodSettings()->setAutoSavePath(new QString(settings.m_autoSavePath)); response.getAptDemodSettings()->setAutoSaveMinScanLines(settings.m_autoSaveMinScanLines); + response.getAptDemodSettings()->setSaveCombined(settings.m_saveCombined); + response.getAptDemodSettings()->setSaveSeparate(settings.m_saveSeparate); + response.getAptDemodSettings()->setSaveProjection(settings.m_saveProjection); + response.getAptDemodSettings()->setScanlinesPerImageUpdate(settings.m_scanlinesPerImageUpdate); + response.getAptDemodSettings()->setTransparencyThreshold(settings.m_transparencyThreshold); + response.getAptDemodSettings()->setOpacityThreshold(settings.m_opacityThreshold); + response.getAptDemodSettings()->setPalettes(new QString(settings.m_palettes.join(";"))); + response.getAptDemodSettings()->setPalette(settings.m_palette); + response.getAptDemodSettings()->setHorizontalPixelsPerDegree(settings.m_horizontalPixelsPerDegree); + response.getAptDemodSettings()->setVerticalPixelsPerDegree(settings.m_verticalPixelsPerDegree); + response.getAptDemodSettings()->setSatTimeOffset(settings.m_satTimeOffset); + response.getAptDemodSettings()->setSatYaw(settings.m_satYaw); response.getAptDemodSettings()->setRgbColor(settings.m_rgbColor); @@ -599,15 +684,51 @@ void APTDemod::webapiFormatChannelSettings( if (channelSettingsKeys.contains("decodeEnabled") || force) { swgAPTDemodSettings->setDecodeEnabled(settings.m_decodeEnabled); } - if (channelSettingsKeys.contains("m_autoSave") || force) { + if (channelSettingsKeys.contains("autoSave") || force) { swgAPTDemodSettings->setAutoSave(settings.m_autoSave); } - if (channelSettingsKeys.contains("m_autoSavePath") || force) { + if (channelSettingsKeys.contains("autoSavePath") || force) { swgAPTDemodSettings->setAutoSavePath(new QString(settings.m_autoSavePath)); } - if (channelSettingsKeys.contains("m_autoSaveMinScanLines") || force) { + if (channelSettingsKeys.contains("autoSaveMinScanLines") || force) { swgAPTDemodSettings->setAutoSaveMinScanLines(settings.m_autoSaveMinScanLines); } + if (channelSettingsKeys.contains("saveCombined") || force) { + swgAPTDemodSettings->setSaveCombined(settings.m_saveCombined); + } + if (channelSettingsKeys.contains("saveSeparate") || force) { + swgAPTDemodSettings->setSaveSeparate(settings.m_saveSeparate); + } + if (channelSettingsKeys.contains("saveProjection") || force) { + swgAPTDemodSettings->setSaveProjection(settings.m_saveProjection); + } + if (channelSettingsKeys.contains("scanlinesPerImageUpdate") || force) { + swgAPTDemodSettings->setScanlinesPerImageUpdate(settings.m_scanlinesPerImageUpdate); + } + if (channelSettingsKeys.contains("transparencyThreshold") || force) { + swgAPTDemodSettings->setTransparencyThreshold(settings.m_transparencyThreshold); + } + if (channelSettingsKeys.contains("opacityThreshold") || force) { + swgAPTDemodSettings->setOpacityThreshold(settings.m_opacityThreshold); + } + if (channelSettingsKeys.contains("palettes") || force) { + swgAPTDemodSettings->setPalettes(new QString(settings.m_palettes.join(";"))); + } + if (channelSettingsKeys.contains("palette") || force) { + swgAPTDemodSettings->setPalette(settings.m_palette); + } + if (channelSettingsKeys.contains("horizontalPixelsPerDegree") || force) { + swgAPTDemodSettings->setHorizontalPixelsPerDegree(settings.m_horizontalPixelsPerDegree); + } + if (channelSettingsKeys.contains("verticalPixelsPerDegree") || force) { + swgAPTDemodSettings->setVerticalPixelsPerDegree(settings.m_verticalPixelsPerDegree); + } + if (channelSettingsKeys.contains("satTimeOffset") || force) { + swgAPTDemodSettings->setSatTimeOffset(settings.m_satTimeOffset); + } + if (channelSettingsKeys.contains("satYaw") || force) { + swgAPTDemodSettings->setSatYaw(settings.m_satYaw); + } if (channelSettingsKeys.contains("rgbColor") || force) { swgAPTDemodSettings->setRgbColor(settings.m_rgbColor); } @@ -654,6 +775,9 @@ int APTDemod::webapiActionsPost( // Reset for new pass m_imageWorker->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); m_basebandSink->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); + if (m_guiMessageQueue) { + m_guiMessageQueue->push(APTDemod::MsgResetDecoder::create()); + } // Save satellite name m_imageWorker->getInputMessageQueue()->push(APTDemodImageWorker::MsgSetSatelliteName::create(*satelliteName)); @@ -662,10 +786,14 @@ int APTDemod::webapiActionsPost( APTDemodSettings settings = m_settings; settings.m_decodeEnabled = true; settings.m_flip = !aos->getNorthToSouthPass(); + settings.m_tle = *aos->getTle(); + settings.m_aosDateTime = QDateTime::fromString(*aos->getDateTime(), Qt::ISODateWithMs); + settings.m_northToSouth = aos->getNorthToSouthPass(); m_inputMessageQueue.push(MsgConfigureAPTDemod::create(settings, false)); - if (m_guiMessageQueue) + if (m_guiMessageQueue) { m_guiMessageQueue->push(MsgConfigureAPTDemod::create(settings, false)); + } } return 202; diff --git a/plugins/channelrx/demodapt/aptdemod.h b/plugins/channelrx/demodapt/aptdemod.h index 8a7890716..571c01d77 100644 --- a/plugins/channelrx/demodapt/aptdemod.h +++ b/plugins/channelrx/demodapt/aptdemod.h @@ -146,6 +146,28 @@ public: {} }; + // Sent from worker to GUI to indicate name of image on Map + class MsgMapImageName : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getName() const { return m_name; } + + static MsgMapImageName* create(const QString &name) + { + return new MsgMapImageName(name); + } + + private: + QString m_name; + + MsgMapImageName(const QString &name) : + Message(), + m_name(name) + { + } + }; + // Sent from GUI to reset decoder class MsgResetDecoder : public Message { MESSAGE_CLASS_DECLARATION diff --git a/plugins/channelrx/demodapt/aptdemodgui.cpp b/plugins/channelrx/demodapt/aptdemodgui.cpp index 7718d4d90..8c4741f0c 100644 --- a/plugins/channelrx/demodapt/aptdemodgui.cpp +++ b/plugins/channelrx/demodapt/aptdemodgui.cpp @@ -25,8 +25,10 @@ #include #include #include +#include #include #include +#include #include "aptdemodgui.h" @@ -50,6 +52,45 @@ #include "aptdemod.h" #include "aptdemodsink.h" #include "aptdemodsettingsdialog.h" +#include "aptdemodselectdialog.h" + +#include "SWGMapItem.h" + +TempScale::TempScale(QGraphicsItem *parent) : + QGraphicsRectItem(parent) +{ + // Temp scale appears to be -100 to +60C + // We just draw -100 to +50C, so it's nicely divides up according to the palette + setRect(30, 30, 25, 240); + m_gradient.setCoordinateMode(QGradient::ObjectBoundingMode); + m_gradient.setStart(0.0, 0.0); + m_gradient.setFinalStop(0.0, 1.0); + + for (int i = 0; i < 240; i++) + { + int idx = (240 - i) * 3; + QColor color((unsigned char)apt_TempPalette[idx], (unsigned char)apt_TempPalette[idx+1], (unsigned char)apt_TempPalette[idx+2]); + m_gradient.setColorAt(i/240.0, color); + } +} + +void TempScale::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + int left = rect().left() + rect().width() + 10; + painter->setPen(QPen(Qt::black)); + painter->setBrush(m_gradient); + painter->drawRect(rect()); + painter->drawText(left, rect().top(), "50C"); + painter->drawText(left, rect().top() + rect().height() * 1 / 6, "25C"); + painter->drawText(left, rect().top() + rect().height() * 2 / 6, "0C"); + painter->drawText(left, rect().top() + rect().height() * 3 / 6, "-25C"); + painter->drawText(left, rect().top() + rect().height() * 4 / 6, "-50C"); + painter->drawText(left, rect().top() + rect().height() * 5 / 6, "-75C"); + painter->drawText(left, rect().top() + rect().height(), "-100C"); +} APTDemodGUI* APTDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) { @@ -76,7 +117,8 @@ QByteArray APTDemodGUI::serialize() const bool APTDemodGUI::deserialize(const QByteArray& data) { - if(m_settings.deserialize(data)) { + if(m_settings.deserialize(data)) + { displaySettings(); applySettings(true); return true; @@ -103,19 +145,29 @@ bool APTDemodGUI::handleMessage(const Message& message) { const APTDemod::MsgImage& imageMsg = (APTDemod::MsgImage&) message; m_image = imageMsg.getImage(); - m_pixmap.convertFromImage(m_image); - if (m_pixmapItem != nullptr) + // Display can be corrupted if we try to drawn an image with 0 height + if (m_image.height() > 0) { - m_pixmapItem->setPixmap(m_pixmap); - if (ui->zoomAll->isChecked()) { + m_pixmap.convertFromImage(m_image); + if (m_pixmapItem != nullptr) + { + m_pixmapItem->setPixmap(m_pixmap); + if (ui->zoomAll->isChecked()) { + ui->image->fitInView(m_pixmapItem, Qt::KeepAspectRatio); + } + } + else + { + m_pixmapItem = m_scene->addPixmap(m_pixmap); + m_pixmapItem->setPos(0, 0); ui->image->fitInView(m_pixmapItem, Qt::KeepAspectRatio); } - } - else - { - m_pixmapItem = m_scene->addPixmap(m_pixmap); - m_pixmapItem->setPos(0, 0); - ui->image->fitInView(m_pixmapItem, Qt::KeepAspectRatio); + bool temp = m_settings.m_channels == APTDemodSettings::TEMPERATURE; + m_tempScale->setVisible(temp); + m_tempScaleBG->setVisible(temp); + if (!temp) { + m_tempText->setVisible(false); + } } QStringList imageTypes = imageMsg.getImageTypes(); @@ -126,20 +178,24 @@ bool APTDemodGUI::handleMessage(const Message& message) } else { - if (imageTypes[0].isEmpty()) + if (imageTypes[0].isEmpty()) { ui->channelALabel->setText("Channel A"); - else + } else { ui->channelALabel->setText(imageTypes[0]); - if (imageTypes[1].isEmpty()) + } + if (imageTypes[1].isEmpty()) { ui->channelBLabel->setText("Channel B"); - else + } else { ui->channelBLabel->setText(imageTypes[1]); + } } QString satelliteName = imageMsg.getSatelliteName(); - if (!satelliteName.isEmpty()) + if (!satelliteName.isEmpty()) { ui->imageContainer->setWindowTitle("Received image from " + satelliteName); - else + } else { ui->imageContainer->setWindowTitle("Received image"); + } + return true; } else if (APTDemod::MsgLine::match(message)) @@ -203,6 +259,18 @@ bool APTDemodGUI::handleMessage(const Message& message) return true; } + else if (APTDemod::MsgMapImageName::match(message)) + { + const APTDemod::MsgMapImageName& mapNameMsg = (APTDemod::MsgMapImageName&) message; + QString name = mapNameMsg.getName(); + if (!m_mapImages.contains(name)) { + m_mapImages.append(name); + } + } + else if (APTDemod::MsgResetDecoder::match(message)) + { + resetDecoder(); + } else if (DSPSignalNotification::match(message)) { DSPSignalNotification& notif = (DSPSignalNotification&) message; @@ -261,24 +329,88 @@ void APTDemodGUI::on_fmDev_valueChanged(int value) applySettings(); } -void APTDemodGUI::on_channels_currentIndexChanged(int index) +void APTDemodGUI::displayLabels() { - m_settings.m_channels = (APTDemodSettings::ChannelSelection)index; if (m_settings.m_channels == APTDemodSettings::BOTH_CHANNELS) { ui->channelALabel->setVisible(true); ui->channelBLabel->setVisible(true); + ui->precipitation->setVisible(true); } else if (m_settings.m_channels == APTDemodSettings::CHANNEL_A) { ui->channelALabel->setVisible(true); ui->channelBLabel->setVisible(false); + ui->precipitation->setVisible(true); + } + else if (m_settings.m_channels == APTDemodSettings::CHANNEL_B) + { + ui->channelALabel->setVisible(false); + ui->channelBLabel->setVisible(true); + ui->precipitation->setVisible(true); + } + else if (m_settings.m_channels == APTDemodSettings::TEMPERATURE) + { + ui->channelALabel->setVisible(false); + ui->channelBLabel->setVisible(false); + ui->precipitation->setVisible(false); } else { ui->channelALabel->setVisible(false); - ui->channelBLabel->setVisible(true); + ui->channelBLabel->setVisible(false); + ui->precipitation->setVisible(false); } +} + +void APTDemodGUI::on_channels_currentIndexChanged(int index) +{ + if (index <= (int)APTDemodSettings::CHANNEL_B) + { + m_settings.m_channels = (APTDemodSettings::ChannelSelection)index; + } + else if (index == (int)APTDemodSettings::TEMPERATURE) + { + m_settings.m_channels = APTDemodSettings::TEMPERATURE; + m_settings.m_precipitationOverlay = false; + } + else + { + m_settings.m_channels = APTDemodSettings::PALETTE; + m_settings.m_palette = index - (int)APTDemodSettings::PALETTE; + m_settings.m_precipitationOverlay = false; + } + displayLabels(); + applySettings(); +} + +void APTDemodGUI::on_transparencyThreshold_valueChanged(int value) +{ + m_settings.m_transparencyThreshold = value; + ui->transparencyThresholdText->setText(QString::number(m_settings.m_transparencyThreshold)); + // Don't applySettings while tracking, as processing an image takes a long time + if (!ui->transparencyThreshold->isSliderDown()) { + applySettings(); + } +} + +void APTDemodGUI::on_transparencyThreshold_sliderReleased() +{ + applySettings(); +} + +void APTDemodGUI::on_opacityThreshold_valueChanged(int value) +{ + m_settings.m_opacityThreshold = value; + ui->opacityThresholdText->setText(QString::number(m_settings.m_opacityThreshold)); + // Don't applySettings while tracking, as processing an image takes a long time + if (!ui->opacityThreshold->isSliderDown()) { + applySettings(); + } +} + +void APTDemodGUI::on_opacityThreshold_sliderReleased() +{ applySettings(); } @@ -315,10 +447,11 @@ void APTDemodGUI::on_precipitation_clicked(bool checked) void APTDemodGUI::on_flip_clicked(bool checked) { m_settings.m_flip = checked; - if (m_settings.m_flip) + if (m_settings.m_flip) { ui->image->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); - else + } else { ui->image->setAlignment(Qt::AlignTop | Qt::AlignHCenter); + } applySettings(); } @@ -328,13 +461,22 @@ void APTDemodGUI::on_startStop_clicked(bool checked) applySettings(); } -void APTDemodGUI::on_resetDecoder_clicked() +void APTDemodGUI::resetDecoder() { - if (m_pixmapItem != nullptr) { + if (m_pixmapItem != nullptr) + { + m_image = QImage(); m_pixmapItem->setPixmap(QPixmap()); } ui->imageContainer->setWindowTitle("Received image"); - // Send message to reset decoder + ui->channelALabel->setText("Channel A"); + ui->channelBLabel->setText("Channel B"); +} + +void APTDemodGUI::on_resetDecoder_clicked() +{ + resetDecoder(); + // Send message to reset decoder to other parts of demod m_aptDemod->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); } @@ -342,7 +484,10 @@ void APTDemodGUI::on_showSettings_clicked() { APTDemodSettingsDialog dialog(&m_settings); if (dialog.exec() == QDialog::Accepted) + { + displayPalettes(); applySettings(); + } } // Save image to disk @@ -356,8 +501,9 @@ void APTDemodGUI::on_saveImage_clicked() if (fileNames.size() > 0) { qDebug() << "APT: Saving image to " << fileNames; - if (!m_image.save(fileNames[0])) + if (!m_image.save(fileNames[0])) { QMessageBox::critical(this, "APT Demodulator", QString("Failed to save image to %1").arg(fileNames[0])); + } } } } @@ -489,10 +635,28 @@ APTDemodGUI::APTDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseban m_zoom = new GraphicsViewZoom(ui->image); // Deleted automatically when view is deleted connect(m_zoom, SIGNAL(zoomed()), this, SLOT(on_image_zoomed())); + // Create slightly transparent white background so labels can be seen + m_tempScale = new TempScale(); + m_tempScale->setZValue(2.0); + m_tempScale->setVisible(false); + QRectF rect = m_tempScale->rect(); + m_tempScaleBG = new QGraphicsRectItem(rect.left()-10, rect.top()-15, rect.width()+60, rect.height()+45); + m_tempScaleBG->setPen(QColor(200, 200, 200, 200)); + m_tempScaleBG->setBrush(QColor(200, 200, 200, 200)); + m_tempScaleBG->setZValue(1.0); + m_tempScaleBG->setVisible(false); + m_tempText = new QGraphicsSimpleTextItem(""); + m_tempText->setZValue(3.0); + m_tempText->setVisible(false); m_scene = new QGraphicsScene(ui->image); + m_scene->addItem(m_tempScale); + m_scene->addItem(m_tempScaleBG); + m_scene->addItem(m_tempText); ui->image->setScene(m_scene); ui->image->show(); + m_scene->installEventFilter(this); + displaySettings(); applySettings(true); } @@ -502,6 +666,55 @@ APTDemodGUI::~APTDemodGUI() delete ui; } +bool APTDemodGUI::eventFilter(QObject *obj, QEvent *event) +{ + if ((obj == m_scene) && (m_settings.m_channels == APTDemodSettings::TEMPERATURE)) + { + if (event->type() == QEvent::GraphicsSceneMouseMove) + { + QGraphicsSceneMouseEvent *mouseEvent = static_cast(event); + // Find temperature under cursor + int x = round(mouseEvent->scenePos().x()); + int y = round(mouseEvent->scenePos().y()); + if ((x >= 0) && (y >= 0) && (x < m_image.width()) && (y < m_image.height())) + { + // Map from colored temperature pixel back to greyscale level + // This is perhaps a bit slow - might be better to give GUI access to greyscale image as well + QRgb p = m_image.pixel(x, y); + int r = qRed(p); + int g = qGreen(p); + int b = qBlue(p); + int i; + for (i = 0; i < 256; i++) + { + if ( (r == (unsigned char)apt_TempPalette[i*3]) + && (g == (unsigned char)apt_TempPalette[i*3+1]) + && (b == (unsigned char)apt_TempPalette[i*3+2])) + { + // Map from palette index to degrees C + int temp = (i / 255.0) * 160.0 - 100.0; + m_tempText->setText(QString("%1C").arg(temp)); + int width = m_tempText->boundingRect().width(); + int height = m_tempText->boundingRect().height(); + QRectF rect = m_tempScaleBG->rect(); + m_tempText->setPos(rect.left()+rect.width()/2-width/2, rect.top()+rect.height()-height-5); + m_tempText->setVisible(true); + break; + } + } + if (i == 256) { + m_tempText->setVisible(false); + } + } + else + { + m_tempText->setVisible(false); + } + } + } + return ChannelGUI::eventFilter(obj, event); +} + void APTDemodGUI::blockApplySettings(bool block) { m_doApplySettings = !block; @@ -538,6 +751,11 @@ void APTDemodGUI::displaySettings() ui->fmDevText->setText(QString("%1k").arg(m_settings.m_fmDeviation / 1000.0, 0, 'f', 1)); ui->fmDev->setValue(m_settings.m_fmDeviation / 100.0); + ui->transparencyThreshold->setValue(m_settings.m_transparencyThreshold); + ui->transparencyThresholdText->setText(QString::number(m_settings.m_transparencyThreshold)); + ui->opacityThreshold->setValue(m_settings.m_opacityThreshold); + ui->opacityThresholdText->setText(QString::number(m_settings.m_opacityThreshold)); + ui->startStop->setChecked(m_settings.m_decodeEnabled); ui->cropNoise->setChecked(m_settings.m_cropNoise); ui->denoise->setChecked(m_settings.m_denoise); @@ -552,14 +770,38 @@ void APTDemodGUI::displaySettings() ui->image->setAlignment(Qt::AlignTop | Qt::AlignHCenter); } - ui->channels->setCurrentIndex((int)m_settings.m_channels); - + displayPalettes(); + displayLabels(); displayStreamIndex(); restoreState(m_rollupState); blockApplySettings(false); } +void APTDemodGUI::displayPalettes() +{ + ui->channels->blockSignals(true); + ui->channels->clear(); + ui->channels->addItem("Both"); + ui->channels->addItem("A"); + ui->channels->addItem("B"); + ui->channels->addItem("Temperature"); + for (auto palette : m_settings.m_palettes) + { + QFileInfo fi(palette); + ui->channels->addItem(fi.baseName()); + } + if (m_settings.m_channels == APTDemodSettings::PALETTE) + { + ui->channels->setCurrentIndex(((int)m_settings.m_channels) + m_settings.m_palette); + } + else + { + ui->channels->setCurrentIndex((int)m_settings.m_channels); + } + ui->channels->blockSignals(false); +} + void APTDemodGUI::displayStreamIndex() { if (m_deviceUISet->m_deviceMIMOEngine) { @@ -598,3 +840,47 @@ void APTDemodGUI::tick() m_tickCount++; } + +void APTDemodGUI::on_deleteImageFromMap_clicked() +{ + // If more than one image, pop up a dialog to select which to delete + if (m_mapImages.size() > 1) + { + APTDemodSelectDialog dialog(m_mapImages, this); + if (dialog.exec() == QDialog::Accepted) + { + for (auto name : dialog.getSelected()) + { + deleteImageFromMap(name); + m_mapImages.removeAll(name); + } + } + } + else + { + for (auto name : m_mapImages) { + deleteImageFromMap(name); + } + m_mapImages.clear(); + } +} + +void APTDemodGUI::deleteImageFromMap(const QString &name) +{ + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_aptDemod, "mapitems"); + if (mapMessageQueues) + { + QList::iterator it = mapMessageQueues->begin(); + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(name)); + swgMapItem->setImage(new QString()); // Set image to "" to delete it + swgMapItem->setType(1); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_aptDemod, swgMapItem); + (*it)->push(msg); + } + } +} diff --git a/plugins/channelrx/demodapt/aptdemodgui.h b/plugins/channelrx/demodapt/aptdemodgui.h index ca68df731..368fe4b26 100644 --- a/plugins/channelrx/demodapt/aptdemodgui.h +++ b/plugins/channelrx/demodapt/aptdemodgui.h @@ -30,6 +30,7 @@ #include #include #include +#include #include "channel/channelgui.h" #include "dsp/channelmarker.h" @@ -52,6 +53,16 @@ namespace Ui { } class APTDemodGUI; +// Temperature scale +class TempScale : public QObject, public QGraphicsRectItem { + Q_OBJECT +public: + TempScale(QGraphicsItem *parent = nullptr); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); +private: + QLinearGradient m_gradient; +}; + class APTDemodGUI : public ChannelGUI { Q_OBJECT @@ -63,6 +74,7 @@ public: QByteArray serialize() const; bool deserialize(const QByteArray& data); virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual bool eventFilter(QObject *watched, QEvent *event) override; public slots: void channelMarkerChangedByCursor(); @@ -87,6 +99,11 @@ private: QGraphicsScene* m_scene; QGraphicsPixmapItem* m_pixmapItem; GraphicsViewZoom* m_zoom; + TempScale *m_tempScale; + QGraphicsRectItem *m_tempScaleBG; + QGraphicsSimpleTextItem *m_tempText; + + QList m_mapImages; explicit APTDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); virtual ~APTDemodGUI(); @@ -94,8 +111,12 @@ private: void blockApplySettings(bool block); void applySettings(bool force = false); void displaySettings(); + void displayPalettes(); + void displayLabels(); void displayStreamIndex(); bool handleMessage(const Message& message); + void deleteImageFromMap(const QString &name); + void resetDecoder(); void leaveEvent(QEvent*); void enterEvent(QEvent*); @@ -105,6 +126,11 @@ private slots: void on_rfBW_valueChanged(int index); void on_fmDev_valueChanged(int value); void on_channels_currentIndexChanged(int index); + void on_transparencyThreshold_valueChanged(int value); + void on_transparencyThreshold_sliderReleased(); + void on_opacityThreshold_valueChanged(int value); + void on_opacityThreshold_sliderReleased(); + void on_deleteImageFromMap_clicked(); void on_cropNoise_clicked(bool checked=false); void on_denoise_clicked(bool checked=false); void on_linear_clicked(bool checked=false); diff --git a/plugins/channelrx/demodapt/aptdemodgui.ui b/plugins/channelrx/demodapt/aptdemodgui.ui index 87f000937..03b7ee2cd 100644 --- a/plugins/channelrx/demodapt/aptdemodgui.ui +++ b/plugins/channelrx/demodapt/aptdemodgui.ui @@ -6,7 +6,7 @@ 0 0 - 451 + 440 569 @@ -18,13 +18,12 @@ - 352 + 440 0 - Liberation Sans 9 @@ -105,7 +104,6 @@ - Liberation Mono 12 @@ -198,7 +196,6 @@ - Liberation Mono 8 @@ -275,26 +272,6 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Vertical - - - @@ -352,6 +329,132 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + + + + T<sub>TH</sub> + + + + + + + + 24 + 24 + + + + Transparency threshold for image on map + + + 255 + + + 10 + + + + + + + + 22 + 0 + + + + 255 + + + + + + + Qt::Vertical + + + + + + + O<sub>TH</sub> + + + + + + + + 24 + 24 + + + + Opacity threshold for image on map + + + 255 + + + 10 + + + + + + + + 22 + 0 + + + + 255 + + + + + + + Qt::Vertical + + + + + + + Delete images from map + + + + + + + :/bin.png:/bin.png + + + @@ -729,7 +832,6 @@ deltaFrequency rfBW - fmDev startStop showSettings resetDecoder diff --git a/plugins/channelrx/demodapt/aptdemodimageworker.cpp b/plugins/channelrx/demodapt/aptdemodimageworker.cpp index b26b42f89..18296680b 100644 --- a/plugins/channelrx/demodapt/aptdemodimageworker.cpp +++ b/plugins/channelrx/demodapt/aptdemodimageworker.cpp @@ -19,17 +19,25 @@ #include #include +#include #include +#include "maincore.h" +#include "util/units.h" + #include "aptdemod.h" #include "aptdemodimageworker.h" +#include "SWGMapItem.h" + MESSAGE_CLASS_DEFINITION(APTDemodImageWorker::MsgConfigureAPTDemodImageWorker, Message) MESSAGE_CLASS_DEFINITION(APTDemodImageWorker::MsgSaveImageToDisk, Message) MESSAGE_CLASS_DEFINITION(APTDemodImageWorker::MsgSetSatelliteName, Message) -APTDemodImageWorker::APTDemodImageWorker() : +APTDemodImageWorker::APTDemodImageWorker(APTDemod *aptDemod) : m_messageQueueToGUI(nullptr), + m_aptDemod(aptDemod), + m_sgp4(nullptr), m_running(false), m_mutex(QMutex::Recursive) { @@ -51,6 +59,8 @@ APTDemodImageWorker::~APTDemodImageWorker() delete[] m_image.prow[y]; delete[] m_tempImage.prow[y]; } + + delete m_sgp4; } void APTDemodImageWorker::reset() @@ -129,6 +139,8 @@ bool APTDemodImageWorker::handleMessage(const Message& cmd) void APTDemodImageWorker::applySettings(const APTDemodSettings& settings, bool force) { (void) force; + + bool callRecalcCoords = false; bool callProcessImage = false; if ((settings.m_cropNoise != m_settings.m_cropNoise) || @@ -137,14 +149,52 @@ void APTDemodImageWorker::applySettings(const APTDemodSettings& settings, bool f (settings.m_histogramEqualise != m_settings.m_histogramEqualise) || (settings.m_precipitationOverlay != m_settings.m_precipitationOverlay) || (settings.m_flip != m_settings.m_flip) || - (settings.m_channels != m_settings.m_channels)) + (settings.m_channels != m_settings.m_channels) || + (settings.m_transparencyThreshold != m_settings.m_transparencyThreshold) || + (settings.m_opacityThreshold != m_settings.m_opacityThreshold) || + (settings.m_palettes != m_settings.m_palettes) || + (settings.m_palette != m_settings.m_palette) || + (settings.m_horizontalPixelsPerDegree != m_settings.m_horizontalPixelsPerDegree) || + (settings.m_verticalPixelsPerDegree != m_settings.m_verticalPixelsPerDegree)) { // Call after settings have been applied callProcessImage = true; } + if ((settings.m_satTimeOffset != m_settings.m_satTimeOffset) || + (settings.m_satYaw != m_settings.m_satYaw)) + { + callRecalcCoords = true; + callProcessImage = true; + } + + if (!settings.m_decodeEnabled && m_settings.m_decodeEnabled) + { + // Decode complete - make sure we do a full image update + // so we aren't left we unprocessed lines + callProcessImage = true; + } + + if (settings.m_palettes != m_settings.m_palettes) + { + // Load colour palettes + m_palettes.clear(); + for (auto palette : settings.m_palettes) + { + QImage img; + img.load(palette); + if ((img.width() != 256) || (img.height() != 256)) { + qWarning() << "APT colour palette " << palette << " is not 256x256 pixels - " << img.width() << "x" << img.height(); + } + m_palettes.append(img); + } + } + m_settings = settings; + if (callRecalcCoords) { + recalcCoords(); + } if (callProcessImage) { sendImageToGUI(); } @@ -153,25 +203,234 @@ void APTDemodImageWorker::applySettings(const APTDemodSettings& settings, bool f void APTDemodImageWorker::resetDecoder() { m_image.nrow = 0; + m_image.zenith = 0; m_tempImage.nrow = 0; + m_tempImage.zenith = 0; m_greyImage = QImage(APT_IMG_WIDTH, APT_MAX_HEIGHT, QImage::Format_Grayscale8); m_greyImage.fill(0); m_colourImage = QImage(APT_IMG_WIDTH, APT_MAX_HEIGHT, QImage::Format_RGB888); m_colourImage.fill(0); m_satelliteName = ""; + m_satCoords.clear(); + m_pixelCoords.clear(); + delete m_sgp4; + m_sgp4 = nullptr; +} + +// Convert Qt QDataTime to QGP4 DateTime +static DateTime qDateTimeToDateTime(QDateTime qdt) +{ + QDateTime utc = qdt.toUTC(); + QDate date = utc.date(); + QTime time = utc.time(); + DateTime dt; + dt.Initialise(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second(), time.msec() * 1000); + return dt; +} + +// Get heading in range [0,360) +static double normaliseHeading(double heading) +{ + return fmod(heading + 360.0, 360.0); +} + +// Get longitude in range -180,180 +static double normaliseLongitude(double lon) +{ + return fmod(lon + 540.0, 360.0) - 180.0; +} + +// Calculate heading (azimuth) in degrees +double APTDemodImageWorker::calcHeading(CoordGeodetic from, CoordGeodetic to) const +{ + // From https://en.wikipedia.org/wiki/Azimuth Section In Geodesy + double flattening = 1.0 / 298.257223563; // For WGS84 ellipsoid + double eSq = flattening * (2.0 - flattening); + double oneMinusESq = (1.0 - flattening) * (1.0 - flattening); + + double tl1 = tan(from.latitude); + double tl2 = tan(to.latitude); + double n1 = 1.0 + oneMinusESq * tl2 * tl2; + double d1 = 1.0 + oneMinusESq * tl1 * tl1; + + double l = to.longitude - from.longitude; + + double alpha; + + if (from.latitude == 0.0) + { + alpha = atan2(sin(l), (oneMinusESq * tan(to.latitude))); + } + else + { + double lambda = oneMinusESq * tan(to.latitude) / tan(from.latitude) + eSq * sqrt(n1/d1); + + alpha = atan2(sin(l), ((lambda - cos(l)) * sin(from.latitude))); + } + + double deg = Units::radiansToDegrees(alpha); + if (!m_settings.m_northToSouth) { + deg += 180.0; + } + deg = normaliseHeading(deg); + return deg; +} + +// CoordGeodetic are in radians. Distance in metres. Bearing in radians. +// https://www.movable-type.co.uk/scripts/latlong.html +// This approximates Earth as spherical. If we need more accurate algorithm, see: +// https://www.movable-type.co.uk/scripts/latlong-vincenty.html +static void calcRadialEndPoint(CoordGeodetic start, double distance, double bearing, CoordGeodetic &end) +{ + double earthRadius = 6378137.0; // At equator + double delta = distance/earthRadius; + end.latitude = std::asin(sin(start.latitude)*cos(delta) + cos(start.latitude)*sin(delta)*cos(bearing)); + end.longitude = start.longitude + std::atan2(sin(bearing)*sin(delta)*cos(start.latitude), cos(delta) - sin(start.latitude)*sin(end.latitude)); + end.longitude = normaliseLongitude(end.longitude); +} + +void APTDemodImageWorker::calcPixelCoords(CoordGeodetic centreCoord, double heading) +{ + // Calculate coordinates of each pixel in a row (swath) + // Assume satellite is at centre pixel, and project +-90 degrees from satellite heading + // https://www.ncei.noaa.gov/pub/data/satellite/publications/podguides/N-15%20thru%20N-19/pdf/APPENDIX%20J%20Instrument%20Scan%20Properties.pdf + // Swath for AVHRR/3 of 2926.6km at 833km altitude over spherical Earth + // Some docs say resolution is 4.0km, but it varies as per fig 4.2.3-1 in: + // https://www.ncei.noaa.gov/pub/data/satellite/publications/podguides/N-15%20thru%20N-19/pdf/2.1%20Section%204.0%20Real%20Time%20Data%20Systems%20for%20Local%20Users%20.pdf + // TODO: Could try to adjust for altitude + + QVector pixelCoords(APT_CH_WIDTH); + pixelCoords[APT_CH_WIDTH/2] = centreCoord; + double heading1 = Units::degreesToRadians(heading + m_settings.m_satYaw + 90.0); + double heading2 = Units::degreesToRadians(heading + m_settings.m_satYaw - 90.0); + for (int i = 1; i <= APT_CH_WIDTH/2; i++) + { + double distance = i * 2926600.0/APT_CH_WIDTH; + calcRadialEndPoint(centreCoord, distance, heading1, pixelCoords[APT_CH_WIDTH/2-i]); + calcRadialEndPoint(centreCoord, distance, heading2, pixelCoords[APT_CH_WIDTH/2+i]); + } + + if (m_settings.m_northToSouth) { + m_pixelCoords.append(pixelCoords); + } else { + m_pixelCoords.prepend(pixelCoords); + } +} + +// Recalculate all pixel coordiantes as satTimeOffset or satYaw has changed +void APTDemodImageWorker::recalcCoords() +{ + if (m_sgp4) + { + m_satCoords.clear(); + m_pixelCoords.clear(); + for (int row = 0; row < m_image.nrow; row++) + { + QDateTime qdt = m_settings.m_aosDateTime.addMSecs(m_settings.m_satTimeOffset * 1000.0f + row * 500); + calcCoords(qdt, row); + } + } +} + +// Calculate pixel coordinates for a single row at the given date and time +void APTDemodImageWorker::calcCoords(QDateTime qdt, int row) +{ + try + { + DateTime dt = qDateTimeToDateTime(qdt); + + // Calculate satellite position + Eci eci = m_sgp4->FindPosition(dt); + + // Convert satellite position to geodetic coordinates (lat and long) + CoordGeodetic geo = eci.ToGeodetic(); + + m_satCoords.append(geo); + + // Calculate satellite heading (Could convert eci.Velocity() instead) + double heading; + if (m_satCoords.size() == 2) + { + heading = calcHeading(m_satCoords[0], m_satCoords[1]); + calcPixelCoords(m_satCoords[0], heading); + calcPixelCoords(m_satCoords[1], heading); + } + else if (m_satCoords.size() > 2) + { + heading = calcHeading(m_satCoords[row-1], m_satCoords[row]); + calcPixelCoords(geo, heading); + } + } + catch (SatelliteException& se) + { + qDebug() << "APTDemodImageWorker::calcCoord: " << se.what(); + } + catch (DecayedException& de) + { + qDebug() << "APTDemodImageWorker::calcCoord: " << de.what(); + } + catch (TleException& tlee) + { + qDebug() << "APTDemodImageWorker::calcCoord: " << tlee.what(); + } +} + + +// Calculate satellite's geodetic coordinates and heading +void APTDemodImageWorker::calcCoord(int row) +{ + if (row == 0) + { + QStringList tle = m_settings.m_tle.trimmed().split("\n"); + if (tle.size() == 3) + { + // Initalise SGP4 + Tle tle(tle[0].toStdString(), tle[1].toStdString(), tle[2].toStdString()); + m_sgp4 = new SGP4(tle); + + // Output time so we can check time offset from when AOS is signalled + qDebug() << "APTDemod: Processing row 0 at " << QDateTime::currentDateTime(); + + calcCoords(m_settings.m_aosDateTime, row); + } + else + { + qDebug() << "APTDemodImageWorker::calcCoord: No TLE for satellite. Is Satellite Tracker running?"; + return; + } + } + else if (m_sgp4 == nullptr) + { + return; + } + else + { + // Calculate time at which + // Don't try to use QDateTime::currentDateTime() as processing & scheduling delays mean + // it's not constant and can sometimes even be 0 + // Lines should be transmitted at 2 per second, so just use number of rows since AOS + // We add a user-defined delay to account for delays in transferring SDR data and demodulation + QDateTime qdt = m_settings.m_aosDateTime.addMSecs(m_settings.m_satTimeOffset * 1000.0f + row * 500); + calcCoords(qdt, row); + } } void APTDemodImageWorker::processPixels(const float *pixels) { - std::copy(pixels, pixels + APT_PROW_WIDTH, m_image.prow[m_image.nrow]); + if (m_image.nrow < APT_MAX_HEIGHT) + { + // Calculate lat and lon of centre of row + calcCoord(m_image.nrow); - if (m_image.nrow % 20 == 0) { // send full image only every 20 lines - sendImageToGUI(); - } else { // else send unprocessed line just to show stg is moving - sendLineToGUI(); + std::copy(pixels, pixels + APT_PROW_WIDTH, m_image.prow[m_image.nrow]); + m_image.nrow++; + + if (m_image.nrow % m_settings.m_scanlinesPerImageUpdate == 0) { // send full image only every N lines + sendImageToGUI(); + } else { // else send unprocessed line just to show stg is moving + sendLineToGUI(); + } } - - m_image.nrow++; } void APTDemodImageWorker::sendImageToGUI() @@ -180,8 +439,278 @@ void APTDemodImageWorker::sendImageToGUI() if (m_messageQueueToGUI) { QStringList imageTypes; - QImage image = processImage(imageTypes); + QImage image = processImage(imageTypes, m_settings.m_channels); m_messageQueueToGUI->push(APTDemod::MsgImage::create(image, imageTypes, m_satelliteName)); + if (m_sgp4) { + sendImageToMap(image, imageTypes); + } + } +} + +// Find the value of the pixel closest to the given coordinates +// If we have previously found a pixel, we constrain the search to be nearby, in order to speed up the search +QRgb APTDemodImageWorker::findNearest(const QImage &image, double latitude, double longitude, int xPrevious, int yPrevious, int &xNearest, int &yNearest) const +{ + double dmin = 360.0 * 360.0 + 90.0 * 90.0; + xNearest = -1; + yNearest = -1; + QRgb p = qRgba(0, 0, 0, 0); // Transparent + + int xMin, xMax; + int yMin, yMax; + + int yStartPostCrop; + int yEndPostCrop; + if (m_settings.m_northToSouth) + { + yStartPostCrop = abs(m_tempImage.zenith); + yEndPostCrop = yStartPostCrop + image.height(); + } + else + { + yStartPostCrop = m_image.nrow - m_tempImage.nrow - abs(m_tempImage.zenith); + yEndPostCrop = yStartPostCrop + image.height(); + } + + if (xPrevious == -1) + { + yMin = yStartPostCrop; + yMax = yEndPostCrop; + xMin = 0; + xMax = m_pixelCoords[0].size(); + } + else + { + int searchRadius = 4; + yMin = yPrevious - searchRadius; + yMax = yPrevious + searchRadius + 1; + xMin = xPrevious - searchRadius; + xMax = xPrevious + searchRadius + 1; + yMin = std::max(yMin, yStartPostCrop); + yMax = std::min(yMax, yEndPostCrop); + xMin = std::max(xMin, 0); + xMax = std::min(xMax, m_pixelCoords[0].size()); + } + + const int ySize = yEndPostCrop-1; + const int xSize = m_pixelCoords[0].size()-1; + for (int y = yMin; y < yMax; y++) + { + for (int x = xMin; x < xMax; x++) + { + CoordGeodetic coord = m_pixelCoords[y][x]; + double dlat = coord.latitude - latitude; + double dlon = coord.longitude - longitude; + double d = dlat * dlat + dlon * dlon; + if (d < dmin) + { + dmin = d; + xNearest = x; + yNearest = y; + // Only use color of pixel if we're inside the source image + if ( ((y != yStartPostCrop) || ((y == yStartPostCrop) && (latitude <= coord.latitude))) + && ((y != ySize) || ((y == ySize) && (latitude >= coord.latitude))) + && ((x != 0) || ((x == 0) && (longitude >= coord.longitude))) + && ((x != xSize) || ((x == xSize) && (longitude <= coord.longitude))) + ) + { + p = image.pixel(x, y - yStartPostCrop); + } + else + { + p = qRgba(0, 0, 0, 0); // Transparent + } + } + } + } + + return p; +} + +// Calculate bounding box for projected image in terms of latitude and longitude +// TODO: Handle crossing of anti-meridian +void APTDemodImageWorker::calcBoundingBox(double &east, double &south, double &west, double &north, const QImage &image) +{ + int start; + if (m_settings.m_northToSouth) { + start = abs(m_tempImage.zenith); + } else { + start = m_image.nrow - m_tempImage.nrow - abs(m_tempImage.zenith); + } + int stop = start + image.height(); + + east = -M_PI; + west = M_PI; + north = -M_PI/2.0; + south = M_PI/2.0; + + //FILE *f = fopen("coords.txt", "w"); + for (int y = start; y < stop; y++) + { + for (int x = 0; x < m_pixelCoords[y].size(); x++) + { + double latitude = m_pixelCoords[y][x].latitude; + double longitude = m_pixelCoords[y][x].longitude; + //fprintf(f, "%f,%f ", Units::radiansToDegrees(m_pixelCoords[y][x].latitude), Units::radiansToDegrees(m_pixelCoords[y][x].longitude)); + south = std::min(latitude, south); + north = std::max(latitude, north); + east = std::max(longitude, east); + west = std::min(longitude, west); + } + //fprintf(f, "\n"); + } + //fclose(f); +} + +// Project satellite image to equidistant cyclindrical projection (Plate Carree) for use on 3D Map +// We've previously computed lat and lon for each pixel in satellite image +// so we just work through coords in projected image, trying to find closest pixel in satellite image +// FIXME: How do we handle sat going over the poles? +QImage APTDemodImageWorker::projectImage(const QImage &image) +{ + double east, south, west, north; + + // Calculate bounding box for image tile + calcBoundingBox(east, south, west, north, image); + m_tileEast = ceil(Units::radiansToDegrees(east)); + m_tileWest = floor(Units::radiansToDegrees(west)); + m_tileNorth = ceil(Units::radiansToDegrees(north)); + m_tileSouth = floor(Units::radiansToDegrees(south)); + + double widthDeg = m_tileEast - m_tileWest; + double heightDeg = m_tileNorth - m_tileSouth; + + int width = widthDeg * m_settings.m_horizontalPixelsPerDegree; + int height = heightDeg * m_settings.m_verticalPixelsPerDegree; + + //image.save("source.png"); + //FILE *f = fopen("mapping.txt", "w"); + QImage projection(width, height, QImage::Format_ARGB32); + int xNearest, yNearest, xPrevious, yPrevious; + xPrevious = -1; + yPrevious = -1; + for (int y = 0; y < height; y++) + { + // Calculate geodetic coords of pixel in projected image + double lat = m_tileNorth - (y / (double)m_settings.m_verticalPixelsPerDegree); + // Reverse search direction in alternate rows, so we are always seaching + // close to previously found pixel + if ((y & 1) == 0) + { + for (int x = 0; x < width; x++) + { + double lon = m_tileWest + (x / (double)m_settings.m_horizontalPixelsPerDegree); + // Find closest pixel in source image + QRgb pixel = findNearest(image, Units::degreesToRadians(lat), Units::degreesToRadians(lon), xPrevious, yPrevious, xNearest, yNearest); + xPrevious = xNearest; + yPrevious = yNearest; + projection.setPixel(x, y, pixel); + //fprintf(f, "%f,%f,%d,%d,%d ", lat, lon, xNearest, yNearest, pixel==0); + } + } + else + { + for (int x = width - 1; x >= 0; x--) + { + double lon = m_tileWest + (x / (double)m_settings.m_horizontalPixelsPerDegree); + // Find closest pixel in source image + QRgb pixel = findNearest(image, Units::degreesToRadians(lat), Units::degreesToRadians(lon), xPrevious, yPrevious, xNearest, yNearest); + xPrevious = xNearest; + yPrevious = yNearest; + projection.setPixel(x, y, pixel); + //fprintf(f, "%f,%f,%d,%d,%d ", lat, lon, xNearest, yNearest, pixel==0); + } + } + //fprintf(f, "\n"); + } + //fclose(f); + + return projection; +} + +// Make an image transparent, so when overlaid on 3D map, we can see the underlying terrain +// Image is full transparent below m_transparencyThreshold and fully opaque above m_opacityThreshold +void APTDemodImageWorker::makeTransparent(QImage &image) +{ + for (int y = 0; y < image.height(); y++) + { + for (int x = 0; x < image.width(); x++) + { + QRgb pixel = image.pixel(x, y); + int grey = qGray(pixel); + if (grey < m_settings.m_transparencyThreshold) + { + // Make fully transparent + pixel = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), 0); + image.setPixel(x, y, pixel); + } + else if (grey < m_settings.m_opacityThreshold) + { + // Make slightly transparent + float opacity = 1.0f - ((m_settings.m_opacityThreshold - grey) / (float)(m_settings.m_opacityThreshold - m_settings.m_transparencyThreshold)); + opacity = opacity * 255.0f; + opacity = std::min(255.0f, opacity); + opacity = std::max(0.0f, opacity); + pixel = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), (int)std::round(opacity)); + image.setPixel(x, y, pixel); + } + } + } +} + +void APTDemodImageWorker::sendImageToMap(QImage image, QStringList imageTypes) +{ + // Send to Map feature + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_aptDemod, "mapitems"); + if (mapMessageQueues) + { + // Only display one channel on map + QImage selectedChannel; + if (m_settings.m_channels == APTDemodSettings::BOTH_CHANNELS) { + selectedChannel = extractImage(image, APTDemodSettings::CHANNEL_B); + } else { + selectedChannel = image; + } + + // Project image to geodetic coords (lat & lon) + selectedChannel = projectImage(selectedChannel); + //selectedChannel.save("projected.png"); + + // Use alpha channel to remove land & sea + makeTransparent(selectedChannel); + + // Encode image as base64 PNG + QByteArray ba; + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + selectedChannel.save(&buffer, "PNG"); + QByteArray data = ba.toBase64(); + + // Create name for the image + QString satName = m_satelliteName; + satName.replace(" ", "_"); + QString name = QString("apt_%1_%2").arg(satName).arg(m_settings.m_aosDateTime.toString("yyyyMMdd_hhmmss")); + + // Send name to GUI + m_messageQueueToGUI->push(APTDemod::MsgMapImageName::create(name)); + + QList::iterator it = mapMessageQueues->begin(); + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(name)); + swgMapItem->setImage(new QString(data)); + swgMapItem->setAltitude(3000.0); // Typical cloud height - So it appears above objects on the ground + swgMapItem->setType(1); + swgMapItem->setImageTileEast(m_tileEast); + swgMapItem->setImageTileWest(m_tileWest); + swgMapItem->setImageTileNorth(m_tileNorth); + swgMapItem->setImageTileSouth(m_tileSouth); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_aptDemod, swgMapItem); + (*it)->push(msg); + } } } @@ -189,7 +718,7 @@ void APTDemodImageWorker::sendLineToGUI() { if (m_messageQueueToGUI) { - float *pixels = m_image.prow[m_image.nrow]; + float *pixels = m_image.prow[m_image.nrow-1]; uchar *line; APTDemod::MsgLine *msg = APTDemod::MsgLine::create(&line); @@ -219,12 +748,12 @@ void APTDemodImageWorker::sendLineToGUI() } } -QImage APTDemodImageWorker::processImage(QStringList& imageTypes) +QImage APTDemodImageWorker::processImage(QStringList& imageTypes, APTDemodSettings::ChannelSelection channels) { copyImage(&m_tempImage, &m_image); - // Calibrate channels according to wavelength - if (m_tempImage.nrow >= APT_CALIBRATION_ROWS) + // Calibrate channels according to wavelength (1.7x to stop flickering) + if (m_tempImage.nrow >= 1.7 * APT_CALIBRATION_ROWS) { m_tempImage.chA = apt_calibrate(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); m_tempImage.chB = apt_calibrate(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); @@ -233,9 +762,9 @@ QImage APTDemodImageWorker::processImage(QStringList& imageTypes) "Visible (0.58-0.68 um)", "Near-IR (0.725-1.0 um)", "Near-IR (1.58-1.64 um)", - "Mid-infrared (3.55-3.93 um)", "Thermal-infrared (10.3-11.3 um)", - "Thermal-infrared (11.5-12.5 um)" + "Thermal-infrared (11.5-12.5 um)", + "Mid-infrared (3.55-3.93 um)" }); imageTypes.append(channelTypes[m_tempImage.chA]); @@ -243,8 +772,9 @@ QImage APTDemodImageWorker::processImage(QStringList& imageTypes) } // Crop noise due to low elevation at top and bottom of image - if (m_settings.m_cropNoise) + if (m_settings.m_cropNoise) { m_tempImage.zenith -= apt_cropNoise(&m_tempImage); + } // Denoise filter if (m_settings.m_denoise) @@ -305,10 +835,65 @@ QImage APTDemodImageWorker::processImage(QStringList& imageTypes) } } } - return extractImage(m_colourImage); + return extractImage(m_colourImage, channels); + } + else if (channels == APTDemodSettings::TEMPERATURE) + { + // Temperature calibration + int satnum = 15; + if (m_satelliteName == "NOAA 18") { + satnum = 18; + } else if (m_satelliteName == "NOAA 19") { + satnum = 19; + } + apt_temperature(satnum, &m_tempImage, APT_CHB_OFFSET, APT_CH_WIDTH); + + // Apply colour palette + for (int r = 0; r < m_tempImage.nrow; r++) + { + uchar *l = m_colourImage.scanLine(r); + for (int i = 0; i < APT_CH_WIDTH; i++) + { + float p = m_tempImage.prow[r][i+APT_CHB_OFFSET]; + uchar q = roundAndClip(p); + l[i*3] = apt_TempPalette[q*3]; + l[i*3+1] = apt_TempPalette[q*3+1]; + l[i*3+2] = apt_TempPalette[q*3+2]; + } + } + return m_colourImage.copy(0, 0, APT_CH_WIDTH, m_tempImage.nrow); + } + else if (channels == APTDemodSettings::PALETTE) + { + if ((m_settings.m_palette >= 0) && (m_settings.m_palette < m_palettes.size())) + { + // Apply colour palette + for (int r = 0; r < m_tempImage.nrow; r++) + { + uchar *l = m_colourImage.scanLine(r); + for (int i = 0; i < APT_CH_WIDTH; i++) + { + float pA = m_tempImage.prow[r][i+APT_CHA_OFFSET]; + float pB = m_tempImage.prow[r][i+APT_CHB_OFFSET]; + uchar qA = roundAndClip(pA); + uchar qB = roundAndClip(pB); + QRgb rgb = m_palettes[m_settings.m_palette].pixel(qA, qB); + l[i*3] = qRed(rgb); + l[i*3+1] = qGreen(rgb); + l[i*3+2] = qBlue(rgb); + } + } + return m_colourImage.copy(0, 0, APT_CH_WIDTH, m_tempImage.nrow); + } + else + { + qDebug() << "APTDemodImageWorker::processImage - Illegal palette number: " << m_settings.m_palette; + return QImage(); + } } else { + // Extract grey-scale image for (int r = 0; r < m_tempImage.nrow; r++) { uchar *l = m_greyImage.scanLine(r); @@ -319,43 +904,92 @@ QImage APTDemodImageWorker::processImage(QStringList& imageTypes) l[i] = roundAndClip(p); } } - return extractImage(m_greyImage); + return extractImage(m_greyImage, channels); } } -QImage APTDemodImageWorker::extractImage(QImage image) +QImage APTDemodImageWorker::extractImage(QImage image, APTDemodSettings::ChannelSelection channels) { - if (m_settings.m_channels == APTDemodSettings::BOTH_CHANNELS) { + if (channels == APTDemodSettings::BOTH_CHANNELS) { return image.copy(0, 0, APT_IMG_WIDTH, m_tempImage.nrow); - } else if (m_settings.m_channels == APTDemodSettings::CHANNEL_A) { + } else if (channels == APTDemodSettings::CHANNEL_A) { return image.copy(APT_CHA_OFFSET, 0, APT_CH_WIDTH, m_tempImage.nrow); } else { return image.copy(APT_CHB_OFFSET, 0, APT_CH_WIDTH, m_tempImage.nrow); } } +void APTDemodImageWorker::prependPath(QString &filename) +{ + if (!m_settings.m_autoSavePath.isEmpty()) + { + if (m_settings.m_autoSavePath.endsWith('/')) { + filename = m_settings.m_autoSavePath + filename; + } else { + filename = m_settings.m_autoSavePath + '/' + filename; + } + } +} + void APTDemodImageWorker::saveImageToDisk() { QStringList imageTypes; - QImage image = processImage(imageTypes); + QImage image = processImage(imageTypes, APTDemodSettings::BOTH_CHANNELS); if (image.height() >= m_settings.m_autoSaveMinScanLines) { QString filename; - QDateTime datetime = QDateTime::currentDateTime(); - filename = QString("apt_%1_%2.png").arg(m_satelliteName.replace(" ", "_")).arg(datetime.toString("yyyyMMdd_hhmm")); + QDateTime dateTime; + QString dt; + if (m_settings.m_aosDateTime.isValid()) { + dateTime = m_settings.m_aosDateTime; + } else { + dateTime = QDateTime::currentDateTime(); + } + dt = dateTime.toString("yyyyMMdd_hhmm"); + QString sat = m_satelliteName; + sat.replace(" ", "_"); - if (!m_settings.m_autoSavePath.isEmpty()) + if (m_settings.m_saveCombined) { - if (m_settings.m_autoSavePath.endsWith('/')) { - filename = m_settings.m_autoSavePath + filename; - } else { - filename = m_settings.m_autoSavePath + '/' + filename; + filename = QString("apt_%1_%2.png").arg(sat).arg(dt); + prependPath(filename); + if (!image.save(filename)) { + qCritical() << "Failed to save APT image to: " << filename; } } - if (!image.save(filename)) { - qCritical() << "Failed to save APT image to: " << filename; + QImage chA = extractImage(image, APTDemodSettings::CHANNEL_A); + QImage chB = extractImage(image, APTDemodSettings::CHANNEL_B); + + if (m_settings.m_saveSeparate) + { + filename = QString("apt_%1_%2_cha.png").arg(sat).arg(dt); + prependPath(filename); + if (!chA.save(filename)) { + qCritical() << "Failed to save APT image to: " << filename; + } + filename = QString("apt_%1_%2_chb.png").arg(sat).arg(dt); + prependPath(filename); + if (!chB.save(filename)) { + qCritical() << "Failed to save APT image to: " << filename; + } + } + + if (m_settings.m_saveProjection) + { + filename = QString("apt_%1_%2_cha_eqi_cylindrical.png").arg(sat).arg(dt); + prependPath(filename); + QImage chAProj = projectImage(chA); + if (!chAProj.save(filename)) { + qCritical() << "Failed to save APT image to: " << filename; + } + filename = QString("apt_%1_%2_chb_eqi_cylindrical.png").arg(sat).arg(dt); + prependPath(filename); + QImage chBProj = projectImage(chB); + if (!chBProj.save(filename)) { + qCritical() << "Failed to save APT image to: " << filename; + } } } } diff --git a/plugins/channelrx/demodapt/aptdemodimageworker.h b/plugins/channelrx/demodapt/aptdemodimageworker.h index eb823b8f7..9af23587a 100644 --- a/plugins/channelrx/demodapt/aptdemodimageworker.h +++ b/plugins/channelrx/demodapt/aptdemodimageworker.h @@ -25,14 +25,22 @@ #include +#include +#include +#include +#include + #include "util/messagequeue.h" #include "util/message.h" #include "aptdemodsettings.h" +class APTDemod; + class APTDemodImageWorker : public QObject { Q_OBJECT + public: class MsgConfigureAPTDemodImageWorker : public Message { MESSAGE_CLASS_DECLARATION @@ -95,7 +103,7 @@ public: } }; - APTDemodImageWorker(); + APTDemodImageWorker(APTDemod *aptDemod); ~APTDemodImageWorker(); void reset(); void startWork(); @@ -109,6 +117,7 @@ private: MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication MessageQueue *m_messageQueueToGUI; APTDemodSettings m_settings; + APTDemod *m_aptDemod; // Image buffers apt_image_t m_image; // Received image @@ -117,18 +126,39 @@ private: QImage m_colourImage; QString m_satelliteName; + QList m_satCoords; // Lat,lon for satellite for each image row - in received order for both pass directions + QVector> m_pixelCoords; // Coordinates for each pixel - reversed y order for south to north passes, so always highest lat first + SGP4 *m_sgp4; // For calculating satellite position + double m_tileEast; // Bounding box for projected image, in degrees + double m_tileWest; + double m_tileNorth; + double m_tileSouth; + + QList m_palettes; + bool m_running; QMutex m_mutex; bool handleMessage(const Message& cmd); void applySettings(const APTDemodSettings& settings, bool force = false); void resetDecoder(); + double calcHeading(CoordGeodetic from, CoordGeodetic to) const; + void calcPixelCoords(CoordGeodetic centreCoord, double heading); + void recalcCoords(); + void calcCoords(QDateTime qdt, int row); + void calcCoord(int row); void processPixels(const float *pixels); void sendImageToGUI(); + QRgb findNearest(const QImage &image, double latitude, double longitude, int xPrevious, int yPrevious, int &xNearest, int &yNearest) const; + void calcBoundingBox(double &east, double &south, double &west, double &north, const QImage &image); + QImage projectImage(const QImage &image); + void makeTransparent(QImage &image); + void sendImageToMap(QImage image, QStringList imageTypes); void sendLineToGUI(); + void prependPath(QString &filename); void saveImageToDisk(); - QImage processImage(QStringList& imageTypes); - QImage extractImage(QImage image); + QImage processImage(QStringList& imageTypes, APTDemodSettings::ChannelSelection channels); + QImage extractImage(QImage image, APTDemodSettings::ChannelSelection channels); static void copyImage(apt_image_t *dst, apt_image_t *src); static uchar roundAndClip(float p); diff --git a/plugins/channelrx/demodapt/aptdemodselectdialog.cpp b/plugins/channelrx/demodapt/aptdemodselectdialog.cpp new file mode 100644 index 000000000..f670a7122 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodselectdialog.cpp @@ -0,0 +1,47 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "aptdemodselectdialog.h" + +APTDemodSelectDialog::APTDemodSelectDialog(const QStringList &list, QWidget* parent) : + QDialog(parent), + ui(new Ui::APTDemodSelectDialog) +{ + ui->setupUi(this); + for (auto item : list) { + ui->list->addItem(item); + } +} + +APTDemodSelectDialog::~APTDemodSelectDialog() +{ + delete ui; +} + +void APTDemodSelectDialog::accept() +{ + QList items = ui->list->selectedItems(); + m_selected.clear(); + for (auto item : items) + { + m_selected.append(item->text()); + } + QDialog::accept(); +} diff --git a/plugins/channelrx/demodapt/aptdemodselectdialog.h b/plugins/channelrx/demodapt/aptdemodselectdialog.h new file mode 100644 index 000000000..b6740f3bf --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodselectdialog.h @@ -0,0 +1,40 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMODSELECTDIALOG_H +#define INCLUDE_APTDEMODSELECTDIALOG_H + +#include "ui_aptdemodselectdialog.h" +#include "aptdemodsettings.h" + +class APTDemodSelectDialog : public QDialog { + Q_OBJECT + +public: + explicit APTDemodSelectDialog(const QStringList &list, QWidget* parent = 0); + ~APTDemodSelectDialog(); + QStringList getSelected() const { return m_selected; } + +private slots: + void accept(); + +private: + QStringList m_selected; + Ui::APTDemodSelectDialog* ui; +}; + +#endif // INCLUDE_APTDEMODSELECTDIALOG_H diff --git a/plugins/channelrx/demodapt/aptdemodselectdialog.ui b/plugins/channelrx/demodapt/aptdemodselectdialog.ui new file mode 100644 index 000000000..404a38a84 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodselectdialog.ui @@ -0,0 +1,98 @@ + + + APTDemodSelectDialog + + + + 0 + 0 + 600 + 304 + + + + + 9 + + + + APT Demodulator Settings + + + + + + + 0 + 0 + + + + + + + Select images to delete from the map + + + + + + + QAbstractItemView::MultiSelection + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + buttonBox + accepted() + APTDemodSelectDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + APTDemodSelectDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channelrx/demodapt/aptdemodsettings.cpp b/plugins/channelrx/demodapt/aptdemodsettings.cpp index c162e3bcd..36beb2c3b 100644 --- a/plugins/channelrx/demodapt/aptdemodsettings.cpp +++ b/plugins/channelrx/demodapt/aptdemodsettings.cpp @@ -48,6 +48,18 @@ void APTDemodSettings::resetToDefaults() m_autoSave = false; m_autoSavePath = ""; m_autoSaveMinScanLines = 200; + m_saveCombined = true; + m_saveSeparate = false; + m_saveProjection = false; + m_scanlinesPerImageUpdate = 20; + m_transparencyThreshold = 100; + m_opacityThreshold = 200; + m_palettes.clear(); + m_palette = 0; + m_horizontalPixelsPerDegree = 10; + m_verticalPixelsPerDegree = 20; + m_satTimeOffset = 0.0f; + m_satYaw = 0.0f; m_rgbColor = QColor(216, 112, 169).rgb(); m_title = "APT Demodulator"; @@ -79,6 +91,8 @@ QByteArray APTDemodSettings::serialize() const s.writeBool(15, m_autoSave); s.writeString(16, m_autoSavePath); s.writeS32(17, m_autoSaveMinScanLines); + s.writeBool(18, m_saveProjection); + s.writeS32(19, m_scanlinesPerImageUpdate); if (m_channelMarker) { s.writeBlob(20, m_channelMarker->serialize()); @@ -96,6 +110,17 @@ QByteArray APTDemodSettings::serialize() const s.writeBlob(28, m_rollupState->serialize()); } + s.writeBool(29, m_saveCombined); + s.writeBool(30, m_saveSeparate); + s.writeS32(31, m_transparencyThreshold); + s.writeS32(32, m_opacityThreshold); + s.writeString(33, m_palettes.join(";")); + s.writeS32(34, m_palette); + s.writeS32(35, m_horizontalPixelsPerDegree); + s.writeS32(36, m_verticalPixelsPerDegree); + s.writeFloat(37, m_satTimeOffset); + s.writeFloat(38, m_satYaw); + return s.final(); } @@ -132,6 +157,8 @@ bool APTDemodSettings::deserialize(const QByteArray& data) d.readBool(15, &m_autoSave, false); d.readString(16, &m_autoSavePath, ""); d.readS32(17, &m_autoSaveMinScanLines, 200); + d.readBool(18, &m_saveProjection, false); + d.readS32(19, &m_scanlinesPerImageUpdate, 20); if (m_channelMarker) { @@ -162,6 +189,19 @@ bool APTDemodSettings::deserialize(const QByteArray& data) m_rollupState->deserialize(bytetmp); } + d.readBool(29, &m_saveCombined, true); + d.readBool(30, &m_saveSeparate, false); + d.readS32(31, &m_transparencyThreshold, 100); + d.readS32(32, &m_opacityThreshold, 200); + d.readString(33, &strtmp); + m_palettes = strtmp.split(";"); + m_palettes.removeAll(""); + d.readS32(34, &m_palette, 0); + d.readS32(35, &m_horizontalPixelsPerDegree, 10); + d.readS32(36, &m_verticalPixelsPerDegree, 20); + d.readFloat(37, &m_satTimeOffset, 0.0f); + d.readFloat(38, &m_satYaw, 0.0f); + return true; } else diff --git a/plugins/channelrx/demodapt/aptdemodsettings.h b/plugins/channelrx/demodapt/aptdemodsettings.h index 10ad3c383..7ed001928 100644 --- a/plugins/channelrx/demodapt/aptdemodsettings.h +++ b/plugins/channelrx/demodapt/aptdemodsettings.h @@ -21,6 +21,7 @@ #include #include +#include class Serializable; @@ -35,18 +36,29 @@ struct APTDemodSettings bool m_histogramEqualise; bool m_precipitationOverlay; bool m_flip; - enum ChannelSelection {BOTH_CHANNELS, CHANNEL_A, CHANNEL_B} m_channels; + enum ChannelSelection {BOTH_CHANNELS, CHANNEL_A, CHANNEL_B, TEMPERATURE, PALETTE} m_channels; bool m_decodeEnabled; bool m_satelliteTrackerControl; //! Whether Sat Tracker can set direction of pass QString m_satelliteName; //!< All, NOAA 15, NOAA 18 or NOAA 19 bool m_autoSave; QString m_autoSavePath; int m_autoSaveMinScanLines; + bool m_saveCombined; + bool m_saveSeparate; + bool m_saveProjection; + int m_scanlinesPerImageUpdate; + int m_transparencyThreshold; + int m_opacityThreshold; + QStringList m_palettes; // List of 256x256 images to use a colour palette + int m_palette; // Index in to m_palettes - only if m_channels==PALETTE + int m_horizontalPixelsPerDegree; // Resolution for projected image + int m_verticalPixelsPerDegree; + float m_satTimeOffset; + float m_satYaw; quint32 m_rgbColor; QString m_title; Serializable *m_channelMarker; - QString m_audioDeviceName; int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). bool m_useReverseAPI; QString m_reverseAPIAddress; @@ -55,6 +67,11 @@ struct APTDemodSettings uint16_t m_reverseAPIChannelIndex; Serializable *m_rollupState; + // The following are really working state, rather than settings + QString m_tle; // Satelite two-line elements, from satellite tracker + QDateTime m_aosDateTime; // When decoder was started (may not be current time, if replaying old file) + bool m_northToSouth; // Separate from flip, in case user changes it + APTDemodSettings(); void resetToDefaults(); void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } diff --git a/plugins/channelrx/demodapt/aptdemodsettingsdialog.cpp b/plugins/channelrx/demodapt/aptdemodsettingsdialog.cpp index 67b160826..028000626 100644 --- a/plugins/channelrx/demodapt/aptdemodsettingsdialog.cpp +++ b/plugins/channelrx/demodapt/aptdemodsettingsdialog.cpp @@ -25,12 +25,27 @@ APTDemodSettingsDialog::APTDemodSettingsDialog(APTDemodSettings *settings, QWidg m_settings(settings), ui(new Ui::APTDemodSettingsDialog) { + int idx; ui->setupUi(this); ui->satelliteTrackerControl->setChecked(settings->m_satelliteTrackerControl); ui->satellite->setCurrentText(settings->m_satelliteName); ui->autoSave->setChecked(settings->m_autoSave); + ui->saveCombined->setChecked(settings->m_saveCombined); + ui->saveSeparate->setChecked(settings->m_saveSeparate); + ui->saveProjection->setChecked(settings->m_saveProjection); ui->autoSavePath->setText(settings->m_autoSavePath); ui->minScanlines->setValue(settings->m_autoSaveMinScanLines); + ui->scanlinesPerImageUpdate->setValue(settings->m_scanlinesPerImageUpdate); + idx = ui->horizontalPixelsPerDegree->findText(QString::number(settings->m_horizontalPixelsPerDegree)); + ui->horizontalPixelsPerDegree->setCurrentIndex(idx); + idx = ui->verticalPixelsPerDegree->findText(QString::number(settings->m_verticalPixelsPerDegree)); + ui->verticalPixelsPerDegree->setCurrentIndex(idx); + ui->satTimeOffset->setValue(settings->m_satTimeOffset); + ui->satYaw->setValue(settings->m_satYaw); + for (auto file : settings->m_palettes) { + ui->palettes->addItem(file); + } + on_autoSave_clicked(settings->m_autoSave); } APTDemodSettingsDialog::~APTDemodSettingsDialog() @@ -43,8 +58,20 @@ void APTDemodSettingsDialog::accept() m_settings->m_satelliteTrackerControl = ui->satelliteTrackerControl->isChecked(); m_settings->m_satelliteName = ui->satellite->currentText(); m_settings->m_autoSave = ui->autoSave->isChecked(); + m_settings->m_saveCombined = ui->saveCombined->isChecked(); + m_settings->m_saveSeparate = ui->saveSeparate->isChecked(); + m_settings->m_saveProjection = ui->saveProjection->isChecked(); m_settings->m_autoSavePath = ui->autoSavePath->text(); m_settings->m_autoSaveMinScanLines = ui->minScanlines->value(); + m_settings->m_scanlinesPerImageUpdate = ui->scanlinesPerImageUpdate->value(); + m_settings->m_palettes.clear(); + m_settings->m_horizontalPixelsPerDegree = ui->horizontalPixelsPerDegree->currentText().toInt(); + m_settings->m_verticalPixelsPerDegree = ui->verticalPixelsPerDegree->currentText().toInt(); + m_settings->m_satTimeOffset = ui->satTimeOffset->value(); + m_settings->m_satYaw = ui->satYaw->value(); + for (int i = 0; i < ui->palettes->count(); i++) { + m_settings->m_palettes.append(ui->palettes->item(i)->text()); + } QDialog::accept(); } @@ -54,3 +81,41 @@ void APTDemodSettingsDialog::on_autoSavePathBrowse_clicked() QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); ui->autoSavePath->setText(dir); } + +void APTDemodSettingsDialog::on_autoSave_clicked(bool checked) +{ + /* Commented out until theme greys out disabled widgets + ui->saveProjectionLabel->setEnabled(checked); + ui->saveCombined->setEnabled(checked); + ui->saveSeparate->setEnabled(checked); + ui->saveProjection->setEnabled(checked); + ui->autoSavePathLabel->setEnabled(checked); + ui->autoSavePath->setEnabled(checked); + ui->autoSavePathBrowse->setEnabled(checked); + ui->minScanlinesLabel->setEnabled(checked); + ui->minScanlines->setEnabled(checked); + */ +} + +void APTDemodSettingsDialog::on_addPalette_clicked() +{ + QFileDialog fileDialog(nullptr, "Select palette files", "", "*.png;*.bmp"); + fileDialog.setFileMode(QFileDialog::ExistingFiles); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + for (auto fileName : fileNames) { + ui->palettes->addItem(fileName); + } + } +} + +void APTDemodSettingsDialog::on_removePalette_clicked() +{ + QList items = ui->palettes->selectedItems(); + for (auto item : items) + { + ui->palettes->removeItemWidget(item); + delete item; + } +} diff --git a/plugins/channelrx/demodapt/aptdemodsettingsdialog.h b/plugins/channelrx/demodapt/aptdemodsettingsdialog.h index f43aaddd1..da8dfd65f 100644 --- a/plugins/channelrx/demodapt/aptdemodsettingsdialog.h +++ b/plugins/channelrx/demodapt/aptdemodsettingsdialog.h @@ -33,6 +33,9 @@ public: private slots: void accept(); void on_autoSavePathBrowse_clicked(); + void on_autoSave_clicked(bool checked); + void on_addPalette_clicked(); + void on_removePalette_clicked(); private: Ui::APTDemodSettingsDialog* ui; diff --git a/plugins/channelrx/demodapt/aptdemodsettingsdialog.ui b/plugins/channelrx/demodapt/aptdemodsettingsdialog.ui index 4f146269f..f857c73bc 100644 --- a/plugins/channelrx/demodapt/aptdemodsettingsdialog.ui +++ b/plugins/channelrx/demodapt/aptdemodsettingsdialog.ui @@ -6,13 +6,12 @@ 0 0 - 385 - 212 + 600 + 576 - Liberation Sans 9 @@ -29,69 +28,31 @@ - - + + - Path to save image + Enable Satellite Tracker control - - - - - - Path to save images to - - - - - - - - - - - :/load.png:/load.png - - - - - - - - - Minimum scanlines - - - - - + + - Enter the minimum number of scanlines in an image (after cropping) for it to be automatically saved + Check to enable control by Satellite Tracker feature - - 1 - - - 30000 - - - 100 - - - 200 + + - + Satellite - + Select which satellite this channel will be used for @@ -121,23 +82,316 @@ - - - - Check to enable control by Satellite Tracker feature - + + - Enable Satellite Tracker control + Auto save images - + Check to automatically save images when acquisition is stopped or LOS - Auto save image + + + + + + + + Save combined image + + + + + + + Save a combined image of both channel A and B + + + + + + + + + + Save separate images + + + + + + + Save images from channels A and B to separate files + + + + + + + + + + Save projected images + + + + + + + Saves the equidistant cylindrical projected image + + + + + + + + + + Path to save images + + + + + + + + + Path to save images to + + + + + + + + + + + :/load.png:/load.png + + + + + + + + + Minimum scanlines for auto save + + + + + + + Enter the minimum number of scanlines in an image (after cropping) for it to be automatically saved + + + 1 + + + 30000 + + + 100 + + + 200 + + + + + + + Scanlines per image update + + + + + + + How often the image processing functions are applied to the image and how often it is sent to the map + + + 1 + + + 9999 + + + + + + + Colour palettes + + + + + + + QAbstractItemView::MultiSelection + + + + + + + + + Add + + + + + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Number of pixels per degree longitude in projected image + + + + 10 + + + + + 15 + + + + + 20 + + + + + + + + Number of pixels per degree latitude in projected image + + + + 20 + + + + + 30 + + + + + 40 + + + + + 45 + + + + + 50 + + + + + 55 + + + + + 60 + + + + + + + + Horizontal pixels per degree + + + + + + + Vertical pixels per degree + + + + + + + Satellite position time offset (s) + + + + + + + Time offset in seconds to add when calculating satellites position. +This may be used to help align images on the map. + + + 1 + + + -100.000000000000000 + + + + + + + Satellite yaw correction (°) + + + + + + + Add yaw offset to help with aligning images on the map. + + + 2 + + + -10.000000000000000 + + + 10.000000000000000 + + + 0.250000000000000 @@ -160,9 +414,16 @@ satelliteTrackerControl satellite autoSave + saveCombined + saveSeparate + saveProjection autoSavePath autoSavePathBrowse minScanlines + scanlinesPerImageUpdate + palettes + addPalette + removePalette diff --git a/swagger/sdrangel/api/swagger/include/APTDemod.yaml b/swagger/sdrangel/api/swagger/include/APTDemod.yaml index 76dff0050..70126ec0f 100644 --- a/swagger/sdrangel/api/swagger/include/APTDemod.yaml +++ b/swagger/sdrangel/api/swagger/include/APTDemod.yaml @@ -81,6 +81,12 @@ APTDemodActions: northToSouthPass: description: "Satellite is passing from the North to the South (1) or South to North (0)" type: integer + tle: + description: "Two line elements for satellite" + type: string + dateTime: + description: "Date and time of AOS (May differ from system clock when replaying old passes)" + type: string los: description: "Loss of signal" type: object