File indexing completed on 2024-04-14 14:10:38

0001 /*
0002     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "fitstab.h"
0008 
0009 #include "auxiliary/kspaths.h"
0010 #include "fitsdata.h"
0011 #include "fitshistogrameditor.h"
0012 #include "fitshistogramcommand.h"
0013 #include "fitsview.h"
0014 #include "fitsviewer.h"
0015 #include "ksnotification.h"
0016 #include "kstars.h"
0017 #include "Options.h"
0018 #include "ui_fitsheaderdialog.h"
0019 #include "ui_statform.h"
0020 #include "fitsstretchui.h"
0021 #include "skymap.h"
0022 #include <KMessageBox>
0023 #include <QtConcurrent>
0024 #include <QIcon>
0025 #include "ekos/auxiliary/stellarsolverprofile.h"
0026 
0027 #include <fits_debug.h>
0028 
0029 FITSTab::FITSTab(FITSViewer *parent) : QWidget(parent)
0030 {
0031     viewer    = parent;
0032     undoStack = new QUndoStack(this);
0033     undoStack->setUndoLimit(10);
0034     undoStack->clear();
0035     connect(undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(modifyFITSState(bool)));
0036 
0037     m_PlateSolveWidget = new QDialog(this);
0038     statWidget = new QDialog(this);
0039     fitsHeaderDialog = new QDialog(this);
0040     m_HistogramEditor = new FITSHistogramEditor(this);
0041     connect(m_HistogramEditor, &FITSHistogramEditor::newHistogramCommand, this, [this](FITSHistogramCommand * command)
0042     {
0043         undoStack->push(command);
0044     });
0045 }
0046 
0047 FITSTab::~FITSTab()
0048 {
0049 }
0050 
0051 void FITSTab::saveUnsaved()
0052 {
0053     if (undoStack->isClean() || m_View->getMode() != FITS_NORMAL)
0054         return;
0055 
0056     QString caption = i18n("Save Changes to FITS?");
0057     QString message = i18n("The current FITS file has unsaved changes.  Would you like to save before closing it?");
0058 
0059     int ans = KMessageBox::warningYesNoCancel(nullptr, message, caption, KStandardGuiItem::save(), KStandardGuiItem::discard());
0060     if (ans == KMessageBox::Yes)
0061         saveFile();
0062     if (ans == KMessageBox::No)
0063     {
0064         undoStack->clear();
0065         modifyFITSState();
0066     }
0067 }
0068 
0069 void FITSTab::closeEvent(QCloseEvent *ev)
0070 {
0071     saveUnsaved();
0072 
0073     if (undoStack->isClean())
0074         ev->accept();
0075     else
0076         ev->ignore();
0077 }
0078 QString FITSTab::getPreviewText() const
0079 {
0080     return previewText;
0081 }
0082 
0083 void FITSTab::setPreviewText(const QString &value)
0084 {
0085     previewText = value;
0086 }
0087 
0088 void FITSTab::selectRecentFITS(int i)
0089 {
0090     loadFile(QUrl::fromLocalFile(recentImages->item(i)->text()));
0091 }
0092 
0093 void FITSTab::clearRecentFITS()
0094 {
0095     disconnect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
0096     recentImages->clear();
0097     connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
0098 }
0099 
0100 bool FITSTab::setupView(FITSMode mode, FITSScale filter)
0101 {
0102     if (m_View.isNull())
0103     {
0104         m_View.reset(new FITSView(this, mode, filter));
0105         m_View->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0106         QVBoxLayout *vlayout = new QVBoxLayout();
0107 
0108         connect(m_View.get(), &FITSView::rectangleUpdated, this, [this](QRect roi)
0109         {
0110             displayStats(roi.isValid());
0111         });
0112         fitsSplitter = new QSplitter(Qt::Horizontal, this);
0113         fitsTools = new QToolBox();
0114 
0115         stat.setupUi(statWidget);
0116         m_PlateSolveUI.setupUi(m_PlateSolveWidget);
0117 
0118         connect(m_PlateSolveUI.SolveButton, &QPushButton::clicked, this, &FITSTab::extractImage);
0119 
0120         for (int i = 0; i <= STAT_STDDEV; i++)
0121         {
0122             for (int j = 0; j < 3; j++)
0123             {
0124                 stat.statsTable->setItem(i, j, new QTableWidgetItem());
0125                 stat.statsTable->item(i, j)->setTextAlignment(Qt::AlignHCenter);
0126             }
0127 
0128             // Set col span for items up to HFR
0129             if (i <= STAT_HFR)
0130                 stat.statsTable->setSpan(i, 0, 1, 3);
0131         }
0132 
0133         fitsTools->addItem(statWidget, i18n("Statistics"));
0134 
0135         fitsTools->addItem(m_PlateSolveWidget, i18n("Plate Solving"));
0136         initSolverUI();
0137 
0138         fitsTools->addItem(m_HistogramEditor, i18n("Histogram"));
0139 
0140         header.setupUi(fitsHeaderDialog);
0141         fitsTools->addItem(fitsHeaderDialog, i18n("FITS Header"));
0142 
0143         QVBoxLayout *recentPanelLayout = new QVBoxLayout();
0144         QWidget *recentPanel = new QWidget(fitsSplitter);
0145         recentPanel->setLayout(recentPanelLayout);
0146         fitsTools->addItem(recentPanel, i18n("Recent Images"));
0147         recentImages = new QListWidget(recentPanel);
0148         recentPanelLayout->addWidget(recentImages);
0149         QPushButton *clearRecent = new QPushButton(i18n("Clear"));
0150         recentPanelLayout->addWidget(clearRecent);
0151         connect(clearRecent, &QPushButton::pressed, this, &FITSTab::clearRecentFITS);
0152         connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
0153 
0154         QScrollArea *scrollFitsPanel = new QScrollArea(fitsSplitter);
0155         scrollFitsPanel->setWidgetResizable(true);
0156         scrollFitsPanel->setWidget(fitsTools);
0157 
0158         fitsSplitter->addWidget(scrollFitsPanel);
0159         fitsSplitter->addWidget(m_View.get());
0160 
0161 
0162         //This code allows the fitsTools to start in a closed state
0163         fitsSplitter->setSizes(QList<int>() << 0 << m_View->width() );
0164 
0165         vlayout->addWidget(fitsSplitter);
0166 
0167         stretchUI.reset(new FITSStretchUI(m_View, nullptr));
0168         vlayout->addWidget(stretchUI.get());
0169 
0170         connect(fitsSplitter, &QSplitter::splitterMoved, m_HistogramEditor, &FITSHistogramEditor::resizePlot);
0171 
0172         setLayout(vlayout);
0173         connect(m_View.get(), &FITSView::newStatus, this, &FITSTab::newStatus);
0174         connect(m_View.get(), &FITSView::debayerToggled, this, &FITSTab::debayerToggled);
0175         connect(m_View.get(), &FITSView::updated, this, &FITSTab::updated);
0176 
0177         // On Failure to load
0178         connect(m_View.get(), &FITSView::failed, this, &FITSTab::failed);
0179 
0180         return true;
0181     }
0182 
0183     // returns false if no setup needed.
0184     return false;
0185 }
0186 
0187 void FITSTab::loadFile(const QUrl &imageURL, FITSMode mode, FITSScale filter)
0188 {
0189     // check if the address points to an appropriate address
0190     if (imageURL.isEmpty() || !imageURL.isValid() || !QFileInfo::exists(imageURL.toLocalFile()))
0191         return;
0192 
0193     if (setupView(mode, filter))
0194     {
0195 
0196         // On Success loading image
0197         connect(m_View.get(), &FITSView::loaded, this, [&]()
0198         {
0199             processData();
0200             emit loaded();
0201         });
0202 
0203         connect(m_View.get(), &FITSView::updated, this, &FITSTab::updated);
0204     }
0205     else
0206         // update tab text
0207         modifyFITSState(true, imageURL);
0208 
0209     currentURL = imageURL;
0210 
0211     m_View->setFilter(filter);
0212 
0213     m_View->loadFile(imageURL.toLocalFile());
0214 }
0215 
0216 bool FITSTab::shouldComputeHFR() const
0217 {
0218     if (viewer->isStarsMarked())
0219         return true;
0220     if (!Options::autoHFR())
0221         return false;
0222     return ((!m_View.isNull()) && (m_View->getMode() == FITS_NORMAL));
0223 }
0224 
0225 void FITSTab::processData()
0226 {
0227     const QSharedPointer<FITSData> &imageData = m_View->imageData();
0228 
0229     m_HistogramEditor->setImageData(imageData);
0230 
0231     // Only construct histogram if it is actually visible
0232     // Otherwise wait until histogram is needed before creating it.
0233     //    if (fitsSplitter->sizes().at(0) != 0 && !imageData->isHistogramConstructed() &&
0234     //            !Options::nonLinearHistogram())
0235     //    {
0236     //        imageData->constructHistogram();
0237     //    }
0238 
0239     if (shouldComputeHFR())
0240     {
0241         m_View->searchStars();
0242         qCDebug(KSTARS_FITS) << "FITS HFR:" << imageData->getHFR();
0243     }
0244 
0245     displayStats();
0246 
0247     loadFITSHeader();
0248 
0249     // Don't add it to the list if it is already there
0250     if (recentImages->findItems(currentURL.toLocalFile(), Qt::MatchExactly).count() == 0)
0251     {
0252         //Don't add it to the list if it is a preview
0253         if(!imageData->filename().startsWith(QDir::tempPath()))
0254         {
0255             disconnect(recentImages, &QListWidget::currentRowChanged, this,
0256                        &FITSTab::selectRecentFITS);
0257             recentImages->addItem(imageData->filename());
0258             recentImages->setCurrentRow(recentImages->count() - 1);
0259             connect(recentImages, &QListWidget::currentRowChanged,  this,
0260                     &FITSTab::selectRecentFITS);
0261         }
0262     }
0263 
0264     //     This could both compute the HFRs and setup the graphics, however,
0265     //     if shouldComputeHFR() above is true, then that will compute the HFRs
0266     //     and this would notice that and just setup graphics. They are separated
0267     //     for the case where the graphics is not desired.
0268     if (viewer->isStarsMarked())
0269     {
0270         m_View->toggleStars(true);
0271         m_View->updateFrame();
0272     }
0273 
0274     //    if (Options::nonLinearHistogram())
0275     //        m_HistogramEditor->createNonLinearHistogram();
0276 
0277     stretchUI->generateHistogram();
0278 }
0279 
0280 bool FITSTab::loadData(const QSharedPointer<FITSData> &data, FITSMode mode, FITSScale filter)
0281 {
0282     setupView(mode, filter);
0283 
0284     // Empty URL
0285     currentURL = QUrl();
0286 
0287     if (viewer->isStarsMarked())
0288     {
0289         m_View->toggleStars(true);
0290         //view->updateFrame();
0291     }
0292 
0293     m_View->setFilter(filter);
0294 
0295     if (!m_View->loadData(data))
0296     {
0297         // On Failure to load
0298         // connect(view.get(), &FITSView::failed, this, &FITSTab::failed);
0299         return false;
0300     }
0301 
0302     processData();
0303     return true;
0304 }
0305 
0306 void FITSTab::modifyFITSState(bool clean, const QUrl &imageURL)
0307 {
0308     if (clean)
0309     {
0310         if (undoStack->isClean() == false)
0311             undoStack->setClean();
0312 
0313         mDirty = false;
0314     }
0315     else
0316         mDirty = true;
0317 
0318     emit changeStatus(clean, imageURL);
0319 }
0320 
0321 bool FITSTab::saveImage(const QString &filename)
0322 {
0323     return m_View->saveImage(filename);
0324 }
0325 
0326 void FITSTab::copyFITS()
0327 {
0328     QApplication::clipboard()->setImage(m_View->getDisplayImage());
0329 }
0330 
0331 void FITSTab::histoFITS()
0332 {
0333     fitsTools->setCurrentIndex(1);
0334     if(m_View->width() > 200)
0335         fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
0336     else
0337         fitsSplitter->setSizes(QList<int>() << 50 << 50);
0338 }
0339 
0340 void FITSTab::displayStats(bool roi)
0341 {
0342     const QSharedPointer<FITSData> &imageData = m_View->imageData();
0343 
0344     stat.statsTable->item(STAT_WIDTH, 0)->setText(QString::number(imageData->width(roi)));
0345     stat.statsTable->item(STAT_HEIGHT, 0)->setText(QString::number(imageData->height(roi)));
0346     stat.statsTable->item(STAT_BITPIX, 0)->setText(QString::number(imageData->bpp()));
0347 
0348     if (!roi)
0349         stat.statsTable->item(STAT_HFR, 0)->setText(QString::number(imageData->getHFR(), 'f', 3));
0350     else
0351         stat.statsTable->item(STAT_HFR, 0)->setText("---");
0352 
0353     if (imageData->channels() == 1)
0354     {
0355         for (int i = STAT_MIN; i <= STAT_STDDEV; i++)
0356         {
0357             if (stat.statsTable->columnSpan(i, 0) != 3)
0358                 stat.statsTable->setSpan(i, 0, 1, 3);
0359         }
0360 
0361         stat.statsTable->horizontalHeaderItem(0)->setText(i18n("Value"));
0362         stat.statsTable->hideColumn(1);
0363         stat.statsTable->hideColumn(2);
0364     }
0365     else
0366     {
0367         for (int i = STAT_MIN; i <= STAT_STDDEV; i++)
0368         {
0369             if (stat.statsTable->columnSpan(i, 0) != 1)
0370                 stat.statsTable->setSpan(i, 0, 1, 1);
0371         }
0372 
0373         stat.statsTable->horizontalHeaderItem(0)->setText(i18nc("Red", "R"));
0374         stat.statsTable->showColumn(1);
0375         stat.statsTable->showColumn(2);
0376     }
0377 
0378     if (!Options::nonLinearHistogram() && !imageData->isHistogramConstructed())
0379         imageData->constructHistogram();
0380 
0381     for (int i = 0; i < imageData->channels(); i++)
0382     {
0383         stat.statsTable->item(STAT_MIN, i)->setText(QString::number(imageData->getMin(i, roi), 'f', 3));
0384         stat.statsTable->item(STAT_MAX, i)->setText(QString::number(imageData->getMax(i, roi), 'f', 3));
0385         stat.statsTable->item(STAT_MEAN, i)->setText(QString::number(imageData->getMean(i, roi), 'f', 3));
0386         stat.statsTable->item(STAT_MEDIAN, i)->setText(QString::number(imageData->getMedian(i, roi), 'f', 3));
0387         stat.statsTable->item(STAT_STDDEV, i)->setText(QString::number(imageData->getStdDev(i, roi), 'f', 3));
0388     }
0389 }
0390 
0391 void FITSTab::statFITS()
0392 {
0393     fitsTools->setCurrentIndex(0);
0394     if(m_View->width() > 200)
0395         fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
0396     else
0397         fitsSplitter->setSizes(QList<int>() << 50 << 50);
0398     displayStats();
0399 }
0400 
0401 void FITSTab::loadFITSHeader()
0402 {
0403     const QSharedPointer<FITSData> &imageData = m_View->imageData();
0404 
0405     int nkeys = imageData->getRecords().size();
0406     int counter = 0;
0407     header.tableWidget->setRowCount(nkeys);
0408     for (const auto &oneRecord : imageData->getRecords())
0409     {
0410         QTableWidgetItem *tempItem = new QTableWidgetItem(oneRecord.key);
0411         tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
0412         header.tableWidget->setItem(counter, 0, tempItem);
0413         tempItem = new QTableWidgetItem(oneRecord.value.toString());
0414         tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
0415         header.tableWidget->setItem(counter, 1, tempItem);
0416         tempItem = new QTableWidgetItem(oneRecord.comment);
0417         tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
0418         header.tableWidget->setItem(counter, 2, tempItem);
0419         counter++;
0420     }
0421 
0422     header.tableWidget->setColumnWidth(0, 100);
0423     header.tableWidget->setColumnWidth(1, 100);
0424     header.tableWidget->setColumnWidth(2, 250);
0425 }
0426 
0427 void FITSTab::headerFITS()
0428 {
0429     fitsTools->setCurrentIndex(2);
0430     if(m_View->width() > 200)
0431         fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
0432     else
0433         fitsSplitter->setSizes(QList<int>() << 50 << 50);
0434 }
0435 
0436 bool FITSTab::saveFile()
0437 {
0438     QUrl backupCurrent = currentURL;
0439     QUrl currentDir(Options::fitsDir());
0440     currentDir.setScheme("file");
0441 
0442     if (currentURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || currentURL.toLocalFile().contains("/Temp"))
0443         currentURL.clear();
0444 
0445     // If no changes made, return.
0446     if (mDirty == false && !currentURL.isEmpty())
0447         return false;
0448 
0449     if (currentURL.isEmpty())
0450     {
0451         QString selectedFilter;
0452 #ifdef Q_OS_OSX //For some reason, the other code caused KStars to crash on MacOS
0453         currentURL =
0454             QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save FITS"), currentDir,
0455                                         "Images (*.fits *.fits.gz *.fit *.xisf *.jpg *.jpeg *.png)");
0456 #else
0457         currentURL =
0458             QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save FITS"), currentDir,
0459                                         "FITS (*.fits *.fits.gz *.fit);;XISF (*.xisf);;JPEG (*.jpg *.jpeg);;PNG (*.png)", &selectedFilter);
0460 #endif
0461         // if user presses cancel
0462         if (currentURL.isEmpty())
0463         {
0464             currentURL = backupCurrent;
0465             return false;
0466         }
0467 
0468         // If no extension is selected append one
0469         if (currentURL.toLocalFile().contains('.') == 0)
0470         {
0471             if (selectedFilter.contains("XISF"))
0472                 currentURL.setPath(currentURL.toLocalFile() + ".xisf");
0473             else if (selectedFilter.contains("JPEG"))
0474                 currentURL.setPath(currentURL.toLocalFile() + ".jpg");
0475             else if (selectedFilter.contains("PNG"))
0476                 currentURL.setPath(currentURL.toLocalFile() + ".png");
0477             else
0478                 currentURL.setPath(currentURL.toLocalFile() + ".fits");
0479         }
0480     }
0481 
0482     if (currentURL.isValid())
0483     {
0484         QString localFile = currentURL.toLocalFile();
0485         //        if (localFile.contains(".fit"))
0486         //            localFile = "!" + localFile;
0487 
0488         if (!saveImage(localFile))
0489         {
0490             KSNotification::error(i18n("Image save error: %1", m_View->imageData()->getLastError()), i18n("Image Save"));
0491             return false;
0492         }
0493 
0494         emit newStatus(i18n("File saved to %1", currentURL.url()), FITS_MESSAGE);
0495         modifyFITSState();
0496         return true;
0497     }
0498     else
0499     {
0500         QString message = i18n("Invalid URL: %1", currentURL.url());
0501         KSNotification::sorry(message, i18n("Invalid URL"));
0502         return false;
0503     }
0504 }
0505 
0506 bool FITSTab::saveFileAs()
0507 {
0508     currentURL.clear();
0509     return saveFile();
0510 }
0511 
0512 void FITSTab::ZoomIn()
0513 {
0514     QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
0515     m_View->ZoomIn();
0516     m_View->cleanUpZoom(oldCenter);
0517 }
0518 
0519 void FITSTab::ZoomOut()
0520 {
0521     QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
0522     m_View->ZoomOut();
0523     m_View->cleanUpZoom(oldCenter);
0524 }
0525 
0526 void FITSTab::ZoomDefault()
0527 {
0528     QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
0529     m_View->ZoomDefault();
0530     m_View->cleanUpZoom(oldCenter);
0531 }
0532 
0533 void FITSTab::tabPositionUpdated()
0534 {
0535     undoStack->setActive(true);
0536     emit newStatus(QString("%1%").arg(m_View->getCurrentZoom()), FITS_ZOOM);
0537     emit newStatus(QString("%1x%2").arg(m_View->imageData()->width()).arg(m_View->imageData()->height()),
0538                    FITS_RESOLUTION);
0539 }
0540 
0541 void FITSTab::setStretchValues(double shadows, double midtones, double highlights)
0542 {
0543     if (stretchUI)
0544         stretchUI->setStretchValues(shadows, midtones, highlights);
0545 }
0546 
0547 namespace
0548 {
0549 const QList<SSolver::Parameters> getSSolverParametersList()
0550 {
0551     const QString savedOptionsProfiles = QDir(KSPaths::writableLocation(
0552             QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
0553 
0554     return QFile(savedOptionsProfiles).exists() ?
0555            StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles) :
0556            Ekos::getDefaultAlignOptionsProfiles();
0557 }
0558 } // namespace
0559 
0560 void FITSTab::setupSolver(bool extractOnly)
0561 {
0562     auto parameters = getSSolverParametersList().at(m_PlateSolveUI.kcfg_FitsSolverProfile->currentIndex());
0563     parameters.search_radius = m_PlateSolveUI.kcfg_FitsSolverRadius->value();
0564     if (extractOnly)
0565     {
0566         m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::EXTRACT),  &QObject::deleteLater);
0567         connect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::extractorDone, Qt::UniqueConnection);
0568     }
0569     else
0570     {
0571         m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::SOLVE),  &QObject::deleteLater);
0572         connect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::solverDone, Qt::UniqueConnection);
0573     }
0574 
0575     const int imageWidth = m_View->imageData()->width();
0576     const int imageHeight = m_View->imageData()->height();
0577     if (m_PlateSolveUI.kcfg_FitsSolverUseScale->isChecked() && imageWidth != 0 && imageHeight != 0)
0578     {
0579         const double scale = m_PlateSolveUI.kcfg_FitsSolverScale->value();
0580         double lowScale = scale * 0.8;
0581         double highScale = scale * 1.2;
0582 
0583         // solver utils uses arcsecs per pixel only
0584         const int units = m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->currentIndex();
0585         if (units == SSolver::DEG_WIDTH)
0586         {
0587             lowScale = (lowScale * 3600) / std::max(imageWidth, imageHeight);
0588             highScale = (highScale * 3600) / std::min(imageWidth, imageHeight);
0589         }
0590         else if (units == SSolver::ARCMIN_WIDTH)
0591         {
0592             lowScale = (lowScale * 60) / std::max(imageWidth, imageHeight);
0593             highScale = (highScale * 60) / std::min(imageWidth, imageHeight);
0594         }
0595 
0596         m_Solver->useScale(m_PlateSolveUI.kcfg_FitsSolverUseScale->isChecked(), lowScale, highScale);
0597     }
0598     else m_Solver->useScale(false, 0, 0);
0599 
0600     if (m_PlateSolveUI.kcfg_FitsSolverUsePosition->isChecked())
0601     {
0602         bool ok;
0603         const dms ra = m_PlateSolveUI.FitsSolverEstRA->createDms(&ok);
0604         bool ok2;
0605         const dms dec = m_PlateSolveUI.FitsSolverEstDec->createDms(&ok2);
0606         if (ok && ok2)
0607             m_Solver->usePosition(true, ra.Degrees(), dec.Degrees());
0608         else
0609             m_Solver->usePosition(false, 0, 0);
0610     }
0611     else m_Solver->usePosition(false, 0, 0);
0612 }
0613 
0614 // If it is currently solving an image, then cancel the solve.
0615 // Otherwise start solving.
0616 void FITSTab::extractImage()
0617 {
0618     if (m_Solver.get() && m_Solver->isRunning())
0619     {
0620         m_PlateSolveUI.SolveButton->setText(i18n("Aborting..."));
0621         m_Solver->abort();
0622         return;
0623     }
0624     m_PlateSolveUI.SolveButton->setText(i18n("Cancel"));
0625 
0626     setupSolver(true);
0627 
0628     m_PlateSolveUI.FitsSolverAngle->setText("");
0629     m_PlateSolveUI.Solution1->setText(i18n("Extracting..."));
0630     m_PlateSolveUI.Solution2->setText("");
0631 
0632     m_Solver->runSolver(m_View->imageData());
0633 }
0634 
0635 void FITSTab::solveImage()
0636 {
0637     if (m_Solver.get() && m_Solver->isRunning())
0638     {
0639         m_PlateSolveUI.SolveButton->setText(i18n("Aborting..."));
0640         m_Solver->abort();
0641         return;
0642     }
0643     m_PlateSolveUI.SolveButton->setText(i18n("Cancel"));
0644 
0645     setupSolver(false);
0646 
0647     m_PlateSolveUI.Solution2->setText(i18n("Solving..."));
0648 
0649     m_Solver->runSolver(m_View->imageData());
0650 }
0651 
0652 void FITSTab::extractorDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
0653 {
0654     Q_UNUSED(solution);
0655     disconnect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::extractorDone);
0656     m_PlateSolveUI.Solution2->setText("");
0657 
0658     if (timedOut)
0659     {
0660         const QString result = i18n("Extractor timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
0661         m_PlateSolveUI.Solution1->setText(result);
0662 
0663         // Can't run the solver. Just reset.
0664         m_PlateSolveUI.SolveButton->setText("Solve");
0665         return;
0666     }
0667     else if (!success)
0668     {
0669         const QString result = i18n("Extractor failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
0670         m_PlateSolveUI.Solution1->setText(result);
0671 
0672         // Can't run the solver. Just reset.
0673         m_PlateSolveUI.SolveButton->setText(i18n("Solve"));
0674         return;
0675     }
0676     else
0677     {
0678         const QString starStr = i18n("Extracted %1 stars (%2 unfiltered) in %3s",
0679                                      m_Solver->getNumStarsFound(),
0680                                      m_Solver->getBackground().num_stars_detected,
0681                                      QString("%1").arg(elapsedSeconds, 0, 'f', 1));
0682         m_PlateSolveUI.Solution1->setText(starStr);
0683 
0684         // Set the stars in the FITSData object so the user can view them.
0685         const QList<FITSImage::Star> &starList = m_Solver->getStarList();
0686         QList<Edge*> starCenters;
0687         starCenters.reserve(starList.size());
0688         for (int i = 0; i < starList.size(); i++)
0689         {
0690             const auto &star = starList[i];
0691             Edge *oneEdge = new Edge();
0692             oneEdge->x = star.x;
0693             oneEdge->y = star.y;
0694             oneEdge->val = star.peak;
0695             oneEdge->sum = star.flux;
0696             oneEdge->HFR = star.HFR;
0697             oneEdge->width = star.a;
0698             oneEdge->numPixels = star.numPixels;
0699             if (star.a > 0)
0700                 // See page 63 to find the ellipticity equation for SEP.
0701                 // http://astroa.physics.metu.edu.tr/MANUALS/sextractor/Guide2source_extractor.pdf
0702                 oneEdge->ellipticity = 1 - star.b / star.a;
0703             else
0704                 oneEdge->ellipticity = 0;
0705 
0706             starCenters.append(oneEdge);
0707         }
0708         m_View->imageData()->setStarCenters(starCenters);
0709 
0710         // Now run the solver.
0711         solveImage();
0712     }
0713 }
0714 
0715 void FITSTab::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
0716 {
0717     disconnect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::solverDone);
0718     m_PlateSolveUI.SolveButton->setText("Solve");
0719 
0720     if (m_Solver->isRunning())
0721         qCDebug(KSTARS_FITS) << "solverDone called, but it is still running.";
0722 
0723     if (timedOut)
0724     {
0725         const QString result = i18n("Solver timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
0726         m_PlateSolveUI.Solution2->setText(result);
0727     }
0728     else if (!success)
0729     {
0730         const QString result = i18n("Solver failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
0731         m_PlateSolveUI.Solution2->setText(result);
0732     }
0733     else
0734     {
0735         const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
0736         m_View->imageData()->injectWCS(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
0737         m_View->imageData()->loadWCS();
0738 
0739         const QString result = QString("Solved in %1s").arg(elapsedSeconds, 0, 'f', 1);
0740         const double solverPA = KSUtils::rotationToPositionAngle(solution.orientation);
0741         m_PlateSolveUI.FitsSolverAngle->setText(QString("%1ยบ").arg(solverPA, 0, 'f', 2));
0742 
0743         // Set the scale widget to the current result
0744         const int imageWidth = m_View->imageData()->width();
0745         const int units = m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->currentIndex();
0746         if (units == SSolver::DEG_WIDTH)
0747             m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 3600.0);
0748         else if (units == SSolver::ARCMIN_WIDTH)
0749             m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 60.0);
0750         else
0751             m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale);
0752 
0753         // Set the ra and dec widgets to the current result
0754         m_PlateSolveUI.FitsSolverEstRA->show(dms(solution.ra));
0755         m_PlateSolveUI.FitsSolverEstDec->show(dms(solution.dec));
0756 
0757         m_PlateSolveUI.Solution2->setText(result);
0758     }
0759 }
0760 
0761 void FITSTab::initSolverUI()
0762 {
0763     // Init the profiles combo box.
0764     const QList<SSolver::Parameters> optionsList = getSSolverParametersList();
0765     m_PlateSolveUI.kcfg_FitsSolverProfile->clear();
0766     for(auto &param : optionsList)
0767         m_PlateSolveUI.kcfg_FitsSolverProfile->addItem(param.listName);
0768 
0769     // Restore the stored options.
0770     m_PlateSolveUI.kcfg_FitsSolverProfile->setCurrentIndex(Options::fitsSolverProfile());
0771 
0772     m_PlateSolveUI.kcfg_FitsSolverUseScale->setChecked(Options::fitsSolverUseScale());
0773     m_PlateSolveUI.kcfg_FitsSolverScale->setValue(Options::fitsSolverScale());
0774     m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->setCurrentIndex(Options::fitsSolverImageScaleUnits());
0775 
0776     m_PlateSolveUI.kcfg_FitsSolverUsePosition->setChecked(Options::fitsSolverUsePosition());
0777     m_PlateSolveUI.kcfg_FitsSolverRadius->setValue(Options::fitsSolverRadius());
0778 
0779     m_PlateSolveUI.FitsSolverEstRA->setUnits(dmsBox::HOURS);
0780     m_PlateSolveUI.FitsSolverEstDec->setUnits(dmsBox::DEGREES);
0781 
0782     // Save the values of user controls when the user changes them.
0783     connect(m_PlateSolveUI.kcfg_FitsSolverProfile, QOverload<int>::of(&QComboBox::activated), [](int index)
0784     {
0785         Options::setFitsSolverProfile(index);
0786     });
0787     connect(m_PlateSolveUI.kcfg_FitsSolverUseScale, &QCheckBox::stateChanged, this, [](int state)
0788     {
0789         Options::setFitsSolverUseScale(state);
0790     });
0791     connect(m_PlateSolveUI.kcfg_FitsSolverScale, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [](double value)
0792     {
0793         Options::setFitsSolverScale(value);
0794     });
0795     connect(m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits, QOverload<int>::of(&QComboBox::activated), [](int index)
0796     {
0797         Options::setFitsSolverImageScaleUnits(index);
0798     });
0799 
0800     connect(m_PlateSolveUI.kcfg_FitsSolverUsePosition, &QCheckBox::stateChanged, this, [](int state)
0801     {
0802         Options::setFitsSolverUsePosition(state);
0803     });
0804 
0805     connect(m_PlateSolveUI.kcfg_FitsSolverRadius, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [](double value)
0806     {
0807         Options::setFitsSolverRadius(value);
0808     });
0809     connect(m_PlateSolveUI.UpdatePosition, &QPushButton::clicked, this, [&]()
0810     {
0811         const auto center = SkyMap::Instance()->getCenterPoint();
0812         m_PlateSolveUI.FitsSolverEstRA->show(center.ra());
0813         m_PlateSolveUI.FitsSolverEstDec->show(center.dec());
0814     });
0815 
0816     // Warn if the user is not using the internal StellarSolver solver.
0817     const SSolver::SolverType type = static_cast<SSolver::SolverType>(Options::solverType());
0818     if(type != SSolver::SOLVER_STELLARSOLVER)
0819     {
0820         m_PlateSolveUI.Solution2->setText(i18n("Warning! This tool only supports the internal StellarSolver solver."));
0821         m_PlateSolveUI.Solution1->setText(i18n("Change to that in the Ekos Align options menu."));
0822     }
0823 }