File indexing completed on 2024-05-05 15:56:59

0001 /*
0002     SPDX-FileCopyrightText: 2023 Hy Murveit <hy@murveit.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "imageoverlaycomponent.h"
0008 
0009 #include "kstars.h"
0010 #include "Options.h"
0011 #include "skypainter.h"
0012 #include "skymap.h"
0013 #ifdef HAVE_CFITSIO
0014 #include "fitsviewer/fitsdata.h"
0015 #endif
0016 #include "auxiliary/kspaths.h"
0017 #include "ekos/auxiliary/solverutils.h"
0018 #include "ekos/auxiliary/stellarsolverprofile.h"
0019 
0020 #include <QTableWidget>
0021 #include <QImageReader>
0022 #include <QCheckBox>
0023 #include <QComboBox>
0024 #include <QtConcurrent>
0025 #include <QRegularExpression>
0026 
0027 namespace
0028 {
0029 
0030 enum ColumnIndex
0031 {
0032     FILENAME_COL = 0,
0033     //ENABLED_COL,
0034     //NICKNAME_COL,
0035     STATUS_COL,
0036     RA_COL,
0037     DEC_COL,
0038     ARCSEC_PER_PIXEL_COL,
0039     ORIENTATION_COL,
0040     WIDTH_COL,
0041     HEIGHT_COL,
0042     EAST_TO_RIGHT_COL,
0043     NUM_COLUMNS
0044 };
0045 
0046 // These needs to be syncronized with enum Status and initializeGui::StatusNames().
0047 constexpr int UNPROCESSED_INDEX = 0;
0048 constexpr int OK_INDEX = 4;
0049 
0050 // Helper to create the image overlay table.
0051 // Start the table, displaying the heading and timing information, common to all sessions.
0052 void setupTable(QTableWidget *table)
0053 {
0054     table->clear();
0055     table->setRowCount(0);
0056     table->setColumnCount(NUM_COLUMNS);
0057     table->setShowGrid(false);
0058     table->setWordWrap(true);
0059 
0060     QStringList HeaderNames =
0061     {
0062         i18n("Filename"),
0063         //    "", "Nickname",
0064         i18n("Status"), i18n("RA"), i18n("DEC"), i18n("A-S/px"), i18n("Angle"),
0065         i18n("Width"), i18n("Height"), i18n("EastRight")
0066     };
0067     table->setHorizontalHeaderLabels(HeaderNames);
0068 }
0069 
0070 // This initializes an item in the table widget.
0071 void setupTableItem(QTableWidget *table, int row, int column, const QString &text, bool editable = true)
0072 {
0073     if (table->rowCount() < row + 1)
0074         table->setRowCount(row + 1);
0075     if (column >= NUM_COLUMNS)
0076         return;
0077     QTableWidgetItem *item = new QTableWidgetItem();
0078     item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
0079     item->setText(text);
0080     if (!editable)
0081         item->setFlags(item->flags() ^ Qt::ItemIsEditable);
0082     table->setItem(row, column, item);
0083 }
0084 
0085 // Helper for sorting the overlays alphabetically (case-insensitive).
0086 bool overlaySorter(const ImageOverlay &o1, const ImageOverlay &o2)
0087 {
0088     return o1.m_Filename.toLower() < o2.m_Filename.toLower();
0089 }
0090 
0091 // The dms method for initializing from text requires knowing if the input is degrees or hours.
0092 // This is a crude way to detect HMS input, and may not internationalize well.
0093 bool isHMS(const QString &input)
0094 {
0095     QString trimmedInput = input.trimmed();
0096     // Just 14h
0097     QRegularExpression re1("^(\\d+)\\s*h$");
0098     // 14h 2m
0099     QRegularExpression re2("^(\\d+)\\s*h\\s(\\d+)\\s*(?:[m\']?)$");
0100     // 14h 2m 3.2s
0101     QRegularExpression re3("^(\\d+)\\s*h\\s(\\d+)\\s*[m\'\\s]\\s*(\\d+\\.*\\d*)\\s*([s\"]?)$");
0102 
0103     return re1.match(trimmedInput).hasMatch() ||
0104            re2.match(trimmedInput).hasMatch() ||
0105            re3.match(trimmedInput).hasMatch();
0106 }
0107 
0108 // Even if an image is already solved, the user may have changed the status in the UI.
0109 bool shouldSolveAnyway(QTableWidget *table, int row)
0110 {
0111     QComboBox *item = dynamic_cast<QComboBox*>(table->cellWidget(row, STATUS_COL));
0112     if (!item) return false;
0113     return (item->currentIndex() != OK_INDEX);
0114 }
0115 
0116 QString toDecString(const dms &dec)
0117 {
0118     // Sadly DMS::setFromString doesn't parse dms::toDMSString()
0119     // return dec.toDMSString();
0120     return QString("%1 %2' %3\"").arg(dec.degree()).arg(dec.arcmin()).arg(dec.arcsec());
0121 }
0122 
0123 QString toDecString(double dec)
0124 {
0125     return toDecString(dms(dec));
0126 }
0127 }  // namespace
0128 
0129 ImageOverlayComponent::ImageOverlayComponent(SkyComposite *parent) : SkyComponent(parent)
0130 {
0131     QDir dir = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/imageOverlays");
0132     dir.mkpath(".");
0133     m_Directory = dir.absolutePath();
0134     connect(&m_TryAgainTimer, &QTimer::timeout, this, &ImageOverlayComponent::tryAgain, Qt::UniqueConnection);
0135     connect(this, &ImageOverlayComponent::updateLog, this, &ImageOverlayComponent::updateStatusDisplay, Qt::UniqueConnection);
0136 
0137     // Get the latest from the User DB
0138     loadFromUserDB();
0139 
0140     // The rest of the initialization happens when we get the setWidgets() call.
0141 }
0142 
0143 // Validate the user inputs, and if invalid, replace with the previous values.
0144 void ImageOverlayComponent::cellChanged(int row, int col)
0145 {
0146     if (!m_Initialized || col < 0 || col >= NUM_COLUMNS || row < 0 || row >= m_ImageOverlayTable->rowCount()) return;
0147     // Note there are a couple columns with widgets instead of normal text items.
0148     // This method shouldn't get called for those, but...
0149     if (col == STATUS_COL || col == EAST_TO_RIGHT_COL) return;
0150 
0151     QTableWidgetItem *item = m_ImageOverlayTable->item(row, col);
0152     if (!item) return;
0153 
0154     disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged);
0155     QString itemString = item->text();
0156     auto overlay = m_Overlays[row];
0157     if (col == RA_COL)
0158     {
0159         dms raDMS;
0160         const bool useHMS = isHMS(itemString);
0161         const bool raOK = raDMS.setFromString(itemString, !useHMS);
0162         if (!raOK)
0163         {
0164             item->setText(dms(overlay.m_RA).toHMSString());
0165             QString msg = i18n("Bad RA string entered for %1. Reset to original value.", overlay.m_Filename);
0166             emit updateLog(msg);
0167         }
0168         else
0169             // Re-format the user-entered value.
0170             item->setText(raDMS.toHMSString());
0171     }
0172     else if (col == DEC_COL)
0173     {
0174         dms decDMS;
0175         const bool decOK = decDMS.setFromString(itemString);
0176         if (!decOK)
0177         {
0178             item->setText(toDecString(overlay.m_DEC));
0179             QString msg = i18n("Bad DEC string entered for %1. Reset to original value.", overlay.m_Filename);
0180             emit updateLog(msg);
0181         }
0182         else
0183             item->setText(toDecString(decDMS));
0184     }
0185     else if (col == ORIENTATION_COL)
0186     {
0187         bool angleOK = false;
0188         double angle = itemString.toDouble(&angleOK);
0189         if (!angleOK || angle > 360 || angle < -360)
0190         {
0191             item->setText(QString("%1").arg(overlay.m_Orientation, 0, 'f', 2));
0192             QString msg = i18n("Bad orientation angle string entered for %1. Reset to original value.", overlay.m_Filename);
0193             emit updateLog(msg);
0194         }
0195     }
0196     else if (col == ARCSEC_PER_PIXEL_COL)
0197     {
0198         bool scaleOK = false;
0199         double scale = itemString.toDouble(&scaleOK);
0200         if (!scaleOK || scale < 0 || scale > 1000)
0201         {
0202             item->setText(QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2));
0203             QString msg = i18n("Bad scale angle string entered for %1. Reset to original value.", overlay.m_Filename);
0204             emit updateLog(msg);
0205         }
0206     }
0207     connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection);
0208 }
0209 
0210 // Like cellChanged() but for the status column which contains QComboxBox widgets.
0211 void ImageOverlayComponent::statusCellChanged(int row)
0212 {
0213     if (row < 0 || row >= m_ImageOverlayTable->rowCount()) return;
0214 
0215     auto overlay = m_Overlays[row];
0216     disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged);
0217 
0218     // If the user changed the status of a column to OK,
0219     // then we check to make sure the required columns are filled out.
0220     // If so, then we also save it to the DB.
0221     // If the required columns are not filled out, the QComboBox value is reverted to UNPROCESSED.
0222     QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, STATUS_COL));
0223     bool failed = false;
0224     if (statusItem->currentIndex() == OK_INDEX)
0225     {
0226         dms raDMS;
0227         QTableWidgetItem *raItem = m_ImageOverlayTable->item(row, RA_COL);
0228         if (!raItem) return;
0229         const bool useHMS = isHMS(raItem->text());
0230         const bool raOK = raDMS.setFromString(raItem->text(), !useHMS);
0231         if (!raOK || raDMS.Degrees() == 0)
0232         {
0233             QString msg = i18n("Cannot set status to OK. Legal non-0 RA value required.");
0234             emit updateLog(msg);
0235             failed = true;
0236         }
0237 
0238         dms decDMS;
0239         QTableWidgetItem *decItem = m_ImageOverlayTable->item(row, DEC_COL);
0240         if (!decItem) return;
0241         const bool decOK = decDMS.setFromString(decItem->text());
0242         if (!decOK)
0243         {
0244             QString msg = i18n("Cannot set status to OK. Legal non-0 DEC value required.");
0245             emit updateLog(msg);
0246             failed = true;
0247         }
0248 
0249         bool angleOK = false;
0250         QTableWidgetItem *angleItem = m_ImageOverlayTable->item(row, ORIENTATION_COL);
0251         if (!angleItem) return;
0252         const double angle = angleItem->text().toDouble(&angleOK);
0253         if (!angleOK || angle > 360 || angle < -360)
0254         {
0255             QString msg = i18n("Cannot set status to OK. Legal orientation value required.");
0256             emit updateLog(msg);
0257             failed = true;
0258         }
0259 
0260         bool scaleOK = false;
0261         QTableWidgetItem *scaleItem = m_ImageOverlayTable->item(row, ARCSEC_PER_PIXEL_COL);
0262         if (!scaleItem) return;
0263         const double scale = scaleItem->text().toDouble(&scaleOK);
0264         if (!scaleOK || scale < 0 || scale > 1000)
0265         {
0266             QString msg = i18n("Cannot set status to OK. Legal non-0 a-s/px value required.");
0267             emit updateLog(msg);
0268             failed = true;
0269         }
0270 
0271         if (failed)
0272         {
0273             QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, STATUS_COL));
0274             statusItem->setCurrentIndex(UNPROCESSED_INDEX);
0275         }
0276         else
0277         {
0278             m_Overlays[row].m_Status = ImageOverlay::AVAILABLE;
0279             m_Overlays[row].m_RA = raDMS.Degrees();
0280             m_Overlays[row].m_DEC = decDMS.Degrees();
0281             m_Overlays[row].m_ArcsecPerPixel = scale;
0282             m_Overlays[row].m_Orientation = angle;
0283             const QComboBox *ewItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, EAST_TO_RIGHT_COL));
0284             m_Overlays[row].m_EastToTheRight = ewItem->currentIndex();
0285 
0286             if (m_Overlays[row].m_Img.get() == nullptr)
0287             {
0288                 // Load the image.
0289                 const QString fullFilename = QString("%1/%2").arg(m_Directory).arg(m_Overlays[row].m_Filename);
0290                 QImage *img = loadImageFile(fullFilename, !m_Overlays[row].m_EastToTheRight);
0291                 m_Overlays[row].m_Width = img->width();
0292                 m_Overlays[row].m_Height = img->height();
0293                 m_Overlays[row].m_Img.reset(img);
0294             }
0295             saveToUserDB();
0296             QString msg = i18n("Stored OK status for %1.", m_Overlays[row].m_Filename);
0297             emit updateLog(msg);
0298         }
0299     }
0300     connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection);
0301 }
0302 
0303 ImageOverlayComponent::~ImageOverlayComponent()
0304 {
0305     if (m_LoadImagesFuture.isRunning())
0306     {
0307         m_LoadImagesFuture.cancel();
0308         m_LoadImagesFuture.waitForFinished();
0309     }
0310 }
0311 
0312 void ImageOverlayComponent::selectionChanged()
0313 {
0314     if (m_Initialized && Options::showSelectedImageOverlay())
0315         show();
0316 }
0317 
0318 bool ImageOverlayComponent::selected()
0319 {
0320     return Options::showImageOverlays();
0321 }
0322 
0323 void ImageOverlayComponent::draw(SkyPainter *skyp)
0324 {
0325 #if !defined(KSTARS_LITE)
0326     if (m_Initialized)
0327         skyp->drawImageOverlay(&m_Overlays);
0328 #else
0329     Q_UNUSED(skyp);
0330 #endif
0331 }
0332 
0333 void ImageOverlayComponent::setWidgets(QTableWidget *table, QPlainTextEdit *statusDisplay,
0334                                        QPushButton *solveButton, QGroupBox *tableGroupBox,
0335                                        QComboBox *solverProfile)
0336 {
0337     m_ImageOverlayTable = table;
0338     // Temporarily make the table uneditable.
0339     m_EditTriggers = m_ImageOverlayTable->editTriggers();
0340     table->setEditTriggers(QAbstractItemView::NoEditTriggers);
0341 
0342     m_SolveButton = solveButton;
0343     m_TableGroupBox = tableGroupBox;
0344     m_SolverProfile = solverProfile;
0345     solveButton->setText(i18n("Solve"));
0346 
0347     m_StatusDisplay = statusDisplay;
0348     setupTable(table);
0349     updateTable();
0350     connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection);
0351     connect(m_ImageOverlayTable, &QTableWidget::itemSelectionChanged, this, &ImageOverlayComponent::selectionChanged,
0352             Qt::UniqueConnection);
0353 
0354     initSolverProfiles();
0355     loadAllImageFiles();
0356 }
0357 
0358 void ImageOverlayComponent::initSolverProfiles()
0359 {
0360     QString savedOptionsProfiles = QDir(KSPaths::writableLocation(
0361                                             QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
0362 
0363     QList<SSolver::Parameters> optionsList;
0364     if(QFile(savedOptionsProfiles).exists())
0365         optionsList = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
0366     else
0367         optionsList = Ekos::getDefaultAlignOptionsProfiles();
0368 
0369     m_SolverProfile->clear();
0370     for(auto &param : optionsList)
0371         m_SolverProfile->addItem(param.listName);
0372     m_SolverProfile->setCurrentIndex(Options::solveOptionsProfile());
0373 }
0374 
0375 void ImageOverlayComponent::updateStatusDisplay(const QString &message)
0376 {
0377     if (!m_StatusDisplay)
0378         return;
0379     m_LogText.insert(0, message);
0380     m_StatusDisplay->setPlainText(m_LogText.join("\n"));
0381 }
0382 
0383 // Find all the files in the directory, see if they are in the m_Overlays.
0384 // If no, append to the end of m_Overlays, and set status as unprocessed.
0385 void ImageOverlayComponent::updateTable()
0386 {
0387 #ifdef HAVE_CFITSIO
0388     // Get the list of files from the image overlay directory.
0389     QDir directory(m_Directory);
0390     emit updateLog(i18n("Updating from directory: %1", m_Directory));
0391     QStringList images = directory.entryList(QStringList() << "*", QDir::Files);
0392     QSet<QString> imageFiles;
0393     foreach(QString filename, images)
0394     {
0395         if (!FITSData::readableFilename(filename))
0396             continue;
0397         imageFiles.insert(filename);
0398     }
0399 
0400     // Sort the files alphabetically.
0401     QList<QString> sortedImageFiles;
0402     for (const auto &fn : imageFiles)
0403         sortedImageFiles.push_back(fn);
0404     std::sort(sortedImageFiles.begin(), sortedImageFiles.end(), overlaySorter);
0405 
0406     // Remove any overlays that aren't in the directory.
0407     QList<ImageOverlay> tempOverlays;
0408     QMap<QString, int> tempMap;
0409     int numDeleted = 0;
0410     for (int i = 0; i < m_Overlays.size(); ++i)
0411     {
0412         auto &fname = m_Overlays[i].m_Filename;
0413         if (sortedImageFiles.indexOf(fname) >= 0)
0414         {
0415             tempOverlays.append(m_Overlays[i]);
0416             tempMap[fname] = tempOverlays.size() - 1;
0417         }
0418         else
0419             numDeleted++;
0420     }
0421     m_Overlays = tempOverlays;
0422     m_Filenames = tempMap;
0423 
0424     // Add the new files into the overlay list.
0425     int numNew = 0;
0426     for (const auto &filename : sortedImageFiles)
0427     {
0428         auto item = m_Filenames.find(filename);
0429         if (item == m_Filenames.end())
0430         {
0431             // If it doesn't already exist in our database:
0432             ImageOverlay overlay(filename);
0433             const int size = m_Filenames.size();  // place before push_back().
0434             m_Overlays.push_back(overlay);
0435             m_Filenames[filename] = size;
0436             numNew++;
0437         }
0438     }
0439     emit updateLog(i18n("%1 overlays (%2 new, %3 deleted) %4 solved", m_Overlays.size(), numNew, numDeleted,
0440                         numAvailable()));
0441     m_TableGroupBox->setTitle(i18n("Image Overlays.  %1 images, %2 available.", m_Overlays.size(), numAvailable()));
0442 
0443     initializeGui();
0444     saveToUserDB();
0445 #endif
0446 }
0447 
0448 void ImageOverlayComponent::loadAllImageFiles()
0449 {
0450     m_LoadImagesFuture = QtConcurrent::run(this, &ImageOverlayComponent::loadImageFileLoop);
0451 }
0452 
0453 void ImageOverlayComponent::loadImageFileLoop()
0454 {
0455     emit updateLog(i18n("Loading image files..."));
0456     while (loadImageFile());
0457     int num = 0;
0458     for (const auto &o : m_Overlays)
0459         if (o.m_Img.get() != nullptr)
0460             num++;
0461     emit updateLog(i18n("%1 image files loaded.", num));
0462     // Restore editing for the table.
0463     m_ImageOverlayTable->setEditTriggers(m_EditTriggers);
0464     m_Initialized = true;
0465 }
0466 
0467 QImage *ImageOverlayComponent::loadImageFile (const QString &fullFilename, bool mirror)
0468 {
0469     QSharedPointer<QImage> tempImage(new QImage(fullFilename));
0470     if (tempImage.get() == nullptr) return nullptr;
0471     int scaleWidth = std::min(tempImage->width(), Options::imageOverlayMaxDimension());
0472     QImage *processedImg = new QImage;
0473     if (mirror)
0474         *processedImg = tempImage->mirrored(true, false).scaledToWidth(scaleWidth); // It's reflected horizontally.
0475     else
0476         *processedImg = tempImage->scaledToWidth(scaleWidth);
0477 
0478     return processedImg;
0479 }
0480 
0481 bool ImageOverlayComponent::loadImageFile()
0482 {
0483     bool updatedSomething = false;
0484 
0485     for (auto &o : m_Overlays)
0486     {
0487         if (o.m_Status == o.ImageOverlay::AVAILABLE && o.m_Img.get() == nullptr)
0488         {
0489             QString fullFilename = QString("%1%2%3").arg(m_Directory).arg(QDir::separator()).arg(o.m_Filename);
0490             QImage *img = loadImageFile(fullFilename, !o.m_EastToTheRight);
0491             o.m_Img.reset(img);
0492             updatedSomething = true;
0493 
0494             // Note: The original width and height in o.m_Width/m_Height is kept even
0495             // though the image was rescaled. This is to get the rendering right
0496             // with the original scale.
0497         }
0498     }
0499     return updatedSomething;
0500 }
0501 
0502 
0503 // Copies the info in m_Overlays into m_ImageOverlayTable UI.
0504 void ImageOverlayComponent::initializeGui()
0505 {
0506     if (!m_ImageOverlayTable) return;
0507 
0508     // Don't call callback on programmatic changes.
0509     disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged);
0510 
0511     // This clears the table.
0512     setupTable(m_ImageOverlayTable);
0513 
0514     int row = 0;
0515     for (int i = 0; i < m_Overlays.size(); ++i)
0516     {
0517         const ImageOverlay &overlay = m_Overlays[row];
0518         // False: The user can't edit filename.
0519         setupTableItem(m_ImageOverlayTable, row, FILENAME_COL, overlay.m_Filename, false);
0520 
0521         QStringList StatusNames =
0522         {
0523             i18n("Unprocessed"), i18n("Bad File"), i18n("Solve Failed"), i18n("Error"), i18n("OK")
0524         };
0525         QComboBox *statusBox = new QComboBox();
0526         for (int i = 0; i < ImageOverlay::NUM_STATUS; ++i)
0527             statusBox->addItem(StatusNames[i]);
0528         connect(statusBox, QOverload<int>::of(&QComboBox::activated), this, [row, this](int newIndex)
0529         {
0530             Q_UNUSED(newIndex);
0531             statusCellChanged(row);
0532         });
0533         statusBox->setCurrentIndex(static_cast<int>(overlay.m_Status));
0534         m_ImageOverlayTable->setCellWidget(row, STATUS_COL, statusBox);
0535 
0536         setupTableItem(m_ImageOverlayTable, row, ORIENTATION_COL, QString("%1").arg(overlay.m_Orientation, 0, 'f', 2));
0537         setupTableItem(m_ImageOverlayTable, row, RA_COL, dms(overlay.m_RA).toHMSString());
0538         setupTableItem(m_ImageOverlayTable, row, DEC_COL, toDecString(overlay.m_DEC));
0539         setupTableItem(m_ImageOverlayTable, row, ARCSEC_PER_PIXEL_COL, QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2));
0540 
0541         // The user can't edit width & height--taken from image.
0542         setupTableItem(m_ImageOverlayTable, row, WIDTH_COL, QString("%1").arg(overlay.m_Width), false);
0543         setupTableItem(m_ImageOverlayTable, row, HEIGHT_COL, QString("%1").arg(overlay.m_Height), false);
0544 
0545         QComboBox *mirroredBox = new QComboBox();
0546         mirroredBox->addItem(i18n("West-Right"));
0547         mirroredBox->addItem(i18n("East-Right"));
0548         connect(mirroredBox, QOverload<int>::of(&QComboBox::activated), this, [row](int newIndex)
0549         {
0550             Q_UNUSED(row);
0551             Q_UNUSED(newIndex);
0552             // Don't need to do anything. Will get picked up on change of status to OK.
0553         });
0554         mirroredBox->setCurrentIndex(overlay.m_EastToTheRight ? 1 : 0);
0555         m_ImageOverlayTable->setCellWidget(row, EAST_TO_RIGHT_COL, mirroredBox);
0556 
0557         row++;
0558     }
0559     m_ImageOverlayTable->resizeColumnsToContents();
0560     m_TableGroupBox->setTitle(i18n("Image Overlays.  %1 images, %2 available.", m_Overlays.size(), numAvailable()));
0561     connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection);
0562 
0563 }
0564 
0565 void ImageOverlayComponent::loadFromUserDB()
0566 {
0567     QList<ImageOverlay *> list;
0568     KStarsData::Instance()->userdb()->GetAllImageOverlays(&m_Overlays);
0569     // Alphabetize.
0570     std::sort(m_Overlays.begin(), m_Overlays.end(), overlaySorter);
0571     m_Filenames.clear();
0572     int index = 0;
0573     for (const auto &o : m_Overlays)
0574     {
0575         m_Filenames[o.m_Filename] = index;
0576         index++;
0577     }
0578 }
0579 
0580 void ImageOverlayComponent::saveToUserDB()
0581 {
0582     KStarsData::Instance()->userdb()->DeleteAllImageOverlays();
0583     for (const ImageOverlay &metadata : m_Overlays)
0584         KStarsData::Instance()->userdb()->AddImageOverlay(metadata);
0585 }
0586 
0587 void ImageOverlayComponent::solveImage(const QString &filename)
0588 {
0589     if (!m_Initialized) return;
0590     m_SolveButton->setText(i18n("Abort"));
0591     const int solverTimeout = Options::imageOverlayTimeout();
0592     auto profiles = Ekos::getDefaultAlignOptionsProfiles();
0593     auto parameters = profiles.at(m_SolverProfile->currentIndex());
0594     // Double search radius
0595     parameters.search_radius = parameters.search_radius * 2;
0596 
0597     m_Solver.reset(new SolverUtils(parameters, solverTimeout),  &QObject::deleteLater);
0598     connect(m_Solver.get(), &SolverUtils::done, this, &ImageOverlayComponent::solverDone, Qt::UniqueConnection);
0599 
0600     if (m_RowsToSolve.size() > 1)
0601         emit updateLog(i18n("Solving: %1. %2 in queue.", filename, m_RowsToSolve.size()));
0602     else
0603         emit updateLog(i18n("Solving: %1.", filename));
0604 
0605     // If the user added some RA/DEC/Scale values to the table, they will be used in the solve
0606     // (but aren't remembered in the DB unless the solve is successful).
0607     int row = m_RowsToSolve[0];
0608     QString raString = m_ImageOverlayTable->item(row, RA_COL)->text().toLatin1().data();
0609     QString decString = m_ImageOverlayTable->item(row, DEC_COL)->text().toLatin1().data();
0610     QString scaleString = m_ImageOverlayTable->item(row, ARCSEC_PER_PIXEL_COL)->text().toLatin1().data();
0611 
0612     dms raDMS, decDMS;
0613     const bool useHMS = isHMS(raString);
0614     const bool raOK = raDMS.setFromString(raString, !useHMS) && (raDMS.Degrees() != 0.00);
0615     const bool decOK = decDMS.setFromString(decString) && (decDMS.Degrees() != 0.00);
0616     bool scaleOK = false;
0617     double scale = scaleString.toDouble(&scaleOK);
0618     scaleOK = scaleOK && scale != 0.00;
0619 
0620     // Use the default scale if it is > 0 and scale was not specified in the UI table.
0621     if (!scaleOK && Options::imageOverlayDefaultScale() > 0.0001)
0622     {
0623         scale = Options::imageOverlayDefaultScale();
0624         scaleOK = true;
0625     }
0626 
0627     if (scaleOK)
0628     {
0629         auto lowScale = scale * 0.75;
0630         auto highScale = scale * 1.25;
0631         m_Solver->useScale(true, lowScale, highScale);
0632     }
0633     if (raOK && decOK)
0634         m_Solver->usePosition(true, raDMS.Degrees(), decDMS.Degrees());
0635 
0636     m_Solver->runSolver(filename);
0637 }
0638 
0639 void ImageOverlayComponent::tryAgain()
0640 {
0641     m_TryAgainTimer.stop();
0642     if (!m_Initialized) return;
0643     if (m_RowsToSolve.size() > 0)
0644         startSolving();
0645 }
0646 
0647 int ImageOverlayComponent::numAvailable()
0648 {
0649     int num = 0;
0650     for (const auto &o : m_Overlays)
0651         if (o.m_Status == ImageOverlay::AVAILABLE)
0652             num++;
0653     return num;
0654 }
0655 
0656 void ImageOverlayComponent::show()
0657 {
0658     if (!m_Initialized || !m_ImageOverlayTable) return;
0659     auto selections = m_ImageOverlayTable->selectionModel();
0660     if (selections->hasSelection())
0661     {
0662         auto selectedIndexes = selections->selectedIndexes();
0663         const int row = selectedIndexes.at(0).row();
0664         if (m_Overlays.size() > row && row >= 0)
0665         {
0666             if (m_Overlays[row].m_Status != ImageOverlay::AVAILABLE)
0667             {
0668                 emit updateLog(i18n("Can't show %1. Not plate solved.", m_Overlays[row].m_Filename));
0669                 return;
0670             }
0671             if (m_Overlays[row].m_Img.get() == nullptr)
0672             {
0673                 emit updateLog(i18n("Can't show %1. Image not loaded.", m_Overlays[row].m_Filename));
0674                 return;
0675             }
0676             const double ra = m_Overlays[row].m_RA;
0677             const double dec = m_Overlays[row].m_DEC;
0678 
0679             // Convert the RA/DEC from j2000 to jNow.
0680             auto localTime = KStarsData::Instance()->geo()->UTtoLT(KStarsData::Instance()->clock()->utc());
0681             const dms raDms(ra), decDms(dec);
0682             SkyPoint coord(raDms, decDms);
0683             coord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
0684 
0685             // Is this the right way to move to the SkyMap?
0686             Options::setIsTracking(false);
0687             SkyMap::Instance()->setFocusObject(nullptr);
0688             SkyMap::Instance()->setFocusPoint(nullptr);
0689             SkyMap::Instance()->setFocus(dms(coord.ra()), dms(coord.dec()));
0690 
0691             // Zoom factor is in pixels per radian.
0692             double zoomFactor = (400 * 60.0 *  10800.0) / (m_Overlays[row].m_Width * m_Overlays[row].m_ArcsecPerPixel * dms::PI);
0693             SkyMap::Instance()->setZoomFactor(zoomFactor);
0694 
0695             SkyMap::Instance()->forceUpdate(true);
0696         }
0697     }
0698 }
0699 
0700 void ImageOverlayComponent::abortSolving()
0701 {
0702     if (!m_Initialized) return;
0703     m_RowsToSolve.clear();
0704     if (m_Solver)
0705         m_Solver->abort();
0706     emit updateLog(i18n("Solving aborted."));
0707     m_SolveButton->setText(i18n("Solve"));
0708 }
0709 
0710 void ImageOverlayComponent::startSolving()
0711 {
0712     if (!m_Initialized) return;
0713     if (m_SolveButton->text() == i18n("Abort"))
0714     {
0715         abortSolving();
0716         return;
0717     }
0718     if (m_Solver && m_Solver->isRunning())
0719     {
0720         m_Solver->abort();
0721         if (m_RowsToSolve.size() > 0)
0722             m_TryAgainTimer.start(2000);
0723         return;
0724     }
0725 
0726     if (m_RowsToSolve.size() == 0)
0727     {
0728         QSet<int> selectedRows;
0729         auto selections = m_ImageOverlayTable->selectionModel();
0730         if (selections->hasSelection())
0731         {
0732             // Need to de-dup, as selecting the whole row will select all the columns.
0733             auto selectedIndexes = selections->selectedIndexes();
0734             for (int i = 0; i < selectedIndexes.count(); ++i)
0735             {
0736                 // Don't insert a row that's already solved.
0737                 const int row = selectedIndexes.at(i).row();
0738                 if ((m_Overlays[row].m_Status == ImageOverlay::AVAILABLE) &&
0739                         !shouldSolveAnyway(m_ImageOverlayTable, row))
0740                 {
0741                     emit updateLog(i18n("Skipping already solved: %1.", m_Overlays[row].m_Filename));
0742                     continue;
0743                 }
0744                 selectedRows.insert(row);
0745             }
0746         }
0747         m_RowsToSolve.clear();
0748         for (int row : selectedRows)
0749             m_RowsToSolve.push_back(row);
0750     }
0751 
0752     if (m_RowsToSolve.size() > 0)
0753     {
0754         const int row = m_RowsToSolve[0];
0755         const QString filename =
0756             QString("%1/%2").arg(m_Directory).arg(m_Overlays[row].m_Filename);
0757         if ((m_Overlays[row].m_Status == ImageOverlay::AVAILABLE) &&
0758                 !shouldSolveAnyway(m_ImageOverlayTable, row))
0759         {
0760             emit updateLog(i18n("%1 already solved. Skipping.", filename));
0761             m_RowsToSolve.removeFirst();
0762             if (m_RowsToSolve.size() > 0)
0763                 startSolving();
0764             return;
0765         }
0766 
0767         auto img = new QImage(filename);
0768         m_Overlays[row].m_Width = img->width();
0769         m_Overlays[row].m_Height = img->height();
0770         solveImage(filename);
0771     }
0772 }
0773 
0774 void ImageOverlayComponent::reload()
0775 {
0776     if (!m_Initialized) return;
0777     m_Initialized = false;
0778     emit updateLog(i18n("Reloading. Image overlays temporarily disabled."));
0779     updateTable();
0780     loadAllImageFiles();
0781 }
0782 
0783 void ImageOverlayComponent::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution,
0784                                        double elapsedSeconds)
0785 {
0786     disconnect(m_Solver.get(), &SolverUtils::done, this, &ImageOverlayComponent::solverDone);
0787     m_SolveButton->setText(i18n("Solve"));
0788     if (m_RowsToSolve.size() == 0)
0789         return;
0790 
0791     const int solverRow = m_RowsToSolve[0];
0792     m_RowsToSolve.removeFirst();
0793 
0794     QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, STATUS_COL));
0795     if (timedOut)
0796     {
0797         emit updateLog(i18n("Solver timed out in %1s", QString::number(elapsedSeconds, 'f', 1)));
0798         m_Overlays[solverRow].m_Status = ImageOverlay::PLATE_SOLVE_FAILURE;
0799         statusItem->setCurrentIndex(static_cast<int>(m_Overlays[solverRow].m_Status));
0800     }
0801     else if (!success)
0802     {
0803         emit updateLog(i18n("Solver failed in %1s", QString::number(elapsedSeconds, 'f', 1)));
0804         m_Overlays[solverRow].m_Status = ImageOverlay::PLATE_SOLVE_FAILURE;
0805         statusItem->setCurrentIndex(static_cast<int>(m_Overlays[solverRow].m_Status));
0806     }
0807     else
0808     {
0809         m_Overlays[solverRow].m_Orientation = solution.orientation;
0810         m_Overlays[solverRow].m_RA = solution.ra;
0811         m_Overlays[solverRow].m_DEC = solution.dec;
0812         m_Overlays[solverRow].m_ArcsecPerPixel = solution.pixscale;
0813         m_Overlays[solverRow].m_EastToTheRight = solution.parity;
0814         m_Overlays[solverRow].m_Status = ImageOverlay::AVAILABLE;
0815 
0816         QString msg = i18n("Solver success in %1s: RA %2 DEC %3 Scale %4 Angle %5",
0817                            QString::number(elapsedSeconds, 'f', 1),
0818                            QString::number(solution.ra, 'f', 2),
0819                            QString::number(solution.dec, 'f', 2),
0820                            QString::number(solution.pixscale, 'f', 2),
0821                            QString::number(solution.orientation, 'f', 2));
0822         emit updateLog(msg);
0823 
0824         // Store the new values in the table.
0825         auto overlay = m_Overlays[solverRow];
0826         m_ImageOverlayTable->item(solverRow, RA_COL)->setText(dms(overlay.m_RA).toHMSString());
0827         m_ImageOverlayTable->item(solverRow, DEC_COL)->setText(toDecString(overlay.m_DEC));
0828         m_ImageOverlayTable->item(solverRow, ARCSEC_PER_PIXEL_COL)->setText(
0829             QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2));
0830         m_ImageOverlayTable->item(solverRow, ORIENTATION_COL)->setText(QString("%1").arg(overlay.m_Orientation, 0, 'f', 2));
0831         QComboBox *ewItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, EAST_TO_RIGHT_COL));
0832         ewItem->setCurrentIndex(overlay.m_EastToTheRight ? 1 : 0);
0833 
0834         QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, STATUS_COL));
0835         statusItem->setCurrentIndex(static_cast<int>(overlay.m_Status));
0836 
0837         // Load the image.
0838         QString fullFilename = QString("%1/%2").arg(m_Directory).arg(m_Overlays[solverRow].m_Filename);
0839         QImage *img = loadImageFile(fullFilename, !m_Overlays[solverRow].m_EastToTheRight);
0840         m_Overlays[solverRow].m_Img.reset(img);
0841     }
0842     saveToUserDB();
0843 
0844     if (m_RowsToSolve.size() > 0)
0845         startSolving();
0846     else
0847     {
0848         emit updateLog(i18n("Done solving. %1 available.", numAvailable()));
0849         m_TableGroupBox->setTitle(i18n("Image Overlays.  %1 images, %2 available.", m_Overlays.size(), numAvailable()));
0850     }
0851 }