File indexing completed on 2024-04-28 15:09:01

0001 /*
0002     SPDX-FileCopyrightText: 2013 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003     SPDX-FileCopyrightText: 2013-2021 Jasem Mutlaq <mutlaqja@ikarustech.com>
0004     SPDX-FileCopyrightText: 2018-2020 Robert Lancaster <rlancaste@gmail.com>
0005     SPDX-FileCopyrightText: 2019-2021 Hy Murveit <hy@murveit.com>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "align.h"
0011 #include "alignadaptor.h"
0012 #include "alignview.h"
0013 #include <ekos_align_debug.h>
0014 
0015 // Options
0016 #include "Options.h"
0017 #include "opsalign.h"
0018 #include "opsprograms.h"
0019 #include "opsastrometry.h"
0020 #include "opsastrometryindexfiles.h"
0021 
0022 // Components
0023 #include "mountmodel.h"
0024 #include "polaralignmentassistant.h"
0025 #include "remoteastrometryparser.h"
0026 #include "manualrotator.h"
0027 
0028 // FITS
0029 #include "fitsviewer/fitsdata.h"
0030 
0031 // Auxiliary
0032 #include "auxiliary/QProgressIndicator.h"
0033 #include "auxiliary/ksmessagebox.h"
0034 #include "ekos/auxiliary/darkprocessor.h"
0035 #include "ekos/auxiliary/filtermanager.h"
0036 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
0037 #include "ekos/auxiliary/profilesettings.h"
0038 #include "ekos/auxiliary/opticaltrainmanager.h"
0039 #include "ekos/auxiliary/opticaltrainsettings.h"
0040 #include "ksnotification.h"
0041 #include "kspaths.h"
0042 #include "ksuserdb.h"
0043 #include "fov.h"
0044 #include "kstars.h"
0045 #include "kstarsdata.h"
0046 #include "skymapcomposite.h"
0047 #include "ekos/auxiliary/solverutils.h"
0048 #include "ekos/auxiliary/rotatorutils.h"
0049 
0050 // INDI
0051 #include "ekos/manager.h"
0052 #include "indi/indidome.h"
0053 #include "indi/clientmanager.h"
0054 #include "indi/driverinfo.h"
0055 #include "indi/indifilterwheel.h"
0056 #include "indi/indirotator.h"
0057 #include "profileinfo.h"
0058 
0059 // System Includes
0060 #include <KActionCollection>
0061 #include <basedevice.h>
0062 #include <indicom.h>
0063 #include <memory>
0064 
0065 // Qt version calming
0066 #include <qtendl.h>
0067 
0068 #define MAXIMUM_SOLVER_ITERATIONS 10
0069 #define CAPTURE_RETRY_DELAY       10000
0070 #define CAPTURE_ROTATOR_DELAY     5000  // After 5 seconds estimated value should be not bad
0071 #define PAH_CUTOFF_FOV            10    // Minimum FOV width in arcminutes for PAH to work
0072 #define CHECK_PAH(x) \
0073     m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->x
0074 #define RUN_PAH(x) \
0075     if (m_PolarAlignmentAssistant) m_PolarAlignmentAssistant->x
0076 
0077 namespace Ekos
0078 {
0079 
0080 using PAA = PolarAlignmentAssistant;
0081 
0082 bool isEqual(double a, double b)
0083 {
0084     return std::abs(a - b) < 0.01;
0085 }
0086 
0087 Align::Align(const QSharedPointer<ProfileInfo> &activeProfile) : m_ActiveProfile(activeProfile)
0088 {
0089     setupUi(this);
0090 
0091     qRegisterMetaType<Ekos::AlignState>("Ekos::AlignState");
0092     qDBusRegisterMetaType<Ekos::AlignState>();
0093 
0094     new AlignAdaptor(this);
0095     QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this);
0096 
0097     dirPath = QDir::homePath();
0098 
0099     KStarsData::Instance()->clearTransientFOVs();
0100 
0101     solverFOV.reset(new FOV());
0102     solverFOV->setName(i18n("Solver FOV"));
0103     solverFOV->setObjectName("solver_fov");
0104     solverFOV->setLockCelestialPole(true);
0105     solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name());
0106     solverFOV->setProperty("visible", false);
0107     KStarsData::Instance()->addTransientFOV(solverFOV);
0108 
0109     sensorFOV.reset(new FOV());
0110     sensorFOV->setObjectName("sensor_fov");
0111     sensorFOV->setLockCelestialPole(true);
0112     sensorFOV->setProperty("visible", Options::showSensorFOV());
0113     KStarsData::Instance()->addTransientFOV(sensorFOV);
0114 
0115     QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov");
0116     if (a)
0117         a->setEnabled(true);
0118 
0119     showFITSViewerB->setIcon(
0120         QIcon::fromTheme("kstars_fitsviewer"));
0121     showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0122     connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer);
0123 
0124     toggleFullScreenB->setIcon(
0125         QIcon::fromTheme("view-fullscreen"));
0126     toggleFullScreenB->setShortcut(Qt::Key_F4);
0127     toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0128     connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen);
0129 
0130     // rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem"));
0131     rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0132 
0133     m_AlignView.reset(new AlignView(alignWidget, FITS_ALIGN));
0134     m_AlignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0135     m_AlignView->setBaseSize(alignWidget->size());
0136     m_AlignView->createFloatingToolBar();
0137     QVBoxLayout *vlayout = new QVBoxLayout();
0138 
0139     vlayout->addWidget(m_AlignView.get());
0140     alignWidget->setLayout(vlayout);
0141 
0142     connect(solveB, &QPushButton::clicked, this, &Ekos::Align::captureAndSolve);
0143     connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort);
0144 
0145     // Effective FOV Edit
0146     connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV);
0147 
0148     connect(loadSlewB, &QPushButton::clicked, this, [this]()
0149     {
0150         loadAndSlew();
0151     });
0152 
0153     gotoModeButtonGroup->setId(syncR, GOTO_SYNC);
0154     gotoModeButtonGroup->setId(slewR, GOTO_SLEW);
0155     gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING);
0156 
0157     m_CurrentGotoMode = static_cast<GotoMode>(Options::solverGotoOption());
0158     gotoModeButtonGroup->button(m_CurrentGotoMode)->setChecked(true);
0159 
0160 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
0161     connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this,
0162             [ = ](int id, bool toggled)
0163 #else
0164     connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this,
0165             [ = ](int id, bool toggled)
0166 #endif
0167     {
0168         if (toggled)
0169             this->m_CurrentGotoMode = static_cast<GotoMode>(id);
0170     });
0171 
0172     m_CaptureTimer.setSingleShot(true);
0173     m_CaptureTimer.setInterval(CAPTURE_RETRY_DELAY);
0174     connect(&m_CaptureTimer, &QTimer::timeout, this, &Align::processCaptureTimeout);
0175     m_AlignTimer.setSingleShot(true);
0176     m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
0177     connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout);
0178 
0179     pi.reset(new QProgressIndicator(this));
0180     stopLayout->addWidget(pi.get());
0181 
0182     rememberSolverWCS = Options::astrometrySolverWCS();
0183     rememberAutoWCS   = Options::autoWCS();
0184 
0185     solverModeButtonGroup->setId(localSolverR, SOLVER_LOCAL);
0186     solverModeButtonGroup->setId(remoteSolverR, SOLVER_REMOTE);
0187 
0188     setSolverMode(Options::solverMode());
0189 
0190 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
0191     connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this,
0192             [ = ](int id, bool toggled)
0193 #else
0194     connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this,
0195             [ = ](int id, bool toggled)
0196 #endif
0197     {
0198         if (toggled)
0199             setSolverMode(id);
0200     });
0201 
0202     connect(alignAccuracyThreshold, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]()
0203     {
0204         buildTarget();
0205     });
0206 
0207     connect(alignBinning, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
0208             &Ekos::Align::setBinningIndex);
0209 
0210     connect(this, &Align::newStatus, this, [this](AlignState state)
0211     {
0212         opticalTrainCombo->setEnabled(state < ALIGN_PROGRESS);
0213         trainB->setEnabled(state < ALIGN_PROGRESS);
0214     });
0215 
0216     connect(alignUseCurrentFilter, &QCheckBox::toggled, this, &Align::checkFilter);
0217 
0218     //Note:  This is to prevent a button from being called the default button
0219     //and then executing when the user hits the enter key such as when on a Text Box
0220     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
0221     for (auto &button : qButtons)
0222         button->setAutoDefault(false);
0223 
0224     savedOptionsProfiles = QDir(KSPaths::writableLocation(
0225                                     QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
0226     if(QFile(savedOptionsProfiles).exists())
0227         m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
0228     else
0229         m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
0230 
0231     m_StellarSolver.reset(new StellarSolver());
0232     connect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText);
0233 
0234     setupPolarAlignmentAssistant();
0235     setupManualRotator();
0236     setuptDarkProcessor();
0237     setupPlot();
0238     setupSolutionTable();
0239     setupOptions();
0240 
0241     // Load all settings
0242     loadGlobalSettings();
0243     connectSettings();
0244 
0245     setupOpticalTrainManager();
0246 }
0247 
0248 Align::~Align()
0249 {
0250     if (m_StellarSolver.get() != nullptr)
0251         disconnect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText);
0252 
0253     if (alignWidget->parent() == nullptr)
0254         toggleAlignWidgetFullScreen();
0255 
0256     // Remove temporary FITS files left before by the solver
0257     QDir dir(QDir::tempPath());
0258     dir.setNameFilters(QStringList() << "fits*"
0259                        << "tmp.*");
0260     dir.setFilter(QDir::Files);
0261     for (auto &dirFile : dir.entryList())
0262         dir.remove(dirFile);
0263 }
0264 void Align::selectSolutionTableRow(int row, int column)
0265 {
0266     Q_UNUSED(column)
0267 
0268     solutionTable->selectRow(row);
0269     for (int i = 0; i < alignPlot->itemCount(); i++)
0270     {
0271         QCPAbstractItem *abstractItem = alignPlot->item(i);
0272         if (abstractItem)
0273         {
0274             QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
0275             if (item)
0276             {
0277                 if (i == row)
0278                 {
0279                     item->setColor(Qt::black);
0280                     item->setBrush(Qt::yellow);
0281                 }
0282                 else
0283                 {
0284                     item->setColor(Qt::red);
0285                     item->setBrush(Qt::white);
0286                 }
0287             }
0288         }
0289     }
0290     alignPlot->replot();
0291 }
0292 
0293 void Align::handleHorizontalPlotSizeChange()
0294 {
0295     alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
0296     alignPlot->replot();
0297 }
0298 
0299 void Align::handleVerticalPlotSizeChange()
0300 {
0301     alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
0302     alignPlot->replot();
0303 }
0304 
0305 void Align::resizeEvent(QResizeEvent *event)
0306 {
0307     if (event->oldSize().width() != -1)
0308     {
0309         if (event->oldSize().width() != size().width())
0310             handleHorizontalPlotSizeChange();
0311         else if (event->oldSize().height() != size().height())
0312             handleVerticalPlotSizeChange();
0313     }
0314     else
0315     {
0316         QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange);
0317     }
0318 }
0319 
0320 void Align::handlePointTooltip(QMouseEvent *event)
0321 {
0322     QCPAbstractItem *item = alignPlot->itemAt(event->localPos());
0323     if (item)
0324     {
0325         QCPItemText *label = qobject_cast<QCPItemText *>(item);
0326         if (label)
0327         {
0328             QString labelText = label->text();
0329             int point         = labelText.toInt() - 1;
0330 
0331             if (point < 0)
0332                 return;
0333             QToolTip::showText(event->globalPos(),
0334                                i18n("<table>"
0335                                     "<tr>"
0336                                     "<th colspan=\"2\">Object %1: %2</th>"
0337                                     "</tr>"
0338                                     "<tr>"
0339                                     "<td>RA:</td><td>%3</td>"
0340                                     "</tr>"
0341                                     "<tr>"
0342                                     "<td>DE:</td><td>%4</td>"
0343                                     "</tr>"
0344                                     "<tr>"
0345                                     "<td>dRA:</td><td>%5</td>"
0346                                     "</tr>"
0347                                     "<tr>"
0348                                     "<td>dDE:</td><td>%6</td>"
0349                                     "</tr>"
0350                                     "</table>",
0351                                     point + 1,
0352                                     solutionTable->item(point, 2)->text(),
0353                                     solutionTable->item(point, 0)->text(),
0354                                     solutionTable->item(point, 1)->text(),
0355                                     solutionTable->item(point, 4)->text(),
0356                                     solutionTable->item(point, 5)->text()),
0357                                alignPlot, alignPlot->rect());
0358         }
0359     }
0360 }
0361 
0362 void Align::buildTarget()
0363 {
0364     double accuracyRadius = alignAccuracyThreshold->value();
0365     if (centralTarget)
0366     {
0367         concentricRings->data()->clear();
0368         redTarget->data()->clear();
0369         yellowTarget->data()->clear();
0370         centralTarget->data()->clear();
0371     }
0372     else
0373     {
0374         concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
0375         redTarget       = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
0376         yellowTarget    = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
0377         centralTarget   = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
0378     }
0379     const int pointCount = 200;
0380     QVector<QCPCurveData> circleRings(
0381         pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175%
0382     QVector<QCPCurveData> circleCentral(pointCount);
0383     QVector<QCPCurveData> circleYellow(pointCount);
0384     QVector<QCPCurveData> circleRed(pointCount);
0385 
0386     int circleRingPt = 0;
0387     for (int i = 0; i < pointCount; i++)
0388     {
0389         double theta = i / static_cast<double>(pointCount) * 2 * M_PI;
0390 
0391         for (double ring = 1; ring < 8; ring++)
0392         {
0393             if (ring != 4 && ring != 6)
0394             {
0395                 if (i % (9 - static_cast<int>(ring)) == 0) //This causes fewer points to draw on the inner circles.
0396                 {
0397                     circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta),
0398                                                 accuracyRadius * ring * 0.25 * qSin(theta));
0399                     circleRingPt++;
0400                 }
0401             }
0402         }
0403 
0404         circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta));
0405         circleYellow[i]  = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta));
0406         circleRed[i]     = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta));
0407     }
0408 
0409     concentricRings->setLineStyle(QCPCurve::lsNone);
0410     concentricRings->setScatterSkip(0);
0411     concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1));
0412 
0413     concentricRings->data()->set(circleRings, true);
0414     redTarget->data()->set(circleRed, true);
0415     yellowTarget->data()->set(circleYellow, true);
0416     centralTarget->data()->set(circleCentral, true);
0417 
0418     concentricRings->setPen(QPen(Qt::white));
0419     redTarget->setPen(QPen(Qt::red));
0420     yellowTarget->setPen(QPen(Qt::yellow));
0421     centralTarget->setPen(QPen(Qt::green));
0422 
0423     concentricRings->setBrush(Qt::NoBrush);
0424     redTarget->setBrush(QBrush(QColor(255, 0, 0, 50)));
0425     yellowTarget->setBrush(
0426         QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow.  It is green on top of red with equal opacity.
0427     centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50)));
0428 
0429     if (alignPlot->size().width() > 0)
0430         alignPlot->replot();
0431 }
0432 
0433 void Align::slotAutoScaleGraph()
0434 {
0435     double accuracyRadius = alignAccuracyThreshold->value();
0436     alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
0437     alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
0438 
0439     alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
0440 
0441     alignPlot->replot();
0442 }
0443 
0444 
0445 void Align::slotClearAllSolutionPoints()
0446 {
0447     if (solutionTable->rowCount() == 0)
0448         return;
0449 
0450     connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
0451     {
0452         //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
0453         KSMessageBox::Instance()->disconnect(this);
0454 
0455         solutionTable->setRowCount(0);
0456         alignPlot->graph(0)->data()->clear();
0457         alignPlot->clearItems();
0458         buildTarget();
0459 
0460         slotAutoScaleGraph();
0461 
0462     });
0463 
0464     KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"),
0465                                             i18n("Clear Solution Points"), 60);
0466 }
0467 
0468 void Align::slotRemoveSolutionPoint()
0469 {
0470     auto abstractItem = alignPlot->item(solutionTable->currentRow());
0471     if (abstractItem)
0472     {
0473         auto item = qobject_cast<QCPItemText *>(abstractItem);
0474         if (item)
0475         {
0476             double point = item->position->key();
0477             alignPlot->graph(0)->data()->remove(point);
0478         }
0479     }
0480 
0481     alignPlot->removeItem(solutionTable->currentRow());
0482 
0483     for (int i = 0; i < alignPlot->itemCount(); i++)
0484     {
0485         auto oneItem = alignPlot->item(i);
0486         if (oneItem)
0487         {
0488             auto item = qobject_cast<QCPItemText *>(oneItem);
0489             if (item)
0490                 item->setText(QString::number(i + 1));
0491         }
0492     }
0493     solutionTable->removeRow(solutionTable->currentRow());
0494     alignPlot->replot();
0495 }
0496 
0497 void Align::slotMountModel()
0498 {
0499     if (!m_MountModel)
0500     {
0501         m_MountModel = new MountModel(this);
0502         connect(m_MountModel, &Ekos::MountModel::newLog, this, &Ekos::Align::appendLogText, Qt::UniqueConnection);
0503         connect(m_MountModel, &Ekos::MountModel::aborted, this, [this]()
0504         {
0505             if (m_Mount && m_Mount->isSlewing())
0506                 m_Mount->abort();
0507             abort();
0508         });
0509         connect(this, &Ekos::Align::newStatus, m_MountModel, &Ekos::MountModel::setAlignStatus, Qt::UniqueConnection);
0510     }
0511 
0512     m_MountModel->show();
0513 }
0514 
0515 
0516 bool Align::isParserOK()
0517 {
0518     return true; //For now
0519     //    Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid.");
0520 
0521     //    bool rc = parser->init();
0522 
0523     //    if (rc)
0524     //    {
0525     //        connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
0526     //        connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
0527     //    }
0528 
0529     //    return rc;
0530 }
0531 
0532 void Align::checkAlignmentTimeout()
0533 {
0534     if (m_SolveFromFile || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
0535         abort();
0536     else
0537     {
0538         appendLogText(i18n("Solver timed out."));
0539         parser->stopSolver();
0540 
0541         setAlignTableResult(ALIGN_RESULT_FAILED);
0542         captureAndSolve();
0543     }
0544     // TODO must also account for loadAndSlew. Retain file name
0545 }
0546 
0547 void Align::setSolverMode(int mode)
0548 {
0549     if (sender() == nullptr && mode >= 0 && mode <= 1)
0550     {
0551         solverModeButtonGroup->button(mode)->setChecked(true);
0552     }
0553 
0554     Options::setSolverMode(mode);
0555 
0556     if (mode == SOLVER_REMOTE)
0557     {
0558         if (remoteParser.get() != nullptr && m_RemoteParserDevice != nullptr)
0559         {
0560             parser = remoteParser.get();
0561             (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(m_RemoteParserDevice);
0562             return;
0563         }
0564 
0565         remoteParser.reset(new Ekos::RemoteAstrometryParser());
0566         parser = remoteParser.get();
0567         (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(m_RemoteParserDevice);
0568         if (m_Camera)
0569             (dynamic_cast<RemoteAstrometryParser *>(parser))->setCCD(m_Camera->getDeviceName());
0570 
0571         parser->setAlign(this);
0572         if (parser->init())
0573         {
0574             connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
0575             connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
0576         }
0577         else
0578             parser->disconnect();
0579     }
0580 }
0581 
0582 QString Align::camera()
0583 {
0584     if (m_Camera)
0585         return m_Camera->getDeviceName();
0586 
0587     return QString();
0588 }
0589 
0590 void Align::checkCamera()
0591 {
0592     if (!m_Camera)
0593         return;
0594 
0595     // Do NOT perform checks if align is in progress as this may result
0596     // in signals/slots getting disconnected.
0597     switch (state)
0598     {
0599         // Idle, camera change is OK.
0600         case ALIGN_IDLE:
0601         case ALIGN_COMPLETE:
0602         case ALIGN_FAILED:
0603         case ALIGN_ABORTED:
0604         case ALIGN_SUCCESSFUL:
0605             break;
0606 
0607         // Busy, camera change is not OK.
0608         case ALIGN_PROGRESS:
0609         case ALIGN_SYNCING:
0610         case ALIGN_SLEWING:
0611         case ALIGN_SUSPENDED:
0612         case ALIGN_ROTATING:
0613             return;
0614     }
0615 
0616     auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
0617     if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
0618         return;
0619 
0620     if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
0621         (dynamic_cast<RemoteAstrometryParser *>(remoteParser.get()))->setCCD(m_Camera->getDeviceName());
0622 
0623     syncCameraInfo();
0624     syncCameraControls();
0625     syncTelescopeInfo();
0626 }
0627 
0628 bool Align::setCamera(ISD::Camera *device)
0629 {
0630     if (m_Camera && m_Camera == device)
0631     {
0632         checkCamera();
0633         return false;
0634     }
0635 
0636     if (m_Camera)
0637         m_Camera->disconnect(this);
0638 
0639     m_Camera = device;
0640 
0641     if (m_Camera)
0642     {
0643         connect(m_Camera, &ISD::ConcreteDevice::Connected, this, [this]()
0644         {
0645             auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
0646             controlBox->setEnabled(isConnected);
0647             gotoBox->setEnabled(isConnected);
0648             plateSolverOptionsGroup->setEnabled(isConnected);
0649             tabWidget->setEnabled(isConnected);
0650         });
0651         connect(m_Camera, &ISD::ConcreteDevice::Disconnected, this, [this]()
0652         {
0653             auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
0654             controlBox->setEnabled(isConnected);
0655             gotoBox->setEnabled(isConnected);
0656             plateSolverOptionsGroup->setEnabled(isConnected);
0657             tabWidget->setEnabled(isConnected);
0658 
0659             opticalTrainCombo->setEnabled(true);
0660             trainLabel->setEnabled(true);
0661         });
0662     }
0663 
0664     auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
0665     controlBox->setEnabled(isConnected);
0666     gotoBox->setEnabled(isConnected);
0667     plateSolverOptionsGroup->setEnabled(isConnected);
0668     tabWidget->setEnabled(isConnected);
0669 
0670     if (!m_Camera)
0671         return false;
0672 
0673     checkCamera();
0674 
0675     return true;
0676 }
0677 
0678 bool Align::setMount(ISD::Mount *device)
0679 {
0680     if (m_Mount && m_Mount == device)
0681     {
0682         syncTelescopeInfo();
0683         return false;
0684     }
0685 
0686     if (m_Mount)
0687         m_Mount->disconnect(this);
0688 
0689     m_Mount = device;
0690 
0691     if (m_Mount)
0692     {
0693         connect(m_Mount, &ISD::ConcreteDevice::Connected, this, [this]()
0694         {
0695             auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
0696             controlBox->setEnabled(isConnected);
0697             gotoBox->setEnabled(isConnected);
0698             plateSolverOptionsGroup->setEnabled(isConnected);
0699             tabWidget->setEnabled(isConnected);
0700         });
0701         connect(m_Mount, &ISD::ConcreteDevice::Disconnected, this, [this]()
0702         {
0703             auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
0704             controlBox->setEnabled(isConnected);
0705             gotoBox->setEnabled(isConnected);
0706             plateSolverOptionsGroup->setEnabled(isConnected);
0707             tabWidget->setEnabled(isConnected);
0708 
0709             opticalTrainCombo->setEnabled(true);
0710             trainLabel->setEnabled(true);
0711         });
0712     }
0713 
0714     auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
0715     controlBox->setEnabled(isConnected);
0716     gotoBox->setEnabled(isConnected);
0717     plateSolverOptionsGroup->setEnabled(isConnected);
0718     tabWidget->setEnabled(isConnected);
0719 
0720     if (!m_Mount)
0721         return false;
0722 
0723     RUN_PAH(setCurrentTelescope(m_Mount));
0724 
0725     connect(m_Mount, &ISD::Mount::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
0726     connect(m_Mount, &ISD::Mount::Disconnected, this, [this]()
0727     {
0728         m_isRateSynced = false;
0729     });
0730 
0731     syncTelescopeInfo();
0732     return true;
0733 }
0734 
0735 bool Align::setDome(ISD::Dome *device)
0736 {
0737     if (m_Dome && m_Dome == device)
0738         return false;
0739 
0740     if (m_Dome)
0741         m_Dome->disconnect(this);
0742 
0743     m_Dome = device;
0744 
0745     if (!m_Dome)
0746         return false;
0747 
0748     connect(m_Dome, &ISD::Dome::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
0749     return true;
0750 }
0751 
0752 void Align::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
0753 {
0754     auto name = device->getDeviceName();
0755     device->disconnect(this);
0756 
0757     // Check mounts
0758     if (m_Mount && m_Mount->getDeviceName() == name)
0759     {
0760         m_Mount->disconnect(this);
0761         m_Mount = nullptr;
0762     }
0763 
0764     // Check domes
0765     if (m_Dome && m_Dome->getDeviceName() == name)
0766     {
0767         m_Dome->disconnect(this);
0768         m_Dome = nullptr;
0769     }
0770 
0771     // Check rotators
0772     if (m_Rotator && m_Rotator->getDeviceName() == name)
0773     {
0774         m_Rotator->disconnect(this);
0775         m_Rotator = nullptr;
0776     }
0777 
0778     // Check cameras
0779     if (m_Camera && m_Camera->getDeviceName() == name)
0780     {
0781         m_Camera->disconnect(this);
0782         m_Camera = nullptr;
0783 
0784         QTimer::singleShot(1000, this, &Align::checkCamera);
0785     }
0786 
0787     // Check Remote Astrometry
0788     if (m_RemoteParserDevice && m_RemoteParserDevice->getDeviceName() == name)
0789     {
0790         m_RemoteParserDevice.clear();
0791     }
0792 
0793     // Check Filter Wheels
0794     if (m_FilterWheel && m_FilterWheel->getDeviceName() == name)
0795     {
0796         m_FilterWheel->disconnect(this);
0797         m_FilterWheel = nullptr;
0798 
0799         QTimer::singleShot(1000, this, &Align::checkFilter);
0800     }
0801 
0802 }
0803 
0804 bool Align::syncTelescopeInfo()
0805 {
0806     if (m_Mount == nullptr || m_Mount->isConnected() == false)
0807         return false;
0808 
0809     if (m_isRateSynced == false)
0810     {
0811         auto speed = m_Settings["PAHMountSpeed"];
0812         auto slewRates = m_Mount->slewRates();
0813         if (speed.isValid())
0814         {
0815             RUN_PAH(syncMountSpeed(speed.toString()));
0816         }
0817         else if (!slewRates.isEmpty())
0818         {
0819             RUN_PAH(syncMountSpeed(slewRates.last()));
0820         }
0821 
0822         m_isRateSynced = !slewRates.empty();
0823     }
0824 
0825     canSync = m_Mount->canSync();
0826 
0827     if (canSync == false && syncR->isEnabled())
0828     {
0829         slewR->setChecked(true);
0830         appendLogText(i18n("Mount does not support syncing."));
0831     }
0832 
0833     syncR->setEnabled(canSync);
0834 
0835     if (m_FocalLength == -1 || m_Aperture == -1)
0836         return false;
0837 
0838     if (m_CameraPixelWidth != -1 && m_CameraPixelHeight != -1)
0839     {
0840         calculateFOV();
0841         return true;
0842     }
0843 
0844     return false;
0845 }
0846 
0847 void Align::syncCameraInfo()
0848 {
0849     if (!m_Camera)
0850         return;
0851 
0852     auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0853     Q_ASSERT(targetChip);
0854 
0855     // Get Maximum resolution and pixel size
0856     uint8_t bit_depth = 8;
0857     targetChip->getImageInfo(m_CameraWidth, m_CameraHeight, m_CameraPixelWidth, m_CameraPixelHeight, bit_depth);
0858 
0859     setWCSEnabled(Options::astrometrySolverWCS());
0860 
0861     int binx = 1, biny = 1;
0862     alignBinning->setEnabled(targetChip->canBin());
0863     if (targetChip->canBin())
0864     {
0865         alignBinning->blockSignals(true);
0866 
0867         targetChip->getMaxBin(&binx, &biny);
0868         alignBinning->clear();
0869 
0870         for (int i = 0; i < binx; i++)
0871             alignBinning->addItem(QString("%1x%2").arg(i + 1).arg(i + 1));
0872 
0873         auto binning = m_Settings["alignBinning"];
0874         if (binning.isValid())
0875             alignBinning->setCurrentText(binning.toString());
0876 
0877         alignBinning->blockSignals(false);
0878     }
0879 
0880     // In case ROI is different (smaller) than maximum resolution, let's use that.
0881     // N.B. 2022.08.14 JM: We must account for binning since this value is used for FOV calculations.
0882     int roiW = 0, roiH = 0;
0883     targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &roiW, nullptr, &roiH);
0884     roiW *= binx;
0885     roiH *= biny;
0886     if ( (roiW > 0 && roiW < m_CameraWidth) || (roiH > 0 && roiH < m_CameraHeight))
0887     {
0888         m_CameraWidth = roiW;
0889         m_CameraHeight = roiH;
0890     }
0891 
0892     if (m_CameraPixelWidth > 0 && m_CameraPixelHeight > 0 && m_FocalLength > 0 && m_Aperture > 0)
0893     {
0894         calculateFOV();
0895     }
0896 }
0897 
0898 void Align::syncCameraControls()
0899 {
0900     if (m_Camera == nullptr)
0901         return;
0902 
0903     auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
0904     if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
0905         return;
0906 
0907     auto isoList = targetChip->getISOList();
0908     alignISO->clear();
0909 
0910     if (isoList.isEmpty())
0911     {
0912         alignISO->setEnabled(false);
0913     }
0914     else
0915     {
0916         alignISO->setEnabled(true);
0917         alignISO->addItems(isoList);
0918         alignISO->setCurrentIndex(targetChip->getISOIndex());
0919     }
0920 
0921     // Gain Check
0922     if (m_Camera->hasGain())
0923     {
0924         double min, max, step, value;
0925         m_Camera->getGainMinMaxStep(&min, &max, &step);
0926 
0927         // Allow the possibility of no gain value at all.
0928         alignGainSpecialValue = min - step;
0929         alignGain->setRange(alignGainSpecialValue, max);
0930         alignGain->setSpecialValueText(i18n("--"));
0931         alignGain->setEnabled(true);
0932         alignGain->setSingleStep(step);
0933         m_Camera->getGain(&value);
0934 
0935         auto gain = m_Settings["alignGain"];
0936         // Set the custom gain if we have one
0937         // otherwise it will not have an effect.
0938         if (gain.isValid())
0939             TargetCustomGainValue = gain.toDouble();
0940         if (TargetCustomGainValue > 0)
0941             alignGain->setValue(TargetCustomGainValue);
0942         else
0943             alignGain->setValue(alignGainSpecialValue);
0944 
0945         alignGain->setReadOnly(m_Camera->getGainPermission() == IP_RO);
0946 
0947         connect(alignGain, &QDoubleSpinBox::editingFinished, this, [this]()
0948         {
0949             if (alignGain->value() > alignGainSpecialValue)
0950                 TargetCustomGainValue = alignGain->value();
0951         });
0952     }
0953     else
0954         alignGain->setEnabled(false);
0955 }
0956 
0957 void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale)
0958 {
0959     fov_w     = m_FOVWidth;
0960     fov_h     = m_FOVHeight;
0961     fov_scale = m_FOVPixelScale;
0962 }
0963 
0964 QList<double> Align::fov()
0965 {
0966     QList<double> result;
0967 
0968     result << m_FOVWidth << m_FOVHeight << m_FOVPixelScale;
0969 
0970     return result;
0971 }
0972 
0973 QList<double> Align::cameraInfo()
0974 {
0975     QList<double> result;
0976 
0977     result << m_CameraWidth << m_CameraHeight << m_CameraPixelWidth << m_CameraPixelHeight;
0978 
0979     return result;
0980 }
0981 
0982 QList<double> Align::telescopeInfo()
0983 {
0984     QList<double> result;
0985 
0986     result << m_FocalLength << m_Aperture << m_Reducer;
0987 
0988     return result;
0989 }
0990 
0991 void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale)
0992 {
0993     // FOV in arcsecs
0994     // DSLR
0995     auto reducedFocalLength = m_Reducer * m_FocalLength;
0996     if (m_FocalRatio > 0)
0997     {
0998         // The forumla is in radians, must convert to degrees.
0999         // Then to arcsecs
1000         fov_w = 3600 * 2 * atan(m_CameraWidth * (m_CameraPixelWidth / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1001         fov_h = 3600 * 2 * atan(m_CameraHeight * (m_CameraPixelHeight / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1002     }
1003     // Telescope
1004     else
1005     {
1006         fov_w = 206264.8062470963552 * m_CameraWidth * m_CameraPixelWidth / 1000.0 / reducedFocalLength;
1007         fov_h = 206264.8062470963552 * m_CameraHeight * m_CameraPixelHeight / 1000.0 / reducedFocalLength;
1008     }
1009 
1010     // Pix Scale
1011     fov_scale = (fov_w * (alignBinning->currentIndex() + 1)) / m_CameraWidth;
1012 
1013     // FOV in arcmins
1014     fov_w /= 60.0;
1015     fov_h /= 60.0;
1016 }
1017 
1018 void Align::calculateEffectiveFocalLength(double newFOVW)
1019 {
1020     if (newFOVW < 0 || isEqual(newFOVW, m_FOVWidth))
1021         return;
1022 
1023     auto reducedFocalLength = m_Reducer * m_FocalLength;
1024     double new_focal_length = 0;
1025 
1026     if (m_FocalRatio > 0)
1027         new_focal_length = ((m_CameraWidth * m_CameraPixelWidth / 1000.0) / tan(newFOVW / 2)) / 2;
1028     else
1029         new_focal_length = ((m_CameraWidth * m_CameraPixelWidth / 1000.0) * 206264.8062470963552) / (newFOVW * 60.0);
1030     double focal_diff = std::fabs(new_focal_length - reducedFocalLength);
1031 
1032     if (focal_diff > 1)
1033     {
1034         m_EffectiveFocalLength = new_focal_length / m_Reducer;
1035         appendLogText(i18n("Effective telescope focal length is updated to %1 mm.", m_EffectiveFocalLength));
1036     }
1037 }
1038 
1039 void Align::calculateFOV()
1040 {
1041     auto reducedFocalLength = m_Reducer * m_FocalLength;
1042     auto reducecdEffectiveFocalLength = m_Reducer * m_EffectiveFocalLength;
1043     auto reducedFocalRatio = m_Reducer * m_FocalRatio;
1044 
1045     if (m_FocalRatio > 0)
1046     {
1047         // The forumla is in radians, must convert to degrees.
1048         // Then to arcsecs
1049         m_FOVWidth = 3600 * 2 * atan(m_CameraWidth * (m_CameraPixelWidth / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1050         m_FOVHeight = 3600 * 2 * atan(m_CameraHeight * (m_CameraPixelHeight / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1051     }
1052     // Telescope
1053     else
1054     {
1055         m_FOVWidth = 206264.8062470963552 * m_CameraWidth * m_CameraPixelWidth / 1000.0 / reducedFocalLength;
1056         m_FOVHeight = 206264.8062470963552 * m_CameraHeight * m_CameraPixelHeight / 1000.0 / reducedFocalLength;
1057     }
1058 
1059     // Calculate FOV
1060 
1061     // Pix Scale
1062     m_FOVPixelScale = (m_FOVWidth * (alignBinning->currentIndex() + 1)) / m_CameraWidth;
1063 
1064     // FOV in arcmins
1065     m_FOVWidth /= 60.0;
1066     m_FOVHeight /= 60.0;
1067 
1068     double calculated_fov_x = m_FOVWidth;
1069     double calculated_fov_y = m_FOVHeight;
1070 
1071     QString calculatedFOV = (QString("%1' x %2'").arg(m_FOVWidth, 0, 'f', 1).arg(m_FOVHeight, 0, 'f', 1));
1072 
1073     // Put FOV upper limit as 180 degrees
1074     if (m_FOVWidth < 1 || m_FOVWidth > 60 * 180 || m_FOVHeight < 1 || m_FOVHeight > 60 * 180)
1075     {
1076         appendLogText(
1077             i18n("Warning! The calculated field of view (%1) is out of bounds. Ensure the telescope focal length and camera pixel size are correct.",
1078                  calculatedFOV));
1079         return;
1080     }
1081 
1082     FocalLengthOut->setText(QString("%1 (%2)").arg(reducedFocalLength, 0, 'f', 1).
1083                             arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength : reducedFocalLength, 0, 'f', 1));
1084     // DSLR
1085     if (m_FocalRatio > 0)
1086         FocalRatioOut->setText(QString("%1 (%2)").arg(reducedFocalRatio, 0, 'f', 1).
1087                                arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength / m_Aperture : reducedFocalRatio, 0,
1088                                    'f', 1));
1089     // Telescope
1090     else if (m_Aperture > 0)
1091         FocalRatioOut->setText(QString("%1 (%2)").arg(reducedFocalLength / m_Aperture, 0, 'f', 1).
1092                                arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength / m_Aperture : reducedFocalLength / m_Aperture, 0,
1093                                    'f', 1));
1094     ReducerOut->setText(QString("%1x").arg(m_Reducer, 0, 'f', 2));
1095 
1096     if (m_EffectiveFocalLength > 0)
1097     {
1098         double focal_diff = std::fabs(m_EffectiveFocalLength  - m_FocalLength);
1099         if (focal_diff < 5)
1100             FocalLengthOut->setStyleSheet("color:green");
1101         else if (focal_diff < 15)
1102             FocalLengthOut->setStyleSheet("color:yellow");
1103         else
1104             FocalLengthOut->setStyleSheet("color:red");
1105     }
1106 
1107     // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV
1108     // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique
1109     // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete.
1110     getEffectiveFOV();
1111 
1112     if (m_FOVWidth == 0)
1113     {
1114         //FOVOut->setReadOnly(false);
1115         FOVOut->setToolTip(
1116             i18n("<p>Effective field of view size in arcminutes.</p><p>Please capture and solve once to measure the effective FOV or enter the values manually.</p><p>Calculated FOV: %1</p>",
1117                  calculatedFOV));
1118         m_FOVWidth = calculated_fov_x;
1119         m_FOVHeight = calculated_fov_y;
1120         m_EffectiveFOVPending = true;
1121     }
1122     else
1123     {
1124         m_EffectiveFOVPending = false;
1125         FOVOut->setToolTip(i18n("<p>Effective field of view size in arcminutes.</p>"));
1126     }
1127 
1128     solverFOV->setSize(m_FOVWidth, m_FOVHeight);
1129     sensorFOV->setSize(m_FOVWidth, m_FOVHeight);
1130     if (m_Camera)
1131         sensorFOV->setName(m_Camera->getDeviceName());
1132 
1133     FOVOut->setText(QString("%1' x %2'").arg(m_FOVWidth, 0, 'f', 1).arg(m_FOVHeight, 0, 'f', 1));
1134 
1135     // Enable or Disable PAA depending on current FOV
1136     const bool fovOK = ((m_FOVWidth + m_FOVHeight) / 2.0) > PAH_CUTOFF_FOV;
1137     if (m_PolarAlignmentAssistant != nullptr)
1138         m_PolarAlignmentAssistant->setEnabled(fovOK);
1139 
1140     if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked())
1141     {
1142         int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex();
1143 
1144         // Degrees
1145         if (unitType == 0)
1146         {
1147             double fov_low  = qMin(m_FOVWidth / 60, m_FOVHeight / 60);
1148             double fov_high = qMax(m_FOVWidth / 60, m_FOVHeight / 60);
1149             opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1150             opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1151 
1152             Options::setAstrometryImageScaleLow(fov_low);
1153             Options::setAstrometryImageScaleHigh(fov_high);
1154         }
1155         // Arcmins
1156         else if (unitType == 1)
1157         {
1158             double fov_low  = qMin(m_FOVWidth, m_FOVHeight);
1159             double fov_high = qMax(m_FOVWidth, m_FOVHeight);
1160             opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1161             opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1162 
1163             Options::setAstrometryImageScaleLow(fov_low);
1164             Options::setAstrometryImageScaleHigh(fov_high);
1165         }
1166         // Arcsec per pixel
1167         else
1168         {
1169             opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(m_FOVPixelScale * 0.9);
1170             opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(m_FOVPixelScale * 1.1);
1171 
1172             // 10% boundary
1173             Options::setAstrometryImageScaleLow(m_FOVPixelScale * 0.9);
1174             Options::setAstrometryImageScaleHigh(m_FOVPixelScale * 1.1);
1175         }
1176     }
1177 }
1178 
1179 QStringList Align::generateRemoteOptions(const QVariantMap &optionsMap)
1180 {
1181     QStringList solver_args;
1182 
1183     // -O overwrite
1184     // -3 Expected RA
1185     // -4 Expected DEC
1186     // -5 Radius (deg)
1187     // -L lower scale of image in arcminutes
1188     // -H upper scale of image in arcminutes
1189     // -u aw set scale to be in arcminutes
1190     // -W solution.wcs name of solution file
1191     // apog1.jpg name of target file to analyze
1192     //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1193 
1194     // Start with always-used arguments
1195     solver_args << "-O"
1196                 << "--no-plots";
1197 
1198     // Now go over boolean options
1199 
1200     // noverify
1201     if (optionsMap.contains("noverify"))
1202         solver_args << "--no-verify";
1203 
1204     // noresort
1205     if (optionsMap.contains("resort"))
1206         solver_args << "--resort";
1207 
1208     // fits2fits
1209     if (optionsMap.contains("nofits2fits"))
1210         solver_args << "--no-fits2fits";
1211 
1212     // downsample
1213     if (optionsMap.contains("downsample"))
1214         solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt());
1215 
1216     // image scale low
1217     if (optionsMap.contains("scaleL"))
1218         solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble());
1219 
1220     // image scale high
1221     if (optionsMap.contains("scaleH"))
1222         solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble());
1223 
1224     // image scale units
1225     if (optionsMap.contains("scaleUnits"))
1226         solver_args << "-u" << optionsMap.value("scaleUnits").toString();
1227 
1228     // RA
1229     if (optionsMap.contains("ra"))
1230         solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble());
1231 
1232     // DE
1233     if (optionsMap.contains("de"))
1234         solver_args << "-4" << QString::number(optionsMap.value("de").toDouble());
1235 
1236     // Radius
1237     if (optionsMap.contains("radius"))
1238         solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble());
1239 
1240     // Custom
1241     if (optionsMap.contains("custom"))
1242         solver_args << optionsMap.value("custom").toString();
1243 
1244     return solver_args;
1245 }
1246 
1247 //This will generate the high and low scale of the imager field size based on the stated units.
1248 void Align::generateFOVBounds(double fov_w, QString &fov_low, QString &fov_high, double tolerance)
1249 {
1250     // This sets the percentage we search outside the lower and upper boundary limits
1251     // by default, we stretch the limits by 5% (tolerance = 0.05)
1252     double lower_boundary = 1.0 - tolerance;
1253     double upper_boundary = 1.0 + tolerance;
1254 
1255     // let's stretch the boundaries by 5%
1256     //    fov_lower = ((fov_h < fov_v) ? (fov_h * lower_boundary) : (fov_v * lower_boundary));
1257     //    fov_upper = ((fov_h > fov_v) ? (fov_h * upper_boundary) : (fov_v * upper_boundary));
1258 
1259     // JM 2019-10-20: The bounds consider image width only, not height.
1260     double fov_lower = fov_w * lower_boundary;
1261     double fov_upper = fov_w * upper_boundary;
1262 
1263     //No need to do anything if they are aw, since that is the default
1264     fov_low  = QString::number(fov_lower);
1265     fov_high = QString::number(fov_upper);
1266 }
1267 
1268 
1269 QStringList Align::generateRemoteArgs(const QSharedPointer<FITSData> &data)
1270 {
1271     QVariantMap optionsMap;
1272 
1273     // -O overwrite
1274     // -3 Expected RA
1275     // -4 Expected DEC
1276     // -5 Radius (deg)
1277     // -L lower scale of image in arcminutes
1278     // -H upper scale of image in arcminutes
1279     // -u aw set scale to be in arcminutes
1280     // -W solution.wcs name of solution file
1281     // apog1.jpg name of target file to analyze
1282     //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1283 
1284     if (Options::astrometryUseNoVerify())
1285         optionsMap["noverify"] = true;
1286 
1287     if (Options::astrometryUseResort())
1288         optionsMap["resort"] = true;
1289 
1290     if (Options::astrometryUseNoFITS2FITS())
1291         optionsMap["nofits2fits"] = true;
1292 
1293     if (data == nullptr)
1294     {
1295         if (Options::astrometryUseDownsample())
1296         {
1297             if (Options::astrometryAutoDownsample() && m_CameraWidth && m_CameraHeight)
1298             {
1299                 uint16_t w = m_CameraWidth / (alignBinning->currentIndex() + 1);
1300                 optionsMap["downsample"] = getSolverDownsample(w);
1301             }
1302             else
1303                 optionsMap["downsample"] = Options::astrometryDownsample();
1304         }
1305 
1306         //Options needed for Sextractor
1307         optionsMap["image_width"] = m_CameraWidth / (alignBinning->currentIndex() + 1);
1308         optionsMap["image_height"] = m_CameraHeight / (alignBinning->currentIndex() + 1);
1309 
1310         if (Options::astrometryUseImageScale() && m_FOVWidth > 0 && m_FOVHeight > 0)
1311         {
1312             QString units = "dw";
1313             if (Options::astrometryImageScaleUnits() == 1)
1314                 units = "aw";
1315             else if (Options::astrometryImageScaleUnits() == 2)
1316                 units = "app";
1317             if (Options::astrometryAutoUpdateImageScale())
1318             {
1319                 QString fov_low, fov_high;
1320                 double fov_w = m_FOVWidth;
1321                 //double fov_h = m_FOVHeight;
1322 
1323                 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH)
1324                 {
1325                     fov_w /= 60;
1326                     //fov_h /= 60;
1327                 }
1328                 else if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
1329                 {
1330                     fov_w = m_FOVPixelScale;
1331                     //fov_h = m_FOVPixelScale;
1332                 }
1333 
1334                 // If effective FOV is pending, let's set a wider tolerance range
1335                 generateFOVBounds(fov_w, fov_low, fov_high, m_EffectiveFOVPending ? 0.3 : 0.05);
1336 
1337                 optionsMap["scaleL"]     = fov_low;
1338                 optionsMap["scaleH"]     = fov_high;
1339                 optionsMap["scaleUnits"] = units;
1340             }
1341             else
1342             {
1343                 optionsMap["scaleL"]     = Options::astrometryImageScaleLow();
1344                 optionsMap["scaleH"]     = Options::astrometryImageScaleHigh();
1345                 optionsMap["scaleUnits"] = units;
1346             }
1347         }
1348 
1349         if (Options::astrometryUsePosition() && m_Mount != nullptr)
1350         {
1351             double ra = 0, dec = 0;
1352             m_Mount->getEqCoords(&ra, &dec);
1353 
1354             optionsMap["ra"]     = ra * 15.0;
1355             optionsMap["de"]     = dec;
1356             optionsMap["radius"] = Options::astrometryRadius();
1357         }
1358     }
1359     else
1360     {
1361         // Downsample
1362         QVariant width;
1363         data->getRecordValue("NAXIS1", width);
1364         if (width.isValid())
1365         {
1366             optionsMap["downsample"] = getSolverDownsample(width.toInt());
1367         }
1368         else
1369             optionsMap["downsample"] = Options::astrometryDownsample();
1370 
1371         // Pixel Scale
1372         QVariant pixscale;
1373         data->getRecordValue("SCALE", pixscale);
1374         if (pixscale.isValid())
1375         {
1376             optionsMap["scaleL"]     = 0.8 * pixscale.toDouble();
1377             optionsMap["scaleH"]     = 1.2 * pixscale.toDouble();
1378             optionsMap["scaleUnits"] = "app";
1379         }
1380 
1381         // Position
1382         QVariant ra, de;
1383         data->getRecordValue("RA", ra);
1384         data->getRecordValue("DEC", de);
1385         if (ra.isValid() && de.isValid())
1386         {
1387             optionsMap["ra"]     = ra.toDouble();
1388             optionsMap["de"]     = de.toDouble();
1389             optionsMap["radius"] = Options::astrometryRadius();
1390         }
1391     }
1392 
1393     if (Options::astrometryCustomOptions().isEmpty() == false)
1394         optionsMap["custom"] = Options::astrometryCustomOptions();
1395 
1396     return generateRemoteOptions(optionsMap);
1397 }
1398 
1399 bool Align::captureAndSolve()
1400 {
1401     m_AlignTimer.stop();
1402     m_CaptureTimer.stop();
1403 
1404     if (m_Camera == nullptr)
1405     {
1406         appendLogText(i18n("Error: No camera detected."));
1407         return false;
1408     }
1409 
1410     if (m_Camera->isConnected() == false)
1411     {
1412         appendLogText(i18n("Error: lost connection to camera."));
1413         KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::Align,
1414                               KSNotification::Alert);
1415         return false;
1416     }
1417 
1418     if (m_Camera->isBLOBEnabled() == false)
1419     {
1420         m_Camera->setBLOBEnabled(true);
1421     }
1422 
1423     //if (parser->init() == false)
1424     //    return false;
1425 
1426     if (m_FocalLength == -1 || m_Aperture == -1)
1427     {
1428         KSNotification::error(
1429             i18n("Telescope aperture and focal length are missing. Please check your optical train settings and try again."));
1430         return false;
1431     }
1432 
1433     if (m_CameraPixelWidth == -1 || m_CameraPixelHeight == -1)
1434     {
1435         KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again."));
1436         return false;
1437     }
1438 
1439     if (m_FilterWheel != nullptr)
1440     {
1441         if (m_FilterWheel->isConnected() == false)
1442         {
1443             appendLogText(i18n("Error: lost connection to filter wheel."));
1444             return false;
1445         }
1446 
1447         int targetPosition = alignFilter->currentIndex() + 1;
1448 
1449         if (targetPosition > 0 && targetPosition != currentFilterPosition)
1450         {
1451             filterPositionPending    = true;
1452             // Disabling the autofocus policy for align.
1453             m_FilterManager->setFilterPosition(targetPosition, FilterManager::NO_AUTOFOCUS_POLICY);
1454             setState(ALIGN_PROGRESS);
1455             return true;
1456         }
1457     }
1458 
1459     auto clientManager = m_Camera->getDriverInfo()->getClientManager();
1460     if (clientManager && clientManager->getBLOBMode(m_Camera->getDeviceName().toLatin1().constData(), "CCD1") == B_NEVER)
1461     {
1462         if (KMessageBox::questionYesNo(
1463                     nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
1464                 KMessageBox::Yes)
1465         {
1466             clientManager->setBLOBMode(B_ONLY, m_Camera->getDeviceName().toLatin1().constData(), "CCD1");
1467             clientManager->setBLOBMode(B_ONLY, m_Camera->getDeviceName().toLatin1().constData(), "CCD2");
1468         }
1469         else
1470         {
1471             return false;
1472         }
1473     }
1474 
1475     double seqExpose = alignExposure->value();
1476 
1477     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1478 
1479     if (m_FocusState >= FOCUS_PROGRESS)
1480     {
1481         appendLogText(i18n("Cannot capture while focus module is busy. Retrying in %1 seconds...",
1482                            CAPTURE_RETRY_DELAY / 1000));
1483         m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1484         return true;
1485     }
1486 
1487     if (targetChip->isCapturing())
1488     {
1489         appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in %1 seconds...",
1490                            CAPTURE_RETRY_DELAY / 1000));
1491         m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1492         return true;
1493     }
1494 
1495     // Let rotate the camera BEFORE taking a capture in [Capture & Solve]
1496     if (!m_SolveFromFile && m_Rotator && m_Rotator->absoluteAngleState() == IPS_BUSY)
1497     {
1498         int TimeOut = CAPTURE_ROTATOR_DELAY;
1499         switch (m_CaptureTimeoutCounter)
1500         {
1501             case 0:// Set start time & start angle and estimate rotator time frame during first timeout
1502             {
1503                 auto absAngle = 0;
1504                 if ((absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE")->at(0)->getValue()))
1505                 {
1506                     RotatorUtils::Instance()->startTimeFrame(absAngle);
1507                     m_estimateRotatorTimeFrame = true;
1508                     appendLogText(i18n("Cannot capture while rotator is busy: Time delay estimate started..."));
1509                 }
1510                 m_CaptureTimer.start(TimeOut);
1511                 break;
1512             }
1513             case 1:// Use estimated time frame (in updateProperty()) for second timeout
1514             {
1515                 TimeOut = m_RotatorTimeFrame * 1000;
1516                 [[fallthrough]];
1517             }
1518             default:
1519             {
1520                 TimeOut *= m_CaptureTimeoutCounter; // Extend Timeout in case estimated value is too short
1521                 m_estimateRotatorTimeFrame = false;
1522                 appendLogText(i18n("Cannot capture while rotator is busy: Retrying in %1 seconds...", TimeOut / 1000));
1523                 m_CaptureTimer.start(TimeOut);
1524             }
1525         }
1526         return true; // Return value is used in 'Scheduler::startAstrometry()'
1527     }
1528 
1529     m_AlignView->setBaseSize(alignWidget->size());
1530     m_AlignView->setProperty("suspended", (solverModeButtonGroup->checkedId() == SOLVER_LOCAL
1531                                            && alignDarkFrame->isChecked()));
1532 
1533     connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
1534     connect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
1535 
1536     // In case of remote solver, check if we need to update active CCD
1537     if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
1538     {
1539         if (m_RemoteParserDevice == nullptr)
1540         {
1541             appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
1542             setSolverMode(SOLVER_LOCAL);
1543         }
1544         else
1545         {
1546             // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD
1547             auto activeDevices = m_RemoteParserDevice->getBaseDevice().getText("ACTIVE_DEVICES");
1548             if (activeDevices)
1549             {
1550                 auto activeCCD = activeDevices.findWidgetByName("ACTIVE_CCD");
1551                 if (QString(activeCCD->text) != m_Camera->getDeviceName())
1552                 {
1553                     activeCCD->setText(m_Camera->getDeviceName().toLatin1().constData());
1554                     m_RemoteParserDevice->getClientManager()->sendNewProperty(activeDevices);
1555                 }
1556             }
1557 
1558             // Enable remote parse
1559             dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(true);
1560             dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->sendArgs(generateRemoteArgs(QSharedPointer<FITSData>()));
1561             solverTimer.start();
1562         }
1563     }
1564 
1565     // Remove temporary FITS files left before by the solver
1566     QDir dir(QDir::tempPath());
1567     dir.setNameFilters(QStringList() << "fits*"  << "tmp.*");
1568     dir.setFilter(QDir::Files);
1569     for (auto &dirFile : dir.entryList())
1570         dir.remove(dirFile);
1571 
1572     prepareCapture(targetChip);
1573 
1574     // In case we're in refresh phase of the polar alignment helper then we use capture value from there
1575     if (matchPAHStage(PAA::PAH_REFRESH))
1576         targetChip->capture(m_PolarAlignmentAssistant->getPAHExposureDuration());
1577     else
1578         targetChip->capture(seqExpose);
1579 
1580     solveB->setEnabled(false);
1581     loadSlewB->setEnabled(false);
1582     stopB->setEnabled(true);
1583     pi->startAnimation();
1584 
1585     differentialSlewingActivated = false;
1586 
1587     setState(ALIGN_PROGRESS);
1588     emit newStatus(state);
1589     solverFOV->setProperty("visible", true);
1590 
1591     // If we're just refreshing, then we're done
1592     if (matchPAHStage(PAA::PAH_REFRESH))
1593         return true;
1594 
1595     appendLogText(i18n("Capturing image..."));
1596 
1597     if (!m_Mount)
1598         return true;
1599 
1600     //This block of code will create the row in the solution table and populate RA, DE, and object name.
1601     //It also starts the progress indicator.
1602     double ra, dec;
1603     m_Mount->getEqCoords(&ra, &dec);
1604     if (!m_SolveFromFile)
1605     {
1606         int currentRow = solutionTable->rowCount();
1607         solutionTable->insertRow(currentRow);
1608         for (int i = 4; i < 6; i++)
1609         {
1610             QTableWidgetItem *disabledBox = new QTableWidgetItem();
1611             disabledBox->setFlags(Qt::ItemIsSelectable);
1612             solutionTable->setItem(currentRow, i, disabledBox);
1613         }
1614 
1615         QTableWidgetItem *RAReport = new QTableWidgetItem();
1616         RAReport->setText(ScopeRAOut->text());
1617         RAReport->setTextAlignment(Qt::AlignHCenter);
1618         RAReport->setFlags(Qt::ItemIsSelectable);
1619         solutionTable->setItem(currentRow, 0, RAReport);
1620 
1621         QTableWidgetItem *DECReport = new QTableWidgetItem();
1622         DECReport->setText(ScopeDecOut->text());
1623         DECReport->setTextAlignment(Qt::AlignHCenter);
1624         DECReport->setFlags(Qt::ItemIsSelectable);
1625         solutionTable->setItem(currentRow, 1, DECReport);
1626 
1627         double maxrad = 1.0;
1628         SkyObject *so =
1629             KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad);
1630         QString name;
1631         if (so)
1632         {
1633             name = so->longname();
1634         }
1635         else
1636         {
1637             name = "None";
1638         }
1639         QTableWidgetItem *ObjNameReport = new QTableWidgetItem();
1640         ObjNameReport->setText(name);
1641         ObjNameReport->setTextAlignment(Qt::AlignHCenter);
1642         ObjNameReport->setFlags(Qt::ItemIsSelectable);
1643         solutionTable->setItem(currentRow, 2, ObjNameReport);
1644 #ifdef Q_OS_OSX
1645         repaint(); //This is a band-aid for a bug in QT 5.10.0
1646 #endif
1647 
1648         QProgressIndicator *alignIndicator = new QProgressIndicator(this);
1649         solutionTable->setCellWidget(currentRow, 3, alignIndicator);
1650         alignIndicator->startAnimation();
1651 #ifdef Q_OS_OSX
1652         repaint(); //This is a band-aid for a bug in QT 5.10.0
1653 #endif
1654     }
1655 
1656     return true;
1657 }
1658 
1659 void Align::processData(const QSharedPointer<FITSData> &data)
1660 {
1661     auto chip = data->property("chip");
1662     if (chip.isValid() && chip.toInt() == ISD::CameraChip::GUIDE_CCD)
1663         return;
1664 
1665     disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
1666     disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
1667 
1668     if (data)
1669     {
1670         m_AlignView->loadData(data);
1671         m_ImageData = data;
1672     }
1673     else
1674         m_ImageData.reset();
1675 
1676     RUN_PAH(setImageData(m_ImageData));
1677 
1678     // If it's Refresh, we're done
1679     if (matchPAHStage(PAA::PAH_REFRESH))
1680     {
1681         setCaptureComplete();
1682         return;
1683     }
1684     else
1685         appendLogText(i18n("Image received."));
1686 
1687     // If Local solver, then set capture complete or perform calibration first.
1688     if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1689     {
1690         // Only perform dark image subtraction on local images.
1691         if (alignDarkFrame->isChecked())
1692         {
1693             int x, y, w, h, binx = 1, biny = 1;
1694             ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1695             targetChip->getFrame(&x, &y, &w, &h);
1696             targetChip->getBinning(&binx, &biny);
1697 
1698             uint16_t offsetX = x / binx;
1699             uint16_t offsetY = y / biny;
1700 
1701             m_DarkProcessor->denoise(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()),
1702                                      targetChip, m_ImageData, alignExposure->value(), offsetX, offsetY);
1703             return;
1704         }
1705 
1706         setCaptureComplete();
1707     }
1708 }
1709 
1710 void Align::prepareCapture(ISD::CameraChip *targetChip)
1711 {
1712     if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1713     {
1714         rememberUploadMode = ISD::Camera::UPLOAD_LOCAL;
1715         m_Camera->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
1716     }
1717 
1718     if (m_Camera->isFastExposureEnabled())
1719     {
1720         m_RememberCameraFastExposure = true;
1721         m_Camera->setFastExposureEnabled(false);
1722     }
1723 
1724     m_Camera->setEncodingFormat("FITS");
1725     targetChip->resetFrame();
1726     targetChip->setBatchMode(false);
1727     targetChip->setCaptureMode(FITS_ALIGN);
1728     targetChip->setFrameType(FRAME_LIGHT);
1729 
1730     int bin = alignBinning->currentIndex() + 1;
1731     targetChip->setBinning(bin, bin);
1732 
1733     // Set gain if applicable
1734     if (m_Camera->hasGain() && alignGain->isEnabled() && alignGain->value() > alignGainSpecialValue)
1735         m_Camera->setGain(alignGain->value());
1736     // Set ISO if applicable
1737     if (alignISO->currentIndex() >= 0)
1738         targetChip->setISOIndex(alignISO->currentIndex());
1739 }
1740 
1741 
1742 void Align::setCaptureComplete()
1743 {
1744     if (matchPAHStage(PAA::PAH_REFRESH))
1745     {
1746         emit newFrame(m_AlignView);
1747         m_PolarAlignmentAssistant->processPAHRefresh();
1748         return;
1749     }
1750 
1751     emit newImage(m_AlignView);
1752 
1753     solverFOV->setImage(m_AlignView->getDisplayImage());
1754 
1755     // If align logging is enabled, let's save the frame.
1756     if (Options::saveAlignImages())
1757     {
1758         QDir dir;
1759         QDateTime now = KStarsData::Instance()->lt();
1760         QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/" +
1761                        now.toString("yyyy-MM-dd"));
1762         dir.mkpath(path);
1763         // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
1764         // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
1765         QString name     = "align_frame_" + now.toString("HH-mm-ss") + ".fits";
1766         QString filename = path + QStringLiteral("/") + name;
1767         if (m_ImageData)
1768             m_ImageData->saveImage(filename);
1769     }
1770 
1771     startSolving();
1772 }
1773 
1774 void Align::setSolverAction(int mode)
1775 {
1776     gotoModeButtonGroup->button(mode)->setChecked(true);
1777     m_CurrentGotoMode = static_cast<GotoMode>(mode);
1778 }
1779 
1780 void Align::startSolving()
1781 {
1782     //RUN_PAH(syncStage());
1783 
1784     // This is needed because they might have directories stored in the config file.
1785     // So we can't just use the options folder list.
1786     QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs();
1787     disconnect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving);
1788 
1789     m_UsedScale = false;
1790     m_UsedPosition = false;
1791     m_ScaleUsed = 0;
1792     m_RAUsed = 0;
1793     m_DECUsed = 0;
1794 
1795     if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1796     {
1797         if(Options::solverType() != SSolver::SOLVER_ASTAP
1798                 && Options::solverType() != SSolver::SOLVER_WATNEYASTROMETRY) //You don't need astrometry index files to use ASTAP or Watney
1799         {
1800             bool foundAnIndex = false;
1801             for(auto &dataDir : astrometryDataDirs)
1802             {
1803                 QDir dir = QDir(dataDir);
1804                 if(dir.exists())
1805                 {
1806                     dir.setNameFilters(QStringList() << "*.fits");
1807                     QStringList indexList = dir.entryList();
1808                     if(indexList.count() > 0)
1809                         foundAnIndex = true;
1810                 }
1811             }
1812             if(!foundAnIndex)
1813             {
1814                 appendLogText(
1815                     i18n("No index files were found on your system in the specified index file directories.  Please download some index files or add the correct directory to the list."));
1816                 KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings");
1817                 if(alignSettings && m_IndexFilesPage)
1818                 {
1819                     alignSettings->setCurrentPage(m_IndexFilesPage);
1820                     alignSettings->show();
1821                 }
1822             }
1823         }
1824         if (m_StellarSolver->isRunning())
1825             m_StellarSolver->abort();
1826         if (!m_ImageData)
1827             m_ImageData = m_AlignView->imageData();
1828         m_StellarSolver->loadNewImageBuffer(m_ImageData->getStatistics(), m_ImageData->getImageBuffer());
1829         m_StellarSolver->setProperty("ProcessType", SSolver::SOLVE);
1830         m_StellarSolver->setProperty("ExtractorType", Options::solveSextractorType());
1831         m_StellarSolver->setProperty("SolverType", Options::solverType());
1832         connect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
1833         m_StellarSolver->setIndexFolderPaths(Options::astrometryIndexFolderList());
1834 
1835         auto params = m_StellarSolverProfiles.at(Options::solveOptionsProfile());
1836         params.partition = Options::stellarSolverPartition();
1837         m_StellarSolver->setParameters(params);
1838 
1839         const SSolver::SolverType type = static_cast<SSolver::SolverType>(m_StellarSolver->property("SolverType").toInt());
1840         if(type == SSolver::SOLVER_LOCALASTROMETRY || type == SSolver::SOLVER_ASTAP || type == SSolver::SOLVER_WATNEYASTROMETRY)
1841         {
1842             QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1843                                    QRegularExpression("[-{}]")));
1844             m_AlignView->saveImage(filename);
1845             m_StellarSolver->setProperty("FileToProcess", filename);
1846             ExternalProgramPaths externalPaths;
1847             externalPaths.sextractorBinaryPath = Options::sextractorBinary();
1848             externalPaths.solverPath = Options::astrometrySolverBinary();
1849             externalPaths.astapBinaryPath = Options::aSTAPExecutable();
1850             externalPaths.watneyBinaryPath = Options::watneyBinary();
1851             externalPaths.wcsPath = Options::astrometryWCSInfo();
1852             m_StellarSolver->setExternalFilePaths(externalPaths);
1853 
1854             //No need for a conf file this way.
1855             m_StellarSolver->setProperty("AutoGenerateAstroConfig", true);
1856         }
1857 
1858         if(type == SSolver::SOLVER_ONLINEASTROMETRY )
1859         {
1860             QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1861                                    QRegularExpression("[-{}]")));
1862             m_AlignView->saveImage(filename);
1863 
1864             m_StellarSolver->setProperty("FileToProcess", filename);
1865             m_StellarSolver->setProperty("AstrometryAPIKey", Options::astrometryAPIKey());
1866             m_StellarSolver->setProperty("AstrometryAPIURL", Options::astrometryAPIURL());
1867         }
1868 
1869         bool useImageScale = Options::astrometryUseImageScale();
1870         if (useBlindScale == BLIND_ENGAGNED)
1871         {
1872             useImageScale = false;
1873             useBlindScale = BLIND_USED;
1874             appendLogText(i18n("Solving with blind image scale..."));
1875         }
1876 
1877         bool useImagePosition = Options::astrometryUsePosition();
1878         if (useBlindPosition == BLIND_ENGAGNED)
1879         {
1880             useImagePosition = false;
1881             useBlindPosition = BLIND_USED;
1882             appendLogText(i18n("Solving with blind image position..."));
1883         }
1884 
1885         if (m_SolveFromFile)
1886         {
1887             FITSImage::Solution solution;
1888             m_ImageData->parseSolution(solution);
1889 
1890             if (useImageScale && solution.pixscale > 0)
1891             {
1892                 m_UsedScale = true;
1893                 m_ScaleUsed = solution.pixscale;
1894                 m_StellarSolver->setSearchScale(solution.pixscale * 0.8,
1895                                                 solution.pixscale * 1.2,
1896                                                 SSolver::ARCSEC_PER_PIX);
1897             }
1898             else
1899                 m_StellarSolver->setProperty("UseScale", false);
1900 
1901             if (useImagePosition && solution.ra > 0)
1902             {
1903                 m_UsedPosition = true;
1904                 m_RAUsed = solution.ra;
1905                 m_DECUsed = solution.dec;
1906                 m_StellarSolver->setSearchPositionInDegrees(solution.ra, solution.dec);
1907             }
1908             else
1909                 m_StellarSolver->setProperty("UsePosition", false);
1910 
1911             QVariant value = "";
1912             if (!m_ImageData->getRecordValue("PIERSIDE", value))
1913             {
1914                 appendLogText(i18n("Loaded image does not have pierside information"));
1915                 m_TargetPierside = ISD::Mount::PIER_UNKNOWN;
1916             }
1917             else
1918             {
1919                 appendLogText(i18n("Loaded image was taken on pierside %1", value.toString()));
1920                 (value == "WEST") ? m_TargetPierside = ISD::Mount::PIER_WEST : m_TargetPierside = ISD::Mount::PIER_EAST;
1921             }
1922             RotatorUtils::Instance()->Instance()->setImagePierside(m_TargetPierside);
1923         }
1924         else
1925         {
1926             //Setting the initial search scale settings
1927             if (useImageScale)
1928             {
1929                 m_UsedScale = true;
1930                 m_ScaleUsed = Options::astrometryImageScaleLow();
1931 
1932                 SSolver::ScaleUnits units = static_cast<SSolver::ScaleUnits>(Options::astrometryImageScaleUnits());
1933                 // Extend search scale from 80% to 120%
1934                 m_StellarSolver->setSearchScale(Options::astrometryImageScaleLow() * 0.8,
1935                                                 Options::astrometryImageScaleHigh() * 1.2,
1936                                                 units);
1937             }
1938             else
1939                 m_StellarSolver->setProperty("UseScale", false);
1940             //Setting the initial search location settings
1941             if(useImagePosition)
1942             {
1943                 m_StellarSolver->setSearchPositionInDegrees(m_TelescopeCoord.ra().Degrees(), m_TelescopeCoord.dec().Degrees());
1944                 m_UsedPosition = true;
1945                 m_RAUsed = m_TelescopeCoord.ra().Degrees();
1946                 m_DECUsed = m_TelescopeCoord.dec().Degrees();
1947             }
1948             else
1949                 m_StellarSolver->setProperty("UsePosition", false);
1950         }
1951 
1952         if(Options::alignmentLogging())
1953         {
1954             // Not trusting SSolver logging right now (Hy Aug 1, 2022)
1955             // m_StellarSolver->setLogLevel(static_cast<SSolver::logging_level>(Options::loggerLevel()));
1956             // m_StellarSolver->setSSLogLevel(SSolver::LOG_NORMAL);
1957             m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
1958             m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
1959             if(Options::astrometryLogToFile())
1960             {
1961                 m_StellarSolver->setProperty("LogToFile", true);
1962                 m_StellarSolver->setProperty("LogFileName", Options::astrometryLogFilepath());
1963             }
1964         }
1965         else
1966         {
1967             m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
1968             m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
1969         }
1970 
1971         SolverUtils::patchMultiAlgorithm(m_StellarSolver.get());
1972 
1973         // Start solving process
1974         m_StellarSolver->start();
1975     }
1976     else
1977     {
1978         if (m_ImageData.isNull())
1979             m_ImageData = m_AlignView->imageData();
1980         // This should run only for load&slew. For regular solve, we don't get here
1981         // as the image is read and solved server-side.
1982         remoteParser->startSolver(m_ImageData->filename(), generateRemoteArgs(m_ImageData), false);
1983     }
1984 
1985     // In these cases, the error box is not used, and we don't want it polluted
1986     // from some previous operation.
1987     if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
1988             matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
1989             matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
1990             matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
1991             matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
1992             matchPAHStage(PAA::PAH_THIRD_SOLVE) ||
1993             nothingR->isChecked() ||
1994             syncR->isChecked())
1995         errOut->clear();
1996 
1997     // Kick off timer
1998     solverTimer.start();
1999 
2000     setState(ALIGN_PROGRESS);
2001     emit newStatus(state);
2002 }
2003 
2004 void Align::solverComplete()
2005 {
2006     disconnect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
2007     if(!m_StellarSolver->solvingDone() || m_StellarSolver->failed())
2008     {
2009         if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
2010                 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
2011                 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
2012                 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
2013                 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
2014                 matchPAHStage(PAA::PAH_THIRD_SOLVE))
2015         {
2016             if (CHECK_PAH(processSolverFailure()))
2017                 return;
2018             else
2019                 setState(ALIGN_ABORTED);
2020         }
2021         solverFailed();
2022         return;
2023     }
2024     else
2025     {
2026         FITSImage::Solution solution = m_StellarSolver->getSolution();
2027         const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
2028         solverFinished(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
2029     }
2030 }
2031 
2032 void Align::solverFinished(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
2033 {
2034     pi->stopAnimation();
2035     stopB->setEnabled(false);
2036     solveB->setEnabled(true);
2037 
2038     sOrientation = orientation;
2039     sRA          = ra;
2040     sDEC         = dec;
2041 
2042     double elapsed = solverTimer.elapsed() / 1000.0;
2043     if (elapsed > 0)
2044         appendLogText(i18n("Solver completed after %1 seconds.", QString::number(elapsed, 'f', 2)));
2045 
2046     m_AlignTimer.stop();
2047     if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice && remoteParser.get())
2048     {
2049         // Disable remote parse
2050         dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(false);
2051     }
2052 
2053     int binx, biny;
2054     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2055     targetChip->getBinning(&binx, &biny);
2056 
2057     if (Options::alignmentLogging())
2058     {
2059         QString parityString = eastToTheRight ? "neg" : "pos";
2060         appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4) Parity (%5)", QString::number(ra, 'f', 5),
2061                            QString::number(dec, 'f', 5), QString::number(orientation, 'f', 5),
2062                            QString::number(pixscale, 'f', 5), parityString));
2063     }
2064 
2065     // When solving (without Load&Slew), update effective FOV and focal length accordingly.
2066     if (!m_SolveFromFile &&
2067             (isEqual(m_FOVWidth, 0) || m_EffectiveFOVPending || std::fabs(pixscale - m_FOVPixelScale) > 0.005) &&
2068             pixscale > 0)
2069     {
2070         double newFOVW = m_CameraWidth * pixscale / binx / 60.0;
2071         double newFOVH = m_CameraHeight * pixscale / biny / 60.0;
2072 
2073         calculateEffectiveFocalLength(newFOVW);
2074         saveNewEffectiveFOV(newFOVW, newFOVH);
2075 
2076         m_EffectiveFOVPending = false;
2077     }
2078 
2079     m_AlignCoord.setRA0(ra / 15.0);
2080     m_AlignCoord.setDec0(dec);
2081 
2082     // Convert to JNow
2083     m_AlignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
2084     // Get horizontal coords
2085     m_AlignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
2086 
2087     // Do not update diff if we are performing load & slew.
2088     if (!m_SolveFromFile)
2089     {
2090         pixScaleOut->setText(QString::number(pixscale, 'f', 2));
2091         calculateAlignTargetDiff();
2092     }
2093 
2094     // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack.
2095     // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA
2096     // PA = Orientation + 180
2097     double solverPA = KSUtils::rotationToPositionAngle(orientation);
2098     solverFOV->setCenter(m_AlignCoord);
2099     solverFOV->setPA(solverPA);
2100     solverFOV->setImageDisplay(Options::astrometrySolverOverlay());
2101     // Sensor FOV as well
2102     sensorFOV->setPA(solverPA);
2103 
2104     PAOut->setText(QString::number(solverPA, 'f', 2)); // Two decimals are reasonable
2105 
2106     QString ra_dms, dec_dms;
2107     getFormattedCoords(m_AlignCoord.ra().Hours(), m_AlignCoord.dec().Degrees(), ra_dms, dec_dms);
2108 
2109     SolverRAOut->setText(ra_dms);
2110     SolverDecOut->setText(dec_dms);
2111 
2112     if (Options::astrometrySolverWCS())
2113     {
2114         auto ccdRotation = m_Camera->getNumber("CCD_ROTATION");
2115         if (ccdRotation)
2116         {
2117             auto rotation = ccdRotation->findWidgetByName("CCD_ROTATION_VALUE");
2118             if (rotation)
2119             {
2120                 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
2121                 rotation->setValue(orientation);
2122                 clientManager->sendNewProperty(ccdRotation);
2123 
2124                 if (m_wcsSynced == false)
2125                 {
2126                     appendLogText(
2127                         i18n("WCS information updated. Images captured from this point forward shall have valid WCS."));
2128 
2129                     // Just send telescope info in case the CCD driver did not pick up before.
2130                     auto telescopeInfo = m_Mount->getNumber("TELESCOPE_INFO");
2131                     if (telescopeInfo)
2132                         clientManager->sendNewProperty(telescopeInfo);
2133 
2134                     m_wcsSynced = true;
2135                 }
2136             }
2137         }
2138     }
2139 
2140     m_CaptureErrorCounter = 0;
2141     m_SlewErrorCounter = 0;
2142     m_CaptureTimeoutCounter = 0;
2143 
2144     appendLogText(
2145         i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4) Target Coordinates: RA (%5) DEC (%6)",
2146              m_AlignCoord.ra().toHMSString(),
2147              m_AlignCoord.dec().toDMSString(),
2148              m_TelescopeCoord.ra().toHMSString(),
2149              m_TelescopeCoord.dec().toDMSString(),
2150              m_TargetCoord.ra().toHMSString(),
2151              m_TargetCoord.dec().toDMSString()));
2152 
2153     if (!m_SolveFromFile && m_CurrentGotoMode == GOTO_SLEW)
2154     {
2155         dms diffDeg(m_TargetDiffTotal / 3600.0);
2156         appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString()));
2157     }
2158 
2159     if (rememberUploadMode != m_Camera->getUploadMode())
2160         m_Camera->setUploadMode(rememberUploadMode);
2161 
2162     // Remember to reset fast exposure
2163     if (m_RememberCameraFastExposure)
2164     {
2165         m_RememberCameraFastExposure = false;
2166         m_Camera->setFastExposureEnabled(true);
2167     }
2168 
2169     //This block of code along with some sections in the switch below will set the status report in the solution table for this item.
2170     std::unique_ptr<QTableWidgetItem> statusReport(new QTableWidgetItem());
2171     int currentRow = solutionTable->rowCount() - 1;
2172     if (!m_SolveFromFile) // [Capture & Solve]
2173     {
2174         stopProgressAnimation();
2175         solutionTable->setCellWidget(currentRow, 3, new QWidget());
2176         statusReport->setFlags(Qt::ItemIsSelectable);
2177         // Calibration: determine camera offset from capture
2178         if (m_Rotator != nullptr && m_Rotator->isConnected())
2179         {
2180             if (auto absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE"))
2181                 // if (absAngle && std::isnan(m_TargetPositionAngle) == true)
2182             {
2183                 sRawAngle = absAngle->at(0)->getValue();
2184                 double OffsetAngle = RotatorUtils::Instance()->calcOffsetAngle(sRawAngle, solverPA);
2185                 RotatorUtils::Instance()->updateOffset(OffsetAngle);
2186                 // Debug info
2187                 auto reverseStatus = "Unknown";
2188                 auto reverseProperty = m_Rotator->getSwitch("ROTATOR_REVERSE");
2189                 if (reverseProperty)
2190                 {
2191                     if (reverseProperty->at(0)->getState() == ISS_ON)
2192                         reverseStatus = "Reversed Direction";
2193                     else
2194                         reverseStatus = "Normal Direction";
2195                 }
2196                 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << sRawAngle << "Rotator PA:" << solverPA
2197                                            << "Rotator Offset:" << OffsetAngle << "Direction:" << reverseStatus;
2198                 // Flow is: newSolverResults() -> capture: setAlignresult() -> RotatorSettings: refresh()
2199                 emit newSolverResults(solverPA, ra, dec, pixscale);
2200                 // appendLogText(i18n("Camera offset angle is %1 degrees.", OffsetAngle));
2201                 appendLogText(i18n("Camera position angle is %1 degrees.", RotatorUtils::Instance()->calcCameraAngle(sRawAngle, false)));
2202             }
2203         }
2204     }
2205 
2206     QJsonObject solution =
2207     {
2208         {"camera", m_Camera->getDeviceName()},
2209         {"ra", SolverRAOut->text()},
2210         {"de", SolverDecOut->text()},
2211         {"dRA", m_TargetDiffRA},
2212         {"dDE", m_TargetDiffDE},
2213         {"targetDiff", m_TargetDiffTotal},
2214         {"pix", pixscale},
2215         {"PA", solverPA},
2216         {"fov", FOVOut->text()},
2217     };
2218     emit newSolution(solution.toVariantMap());
2219 
2220     setState(ALIGN_SUCCESSFUL);
2221     emit newStatus(state);
2222     solverIterations = 0;
2223     KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"),
2224                           KSNotification::Align);
2225 
2226     switch (m_CurrentGotoMode)
2227     {
2228         case GOTO_SYNC:
2229             executeGOTO();
2230 
2231             if (!m_SolveFromFile)
2232             {
2233                 stopProgressAnimation();
2234                 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2235                 solutionTable->setItem(currentRow, 3, statusReport.release());
2236             }
2237 
2238             return;
2239 
2240         case GOTO_SLEW:
2241             if (m_SolveFromFile || m_TargetDiffTotal > static_cast<double>(alignAccuracyThreshold->value()))
2242             {
2243                 if (!m_SolveFromFile && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
2244                 {
2245                     appendLogText(i18n("Maximum number of iterations reached. Solver failed."));
2246 
2247                     if (!m_SolveFromFile)
2248                     {
2249                         statusReport->setIcon(QIcon(":/icons/AlignFailure.svg"));
2250                         solutionTable->setItem(currentRow, 3, statusReport.release());
2251                     }
2252 
2253                     solverFailed();
2254                     return;
2255                 }
2256 
2257                 targetAccuracyNotMet = true;
2258 
2259                 if (!m_SolveFromFile)
2260                 {
2261                     stopProgressAnimation();
2262                     statusReport->setIcon(QIcon(":/icons/AlignWarning.svg"));
2263                     solutionTable->setItem(currentRow, 3, statusReport.release());
2264                 }
2265 
2266                 executeGOTO();
2267                 return;
2268             }
2269 
2270             stopProgressAnimation();
2271             statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2272             solutionTable->setItem(currentRow, 3, statusReport.release());
2273 
2274             appendLogText(i18n("Target is within acceptable range."));
2275             break;
2276 
2277         case GOTO_NOTHING:
2278             if (!m_SolveFromFile)
2279             {
2280                 stopProgressAnimation();
2281                 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2282                 solutionTable->setItem(currentRow, 3, statusReport.release());
2283             }
2284             break;
2285     }
2286 
2287     solverFOV->setProperty("visible", true);
2288 
2289     if (!matchPAHStage(PAA::PAH_IDLE))
2290         m_PolarAlignmentAssistant->processPAHStage(orientation, ra, dec, pixscale, eastToTheRight,
2291                 m_StellarSolver->getSolutionHealpix(),
2292                 m_StellarSolver->getSolutionIndexNumber());
2293     else
2294     {
2295 
2296         if (checkIfRotationRequired())
2297         {
2298             solveB->setEnabled(false);
2299             loadSlewB->setEnabled(false);
2300             return;
2301         }
2302 
2303         // We are done!
2304         setState(ALIGN_COMPLETE);
2305         emit newStatus(state);
2306 
2307         solveB->setEnabled(true);
2308         loadSlewB->setEnabled(true);
2309     }
2310 }
2311 
2312 void Align::solverFailed()
2313 {
2314 
2315     // If failed-align logging is enabled, let's save the frame.
2316     if (Options::saveFailedAlignImages())
2317     {
2318         QDir dir;
2319         QDateTime now = KStarsData::Instance()->lt();
2320         QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/failed");
2321         dir.mkpath(path);
2322         QString extraFilenameInfo;
2323         if (m_UsedScale)
2324             extraFilenameInfo.append(QString("_s%1u%2").arg(m_ScaleUsed, 0, 'f', 3)
2325                                      .arg(Options::astrometryImageScaleUnits()));
2326         if (m_UsedPosition)
2327             extraFilenameInfo.append(QString("_r%1_d%2").arg(m_RAUsed, 0, 'f', 5).arg(m_DECUsed, 0, 'f', 5));
2328 
2329         // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
2330         // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
2331         QString name     = "failed_align_frame_" + now.toString("yyyy-MM-dd-HH-mm-ss") + extraFilenameInfo + ".fits";
2332         QString filename = path + QStringLiteral("/") + name;
2333         if (m_ImageData)
2334         {
2335             m_ImageData->saveImage(filename);
2336             appendLogText(i18n("Saving failed solver image to %1", filename));
2337         }
2338 
2339     }
2340     if (state != ALIGN_ABORTED)
2341     {
2342         // Try to solve with scale turned off, if not turned off already
2343         if (Options::astrometryUseImageScale() && useBlindScale == BLIND_IDLE)
2344         {
2345             appendLogText(i18n("Solver failed. Retrying without scale constraint."));
2346             useBlindScale = BLIND_ENGAGNED;
2347             setAlignTableResult(ALIGN_RESULT_FAILED);
2348             captureAndSolve();
2349             return;
2350         }
2351 
2352         // Try to solve with the position turned off, if not turned off already
2353         if (Options::astrometryUsePosition() && useBlindPosition == BLIND_IDLE)
2354         {
2355             appendLogText(i18n("Solver failed. Retrying without position constraint."));
2356             useBlindPosition = BLIND_ENGAGNED;
2357             setAlignTableResult(ALIGN_RESULT_FAILED);
2358             captureAndSolve();
2359             return;
2360         }
2361 
2362 
2363         appendLogText(i18n("Solver Failed."));
2364         if(!Options::alignmentLogging())
2365             appendLogText(
2366                 i18n("Please check you have sufficient stars in the image, the indicated FOV is correct, and the necessary index files are installed. Enable Alignment Logging in Setup Tab -> Logs to get detailed information on the failure."));
2367 
2368         KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"),
2369                               KSNotification::Align, KSNotification::Alert);
2370     }
2371 
2372     pi->stopAnimation();
2373     stopB->setEnabled(false);
2374     solveB->setEnabled(true);
2375     loadSlewB->setEnabled(true);
2376 
2377     m_AlignTimer.stop();
2378 
2379     m_SolveFromFile = false;
2380     solverIterations = 0;
2381     m_CaptureErrorCounter = 0;
2382     m_CaptureTimeoutCounter = 0;
2383     m_SlewErrorCounter = 0;
2384 
2385     setState(ALIGN_FAILED);
2386     emit newStatus(state);
2387 
2388     solverFOV->setProperty("visible", false);
2389 
2390     setAlignTableResult(ALIGN_RESULT_FAILED);
2391 }
2392 
2393 bool Align::checkIfRotationRequired()
2394 {
2395     // Check if we need to perform any rotations.
2396     if (Options::astrometryUseRotator())
2397     {
2398         if (m_SolveFromFile) // [Load & Slew] Program flow never lands here!?
2399         {
2400             m_TargetPositionAngle = solverFOV->PA();
2401             // We are not done yet.
2402             qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to:" << m_TargetPositionAngle;
2403         }
2404         else // [Capture & Solve]: "direct" or within [Load & Slew]
2405         {
2406             currentRotatorPA = solverFOV->PA();
2407             if (std::isnan(m_TargetPositionAngle) == false) // [Load & Slew] only
2408             {
2409                 // If image pierside versus mount pierside is different and policy is lenient ...
2410                 if (RotatorUtils::Instance()->Instance()->checkImageFlip() && (Options::astrometryFlipRotationAllowed()))
2411                 {
2412                     // ... calculate "flipped" PA ...
2413                     sRawAngle = RotatorUtils::Instance()->calcRotatorAngle(m_TargetPositionAngle);
2414                     m_TargetPositionAngle = RotatorUtils::Instance()->calcCameraAngle(sRawAngle, true);
2415                     RotatorUtils::Instance()->setImagePierside(ISD::Mount::PIER_UNKNOWN); // ... once!
2416                 }
2417                 // Match the position angle with rotator
2418                 if  (m_Rotator != nullptr && m_Rotator->isConnected())
2419                 {
2420                     if(fabs(KSUtils::rangePA(currentRotatorPA - m_TargetPositionAngle)) * 60 >
2421                             Options::astrometryRotatorThreshold())
2422                     {
2423                         // Signal flow: newSolverResults() -> capture: setAlignresult() -> RS: refresh()
2424                         emit newSolverResults(m_TargetPositionAngle, 0, 0, 0);
2425                         appendLogText(i18n("Setting camera position angle to %1 degrees ...", m_TargetPositionAngle));
2426                         setState(ALIGN_ROTATING);
2427                         emit newStatus(state); // Evoke 'updateProperty()' (where the same check is executed again)
2428                         return true;
2429                     }
2430                     else
2431                     {
2432                         appendLogText(i18n("Camera position angle is within acceptable range."));
2433                         // We're done! (Opposed to 'updateProperty()')
2434                         m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
2435                     }
2436                 }
2437                 //  Match the position angle manually
2438                 else
2439                 {
2440                     double current = currentRotatorPA;
2441                     double target = m_TargetPositionAngle;
2442 
2443                     double diff = KSUtils::rangePA(current - target);
2444                     double threshold = Options::astrometryRotatorThreshold() / 60.0;
2445 
2446                     appendLogText(i18n("Current PA is %1; Target PA is %2; diff: %3", current, target, diff));
2447 
2448                     emit manualRotatorChanged(current, target, threshold);
2449 
2450                     m_ManualRotator->setRotatorDiff(current, target, diff);
2451                     if (fabs(diff) > threshold)
2452                     {
2453                         targetAccuracyNotMet = true;
2454                         m_ManualRotator->show();
2455                         m_ManualRotator->raise();
2456                         setState(ALIGN_ROTATING);
2457                         emit newStatus(state);
2458                         return true;
2459                     }
2460                     else
2461                     {
2462                         m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
2463                         targetAccuracyNotMet = false;
2464                     }
2465                 }
2466             }
2467         }
2468     }
2469     return false;
2470 }
2471 
2472 void Align::stop(Ekos::AlignState mode)
2473 {
2474     m_CaptureTimer.stop();
2475     if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
2476         m_StellarSolver->abort();
2477     else if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser)
2478         remoteParser->stopSolver();
2479     //parser->stopSolver();
2480     pi->stopAnimation();
2481     stopB->setEnabled(false);
2482     solveB->setEnabled(true);
2483     loadSlewB->setEnabled(true);
2484 
2485     m_SolveFromFile = false;
2486     solverIterations = 0;
2487     m_CaptureErrorCounter = 0;
2488     m_CaptureTimeoutCounter = 0;
2489     m_SlewErrorCounter = 0;
2490     m_AlignTimer.stop();
2491 
2492     disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
2493     disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
2494 
2495     if (rememberUploadMode != m_Camera->getUploadMode())
2496         m_Camera->setUploadMode(rememberUploadMode);
2497 
2498     // Remember to reset fast exposure
2499     if (m_RememberCameraFastExposure)
2500     {
2501         m_RememberCameraFastExposure = false;
2502         m_Camera->setFastExposureEnabled(true);
2503     }
2504 
2505     auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2506 
2507     // If capture is still in progress, let's stop that.
2508     if (matchPAHStage(PAA::PAH_POST_REFRESH))
2509     {
2510         if (targetChip->isCapturing())
2511             targetChip->abortExposure();
2512 
2513         appendLogText(i18n("Refresh is complete."));
2514     }
2515     else
2516     {
2517         if (targetChip->isCapturing())
2518         {
2519             targetChip->abortExposure();
2520             appendLogText(i18n("Capture aborted."));
2521         }
2522         else
2523         {
2524             double elapsed = solverTimer.elapsed() / 1000.0;
2525             if (elapsed > 0)
2526                 appendLogText(i18n("Solver aborted after %1 seconds.", QString::number(elapsed, 'f', 2)));
2527         }
2528     }
2529 
2530     setState(mode);
2531     emit newStatus(state);
2532 
2533     setAlignTableResult(ALIGN_RESULT_FAILED);
2534 }
2535 
2536 QProgressIndicator * Align::getProgressStatus()
2537 {
2538     int currentRow = solutionTable->rowCount() - 1;
2539 
2540     // check if the current row indicates a progress state
2541     // 1. no row present
2542     if (currentRow < 0)
2543         return nullptr;
2544     // 2. indicator is not present or not a progress indicator
2545     QWidget *indicator = solutionTable->cellWidget(currentRow, 3);
2546     if (indicator == nullptr)
2547         return nullptr;
2548     return dynamic_cast<QProgressIndicator *>(indicator);
2549 }
2550 
2551 void Align::stopProgressAnimation()
2552 {
2553     QProgressIndicator *progress_indicator = getProgressStatus();
2554     if (progress_indicator != nullptr)
2555         progress_indicator->stopAnimation();
2556 }
2557 
2558 QList<double> Align::getSolutionResult()
2559 {
2560     QList<double> result;
2561 
2562     result << sOrientation << sRA << sDEC;
2563 
2564     return result;
2565 }
2566 
2567 void Align::appendLogText(const QString &text)
2568 {
2569     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
2570                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
2571 
2572     qCInfo(KSTARS_EKOS_ALIGN) << text;
2573 
2574     emit newLog(text);
2575 }
2576 
2577 void Align::clearLog()
2578 {
2579     m_LogText.clear();
2580     emit newLog(QString());
2581 }
2582 
2583 void Align::updateProperty(INDI::Property prop)
2584 {
2585     if (prop.isNameMatch("EQUATORIAL_EOD_COORD") || prop.isNameMatch("EQUATORIAL_COORD"))
2586     {
2587         auto nvp = prop.getNumber();
2588         QString ra_dms, dec_dms;
2589 
2590         getFormattedCoords(m_TelescopeCoord.ra().Hours(), m_TelescopeCoord.dec().Degrees(), ra_dms, dec_dms);
2591 
2592         ScopeRAOut->setText(ra_dms);
2593         ScopeDecOut->setText(dec_dms);
2594 
2595         switch (nvp->s)
2596         {
2597             // Idle --> Mount not tracking or slewing
2598             case IPS_IDLE:
2599                 m_wasSlewStarted = false;
2600                 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE";
2601                 break;
2602 
2603             // Ok --> Mount Tracking. If m_wasSlewStarted is true
2604             // then it just finished slewing
2605             case IPS_OK:
2606             {
2607                 // Update the boxes as the mount just finished slewing
2608                 if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition())
2609                 {
2610                     //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position...";
2611                     opsAstrometry->estRA->setText(ra_dms);
2612                     opsAstrometry->estDec->setText(dec_dms);
2613 
2614                     Options::setAstrometryPositionRA(nvp->np[0].value * 15);
2615                     Options::setAstrometryPositionDE(nvp->np[1].value);
2616 
2617                     //generateArgs();
2618                 }
2619 
2620                 // If dome is syncing, wait until it stops
2621                 if (m_Dome && m_Dome->isMoving())
2622                 {
2623                     domeReady = false;
2624                     return;
2625                 }
2626 
2627                 // If we are looking for celestial pole
2628                 if (m_wasSlewStarted && matchPAHStage(PAA::PAH_FIND_CP))
2629                 {
2630                     //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE";
2631                     m_wasSlewStarted = false;
2632                     appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify."));
2633                     setSolverAction(GOTO_NOTHING);
2634 
2635                     m_PolarAlignmentAssistant->setPAHStage(PAA::PAH_FIRST_CAPTURE);
2636                     return;
2637                 }
2638 
2639                 switch (state)
2640                 {
2641                     case ALIGN_PROGRESS:
2642                         break;
2643 
2644                     case ALIGN_SYNCING:
2645                     {
2646                         m_wasSlewStarted = false;
2647                         //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE";
2648                         if (m_CurrentGotoMode == GOTO_SLEW)
2649                         {
2650                             Slew();
2651                             return;
2652                         }
2653                         else
2654                         {
2655                             appendLogText(i18n("Mount is synced to solution coordinates."));
2656 
2657                             /* if (checkIfRotationRequired())
2658                                 return; */
2659 
2660                             KSNotification::event(QLatin1String("AlignSuccessful"),
2661                                                   i18n("Astrometry alignment completed successfully"), KSNotification::Align);
2662                             setState(ALIGN_COMPLETE);
2663                             emit newStatus(state);
2664                             solverIterations = 0;
2665                             loadSlewB->setEnabled(true);
2666                         }
2667                     }
2668                     break;
2669 
2670                     case ALIGN_SLEWING:
2671 
2672                         if (!didSlewStart())
2673                         {
2674                             // If mount has not started slewing yet, then skip
2675                             //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet...";
2676                             break;
2677                         }
2678 
2679                         //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed.";
2680                         m_wasSlewStarted = false;
2681                         if (m_SolveFromFile)
2682                         {
2683                             m_SolveFromFile = false;
2684                             m_TargetPositionAngle = solverFOV->PA();
2685                             qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to" << m_TargetPositionAngle;
2686 
2687                             setState(ALIGN_PROGRESS);
2688                             emit newStatus(state);
2689 
2690                             if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
2691                                 appendLogText(i18n("Settling..."));
2692                             m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
2693                             m_CaptureTimer.start(alignSettlingTime->value());
2694                             return;
2695                         }
2696                         else if (m_CurrentGotoMode == GOTO_SLEW || (m_MountModel && m_MountModel->isRunning()))
2697                         {
2698                             if (targetAccuracyNotMet)
2699                                 appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again..."));
2700                             else
2701                                 appendLogText(i18n("Slew complete. Solving Alignment Point. . ."));
2702 
2703                             targetAccuracyNotMet = false;
2704 
2705                             setState(ALIGN_PROGRESS);
2706                             emit newStatus(state);
2707 
2708                             if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
2709                                 appendLogText(i18n("Settling..."));
2710                             m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
2711                             m_CaptureTimer.start(alignSettlingTime->value());
2712                         }
2713                         break;
2714 
2715                     default:
2716                     {
2717                         //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE";
2718                         m_wasSlewStarted = false;
2719                     }
2720                     break;
2721                 }
2722             }
2723             break;
2724 
2725             // Busy --> Mount Slewing or Moving (NSWE buttons)
2726             case IPS_BUSY:
2727             {
2728                 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew running.";
2729                 m_wasSlewStarted = true;
2730 
2731                 handleMountMotion();
2732             }
2733             break;
2734 
2735             // Alert --> Mount has problem moving or communicating.
2736             case IPS_ALERT:
2737             {
2738                 //qCDebug(KSTARS_EKOS_ALIGN) << "IPS_ALERT --> setting slewStarted to FALSE";
2739                 m_wasSlewStarted = false;
2740 
2741                 if (state == ALIGN_SYNCING || state == ALIGN_SLEWING)
2742                 {
2743                     if (state == ALIGN_SYNCING)
2744                         appendLogText(i18n("Syncing failed."));
2745                     else
2746                         appendLogText(i18n("Slewing failed."));
2747 
2748                     if (++m_SlewErrorCounter == 3)
2749                     {
2750                         abort();
2751                         return;
2752                     }
2753                     else
2754                     {
2755                         if (m_CurrentGotoMode == GOTO_SLEW)
2756                             Slew();
2757                         else
2758                             Sync();
2759                     }
2760                 }
2761 
2762                 return;
2763             }
2764         }
2765 
2766         RUN_PAH(processMountRotation(m_TelescopeCoord.ra(), alignSettlingTime->value()));
2767     }
2768     else if (prop.isNameMatch("ABS_ROTATOR_ANGLE"))
2769     {
2770         auto nvp = prop.getNumber();
2771         double RAngle = nvp->np[0].value;
2772         currentRotatorPA = RotatorUtils::Instance()->calcCameraAngle(RAngle, false);
2773         /*QString logtext = "Alignstate: " + QString::number(state)
2774                         + " IPSstate: " + QString::number(nvp->s)
2775                         + " Raw Rotator Angle:" + QString::number(nvp->np[0].value)
2776                         + " Current PA:" + QString::number(currentRotatorPA)
2777                         + " Target PA:" + QString::number(m_TargetPositionAngle)
2778                         + " Offset:" + QString::number(Options::pAOffset());
2779         appendLogText(logtext);*/
2780         // loadSlewTarget defined if activation through [Load & Slew] and rotator just reached position
2781         if (std::isnan(m_TargetPositionAngle) == false && state == ALIGN_ROTATING && nvp->s == IPS_OK)
2782         {
2783             auto diff = fabs(RotatorUtils::Instance()->DiffPA(currentRotatorPA - m_TargetPositionAngle)) * 60;
2784             qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << RAngle << "Current PA:" << currentRotatorPA
2785                                        << "Target PA:" << m_TargetPositionAngle << "Diff (arcmin):" << diff << "Offset:"
2786                                        << Options::pAOffset();
2787 
2788             if (diff <= Options::astrometryRotatorThreshold())
2789             {
2790                 appendLogText(i18n("Rotator reached camera position angle."));
2791                 // Check angle once again (no slew -> no settle time)
2792                 // QTimer::singleShot(alignSettlingTime->value(), this, &Ekos::Align::executeGOTO);
2793                 executeGOTO();
2794             }
2795             else
2796             {
2797                 // Sometimes update of "nvp->s" is a bit slow, so check state again, if it's really ok
2798                 QTimer::singleShot(300, [ = ]
2799                 {
2800                     if (nvp->s == IPS_OK)
2801                     {
2802                         appendLogText(i18n("Rotator failed to arrive at the requested position angle (Deviation %1 arcmin).", diff));
2803                         setState(ALIGN_FAILED);
2804                         emit newStatus(state);
2805                         solveB->setEnabled(true);
2806                         loadSlewB->setEnabled(true);
2807                     }
2808                 });
2809             }
2810         }
2811         else if (m_estimateRotatorTimeFrame) // Estimate time frame during first timeout
2812         {
2813             m_RotatorTimeFrame = RotatorUtils::Instance()->calcTimeFrame(RAngle);
2814         }
2815     }
2816     else if (prop.isNameMatch("DOME_MOTION"))
2817     {
2818         // If dome is not ready and state is now
2819         if (domeReady == false && prop.getState() == IPS_OK)
2820         {
2821             domeReady = true;
2822             // trigger process number for mount so that it proceeds with normal workflow since
2823             // it was stopped by dome not being ready
2824             handleMountStatus();
2825         }
2826     }
2827     else if (prop.isNameMatch("TELESCOPE_MOTION_NS") || prop.isNameMatch("TELESCOPE_MOTION_WE"))
2828     {
2829         switch (prop.getState())
2830         {
2831             case IPS_BUSY:
2832                 // react upon mount motion
2833                 handleMountMotion();
2834                 m_wasSlewStarted = true;
2835                 break;
2836             default:
2837                 qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished.";
2838                 handleMountStatus();
2839                 break;
2840         }
2841     }
2842 }
2843 
2844 void Align::handleMountMotion()
2845 {
2846     if (state == ALIGN_PROGRESS)
2847     {
2848         if (matchPAHStage(PAA::PAH_IDLE))
2849         {
2850             // whoops, mount slews during alignment
2851             appendLogText(i18n("Slew detected, suspend solving..."));
2852             suspend();
2853         }
2854 
2855         setState(ALIGN_SLEWING);
2856     }
2857 }
2858 
2859 void Align::handleMountStatus()
2860 {
2861     auto nvp = m_Mount->getNumber(m_Mount->isJ2000() ? "EQUATORIAL_COORD" :
2862                                   "EQUATORIAL_EOD_COORD");
2863 
2864     if (nvp)
2865         updateProperty(nvp);
2866 }
2867 
2868 
2869 void Align::executeGOTO()
2870 {
2871     if (m_SolveFromFile)
2872     {
2873         m_TargetCoord = m_AlignCoord;
2874 
2875         qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file. Setting Target Coordinates align coords. RA:"
2876                                    << m_TargetCoord.ra().toHMSString()
2877                                    << "DE:" << m_TargetCoord.dec().toDMSString();
2878 
2879         SlewToTarget();
2880     }
2881     else if (m_CurrentGotoMode == GOTO_SYNC)
2882         Sync();
2883     else if (m_CurrentGotoMode == GOTO_SLEW)
2884         SlewToTarget();
2885 }
2886 
2887 void Align::Sync()
2888 {
2889     setState(ALIGN_SYNCING);
2890 
2891     if (m_Mount->Sync(&m_AlignCoord))
2892     {
2893         emit newStatus(state);
2894         appendLogText(
2895             i18n("Syncing to RA (%1) DEC (%2)", m_AlignCoord.ra().toHMSString(), m_AlignCoord.dec().toDMSString()));
2896     }
2897     else
2898     {
2899         setState(ALIGN_IDLE);
2900         emit newStatus(state);
2901         appendLogText(i18n("Syncing failed."));
2902     }
2903 }
2904 
2905 void Align::Slew()
2906 {
2907     setState(ALIGN_SLEWING);
2908     emit newStatus(state);
2909 
2910     //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2911     //m_wasSlewStarted = currentTelescope->Slew(&m_targetCoord);
2912     //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2913 
2914     // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered
2915     // from Goto
2916     if (m_Mount->Slew(&m_TargetCoord))
2917     {
2918         slewStartTimer.start();
2919         appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", m_TargetCoord.ra().toHMSString(),
2920                            m_TargetCoord.dec().toDMSString()));
2921     }
2922     else // inform user about failure
2923     {
2924         appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2) is rejected. (see notification)",
2925                            m_TargetCoord.ra().toHMSString(),
2926                            m_TargetCoord.dec().toDMSString()));
2927         setState(ALIGN_FAILED);
2928         emit newStatus(state);
2929         solveB->setEnabled(true);
2930         loadSlewB->setEnabled(true);
2931     }
2932 }
2933 
2934 void Align::SlewToTarget()
2935 {
2936     if (canSync && !m_SolveFromFile)
2937     {
2938         // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided
2939 #if 0
2940         if (Ekos::Manager::Instance()->getCurrentJobName().isEmpty())
2941         {
2942             KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"),
2943                                   i18n("Ekos job (%1) - Telescope synced",
2944                                        Ekos::Manager::Instance()->getCurrentJobName()));
2945         }
2946 #endif
2947 
2948         // Do we perform a regular sync or use differential slewing?
2949         if (Options::astrometryDifferentialSlewing())
2950         {
2951             dms m_TargetDiffRA = m_AlignCoord.ra().deltaAngle(m_TargetCoord.ra());
2952             dms m_TargetDiffDE = m_AlignCoord.dec().deltaAngle(m_TargetCoord.dec());
2953             m_TargetCoord.setRA(m_TargetCoord.ra() - m_TargetDiffRA);
2954             m_TargetCoord.setDec(m_TargetCoord.dec() - m_TargetDiffDE);
2955             qCDebug(KSTARS_EKOS_ALIGN) << "Using differential slewing. Setting Target Coordinates to RA:"
2956                                        << m_TargetCoord.ra().toHMSString()
2957                                        << "DE:" << m_TargetCoord.dec().toDMSString();
2958             Slew();
2959         }
2960         else
2961             Sync();
2962         return;
2963     }
2964     Slew();
2965 }
2966 
2967 
2968 void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str)
2969 {
2970     dms ra_s, dec_s;
2971     ra_s.setH(ra);
2972     dec_s.setD(dec);
2973 
2974     ra_str = QString("%1:%2:%3")
2975              .arg(ra_s.hour(), 2, 10, QChar('0'))
2976              .arg(ra_s.minute(), 2, 10, QChar('0'))
2977              .arg(ra_s.second(), 2, 10, QChar('0'));
2978     if (dec_s.Degrees() < 0)
2979         dec_str = QString("-%1:%2:%3")
2980                   .arg(abs(dec_s.degree()), 2, 10, QChar('0'))
2981                   .arg(abs(dec_s.arcmin()), 2, 10, QChar('0'))
2982                   .arg(dec_s.arcsec(), 2, 10, QChar('0'));
2983     else
2984         dec_str = QString("%1:%2:%3")
2985                   .arg(dec_s.degree(), 2, 10, QChar('0'))
2986                   .arg(dec_s.arcmin(), 2, 10, QChar('0'))
2987                   .arg(dec_s.arcsec(), 2, 10, QChar('0'));
2988 }
2989 
2990 bool Align::loadAndSlew(QString fileURL)
2991 {
2992     if (fileURL.isEmpty())
2993         fileURL = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Load Image"), dirPath,
2994                                                "Images (*.fits *.fits.fz *.fit *.fts *.xisf "
2995                                                "*.jpg *.jpeg *.png *.gif *.bmp "
2996                                                "*.cr2 *.cr3 *.crw *.nef *.raf *.dng *.arw *.orf)");
2997 
2998     if (fileURL.isEmpty())
2999         return false;
3000 
3001     QFileInfo fileInfo(fileURL);
3002     if (fileInfo.exists() == false)
3003         return false;
3004 
3005     dirPath = fileInfo.absolutePath();
3006 
3007     differentialSlewingActivated = false;
3008 
3009     m_SolveFromFile = true;
3010 
3011     if (m_PolarAlignmentAssistant)
3012         m_PolarAlignmentAssistant->stopPAHProcess();
3013 
3014     slewR->setChecked(true);
3015     m_CurrentGotoMode = GOTO_SLEW;
3016 
3017     solveB->setEnabled(false);
3018     loadSlewB->setEnabled(false);
3019     stopB->setEnabled(true);
3020     pi->startAnimation();
3021 
3022     if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice == nullptr)
3023     {
3024         appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
3025         setSolverMode(SOLVER_LOCAL);
3026     }
3027 
3028     m_ImageData.clear();
3029 
3030     m_AlignView->loadFile(fileURL);
3031     //m_FileToSolve = fileURL;
3032     connect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving);
3033 
3034     return true;
3035 }
3036 
3037 bool Align::loadAndSlew(const QByteArray &image, const QString &extension)
3038 {
3039     differentialSlewingActivated = false;
3040     m_SolveFromFile = true;
3041     RUN_PAH(stopPAHProcess());
3042     slewR->setChecked(true);
3043     m_CurrentGotoMode = GOTO_SLEW;
3044     solveB->setEnabled(false);
3045     loadSlewB->setEnabled(false);
3046     stopB->setEnabled(true);
3047     pi->startAnimation();
3048 
3049     // Must clear image data so we are forced to read the
3050     // image data again from align view when solving begins.
3051     m_ImageData.clear();
3052     QSharedPointer<FITSData> data;
3053     data.reset(new FITSData(), &QObject::deleteLater);
3054     data->loadFromBuffer(image, extension);
3055     m_AlignView->loadData(data);
3056     startSolving();
3057     return true;
3058 }
3059 
3060 void Align::setExposure(double value)
3061 {
3062     alignExposure->setValue(value);
3063 }
3064 
3065 void Align::setBinningIndex(int binIndex)
3066 {
3067     // If sender is not our combo box, then we need to update the combobox itself
3068     if (dynamic_cast<QComboBox *>(sender()) != alignBinning)
3069     {
3070         alignBinning->blockSignals(true);
3071         alignBinning->setCurrentIndex(binIndex);
3072         alignBinning->blockSignals(false);
3073     }
3074 
3075     // Need to calculate FOV and args for APP
3076     if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
3077         calculateFOV();
3078 }
3079 
3080 bool Align::setFilterWheel(ISD::FilterWheel * device)
3081 {
3082     if (m_FilterWheel && m_FilterWheel == device)
3083     {
3084         checkFilter();
3085         return false;
3086     }
3087 
3088     if (m_FilterWheel)
3089         m_FilterWheel->disconnect(this);
3090 
3091     m_FilterWheel = device;
3092 
3093     if (m_FilterWheel)
3094     {
3095         connect(m_FilterWheel, &ISD::ConcreteDevice::Connected, this, [this]()
3096         {
3097             FilterPosLabel->setEnabled(true);
3098             alignFilter->setEnabled(true);
3099         });
3100         connect(m_FilterWheel, &ISD::ConcreteDevice::Disconnected, this, [this]()
3101         {
3102             FilterPosLabel->setEnabled(false);
3103             alignFilter->setEnabled(false);
3104         });
3105     }
3106 
3107     auto isConnected = m_FilterWheel && m_FilterWheel->isConnected();
3108     FilterPosLabel->setEnabled(isConnected);
3109     alignFilter->setEnabled(isConnected);
3110 
3111     checkFilter();
3112     return true;
3113 }
3114 
3115 QString Align::filterWheel()
3116 {
3117     if (m_FilterWheel)
3118         return m_FilterWheel->getDeviceName();
3119 
3120     return QString();
3121 }
3122 
3123 bool Align::setFilter(const QString &filter)
3124 {
3125     if (m_FilterWheel)
3126     {
3127         alignFilter->setCurrentText(filter);
3128         return true;
3129     }
3130 
3131     return false;
3132 }
3133 
3134 
3135 QString Align::filter()
3136 {
3137     return alignFilter->currentText();
3138 }
3139 
3140 void Align::checkFilter()
3141 {
3142     alignFilter->clear();
3143 
3144     if (!m_FilterWheel)
3145     {
3146         FilterPosLabel->setEnabled(false);
3147         alignFilter->setEnabled(false);
3148         return;
3149     }
3150 
3151     auto isConnected = m_FilterWheel->isConnected();
3152     FilterPosLabel->setEnabled(isConnected);
3153     alignFilter->setEnabled(alignUseCurrentFilter->isChecked() == false);
3154 
3155     setupFilterManager();
3156 
3157     alignFilter->addItems(m_FilterManager->getFilterLabels());
3158     currentFilterPosition = m_FilterManager->getFilterPosition();
3159 
3160     if (alignUseCurrentFilter->isChecked())
3161     {
3162         // use currently selected filter
3163         alignFilter->setCurrentIndex(currentFilterPosition - 1);
3164     }
3165     else
3166     {
3167         // use the fixed filter
3168         auto filter = m_Settings["alignFilter"];
3169         if (filter.isValid())
3170             alignFilter->setCurrentText(filter.toString());
3171     }
3172 }
3173 
3174 void Align::setRotator(ISD::Rotator * Device)
3175 {
3176     if ((Manager::Instance()->existRotatorController()) && (!m_Rotator || !(Device == m_Rotator)))
3177     {
3178         rotatorB->setEnabled(false);
3179         if (m_Rotator)
3180         {
3181             m_Rotator->disconnect(this);
3182             m_RotatorControlPanel->close();
3183         }
3184         m_Rotator = Device;
3185         if (m_Rotator)
3186         {
3187             if (Manager::Instance()->getRotatorController(m_Rotator->getDeviceName(), m_RotatorControlPanel))
3188             {
3189                 connect(m_Rotator, &ISD::Rotator::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
3190                 connect(rotatorB, &QPushButton::clicked, this, [this]()
3191                 {
3192                     m_RotatorControlPanel->show();
3193                     m_RotatorControlPanel->raise();
3194                 });
3195                 rotatorB->setEnabled(true);
3196             }
3197         }
3198     }
3199 }
3200 
3201 void Align::setWCSEnabled(bool enable)
3202 {
3203     if (!m_Camera)
3204         return;
3205 
3206     auto wcsControl = m_Camera->getSwitch("WCS_CONTROL");
3207 
3208     if (!wcsControl)
3209         return;
3210 
3211     auto wcs_enable  = wcsControl->findWidgetByName("WCS_ENABLE");
3212     auto wcs_disable = wcsControl->findWidgetByName("WCS_DISABLE");
3213 
3214     if (!wcs_enable || !wcs_disable)
3215         return;
3216 
3217     if ((wcs_enable->getState() == ISS_ON && enable) || (wcs_disable->getState() == ISS_ON && !enable))
3218         return;
3219 
3220     wcsControl->reset();
3221     if (enable)
3222     {
3223         appendLogText(i18n("World Coordinate System (WCS) is enabled."));
3224         wcs_enable->setState(ISS_ON);
3225     }
3226     else
3227     {
3228         appendLogText(i18n("World Coordinate System (WCS) is disabled."));
3229         wcs_disable->setState(ISS_ON);
3230         m_wcsSynced    = false;
3231     }
3232 
3233     auto clientManager = m_Camera->getDriverInfo()->getClientManager();
3234     if (clientManager)
3235         clientManager->sendNewProperty(wcsControl);
3236 }
3237 
3238 void Align::checkCameraExposureProgress(ISD::CameraChip * targetChip, double remaining, IPState state)
3239 {
3240     INDI_UNUSED(targetChip);
3241     INDI_UNUSED(remaining);
3242 
3243     if (state == IPS_ALERT)
3244     {
3245         if (++m_CaptureErrorCounter == 3 && !matchPAHStage(PolarAlignmentAssistant::PAH_REFRESH))
3246         {
3247             appendLogText(i18n("Capture error. Aborting..."));
3248             abort();
3249             return;
3250         }
3251 
3252         appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter));
3253         setAlignTableResult(ALIGN_RESULT_FAILED);
3254         captureAndSolve();
3255     }
3256 }
3257 
3258 void Align::setAlignTableResult(AlignResult result)
3259 {
3260     // Do nothing if the progress indicator is not running.
3261     // This is necessary since it could happen that a problem occurs
3262     // before #captureAndSolve() has been started and there does not
3263     // exist a table entry for the current run.
3264     QProgressIndicator *progress_indicator = getProgressStatus();
3265     if (progress_indicator == nullptr || ! progress_indicator->isAnimated())
3266         return;
3267     stopProgressAnimation();
3268 
3269     QIcon icon;
3270     switch (result)
3271     {
3272         case ALIGN_RESULT_SUCCESS:
3273             icon = QIcon(":/icons/AlignSuccess.svg");
3274             break;
3275 
3276         case ALIGN_RESULT_WARNING:
3277             icon = QIcon(":/icons/AlignWarning.svg");
3278             break;
3279 
3280         case ALIGN_RESULT_FAILED:
3281         default:
3282             icon = QIcon(":/icons/AlignFailure.svg");
3283             break;
3284     }
3285     int currentRow = solutionTable->rowCount() - 1;
3286     solutionTable->setCellWidget(currentRow, 3, new QWidget());
3287     QTableWidgetItem *statusReport = new QTableWidgetItem();
3288     statusReport->setIcon(icon);
3289     statusReport->setFlags(Qt::ItemIsSelectable);
3290     solutionTable->setItem(currentRow, 3, statusReport);
3291 }
3292 
3293 void Align::setFocusStatus(Ekos::FocusState state)
3294 {
3295     m_FocusState = state;
3296 }
3297 
3298 uint8_t Align::getSolverDownsample(uint16_t binnedW)
3299 {
3300     uint8_t downsample = Options::astrometryDownsample();
3301 
3302     if (!Options::astrometryAutoDownsample())
3303         return downsample;
3304 
3305     while (downsample < 8)
3306     {
3307         if (binnedW / downsample <= 1024)
3308             break;
3309 
3310         downsample += 2;
3311     }
3312 
3313     return downsample;
3314 }
3315 
3316 void Align::setCaptureStatus(CaptureState newState)
3317 {
3318     switch (newState)
3319     {
3320         case CAPTURE_ALIGNING:
3321             if (m_Mount && m_Mount->hasAlignmentModel() && Options::resetMountModelAfterMeridian())
3322             {
3323                 qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (m_Mount->clearAlignmentModel() ?
3324                                            "successful." : "failed.");
3325             }
3326             if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
3327                 appendLogText(i18n("Settling..."));
3328             m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
3329             m_CaptureTimer.start(alignSettlingTime->value());
3330             break;
3331         // Is this needed anymore with new flip policy? (sb 2023-10-20)
3332         // On meridian flip, reset Target Position Angle to fully rotated value
3333         // expected after MF so that we do not end up with reversed camera rotation
3334         case CAPTURE_MERIDIAN_FLIP:
3335             if (std::isnan(m_TargetPositionAngle) == false)
3336                 m_TargetPositionAngle = KSUtils::rangePA(m_TargetPositionAngle + 180.0);
3337             break;
3338         default:
3339             break;
3340     }
3341 
3342     m_CaptureState = newState;
3343 }
3344 
3345 void Align::showFITSViewer()
3346 {
3347     static int lastFVTabID = -1;
3348     if (m_ImageData)
3349     {
3350         QUrl url = QUrl::fromLocalFile("align.fits");
3351         if (fv.isNull())
3352         {
3353             fv = KStars::Instance()->createFITSViewer();
3354             fv->loadData(m_ImageData, url, &lastFVTabID);
3355             connect(fv.get(), &FITSViewer::terminated, this, [this]()
3356             {
3357                 fv.clear();
3358             });
3359         }
3360         else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
3361             fv->loadData(m_ImageData, url, &lastFVTabID);
3362 
3363         fv->show();
3364     }
3365 }
3366 
3367 void Align::toggleAlignWidgetFullScreen()
3368 {
3369     if (alignWidget->parent() == nullptr)
3370     {
3371         alignWidget->setParent(this);
3372         rightLayout->insertWidget(0, alignWidget);
3373         alignWidget->showNormal();
3374     }
3375     else
3376     {
3377         alignWidget->setParent(nullptr);
3378         alignWidget->setWindowTitle(i18nc("@title:window", "Align Frame"));
3379         alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
3380         alignWidget->showMaximized();
3381         alignWidget->show();
3382     }
3383 }
3384 
3385 void Align::setMountStatus(ISD::Mount::Status newState)
3386 {
3387     switch (newState)
3388     {
3389         case ISD::Mount::MOUNT_PARKED:
3390             solveB->setEnabled(false);
3391             loadSlewB->setEnabled(false);
3392             break;
3393         case ISD::Mount::MOUNT_IDLE:
3394             solveB->setEnabled(true);
3395             loadSlewB->setEnabled(true);
3396             break;
3397         case ISD::Mount::MOUNT_PARKING:
3398             solveB->setEnabled(false);
3399             loadSlewB->setEnabled(false);
3400             break;
3401         case ISD::Mount::MOUNT_SLEWING:
3402         case ISD::Mount::MOUNT_MOVING:
3403             solveB->setEnabled(false);
3404             loadSlewB->setEnabled(false);
3405             break;
3406 
3407         default:
3408             if (state != ALIGN_PROGRESS)
3409             {
3410                 solveB->setEnabled(true);
3411                 if (matchPAHStage(PAA::PAH_IDLE))
3412                 {
3413                     loadSlewB->setEnabled(true);
3414                 }
3415             }
3416             break;
3417     }
3418 
3419     RUN_PAH(setMountStatus(newState));
3420 }
3421 
3422 void Align::setAstrometryDevice(const QSharedPointer<ISD::GenericDevice> &device)
3423 {
3424     m_RemoteParserDevice = device;
3425 
3426     remoteSolverR->setEnabled(true);
3427     if (remoteParser.get() != nullptr)
3428     {
3429         remoteParser->setAstrometryDevice(m_RemoteParserDevice);
3430         connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
3431         connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
3432     }
3433 }
3434 
3435 void Align::refreshAlignOptions()
3436 {
3437     solverFOV->setImageDisplay(Options::astrometrySolverWCS());
3438     m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
3439     if (m_Rotator)
3440         m_RotatorControlPanel->updateFlipPolicy(Options::astrometryFlipRotationAllowed());
3441 }
3442 
3443 void Align::setupOptions()
3444 {
3445     KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self());
3446 
3447 #ifdef Q_OS_OSX
3448     dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
3449 #endif
3450 
3451     opsAlign = new OpsAlign(this);
3452     connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions);
3453     KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("StellarSolver Options"));
3454     page->setIcon(QIcon(":/icons/StellarSolverIcon.png"));
3455     // connect(rotatorB, &QPushButton::clicked, dialog, &KConfigDialog::show);
3456 
3457     opsPrograms = new OpsPrograms(this);
3458     page = dialog->addPage(opsPrograms, i18n("External & Online Programs"));
3459     page->setIcon(QIcon(":/icons/astrometry.svg"));
3460 
3461     opsAstrometry = new OpsAstrometry(this);
3462     page = dialog->addPage(opsAstrometry, i18n("Scale & Position"));
3463     page->setIcon(QIcon(":/icons/center_telescope_red.svg"));
3464 
3465     optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::AlignProfiles, dialog);
3466     page = dialog->addPage(optionsProfileEditor, i18n("Align Options Profiles Editor"));
3467     connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, [this]()
3468     {
3469         if(QFile(savedOptionsProfiles).exists())
3470             m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
3471         else
3472             m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
3473         opsAlign->reloadOptionsProfiles();
3474     });
3475     page->setIcon(QIcon::fromTheme("configure"));
3476 
3477     connect(opsAlign, &OpsAlign::needToLoadProfile, this, [this, dialog, page](const QString & profile)
3478     {
3479         optionsProfileEditor->loadProfile(profile);
3480         dialog->setCurrentPage(page);
3481     });
3482 
3483     opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this);
3484     m_IndexFilesPage = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files"));
3485     m_IndexFilesPage->setIcon(QIcon::fromTheme("map-flat"));
3486 }
3487 
3488 void Align::setupSolutionTable()
3489 {
3490     solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
3491 
3492     clearAllSolutionsB->setIcon(
3493         QIcon::fromTheme("application-exit"));
3494     clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3495 
3496     removeSolutionB->setIcon(QIcon::fromTheme("list-remove"));
3497     removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3498 
3499     exportSolutionsCSV->setIcon(
3500         QIcon::fromTheme("document-save-as"));
3501     exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3502 
3503     autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best"));
3504     autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3505 
3506     connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints);
3507     connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint);
3508     connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints);
3509     connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph);
3510     connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel);
3511     connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow);
3512 }
3513 
3514 void Align::setupPlot()
3515 {
3516     double accuracyRadius = alignAccuracyThreshold->value();
3517 
3518     alignPlot->setBackground(QBrush(Qt::black));
3519     alignPlot->setSelectionTolerance(10);
3520 
3521     alignPlot->xAxis->setBasePen(QPen(Qt::white, 1));
3522     alignPlot->yAxis->setBasePen(QPen(Qt::white, 1));
3523 
3524     alignPlot->xAxis->setTickPen(QPen(Qt::white, 1));
3525     alignPlot->yAxis->setTickPen(QPen(Qt::white, 1));
3526 
3527     alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
3528     alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));
3529 
3530     alignPlot->xAxis->setTickLabelColor(Qt::white);
3531     alignPlot->yAxis->setTickLabelColor(Qt::white);
3532 
3533     alignPlot->xAxis->setLabelColor(Qt::white);
3534     alignPlot->yAxis->setLabelColor(Qt::white);
3535 
3536     alignPlot->xAxis->setLabelFont(QFont(font().family(), 10));
3537     alignPlot->yAxis->setLabelFont(QFont(font().family(), 10));
3538 
3539     alignPlot->xAxis->setLabelPadding(2);
3540     alignPlot->yAxis->setLabelPadding(2);
3541 
3542     alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
3543     alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
3544     alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
3545     alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
3546     alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
3547     alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
3548 
3549     alignPlot->xAxis->setLabel(i18n("dRA (arcsec)"));
3550     alignPlot->yAxis->setLabel(i18n("dDE (arcsec)"));
3551 
3552     alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
3553     alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
3554 
3555     alignPlot->setInteractions(QCP::iRangeZoom);
3556     alignPlot->setInteraction(QCP::iRangeDrag, true);
3557 
3558     alignPlot->addGraph();
3559     alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone);
3560     alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15));
3561 
3562     buildTarget();
3563 
3564     connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip);
3565     connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange);
3566     connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange);
3567 
3568     alignPlot->resize(190, 190);
3569     alignPlot->replot();
3570 }
3571 
3572 void Align::setupFilterManager()
3573 {
3574     // Do we have an existing filter manager?
3575     if (m_FilterManager)
3576         m_FilterManager->disconnect(this);
3577 
3578     // Create new or refresh device
3579     Ekos::Manager::Instance()->createFilterManager(m_FilterWheel);
3580 
3581     // Return global filter manager for this filter wheel.
3582     Ekos::Manager::Instance()->getFilterManager(m_FilterWheel->getDeviceName(), m_FilterManager);
3583 
3584     connect(m_FilterManager.get(), &FilterManager::ready, this, [this]()
3585     {
3586         if (filterPositionPending)
3587         {
3588             m_FocusState = FOCUS_IDLE;
3589             filterPositionPending = false;
3590             captureAndSolve();
3591         }
3592     });
3593 
3594     connect(m_FilterManager.get(), &FilterManager::failed, this, [this]()
3595     {
3596         appendLogText(i18n("Filter operation failed."));
3597         abort();
3598     }
3599            );
3600 
3601     connect(m_FilterManager.get(), &FilterManager::newStatus, this, [this](Ekos::FilterState filterState)
3602     {
3603         if (filterPositionPending)
3604         {
3605             switch (filterState)
3606             {
3607                 case FILTER_OFFSET:
3608                     appendLogText(i18n("Changing focus offset by %1 steps...", m_FilterManager->getTargetFilterOffset()));
3609                     break;
3610 
3611                 case FILTER_CHANGE:
3612                 {
3613                     const int filterComboIndex = m_FilterManager->getTargetFilterPosition() - 1;
3614                     if (filterComboIndex >= 0 && filterComboIndex < alignFilter->count())
3615                         appendLogText(i18n("Changing filter to %1...", alignFilter->itemText(filterComboIndex)));
3616                 }
3617                 break;
3618 
3619                 case FILTER_AUTOFOCUS:
3620                     appendLogText(i18n("Auto focus on filter change..."));
3621                     break;
3622 
3623                 default:
3624                     break;
3625             }
3626         }
3627     });
3628 
3629     connect(m_FilterManager.get(), &FilterManager::labelsChanged, this, &Align::checkFilter);
3630     connect(m_FilterManager.get(), &FilterManager::positionChanged, this, &Align::checkFilter);
3631 }
3632 
3633 QVariantMap Align::getEffectiveFOV()
3634 {
3635     KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs);
3636 
3637     m_FOVWidth = m_FOVHeight = 0;
3638 
3639     for (auto &map : effectiveFOVs)
3640     {
3641         if (map["Profile"].toString() == m_ActiveProfile->name && map["Train"].toString() == opticalTrain())
3642         {
3643             if (isEqual(map["Width"].toInt(), m_CameraWidth) &&
3644                     isEqual(map["Height"].toInt(), m_CameraHeight) &&
3645                     isEqual(map["PixelW"].toDouble(), m_CameraPixelWidth) &&
3646                     isEqual(map["PixelH"].toDouble(), m_CameraPixelHeight) &&
3647                     isEqual(map["FocalLength"].toDouble(), m_FocalLength) &&
3648                     isEqual(map["FocalRedcuer"].toDouble(), m_Reducer) &&
3649                     isEqual(map["FocalRatio"].toDouble(), m_FocalRatio))
3650             {
3651                 m_FOVWidth = map["FovW"].toDouble();
3652                 m_FOVHeight = map["FovH"].toDouble();
3653                 return map;
3654             }
3655         }
3656     }
3657 
3658     return QVariantMap();
3659 }
3660 
3661 void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH)
3662 {
3663     if (newFOVW < 0 || newFOVH < 0 || (isEqual(newFOVW, m_FOVWidth) && isEqual(newFOVH, m_FOVHeight)))
3664         return;
3665 
3666     QVariantMap effectiveMap = getEffectiveFOV();
3667 
3668     // If ID exists, delete it first.
3669     if (effectiveMap.isEmpty() == false)
3670         KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString());
3671 
3672     // If FOV is 0x0, then we just remove existing effective FOV
3673     if (newFOVW == 0.0 && newFOVH == 0.0)
3674     {
3675         calculateFOV();
3676         return;
3677     }
3678 
3679     effectiveMap["Profile"] = m_ActiveProfile->name;
3680     effectiveMap["Train"] = opticalTrainCombo->currentText();
3681     effectiveMap["Width"] = m_CameraWidth;
3682     effectiveMap["Height"] = m_CameraHeight;
3683     effectiveMap["PixelW"] = m_CameraPixelWidth;
3684     effectiveMap["PixelH"] = m_CameraPixelHeight;
3685     effectiveMap["FocalLength"] = m_FocalLength;
3686     effectiveMap["FocalReducer"] = m_Reducer;
3687     effectiveMap["FocalRatio"] = m_FocalRatio;
3688     effectiveMap["FovW"] = newFOVW;
3689     effectiveMap["FovH"] = newFOVH;
3690 
3691     KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap);
3692 
3693     calculateFOV();
3694 
3695 }
3696 
3697 void Align::zoomAlignView()
3698 {
3699     m_AlignView->ZoomDefault();
3700 
3701     // Frame update is not immediate to reduce too many refreshes
3702     // So emit updated frame in 500ms
3703     QTimer::singleShot(500, this, [this]()
3704     {
3705         emit newFrame(m_AlignView);
3706     });
3707 }
3708 
3709 void Align::setAlignZoom(double scale)
3710 {
3711     if (scale > 1)
3712         m_AlignView->ZoomIn();
3713     else if (scale < 1)
3714         m_AlignView->ZoomOut();
3715 
3716     // Frame update is not immediate to reduce too many refreshes
3717     // So emit updated frame in 500ms
3718     QTimer::singleShot(500, this, [this]()
3719     {
3720         emit newFrame(m_AlignView);
3721     });
3722 }
3723 
3724 void Align::syncFOV()
3725 {
3726     QString newFOV = FOVOut->text();
3727     QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)");
3728     QRegularExpressionMatch match = re.match(newFOV);
3729     if (match.hasMatch())
3730     {
3731         double newFOVW = match.captured(1).toDouble();
3732         double newFOVH = match.captured(2).toDouble();
3733 
3734         //if (newFOVW > 0 && newFOVH > 0)
3735         saveNewEffectiveFOV(newFOVW, newFOVH);
3736 
3737         FOVOut->setStyleSheet(QString());
3738     }
3739     else
3740     {
3741         KSNotification::error(i18n("Invalid FOV."));
3742         FOVOut->setStyleSheet("background-color:red");
3743     }
3744 }
3745 
3746 // m_wasSlewStarted can't be false for more than 10s after a slew starts.
3747 bool Align::didSlewStart()
3748 {
3749     if (m_wasSlewStarted)
3750         return true;
3751     if (slewStartTimer.isValid() && slewStartTimer.elapsed() > MAX_WAIT_FOR_SLEW_START_MSEC)
3752     {
3753         qCDebug(KSTARS_EKOS_ALIGN) << "Slew timed out...waited > 10s, it must have started already.";
3754         return true;
3755     }
3756     return false;
3757 }
3758 
3759 void Align::setTargetCoords(double ra0, double de0)
3760 {
3761     SkyPoint target;
3762     target.setRA0(ra0);
3763     target.setDec0(de0);
3764     target.updateCoordsNow(KStarsData::Instance()->updateNum());
3765     setTarget(target);
3766 }
3767 
3768 void Align::setTarget(const SkyPoint &targetCoord)
3769 {
3770     m_TargetCoord = targetCoord;
3771     qCInfo(KSTARS_EKOS_ALIGN) << "Target coordinates updated to JNow RA:" << m_TargetCoord.ra().toHMSString()
3772                               << "DE:" << m_TargetCoord.dec().toDMSString();
3773 }
3774 
3775 QList<double> Align::getTargetCoords()
3776 {
3777     return QList<double>() << m_TargetCoord.ra0().Hours() << m_TargetCoord.dec0().Degrees();
3778 }
3779 
3780 void Align::setTargetPositionAngle(double value)
3781 {
3782     m_TargetPositionAngle =  value;
3783     qCDebug(KSTARS_EKOS_ALIGN) << "Target PA updated to: " << m_TargetPositionAngle;
3784 }
3785 
3786 void Align::calculateAlignTargetDiff()
3787 {
3788     if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
3789             matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
3790             matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
3791             matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
3792             matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
3793             matchPAHStage(PAA::PAH_THIRD_SOLVE) ||
3794             nothingR->isChecked() ||
3795             syncR->isChecked())
3796         return;
3797     m_TargetDiffRA = (m_AlignCoord.ra().deltaAngle(m_TargetCoord.ra())).Degrees() * 3600;
3798     m_TargetDiffDE = (m_AlignCoord.dec().deltaAngle(m_TargetCoord.dec())).Degrees() * 3600;
3799 
3800     dms RADiff(fabs(m_TargetDiffRA) / 3600.0), DEDiff(m_TargetDiffDE / 3600.0);
3801 
3802     m_TargetDiffTotal = sqrt(m_TargetDiffRA * m_TargetDiffRA + m_TargetDiffDE * m_TargetDiffDE);
3803 
3804     errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg(
3805                         QString::number(m_TargetDiffTotal, 'f', 0),
3806                         QString::number(m_TargetDiffRA, 'f', 0),
3807                         QString::number(m_TargetDiffDE, 'f', 0)));
3808     if (m_TargetDiffTotal <= static_cast<double>(alignAccuracyThreshold->value()))
3809         errOut->setStyleSheet("color:green");
3810     else if (m_TargetDiffTotal < 1.5 * alignAccuracyThreshold->value())
3811         errOut->setStyleSheet("color:yellow");
3812     else
3813         errOut->setStyleSheet("color:red");
3814 
3815     //This block of code will write the result into the solution table and plot it on the graph.
3816     int currentRow = solutionTable->rowCount() - 1;
3817     QTableWidgetItem *dRAReport = new QTableWidgetItem();
3818     if (dRAReport)
3819     {
3820         dRAReport->setText(QString::number(m_TargetDiffRA, 'f', 3) + "\"");
3821         dRAReport->setTextAlignment(Qt::AlignHCenter);
3822         dRAReport->setFlags(Qt::ItemIsSelectable);
3823         solutionTable->setItem(currentRow, 4, dRAReport);
3824     }
3825 
3826     QTableWidgetItem *dDECReport = new QTableWidgetItem();
3827     if (dDECReport)
3828     {
3829         dDECReport->setText(QString::number(m_TargetDiffDE, 'f', 3) + "\"");
3830         dDECReport->setTextAlignment(Qt::AlignHCenter);
3831         dDECReport->setFlags(Qt::ItemIsSelectable);
3832         solutionTable->setItem(currentRow, 5, dDECReport);
3833     }
3834 
3835     double raPlot  = m_TargetDiffRA;
3836     double decPlot = m_TargetDiffDE;
3837     alignPlot->graph(0)->addData(raPlot, decPlot);
3838 
3839     QCPItemText *textLabel = new QCPItemText(alignPlot);
3840     textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter);
3841 
3842     textLabel->position->setType(QCPItemPosition::ptPlotCoords);
3843     textLabel->position->setCoords(raPlot, decPlot);
3844     textLabel->setColor(Qt::red);
3845     textLabel->setPadding(QMargins(0, 0, 0, 0));
3846     textLabel->setBrush(Qt::white);
3847     textLabel->setPen(Qt::NoPen);
3848     textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' ');
3849     textLabel->setFont(QFont(font().family(), 8));
3850 
3851     if (!alignPlot->xAxis->range().contains(m_TargetDiffRA))
3852     {
3853         alignPlot->graph(0)->rescaleKeyAxis(true);
3854         alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
3855     }
3856     if (!alignPlot->yAxis->range().contains(m_TargetDiffDE))
3857     {
3858         alignPlot->graph(0)->rescaleValueAxis(true);
3859         alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
3860     }
3861     alignPlot->replot();
3862 }
3863 
3864 QStringList Align::getStellarSolverProfiles()
3865 {
3866     QStringList profiles;
3867     for (auto &param : m_StellarSolverProfiles)
3868         profiles << param.listName;
3869 
3870     return profiles;
3871 }
3872 
3873 void Align::exportSolutionPoints()
3874 {
3875     if (solutionTable->rowCount() == 0)
3876         return;
3877 
3878     QUrl exportFile = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Export Solution Points"),
3879                       alignURLPath,
3880                       "CSV File (*.csv)");
3881     if (exportFile.isEmpty()) // if user presses cancel
3882         return;
3883     if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false)
3884         exportFile.setPath(exportFile.toLocalFile() + ".csv");
3885 
3886     QString path = exportFile.toLocalFile();
3887 
3888     if (QFile::exists(path))
3889     {
3890         int r = KMessageBox::warningContinueCancel(nullptr,
3891                 i18n("A file named \"%1\" already exists. "
3892                      "Overwrite it?",
3893                      exportFile.fileName()),
3894                 i18n("Overwrite File?"), KStandardGuiItem::overwrite());
3895         if (r == KMessageBox::Cancel)
3896             return;
3897     }
3898 
3899     if (!exportFile.isValid())
3900     {
3901         QString message = i18n("Invalid URL: %1", exportFile.url());
3902         KSNotification::sorry(message, i18n("Invalid URL"));
3903         return;
3904     }
3905 
3906     QFile file;
3907     file.setFileName(path);
3908     if (!file.open(QIODevice::WriteOnly))
3909     {
3910         QString message = i18n("Unable to write to file %1", path);
3911         KSNotification::sorry(message, i18n("Could Not Open File"));
3912         return;
3913     }
3914 
3915     QTextStream outstream(&file);
3916 
3917     QString epoch = QString::number(KStarsDateTime::currentDateTime().epoch());
3918 
3919     outstream << "RA (J" << epoch << "),DE (J" << epoch
3920               << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << Qt::endl;
3921 
3922     for (int i = 0; i < solutionTable->rowCount(); i++)
3923     {
3924         QTableWidgetItem *raCell      = solutionTable->item(i, 0);
3925         QTableWidgetItem *deCell      = solutionTable->item(i, 1);
3926         QTableWidgetItem *objNameCell = solutionTable->item(i, 2);
3927         QTableWidgetItem *raErrorCell = solutionTable->item(i, 4);
3928         QTableWidgetItem *deErrorCell = solutionTable->item(i, 5);
3929 
3930         if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell)
3931         {
3932             KSNotification::sorry(i18n("Error in table structure."));
3933             return;
3934         }
3935         dms raDMS = dms::fromString(raCell->text(), false);
3936         dms deDMS = dms::fromString(deCell->text(), true);
3937         outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ','
3938                   << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ','
3939                   << deErrorCell->text().remove('\"') << Qt::endl;
3940     }
3941     emit newLog(i18n("Solution Points Saved as: %1", path));
3942     file.close();
3943 }
3944 
3945 void Align::setupPolarAlignmentAssistant()
3946 {
3947     // Create PAA instance
3948     m_PolarAlignmentAssistant = new PolarAlignmentAssistant(this, m_AlignView);
3949     connect(m_PolarAlignmentAssistant, &Ekos::PAA::captureAndSolve, this, &Ekos::Align::captureAndSolve);
3950     connect(m_PolarAlignmentAssistant, &Ekos::PAA::newAlignTableResult, this, &Ekos::Align::setAlignTableResult);
3951     connect(m_PolarAlignmentAssistant, &Ekos::PAA::newFrame, this, &Ekos::Align::newFrame);
3952     connect(m_PolarAlignmentAssistant, &Ekos::PAA::newPAHStage, this, &Ekos::Align::processPAHStage);
3953     connect(m_PolarAlignmentAssistant, &Ekos::PAA::newLog, this, &Ekos::Align::appendLogText);
3954 
3955     tabWidget->addTab(m_PolarAlignmentAssistant, i18n("Polar Alignment"));
3956 }
3957 
3958 void Align::setupManualRotator()
3959 {
3960     if (m_ManualRotator)
3961         return;
3962 
3963     m_ManualRotator = new ManualRotator(this);
3964     connect(m_ManualRotator, &Ekos::ManualRotator::captureAndSolve, this, &Ekos::Align::captureAndSolve);
3965     // If user cancel manual rotator, reset load slew target PA, otherwise it will keep popping up
3966     // for any subsequent solves.
3967     connect(m_ManualRotator, &Ekos::ManualRotator::rejected, this, [this]()
3968     {
3969         m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
3970         stop(ALIGN_IDLE);
3971     });
3972 }
3973 
3974 void Align::setuptDarkProcessor()
3975 {
3976     if (m_DarkProcessor)
3977         return;
3978 
3979     m_DarkProcessor = new DarkProcessor(this);
3980     connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Align::appendLogText);
3981     connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
3982     {
3983         alignDarkFrame->setChecked(completed);
3984         m_AlignView->setProperty("suspended", false);
3985         if (completed)
3986         {
3987             m_AlignView->rescale(ZOOM_KEEP_LEVEL);
3988             m_AlignView->updateFrame();
3989         }
3990         setCaptureComplete();
3991     });
3992 }
3993 
3994 void Align::processPAHStage(int stage)
3995 {
3996     switch (stage)
3997     {
3998         case PAA::PAH_IDLE:
3999             // Abort any solver that might be running.
4000             // Assumes this state change won't happen randomly (e.g. in the middle of align).
4001             // Alternatively could just let the stellarsolver finish naturally.
4002             if (m_StellarSolver && m_StellarSolver->isRunning())
4003                 m_StellarSolver->abort();
4004             break;
4005         case PAA::PAH_POST_REFRESH:
4006         {
4007             Options::setAstrometrySolverWCS(rememberSolverWCS);
4008             Options::setAutoWCS(rememberAutoWCS);
4009             stop(ALIGN_IDLE);
4010         }
4011         break;
4012 
4013         case PAA::PAH_FIRST_CAPTURE:
4014             nothingR->setChecked(true);
4015             m_CurrentGotoMode = GOTO_NOTHING;
4016             loadSlewB->setEnabled(false);
4017 
4018             rememberSolverWCS = Options::astrometrySolverWCS();
4019             rememberAutoWCS   = Options::autoWCS();
4020 
4021             Options::setAutoWCS(false);
4022             Options::setAstrometrySolverWCS(true);
4023             break;
4024         case PAA::PAH_SECOND_CAPTURE:
4025         case PAA::PAH_THIRD_CAPTURE:
4026             if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
4027                 emit newLog(i18n("Settling..."));
4028             m_CaptureTimer.start(alignSettlingTime->value());
4029             break;
4030 
4031         default:
4032             break;
4033     }
4034 
4035     emit newPAAStage(stage);
4036 }
4037 
4038 bool Align::matchPAHStage(uint32_t stage)
4039 {
4040     return m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->getPAHStage() == stage;
4041 }
4042 
4043 void Align::toggleManualRotator(bool toggled)
4044 {
4045     if (toggled)
4046     {
4047         m_ManualRotator->show();
4048         m_ManualRotator->raise();
4049     }
4050     else
4051         m_ManualRotator->close();
4052 }
4053 
4054 void Align::setupOpticalTrainManager()
4055 {
4056     connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &Align::refreshOpticalTrain);
4057     connect(trainB, &QPushButton::clicked, this, [this]()
4058     {
4059         OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
4060     });
4061     connect(opticalTrainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
4062     {
4063         ProfileSettings::Instance()->setOneSetting(ProfileSettings::AlignOpticalTrain,
4064                 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
4065         refreshOpticalTrain();
4066         emit trainChanged();
4067     });
4068 }
4069 
4070 void Align::refreshOpticalTrain()
4071 {
4072     opticalTrainCombo->blockSignals(true);
4073     opticalTrainCombo->clear();
4074     opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
4075     trainB->setEnabled(true);
4076 
4077     QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::AlignOpticalTrain);
4078 
4079     if (trainID.isValid())
4080     {
4081         auto id = trainID.toUInt();
4082 
4083         // If train not found, select the first one available.
4084         if (OpticalTrainManager::Instance()->exists(id) == false)
4085         {
4086             qCWarning(KSTARS_EKOS_ALIGN) << "Optical train doesn't exist for id" << id;
4087             id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
4088         }
4089 
4090         auto name = OpticalTrainManager::Instance()->name(id);
4091 
4092         opticalTrainCombo->setCurrentText(name);
4093 
4094         auto scope = OpticalTrainManager::Instance()->getScope(name);
4095         m_FocalLength = scope["focal_length"].toDouble(-1);
4096         m_Aperture = scope["aperture"].toDouble(-1);
4097         m_FocalRatio = scope["focal_ratio"].toDouble(-1);
4098         m_Reducer = OpticalTrainManager::Instance()->getReducer(name);
4099 
4100         // DSLR Lens Aperture
4101         if (m_Aperture < 0 && m_FocalRatio > 0)
4102             m_Aperture = m_FocalLength / m_FocalRatio;
4103 
4104         auto mount = OpticalTrainManager::Instance()->getMount(name);
4105         setMount(mount);
4106 
4107         auto camera = OpticalTrainManager::Instance()->getCamera(name);
4108         if (camera)
4109         {
4110             camera->setScopeInfo(m_FocalLength * m_Reducer, m_Aperture);
4111             opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
4112         }
4113         setCamera(camera);
4114 
4115         syncTelescopeInfo();
4116 
4117         auto filterWheel = OpticalTrainManager::Instance()->getFilterWheel(name);
4118         setFilterWheel(filterWheel);
4119 
4120         auto rotator = OpticalTrainManager::Instance()->getRotator(name);
4121         setRotator(rotator);
4122 
4123         // Load train settings
4124         OpticalTrainSettings::Instance()->setOpticalTrainID(id);
4125         auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::Align);
4126         if (settings.isValid())
4127             setAllSettings(settings.toJsonObject().toVariantMap());
4128         else
4129             m_Settings = m_GlobalSettings;
4130 
4131         // Need to save information used for Mosaic planner
4132         Options::setTelescopeFocalLength(m_FocalLength);
4133         Options::setCameraPixelWidth(m_CameraPixelWidth);
4134         Options::setCameraPixelHeight(m_CameraPixelHeight);
4135         Options::setCameraWidth(m_CameraWidth);
4136         Options::setCameraHeight(m_CameraHeight);
4137     }
4138 
4139     opticalTrainCombo->blockSignals(false);
4140 }
4141 
4142 void Align::syncSettings()
4143 {
4144     QDoubleSpinBox *dsb = nullptr;
4145     QSpinBox *sb = nullptr;
4146     QCheckBox *cb = nullptr;
4147     QComboBox *cbox = nullptr;
4148     QRadioButton *cradio = nullptr;
4149 
4150     QString key;
4151     QVariant value;
4152 
4153     if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
4154     {
4155         key = dsb->objectName();
4156         value = dsb->value();
4157 
4158     }
4159     else if ( (sb = qobject_cast<QSpinBox*>(sender())))
4160     {
4161         key = sb->objectName();
4162         value = sb->value();
4163     }
4164     else if ( (cb = qobject_cast<QCheckBox*>(sender())))
4165     {
4166         key = cb->objectName();
4167         value = cb->isChecked();
4168     }
4169     else if ( (cbox = qobject_cast<QComboBox*>(sender())))
4170     {
4171         key = cbox->objectName();
4172         value = cbox->currentText();
4173     }
4174     else if ( (cradio = qobject_cast<QRadioButton*>(sender())))
4175     {
4176         key = cradio->objectName();
4177 
4178         // N.B. Only store CHECKED radios, do not store unchecked ones
4179         // as we only have exclusive groups in Align module.
4180         if (cradio->isChecked() == false)
4181         {
4182             // Remove from setting if it was added before
4183             if (m_Settings.contains(key))
4184             {
4185                 m_Settings.remove(key);
4186                 emit settingsUpdated(getAllSettings());
4187                 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4188                 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4189             }
4190             return;
4191         }
4192 
4193         value = true;
4194     }
4195 
4196     // Save immediately
4197     Options::self()->setProperty(key.toLatin1(), value);
4198 
4199     m_Settings[key] = value;
4200     m_GlobalSettings[key] = value;
4201 
4202     emit settingsUpdated(getAllSettings());
4203 
4204     // Save to optical train specific settings as well
4205     OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4206     OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4207 }
4208 
4209 void Align::loadGlobalSettings()
4210 {
4211     QString key;
4212     QVariant value;
4213 
4214     QVariantMap settings;
4215     // All Combo Boxes
4216     for (auto &oneWidget : findChildren<QComboBox*>())
4217     {
4218         if (oneWidget->objectName() == "opticalTrainCombo")
4219             continue;
4220 
4221         key = oneWidget->objectName();
4222         value = Options::self()->property(key.toLatin1());
4223         if (value.isValid())
4224         {
4225             oneWidget->setCurrentText(value.toString());
4226             settings[key] = value;
4227         }
4228     }
4229 
4230     // All Double Spin Boxes
4231     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4232     {
4233         key = oneWidget->objectName();
4234         value = Options::self()->property(key.toLatin1());
4235         if (value.isValid())
4236         {
4237             oneWidget->setValue(value.toDouble());
4238             settings[key] = value;
4239         }
4240     }
4241 
4242     // All Spin Boxes
4243     for (auto &oneWidget : findChildren<QSpinBox*>())
4244     {
4245         key = oneWidget->objectName();
4246         value = Options::self()->property(key.toLatin1());
4247         if (value.isValid())
4248         {
4249             oneWidget->setValue(value.toInt());
4250             settings[key] = value;
4251         }
4252     }
4253 
4254     // All Checkboxes
4255     for (auto &oneWidget : findChildren<QCheckBox*>())
4256     {
4257         key = oneWidget->objectName();
4258         value = Options::self()->property(key.toLatin1());
4259         if (value.isValid())
4260         {
4261             oneWidget->setChecked(value.toBool());
4262             settings[key] = value;
4263         }
4264     }
4265 
4266     m_GlobalSettings = m_Settings = settings;
4267 }
4268 
4269 
4270 void Align::connectSettings()
4271 {
4272     // All Combo Boxes
4273     for (auto &oneWidget : findChildren<QComboBox*>())
4274         connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4275 
4276     // All Double Spin Boxes
4277     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4278         connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4279 
4280     // All Spin Boxes
4281     for (auto &oneWidget : findChildren<QSpinBox*>())
4282         connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4283 
4284     // All Checkboxes
4285     for (auto &oneWidget : findChildren<QCheckBox*>())
4286         connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4287 
4288     // All Radio buttons
4289     for (auto &oneWidget : findChildren<QRadioButton*>())
4290         connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4291 
4292     // Train combo box should NOT be synced.
4293     disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4294 }
4295 
4296 void Align::disconnectSettings()
4297 {
4298     // All Combo Boxes
4299     for (auto &oneWidget : findChildren<QComboBox*>())
4300         disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4301 
4302     // All Double Spin Boxes
4303     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4304         disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4305 
4306     // All Spin Boxes
4307     for (auto &oneWidget : findChildren<QSpinBox*>())
4308         disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4309 
4310     // All Checkboxes
4311     for (auto &oneWidget : findChildren<QCheckBox*>())
4312         disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4313 
4314     // All Radio buttons
4315     for (auto &oneWidget : findChildren<QRadioButton*>())
4316         disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4317 
4318 }
4319 
4320 ///////////////////////////////////////////////////////////////////////////////////////////
4321 ///
4322 ///////////////////////////////////////////////////////////////////////////////////////////
4323 QVariantMap Align::getAllSettings() const
4324 {
4325     QVariantMap settings;
4326 
4327     // All Combo Boxes
4328     for (auto &oneWidget : findChildren<QComboBox*>())
4329         settings.insert(oneWidget->objectName(), oneWidget->currentText());
4330 
4331     // All Double Spin Boxes
4332     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4333         settings.insert(oneWidget->objectName(), oneWidget->value());
4334 
4335     // All Spin Boxes
4336     for (auto &oneWidget : findChildren<QSpinBox*>())
4337         settings.insert(oneWidget->objectName(), oneWidget->value());
4338 
4339     // All Checkboxes
4340     for (auto &oneWidget : findChildren<QCheckBox*>())
4341         settings.insert(oneWidget->objectName(), oneWidget->isChecked());
4342 
4343     return settings;
4344 }
4345 
4346 ///////////////////////////////////////////////////////////////////////////////////////////
4347 ///
4348 ///////////////////////////////////////////////////////////////////////////////////////////
4349 void Align::setAllSettings(const QVariantMap &settings)
4350 {
4351     // Disconnect settings that we don't end up calling syncSettings while
4352     // performing the changes.
4353     disconnectSettings();
4354 
4355     for (auto &name : settings.keys())
4356     {
4357         // Combo
4358         auto comboBox = findChild<QComboBox*>(name);
4359         if (comboBox)
4360         {
4361             syncControl(settings, name, comboBox);
4362             continue;
4363         }
4364 
4365         // Double spinbox
4366         auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
4367         if (doubleSpinBox)
4368         {
4369             syncControl(settings, name, doubleSpinBox);
4370             continue;
4371         }
4372 
4373         // spinbox
4374         auto spinBox = findChild<QSpinBox*>(name);
4375         if (spinBox)
4376         {
4377             syncControl(settings, name, spinBox);
4378             continue;
4379         }
4380 
4381         // checkbox
4382         auto checkbox = findChild<QCheckBox*>(name);
4383         if (checkbox)
4384         {
4385             syncControl(settings, name, checkbox);
4386             continue;
4387         }
4388 
4389         // Radio button
4390         auto radioButton = findChild<QRadioButton*>(name);
4391         if (radioButton)
4392         {
4393             syncControl(settings, name, radioButton);
4394             continue;
4395         }
4396     }
4397 
4398     // Sync to options
4399     for (auto &key : settings.keys())
4400     {
4401         auto value = settings[key];
4402         // Save immediately
4403         Options::self()->setProperty(key.toLatin1(), value);
4404         Options::self()->save();
4405 
4406         m_Settings[key] = value;
4407         m_GlobalSettings[key] = value;
4408     }
4409 
4410     emit settingsUpdated(getAllSettings());
4411 
4412     // Save to optical train specific settings as well
4413     OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4414     OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4415 
4416     // Restablish connections
4417     connectSettings();
4418 }
4419 
4420 ///////////////////////////////////////////////////////////////////////////////////////////
4421 ///
4422 ///////////////////////////////////////////////////////////////////////////////////////////
4423 bool Align::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
4424 {
4425     QSpinBox *pSB = nullptr;
4426     QDoubleSpinBox *pDSB = nullptr;
4427     QCheckBox *pCB = nullptr;
4428     QComboBox *pComboBox = nullptr;
4429     QRadioButton *pRadioButton = nullptr;
4430     bool ok = false;
4431 
4432     if ((pSB = qobject_cast<QSpinBox *>(widget)))
4433     {
4434         const int value = settings[key].toInt(&ok);
4435         if (ok)
4436         {
4437             pSB->setValue(value);
4438             return true;
4439         }
4440     }
4441     else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
4442     {
4443         const double value = settings[key].toDouble(&ok);
4444         if (ok)
4445         {
4446             pDSB->setValue(value);
4447             return true;
4448         }
4449     }
4450     else if ((pCB = qobject_cast<QCheckBox *>(widget)))
4451     {
4452         const bool value = settings[key].toBool();
4453         pCB->setChecked(value);
4454         return true;
4455     }
4456     // ONLY FOR STRINGS, not INDEX
4457     else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
4458     {
4459         const QString value = settings[key].toString();
4460         pComboBox->setCurrentText(value);
4461         return true;
4462     }
4463     else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
4464     {
4465         const bool value = settings[key].toBool();
4466         pRadioButton->setChecked(value);
4467         return true;
4468     }
4469 
4470     return false;
4471 }
4472 
4473 void Align::setState(AlignState value)
4474 {
4475     qCDebug(KSTARS_EKOS_ALIGN) << "Align state changed to" << getAlignStatusString(value);
4476     state = value;
4477 };
4478 
4479 void Align::processCaptureTimeout()
4480 {
4481     if (m_CaptureTimeoutCounter++ > 3)
4482     {
4483         appendLogText(i18n("Capture timed out."));
4484         m_CaptureTimer.stop();
4485         abort();
4486     }
4487     else
4488     {
4489         ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
4490         if (targetChip->isCapturing())
4491         {
4492             appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500));
4493             targetChip->abortExposure();
4494             m_CaptureTimer.start( m_CaptureTimer.interval() * 2);
4495         }
4496         else
4497         {
4498             setAlignTableResult(ALIGN_RESULT_FAILED);
4499             if (m_resetCaptureTimeoutCounter)
4500             {
4501                 m_resetCaptureTimeoutCounter = false;
4502                 m_CaptureTimeoutCounter = 0;
4503             }
4504             captureAndSolve();
4505         }
4506     }
4507 }
4508 }