APT Demod updates.

Add projection of image on to 3D map.
Add support for temperature map.
Add support for colour palettes for image enhancements.
Fix IR channel names.
This commit is contained in:
Jon Beniston 2022-02-04 16:36:02 +00:00
parent 26b8619bb1
commit 7b6708a256
19 changed files with 1974 additions and 155 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

View File

@ -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 $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
endif()

View File

@ -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;

View File

@ -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

View File

@ -25,8 +25,10 @@
#include <QAction>
#include <QRegExp>
#include <QFileDialog>
#include <QFileInfo>
#include <QGraphicsScene>
#include <QGraphicsPixmapItem>
#include <QGraphicsSceneMouseEvent>
#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<QGraphicsSceneMouseEvent *>(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<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_aptDemod, "mapitems");
if (mapMessageQueues)
{
QList<MessageQueue*>::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);
}
}
}

View File

@ -30,6 +30,7 @@
#include <QMenu>
#include <QImage>
#include <QPixmap>
#include <QGraphicsRectItem>
#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<QString> 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);

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>451</width>
<width>440</width>
<height>569</height>
</rect>
</property>
@ -18,13 +18,12 @@
</property>
<property name="minimumSize">
<size>
<width>352</width>
<width>440</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
@ -105,7 +104,6 @@
</property>
<property name="font">
<font>
<family>Liberation Mono</family>
<pointsize>12</pointsize>
</font>
</property>
@ -198,7 +196,6 @@
</property>
<property name="font">
<font>
<family>Liberation Mono</family>
<pointsize>8</pointsize>
</font>
</property>
@ -275,26 +272,6 @@
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="fmDevLabel">
<property name="text">
@ -352,6 +329,132 @@
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="transparencyThresholdLabel">
<property name="text">
<string>T&lt;sub&gt;TH&lt;/sub&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="transparencyThreshold">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Transparency threshold for image on map</string>
</property>
<property name="maximum">
<number>255</number>
</property>
<property name="pageStep">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="transparencyThresholdText">
<property name="minimumSize">
<size>
<width>22</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>255</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="opacityThresholdLabel">
<property name="text">
<string>O&lt;sub&gt;TH&lt;/sub&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="opacityThreshold">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Opacity threshold for image on map</string>
</property>
<property name="maximum">
<number>255</number>
</property>
<property name="pageStep">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="opacityThresholdText">
<property name="minimumSize">
<size>
<width>22</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>255</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="deleteImageFromMap">
<property name="toolTip">
<string>Delete images from map</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/bin.png</normaloff>:/bin.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item>
@ -729,7 +832,6 @@
<tabstops>
<tabstop>deltaFrequency</tabstop>
<tabstop>rfBW</tabstop>
<tabstop>fmDev</tabstop>
<tabstop>startStop</tabstop>
<tabstop>showSettings</tabstop>
<tabstop>resetDecoder</tabstop>

View File

@ -19,17 +19,25 @@
#include <algorithm>
#include <QTime>
#include <QBuffer>
#include <QDebug>
#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<CoordGeodetic> 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<MessageQueue*> *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<MessageQueue*>::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;
}
}
}
}

View File

@ -25,14 +25,22 @@
#include <apt.h>
#include <CoordTopocentric.h>
#include <CoordGeodetic.h>
#include <Observer.h>
#include <SGP4.h>
#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<CoordGeodetic> m_satCoords; // Lat,lon for satellite for each image row - in received order for both pass directions
QVector<QVector<CoordGeodetic>> 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<QImage> 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);

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QFileDialog>
#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<QListWidgetItem *> items = ui->list->selectedItems();
m_selected.clear();
for (auto item : items)
{
m_selected.append(item->text());
}
QDialog::accept();
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>APTDemodSelectDialog</class>
<widget class="QDialog" name="APTDemodSelectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>304</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="windowTitle">
<string>APT Demodulator Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="listLabel">
<property name="text">
<string>Select images to delete from the map</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="list">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
<include location="icons.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>APTDemodSelectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>APTDemodSelectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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

View File

@ -21,6 +21,7 @@
#include <QByteArray>
#include <QHash>
#include <QDateTime>
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; }

View File

@ -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<QListWidgetItem *> items = ui->palettes->selectedItems();
for (auto item : items)
{
ui->palettes->removeItemWidget(item);
delete item;
}
}

View File

@ -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;

View File

@ -6,13 +6,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>385</width>
<height>212</height>
<width>600</width>
<height>576</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
@ -29,69 +28,31 @@
</sizepolicy>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="5" column="0">
<widget class="QLabel" name="autoSavePathLabel">
<item row="0" column="0">
<widget class="QLabel" name="satelliteTrackerControlLabel">
<property name="text">
<string>Path to save image</string>
<string>Enable Satellite Tracker control</string>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="autoSavePath">
<property name="toolTip">
<string>Path to save images to</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="autoSavePathBrowse">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/load.png</normaloff>:/load.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QLabel" name="minScanlinesLabel">
<property name="text">
<string>Minimum scanlines</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="minScanlines">
<item row="0" column="1">
<widget class="QCheckBox" name="satelliteTrackerControl">
<property name="toolTip">
<string>Enter the minimum number of scanlines in an image (after cropping) for it to be automatically saved</string>
<string>Check to enable control by Satellite Tracker feature</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>200</number>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QLabel" name="satelliteLabel">
<property name="text">
<string>Satellite</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="3" column="1">
<widget class="QComboBox" name="satellite">
<property name="toolTip">
<string>Select which satellite this channel will be used for</string>
@ -121,23 +82,316 @@
</item>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="satelliteTrackerControl">
<property name="toolTip">
<string>Check to enable control by Satellite Tracker feature</string>
</property>
<item row="4" column="0">
<widget class="QLabel" name="autoSaveLabel">
<property name="text">
<string>Enable Satellite Tracker control</string>
<string>Auto save images</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="4" column="1">
<widget class="QCheckBox" name="autoSave">
<property name="toolTip">
<string>Check to automatically save images when acquisition is stopped or LOS</string>
</property>
<property name="text">
<string>Auto save image</string>
<string/>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="saveCombinedLabel">
<property name="text">
<string>Save combined image</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="saveCombined">
<property name="toolTip">
<string>Save a combined image of both channel A and B</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="saveSeparateLabel">
<property name="text">
<string>Save separate images</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="saveSeparate">
<property name="toolTip">
<string>Save images from channels A and B to separate files</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="saveProjectionLabel">
<property name="text">
<string>Save projected images</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="saveProjection">
<property name="toolTip">
<string>Saves the equidistant cylindrical projected image</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="autoSavePathLabel">
<property name="text">
<string>Path to save images</string>
</property>
</widget>
</item>
<item row="9" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="autoSavePath">
<property name="toolTip">
<string>Path to save images to</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="autoSavePathBrowse">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/load.png</normaloff>:/load.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="11" column="0">
<widget class="QLabel" name="minScanlinesLabel">
<property name="text">
<string>Minimum scanlines for auto save</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QSpinBox" name="minScanlines">
<property name="toolTip">
<string>Enter the minimum number of scanlines in an image (after cropping) for it to be automatically saved</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>200</number>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="scanlinesPerImageUpdateLabel">
<property name="text">
<string>Scanlines per image update</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QSpinBox" name="scanlinesPerImageUpdate">
<property name="toolTip">
<string>How often the image processing functions are applied to the image and how often it is sent to the map</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9999</number>
</property>
</widget>
</item>
<item row="17" column="0">
<widget class="QLabel" name="palettesLabel">
<property name="text">
<string>Colour palettes</string>
</property>
</widget>
</item>
<item row="17" column="1">
<widget class="QListWidget" name="palettes">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
<item row="18" column="1">
<layout class="QHBoxLayout" name="buttonsHhorizontalLayout">
<item>
<widget class="QPushButton" name="addPalette">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removePalette">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<spacer name="buttonsHorizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="13" column="1">
<widget class="QComboBox" name="horizontalPixelsPerDegree">
<property name="toolTip">
<string>Number of pixels per degree longitude in projected image</string>
</property>
<item>
<property name="text">
<string>10</string>
</property>
</item>
<item>
<property name="text">
<string>15</string>
</property>
</item>
<item>
<property name="text">
<string>20</string>
</property>
</item>
</widget>
</item>
<item row="14" column="1">
<widget class="QComboBox" name="verticalPixelsPerDegree">
<property name="toolTip">
<string>Number of pixels per degree latitude in projected image</string>
</property>
<item>
<property name="text">
<string>20</string>
</property>
</item>
<item>
<property name="text">
<string>30</string>
</property>
</item>
<item>
<property name="text">
<string>40</string>
</property>
</item>
<item>
<property name="text">
<string>45</string>
</property>
</item>
<item>
<property name="text">
<string>50</string>
</property>
</item>
<item>
<property name="text">
<string>55</string>
</property>
</item>
<item>
<property name="text">
<string>60</string>
</property>
</item>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="horizontalPixelsPerDegreeLabel">
<property name="text">
<string>Horizontal pixels per degree</string>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="verticalPixelsPerDegreeLabel">
<property name="text">
<string>Vertical pixels per degree</string>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="satTimeOffsetLabel">
<property name="text">
<string>Satellite position time offset (s)</string>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QDoubleSpinBox" name="satTimeOffset">
<property name="toolTip">
<string>Time offset in seconds to add when calculating satellites position.
This may be used to help align images on the map.</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>-100.000000000000000</double>
</property>
</widget>
</item>
<item row="16" column="0">
<widget class="QLabel" name="satYawLabel">
<property name="text">
<string>Satellite yaw correction (°)</string>
</property>
</widget>
</item>
<item row="16" column="1">
<widget class="QDoubleSpinBox" name="satYaw">
<property name="toolTip">
<string>Add yaw offset to help with aligning images on the map.</string>
</property>
<property name="decimals">
<number>2</number>
</property>
<property name="minimum">
<double>-10.000000000000000</double>
</property>
<property name="maximum">
<double>10.000000000000000</double>
</property>
<property name="singleStep">
<double>0.250000000000000</double>
</property>
</widget>
</item>
@ -160,9 +414,16 @@
<tabstop>satelliteTrackerControl</tabstop>
<tabstop>satellite</tabstop>
<tabstop>autoSave</tabstop>
<tabstop>saveCombined</tabstop>
<tabstop>saveSeparate</tabstop>
<tabstop>saveProjection</tabstop>
<tabstop>autoSavePath</tabstop>
<tabstop>autoSavePathBrowse</tabstop>
<tabstop>minScanlines</tabstop>
<tabstop>scanlinesPerImageUpdate</tabstop>
<tabstop>palettes</tabstop>
<tabstop>addPalette</tabstop>
<tabstop>removePalette</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>

View File

@ -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