1 /*
2     SPDX-FileCopyrightText: 2013 Jasem Mutlaq <mutlaqja@ikarustech.com>
3     SPDX-FileCopyrightText: 2013-2021 Jasem Mutlaq <mutlaqja@ikarustech.com>
4     SPDX-FileCopyrightText: 2018-2020 Robert Lancaster <rlancaste@gmail.com>
5     SPDX-FileCopyrightText: 2019-2021 Hy Murveit <hy@murveit.com>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "align.h"
11 #include "alignadaptor.h"
12 #include "alignview.h"
13 #include <ekos_align_debug.h>
14 
15 // Options
16 #include "Options.h"
17 #include "opsalign.h"
18 #include "opsprograms.h"
19 #include "opsastrometry.h"
20 #include "opsastrometryindexfiles.h"
21 
22 // Components
23 #include "mountmodel.h"
24 #include "polaralignmentassistant.h"
25 #include "remoteastrometryparser.h"
26 #include "polaralign.h"
27 #include "manualrotator.h"
28 
29 // FITS
30 #include "fitsviewer/fitsdata.h"
31 #include "fitsviewer/fitstab.h"
32 
33 // Auxiliary
34 #include "auxiliary/QProgressIndicator.h"
35 #include "auxiliary/ksmessagebox.h"
36 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
37 #include "dialogs/finddialog.h"
38 #include "ksnotification.h"
39 #include "kspaths.h"
40 #include "fov.h"
41 #include "kstars.h"
42 #include "kstarsdata.h"
43 #include "skymapcomposite.h"
44 
45 // INDI
46 #include "ekos/manager.h"
47 #include "indi/clientmanager.h"
48 #include "indi/driverinfo.h"
49 #include "indi/indifilter.h"
50 #include "profileinfo.h"
51 
52 // System Includes
53 #include <KActionCollection>
54 #include <basedevice.h>
55 #include <indicom.h>
56 #include <memory>
57 
58 #define MAXIMUM_SOLVER_ITERATIONS 10
59 #define CAPTURE_RETRY_DELAY       10000
60 #define PAH_CUTOFF_FOV            10 // Minimum FOV width in arcminutes for PAH to work
61 #define CHECK_PAH(x) \
62     m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->x
63 #define RUN_PAH(x) \
64     if (m_PolarAlignmentAssistant) m_PolarAlignmentAssistant->x
65 
66 namespace Ekos
67 {
68 
69 using PAA = PolarAlignmentAssistant;
70 
Align(ProfileInfo * activeProfile)71 Align::Align(ProfileInfo *activeProfile) : m_ActiveProfile(activeProfile)
72 {
73     setupUi(this);
74 
75     qRegisterMetaType<Ekos::AlignState>("Ekos::AlignState");
76     qDBusRegisterMetaType<Ekos::AlignState>();
77 
78     new AlignAdaptor(this);
79     QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this);
80 
81     dirPath = QDir::homePath();
82 
83     KStarsData::Instance()->clearTransientFOVs();
84 
85     //loadSlewMode = false;
86     solverFOV.reset(new FOV());
87     solverFOV->setName(i18n("Solver FOV"));
88     solverFOV->setObjectName("solver_fov");
89     solverFOV->setLockCelestialPole(true);
90     solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name());
91     solverFOV->setProperty("visible", false);
92     KStarsData::Instance()->addTransientFOV(solverFOV);
93 
94     sensorFOV.reset(new FOV());
95     sensorFOV->setObjectName("sensor_fov");
96     sensorFOV->setLockCelestialPole(true);
97     sensorFOV->setProperty("visible", Options::showSensorFOV());
98     KStarsData::Instance()->addTransientFOV(sensorFOV);
99 
100     QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov");
101     if (a)
102         a->setEnabled(true);
103 
104     showFITSViewerB->setIcon(
105         QIcon::fromTheme("kstars_fitsviewer"));
106     showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
107     connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer);
108 
109     toggleFullScreenB->setIcon(
110         QIcon::fromTheme("view-fullscreen"));
111     toggleFullScreenB->setShortcut(Qt::Key_F4);
112     toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
113     connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen);
114 
115     alignView = new AlignView(alignWidget, FITS_ALIGN);
116     alignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
117     alignView->setBaseSize(alignWidget->size());
118     alignView->createFloatingToolBar();
119     QVBoxLayout *vlayout = new QVBoxLayout();
120 
121     vlayout->addWidget(alignView);
122     alignWidget->setLayout(vlayout);
123 
124     connect(solveB, &QPushButton::clicked, [this]()
125     {
126         updateTargetCoords();
127         captureAndSolve();
128     });
129     connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort);
130 
131     // Effective FOV Edit
132     connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV);
133 
134     connect(CCDCaptureCombo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
135             &Ekos::Align::setDefaultCCD);
136     connect(CCDCaptureCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Align::checkCCD);
137 
138     connect(loadSlewB, &QPushButton::clicked, [&]()
139     {
140         loadAndSlew();
141     });
142 
143     FilterDevicesCombo->addItem("--");
144     connect(FilterDevicesCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated),
145             [ = ](const QString & text)
146     {
147         syncSettings();
148         Options::setDefaultAlignFilterWheel(text);
149     });
150 
151     connect(FilterDevicesCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Align::checkFilter);
152 
153     connect(FilterPosCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
154             [ = ](int index)
155     {
156         syncSettings();
157         Options::setLockAlignFilterIndex(index);
158     }
159            );
160 
161     gotoModeButtonGroup->setId(syncR, GOTO_SYNC);
162     gotoModeButtonGroup->setId(slewR, GOTO_SLEW);
163     gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING);
164 
165     connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this,
166             [ = ](int id)
167     {
168         this->m_CurrentGotoMode = static_cast<GotoMode>(id);
169     });
170 
171     m_CaptureTimer.setSingleShot(true);
172     m_CaptureTimer.setInterval(CAPTURE_RETRY_DELAY);
173     connect(&m_CaptureTimer, &QTimer::timeout, [&]()
174     {
175         if (m_CaptureTimeoutCounter++ > 3)
176         {
177             appendLogText(i18n("Capture timed out."));
178             m_CaptureTimer.stop();
179             abort();
180         }
181         else
182         {
183             ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
184             if (targetChip->isCapturing())
185             {
186                 appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500));
187                 targetChip->abortExposure();
188                 m_CaptureTimer.start( m_CaptureTimer.interval() * 2);
189             }
190             else
191             {
192                 setAlignTableResult(ALIGN_RESULT_FAILED);
193                 captureAndSolve();
194             }
195         }
196     });
197 
198     m_AlignTimer.setSingleShot(true);
199     m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
200     connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout);
201 
202     m_CurrentGotoMode = static_cast<GotoMode>(Options::solverGotoOption());
203     gotoModeButtonGroup->button(m_CurrentGotoMode)->setChecked(true);
204 
205     KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self());
206 
207 #ifdef Q_OS_OSX
208     dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
209 #endif
210 
211     opsAlign = new OpsAlign(this);
212     connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions);
213     KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("StellarSolver Options"));
214     page->setIcon(QIcon(":/icons/StellarSolverIcon.png"));
215 
216     opsPrograms = new OpsPrograms(this);
217     page = dialog->addPage(opsPrograms, i18n("External & Online Programs"));
218     page->setIcon(QIcon(":/icons/astrometry.svg"));
219 
220     opsAstrometry = new OpsAstrometry(this);
221     page = dialog->addPage(opsAstrometry, i18n("Scale & Position"));
222     page->setIcon(QIcon(":/icons/center_telescope_red.svg"));
223 
224     optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::AlignProfiles, dialog);
225     page = dialog->addPage(optionsProfileEditor, i18n("Align Options Profiles Editor"));
226     connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, [this]()
227     {
228         if(QFile(savedOptionsProfiles).exists())
229             m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
230         else
231             m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
232         opsAlign->reloadOptionsProfiles();
233     });
234     page->setIcon(QIcon::fromTheme("configure"));
235 
236     connect(opsAlign, &OpsAlign::needToLoadProfile, this, [this, dialog, page](int profile)
237     {
238         optionsProfileEditor->loadProfile(profile);
239         dialog->setCurrentPage(page);
240     });
241 
242     opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this);
243     m_IndexFilesPage = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files"));
244     m_IndexFilesPage->setIcon(QIcon::fromTheme("map-flat"));
245 
246     appendLogText(i18n("Idle."));
247 
248     pi.reset(new QProgressIndicator(this));
249 
250     stopLayout->addWidget(pi.get());
251 
252     exposureIN->setValue(Options::alignExposure());
253     connect(exposureIN, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), [&]()
254     {
255         syncSettings();
256     });
257 
258     rememberSolverWCS = Options::astrometrySolverWCS();
259     rememberAutoWCS   = Options::autoWCS();
260 
261     solverModeButtonGroup->setId(localSolverR, SOLVER_LOCAL);
262     solverModeButtonGroup->setId(remoteSolverR, SOLVER_REMOTE);
263 
264     localSolverR->setChecked(Options::solverMode() == SOLVER_LOCAL);
265     remoteSolverR->setChecked(Options::solverMode() == SOLVER_REMOTE);
266     connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this,
267             &Align::setSolverMode);
268     setSolverMode(solverModeButtonGroup->checkedId());
269 
270     // Which telescope info to use for FOV calculations
271     FOVScopeCombo->setCurrentIndex(Options::solverScopeType());
272     connect(FOVScopeCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
273             &Ekos::Align::updateTelescopeType);
274 
275     accuracySpin->setValue(Options::solverAccuracyThreshold());
276     connect(accuracySpin, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this]()
277     {
278         Options::setSolverAccuracyThreshold(accuracySpin->value());
279         buildTarget();
280     });
281 
282     connect(alignDarkFrameCheck, &QCheckBox::toggled, [this]()
283     {
284         Options::setAlignDarkFrame(alignDarkFrameCheck->isChecked());
285     });
286     alignDarkFrameCheck->setChecked(Options::alignDarkFrame());
287 
288     delaySpin->setValue(Options::settlingTime());
289     connect(delaySpin, &QSpinBox::editingFinished, this, &Ekos::Align::saveSettleTime);
290 
291     connect(binningCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
292             &Ekos::Align::setBinningIndex);
293 
294     double accuracyRadius = accuracySpin->value();
295 
296     alignPlot->setBackground(QBrush(Qt::black));
297     alignPlot->setSelectionTolerance(10);
298 
299     alignPlot->xAxis->setBasePen(QPen(Qt::white, 1));
300     alignPlot->yAxis->setBasePen(QPen(Qt::white, 1));
301 
302     alignPlot->xAxis->setTickPen(QPen(Qt::white, 1));
303     alignPlot->yAxis->setTickPen(QPen(Qt::white, 1));
304 
305     alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
306     alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));
307 
308     alignPlot->xAxis->setTickLabelColor(Qt::white);
309     alignPlot->yAxis->setTickLabelColor(Qt::white);
310 
311     alignPlot->xAxis->setLabelColor(Qt::white);
312     alignPlot->yAxis->setLabelColor(Qt::white);
313 
314     alignPlot->xAxis->setLabelFont(QFont(font().family(), 10));
315     alignPlot->yAxis->setLabelFont(QFont(font().family(), 10));
316 
317     alignPlot->xAxis->setLabelPadding(2);
318     alignPlot->yAxis->setLabelPadding(2);
319 
320     alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
321     alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
322     alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
323     alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
324     alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
325     alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
326 
327     alignPlot->xAxis->setLabel(i18n("dRA (arcsec)"));
328     alignPlot->yAxis->setLabel(i18n("dDE (arcsec)"));
329 
330     alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
331     alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
332 
333     alignPlot->setInteractions(QCP::iRangeZoom);
334     alignPlot->setInteraction(QCP::iRangeDrag, true);
335 
336     alignPlot->addGraph();
337     alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone);
338     alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15));
339 
340     buildTarget();
341 
342     connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip);
343     connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange);
344     connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange);
345 
346     alignPlot->resize(190, 190);
347     alignPlot->replot();
348 
349     solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
350 
351     clearAllSolutionsB->setIcon(
352         QIcon::fromTheme("application-exit"));
353     clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
354 
355     removeSolutionB->setIcon(QIcon::fromTheme("list-remove"));
356     removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
357 
358     exportSolutionsCSV->setIcon(
359         QIcon::fromTheme("document-save-as"));
360     exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect);
361 
362     autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best"));
363     autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
364 
365     connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints);
366     connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint);
367     connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints);
368     connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph);
369     connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel);
370     connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow);
371 
372     //Note:  This is to prevent a button from being called the default button
373     //and then executing when the user hits the enter key such as when on a Text Box
374     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
375     for (auto &button : qButtons)
376         button->setAutoDefault(false);
377 
378     savedOptionsProfiles = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("SavedAlignProfiles.ini");
379     if(QFile(savedOptionsProfiles).exists())
380         m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
381     else
382         m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
383 
384     initPolarAlignmentAssistant();
385     initManualRotator();
386     initDarkProcessor();
387 }
388 
~Align()389 Align::~Align()
390 {
391     if (alignWidget->parent() == nullptr)
392         toggleAlignWidgetFullScreen();
393 
394     // Remove temporary FITS files left before by the solver
395     QDir dir(QDir::tempPath());
396     dir.setNameFilters(QStringList() << "fits*"
397                        << "tmp.*");
398     dir.setFilter(QDir::Files);
399     for (auto &dirFile : dir.entryList())
400         dir.remove(dirFile);
401 }
selectSolutionTableRow(int row,int column)402 void Align::selectSolutionTableRow(int row, int column)
403 {
404     Q_UNUSED(column)
405 
406     solutionTable->selectRow(row);
407     for (int i = 0; i < alignPlot->itemCount(); i++)
408     {
409         QCPAbstractItem *abstractItem = alignPlot->item(i);
410         if (abstractItem)
411         {
412             QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
413             if (item)
414             {
415                 if (i == row)
416                 {
417                     item->setColor(Qt::black);
418                     item->setBrush(Qt::yellow);
419                 }
420                 else
421                 {
422                     item->setColor(Qt::red);
423                     item->setBrush(Qt::white);
424                 }
425             }
426         }
427     }
428     alignPlot->replot();
429 }
430 
handleHorizontalPlotSizeChange()431 void Align::handleHorizontalPlotSizeChange()
432 {
433     alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
434     alignPlot->replot();
435 }
436 
handleVerticalPlotSizeChange()437 void Align::handleVerticalPlotSizeChange()
438 {
439     alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
440     alignPlot->replot();
441 }
442 
resizeEvent(QResizeEvent * event)443 void Align::resizeEvent(QResizeEvent *event)
444 {
445     if (event->oldSize().width() != -1)
446     {
447         if (event->oldSize().width() != size().width())
448             handleHorizontalPlotSizeChange();
449         else if (event->oldSize().height() != size().height())
450             handleVerticalPlotSizeChange();
451     }
452     else
453     {
454         QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange);
455     }
456 }
457 
handlePointTooltip(QMouseEvent * event)458 void Align::handlePointTooltip(QMouseEvent *event)
459 {
460     QCPAbstractItem *item = alignPlot->itemAt(event->localPos());
461     if (item)
462     {
463         QCPItemText *label = qobject_cast<QCPItemText *>(item);
464         if (label)
465         {
466             QString labelText = label->text();
467             int point         = labelText.toInt() - 1;
468 
469             if (point < 0)
470                 return;
471             QToolTip::showText(event->globalPos(),
472                                i18n("<table>"
473                                     "<tr>"
474                                     "<th colspan=\"2\">Object %1: %2</th>"
475                                     "</tr>"
476                                     "<tr>"
477                                     "<td>RA:</td><td>%3</td>"
478                                     "</tr>"
479                                     "<tr>"
480                                     "<td>DE:</td><td>%4</td>"
481                                     "</tr>"
482                                     "<tr>"
483                                     "<td>dRA:</td><td>%5</td>"
484                                     "</tr>"
485                                     "<tr>"
486                                     "<td>dDE:</td><td>%6</td>"
487                                     "</tr>"
488                                     "</table>",
489                                     point + 1,
490                                     solutionTable->item(point, 2)->text(),
491                                     solutionTable->item(point, 0)->text(),
492                                     solutionTable->item(point, 1)->text(),
493                                     solutionTable->item(point, 4)->text(),
494                                     solutionTable->item(point, 5)->text()),
495                                alignPlot, alignPlot->rect());
496         }
497     }
498 }
499 
buildTarget()500 void Align::buildTarget()
501 {
502     double accuracyRadius = accuracySpin->value();
503     if (centralTarget)
504     {
505         concentricRings->data()->clear();
506         redTarget->data()->clear();
507         yellowTarget->data()->clear();
508         centralTarget->data()->clear();
509     }
510     else
511     {
512         concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
513         redTarget       = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
514         yellowTarget    = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
515         centralTarget   = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
516     }
517     const int pointCount = 200;
518     QVector<QCPCurveData> circleRings(
519         pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175%
520     QVector<QCPCurveData> circleCentral(pointCount);
521     QVector<QCPCurveData> circleYellow(pointCount);
522     QVector<QCPCurveData> circleRed(pointCount);
523 
524     int circleRingPt = 0;
525     for (int i = 0; i < pointCount; i++)
526     {
527         double theta = i / static_cast<double>(pointCount) * 2 * M_PI;
528 
529         for (double ring = 1; ring < 8; ring++)
530         {
531             if (ring != 4 && ring != 6)
532             {
533                 if (i % (9 - static_cast<int>(ring)) == 0) //This causes fewer points to draw on the inner circles.
534                 {
535                     circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta),
536                                                 accuracyRadius * ring * 0.25 * qSin(theta));
537                     circleRingPt++;
538                 }
539             }
540         }
541 
542         circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta));
543         circleYellow[i]  = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta));
544         circleRed[i]     = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta));
545     }
546 
547     concentricRings->setLineStyle(QCPCurve::lsNone);
548     concentricRings->setScatterSkip(0);
549     concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1));
550 
551     concentricRings->data()->set(circleRings, true);
552     redTarget->data()->set(circleRed, true);
553     yellowTarget->data()->set(circleYellow, true);
554     centralTarget->data()->set(circleCentral, true);
555 
556     concentricRings->setPen(QPen(Qt::white));
557     redTarget->setPen(QPen(Qt::red));
558     yellowTarget->setPen(QPen(Qt::yellow));
559     centralTarget->setPen(QPen(Qt::green));
560 
561     concentricRings->setBrush(Qt::NoBrush);
562     redTarget->setBrush(QBrush(QColor(255, 0, 0, 50)));
563     yellowTarget->setBrush(
564         QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow.  It is green on top of red with equal opacity.
565     centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50)));
566 
567     if (alignPlot->size().width() > 0)
568         alignPlot->replot();
569 }
570 
slotAutoScaleGraph()571 void Align::slotAutoScaleGraph()
572 {
573     double accuracyRadius = accuracySpin->value();
574     alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
575     alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
576 
577     alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
578 
579     alignPlot->replot();
580 }
581 
582 
slotClearAllSolutionPoints()583 void Align::slotClearAllSolutionPoints()
584 {
585     if (solutionTable->rowCount() == 0)
586         return;
587 
588     connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
589     {
590         //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
591         KSMessageBox::Instance()->disconnect(this);
592 
593         solutionTable->setRowCount(0);
594         alignPlot->graph(0)->data()->clear();
595         alignPlot->clearItems();
596         buildTarget();
597 
598         slotAutoScaleGraph();
599 
600     });
601 
602     KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"),
603                                             i18n("Clear Solution Points"), 60);
604 }
605 
slotRemoveSolutionPoint()606 void Align::slotRemoveSolutionPoint()
607 {
608     QCPAbstractItem *abstractItem = alignPlot->item(solutionTable->currentRow());
609     if (abstractItem)
610     {
611         QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
612         if (item)
613         {
614             double point = item->position->key();
615             alignPlot->graph(0)->data()->remove(point);
616         }
617     }
618     alignPlot->removeItem(solutionTable->currentRow());
619     for (int i = 0; i < alignPlot->itemCount(); i++)
620     {
621         QCPAbstractItem *abstractItem = alignPlot->item(i);
622         if (abstractItem)
623         {
624             QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
625             if (item)
626                 item->setText(QString::number(i + 1));
627         }
628     }
629     solutionTable->removeRow(solutionTable->currentRow());
630     alignPlot->replot();
631 }
632 
slotMountModel()633 void Align::slotMountModel()
634 {
635     if (!m_MountModel)
636     {
637         m_MountModel = new MountModel(this);
638         connect(m_MountModel, &Ekos::MountModel::newLog, this, &Ekos::Align::appendLogText, Qt::UniqueConnection);
639         connect(this, &Ekos::Align::newStatus, m_MountModel, &Ekos::MountModel::setAlignStatus, Qt::UniqueConnection);
640     }
641 
642     m_MountModel->show();
643 
644     //    SkyPoint spWest;
645     //    spWest.setAlt(30);
646     //    spWest.setAz(270);
647     //    spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat());
648     //    mountModel.alignDec->setValue(static_cast<int>(spWest.dec().Degrees()));
649 
650     //    mountModelDialog.show();
651 }
652 
653 
isParserOK()654 bool Align::isParserOK()
655 {
656     return true; //For now
657     Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid.");
658 
659     bool rc = parser->init();
660 
661     if (rc)
662     {
663         connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
664         connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
665     }
666 
667     return rc;
668 }
669 
checkAlignmentTimeout()670 void Align::checkAlignmentTimeout()
671 {
672     if (m_SolveFromFile || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
673         abort();
674     else if (!m_SolveFromFile)
675     {
676         appendLogText(i18n("Solver timed out."));
677         parser->stopSolver();
678 
679         setAlignTableResult(ALIGN_RESULT_FAILED);
680         captureAndSolve();
681     }
682     // TODO must also account for loadAndSlew. Retain file name
683 }
684 
setSolverMode(int mode)685 void Align::setSolverMode(int mode)
686 {
687     if (sender() == nullptr && mode >= 0 && mode <= 1)
688     {
689         solverModeButtonGroup->button(mode)->setChecked(true);
690     }
691 
692     Options::setSolverMode(mode);
693 
694     if (mode == SOLVER_REMOTE)
695     {
696         if (remoteParser.get() != nullptr && remoteParserDevice != nullptr)
697         {
698             parser = remoteParser.get();
699             (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(remoteParserDevice);
700             return;
701         }
702 
703         remoteParser.reset(new Ekos::RemoteAstrometryParser());
704         parser = remoteParser.get();
705         (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(remoteParserDevice);
706         if (currentCCD)
707             (dynamic_cast<RemoteAstrometryParser *>(parser))->setCCD(currentCCD->getDeviceName());
708 
709         parser->setAlign(this);
710         if (parser->init())
711         {
712             connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
713             connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
714         }
715         else
716             parser->disconnect();
717     }
718 }
719 
720 
setCamera(const QString & device)721 bool Align::setCamera(const QString &device)
722 {
723     for (int i = 0; i < CCDCaptureCombo->count(); i++)
724         if (device == CCDCaptureCombo->itemText(i))
725         {
726             CCDCaptureCombo->setCurrentIndex(i);
727             checkCCD(i);
728             return true;
729         }
730 
731     return false;
732 }
733 
camera()734 QString Align::camera()
735 {
736     if (currentCCD)
737         return currentCCD->getDeviceName();
738 
739     return QString();
740 }
741 
setDefaultCCD(QString ccd)742 void Align::setDefaultCCD(QString ccd)
743 {
744     syncSettings();
745     Options::setDefaultAlignCCD(ccd);
746 }
747 
checkCCD(int ccdNum)748 void Align::checkCCD(int ccdNum)
749 {
750     // Do NOT perform checks if align is in progress as this may result
751     // in signals/slots getting disconnected.
752     switch (state)
753     {
754         // Idle, camera change is OK.
755         case ALIGN_IDLE:
756         case ALIGN_COMPLETE:
757         case ALIGN_FAILED:
758         case ALIGN_ABORTED:
759             break;
760 
761         // Busy, camera change is not OK.
762         case ALIGN_PROGRESS:
763         case ALIGN_SYNCING:
764         case ALIGN_SLEWING:
765         case ALIGN_SUSPENDED:
766             return;
767     }
768 
769 
770     if (ccdNum == -1 || ccdNum >= CCDs.count())
771     {
772         ccdNum = CCDCaptureCombo->currentIndex();
773 
774         if (ccdNum == -1)
775             return;
776     }
777 
778     currentCCD = CCDs.at(ccdNum);
779 
780     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
781     if (targetChip && targetChip->isCapturing())
782         return;
783 
784     if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
785         (dynamic_cast<RemoteAstrometryParser *>(remoteParser.get()))->setCCD(currentCCD->getDeviceName());
786 
787     syncCCDInfo();
788 
789     QStringList isoList = targetChip->getISOList();
790     ISOCombo->clear();
791 
792     if (isoList.isEmpty())
793     {
794         ISOCombo->setEnabled(false);
795     }
796     else
797     {
798         ISOCombo->setEnabled(true);
799         ISOCombo->addItems(isoList);
800         ISOCombo->setCurrentIndex(targetChip->getISOIndex());
801     }
802 
803     // Gain Check
804     if (currentCCD->hasGain())
805     {
806         double min, max, step, value;
807         currentCCD->getGainMinMaxStep(&min, &max, &step);
808 
809         // Allow the possibility of no gain value at all.
810         GainSpinSpecialValue = min - step;
811         GainSpin->setRange(GainSpinSpecialValue, max);
812         GainSpin->setSpecialValueText(i18n("--"));
813         GainSpin->setEnabled(true);
814         GainSpin->setSingleStep(step);
815         currentCCD->getGain(&value);
816 
817         // Set the custom gain if we have one
818         // otherwise it will not have an effect.
819         TargetCustomGainValue = Options::solverCameraGain();
820         if (TargetCustomGainValue > 0)
821             GainSpin->setValue(TargetCustomGainValue);
822         else
823             GainSpin->setValue(GainSpinSpecialValue);
824 
825         GainSpin->setReadOnly(currentCCD->getGainPermission() == IP_RO);
826 
827         connect(GainSpin, &QDoubleSpinBox::editingFinished, [this]()
828         {
829             if (GainSpin->value() > GainSpinSpecialValue)
830             {
831                 TargetCustomGainValue = GainSpin->value();
832                 // Save custom gain
833                 Options::setSolverCameraGain(TargetCustomGainValue);
834             }
835         });
836     }
837     else
838         GainSpin->setEnabled(false);
839 
840     syncTelescopeInfo();
841 }
842 
addCCD(ISD::GDInterface * newCCD)843 void Align::addCCD(ISD::GDInterface *newCCD)
844 {
845     if (CCDs.contains(static_cast<ISD::CCD *>(newCCD)))
846     {
847         syncCCDInfo();
848         return;
849     }
850 
851     CCDs.append(static_cast<ISD::CCD *>(newCCD));
852 
853     CCDCaptureCombo->addItem(newCCD->getDeviceName());
854 
855     checkCCD();
856 
857     syncSettings();
858 }
859 
setTelescope(ISD::GDInterface * newTelescope)860 void Align::setTelescope(ISD::GDInterface *newTelescope)
861 {
862     currentTelescope = static_cast<ISD::Telescope *>(newTelescope);
863     currentTelescope->disconnect(this);
864 
865     RUN_PAH(setCurrentTelescope(currentTelescope));
866 
867     connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection);
868     connect(currentTelescope, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection);
869     connect(currentTelescope, &ISD::GDInterface::Disconnected, this, [this]()
870     {
871         m_isRateSynced = false;
872     });
873 
874 
875     if (m_isRateSynced == false)
876     {
877         RUN_PAH(syncMountSpeed());
878         m_isRateSynced = !currentTelescope->slewRates().empty();
879     }
880 
881     syncTelescopeInfo();
882 }
883 
setDome(ISD::GDInterface * newDome)884 void Align::setDome(ISD::GDInterface *newDome)
885 {
886     currentDome = static_cast<ISD::Dome *>(newDome);
887     connect(currentDome, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection);
888 }
889 
removeDevice(ISD::GDInterface * device)890 void Align::removeDevice(ISD::GDInterface *device)
891 {
892     device->disconnect(this);
893     if (currentTelescope && currentTelescope->getDeviceName() == device->getDeviceName())
894     {
895         currentTelescope = nullptr;
896         m_isRateSynced = false;
897     }
898     else if (currentDome && currentDome->getDeviceName() == device->getDeviceName())
899     {
900         currentDome = nullptr;
901     }
902     else if (currentRotator && currentRotator->getDeviceName() == device->getDeviceName())
903     {
904         currentRotator = nullptr;
905     }
906 
907     if (CCDs.contains(static_cast<ISD::CCD *>(device)))
908     {
909         CCDs.removeAll(static_cast<ISD::CCD *>(device));
910         CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName()));
911         CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName() + QString(" Guider")));
912         if (CCDs.empty())
913         {
914             currentCCD = nullptr;
915             CCDCaptureCombo->setCurrentIndex(-1);
916         }
917         else
918         {
919             currentCCD = CCDs[0];
920             CCDCaptureCombo->setCurrentIndex(0);
921         }
922 
923         QTimer::singleShot(1000, this, [this]()
924         {
925             checkCCD();
926         });
927         //checkCCD();
928     }
929 
930     if (Filters.contains(static_cast<ISD::Filter *>(device)))
931     {
932         Filters.removeAll(static_cast<ISD::Filter *>(device));
933         filterManager->removeDevice(device);
934         FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(device->getDeviceName()));
935         if (Filters.empty())
936         {
937             currentFilter = nullptr;
938             FilterDevicesCombo->setCurrentIndex(-1);
939         }
940         else
941             FilterDevicesCombo->setCurrentIndex(0);
942 
943         //checkFilter();
944         QTimer::singleShot(1000, this, [this]()
945         {
946             checkFilter();
947         });
948     }
949 
950 }
951 
syncTelescopeInfo()952 bool Align::syncTelescopeInfo()
953 {
954     if (currentTelescope == nullptr || currentTelescope->isConnected() == false)
955         return false;
956 
957     canSync = currentTelescope->canSync();
958 
959     if (canSync == false && syncR->isEnabled())
960     {
961         slewR->setChecked(true);
962         appendLogText(i18n("Mount does not support syncing."));
963     }
964 
965     syncR->setEnabled(canSync);
966 
967     auto nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO");
968 
969     if (nvp)
970     {
971         auto np = nvp->findWidgetByName("TELESCOPE_APERTURE");
972 
973         if (np && np->getValue() > 0)
974             primaryAperture = np->getValue();
975 
976         np = nvp->findWidgetByName("GUIDER_APERTURE");
977         if (np && np->getValue() > 0)
978             guideAperture = np->getValue();
979 
980         aperture = primaryAperture;
981 
982         //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE)
983         if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE)
984             aperture = guideAperture;
985 
986         np = nvp->findWidgetByName("TELESCOPE_FOCAL_LENGTH");
987         if (np && np->getValue() > 0)
988             primaryFL = np->getValue();
989 
990         np = nvp->findWidgetByName("GUIDER_FOCAL_LENGTH");
991         if (np && np->getValue() > 0)
992             guideFL = np->getValue();
993 
994         focal_length = primaryFL;
995 
996         //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE)
997         if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE)
998             focal_length = guideFL;
999     }
1000 
1001     if (focal_length == -1 || aperture == -1)
1002         return false;
1003 
1004     if (ccd_hor_pixel != -1 && ccd_ver_pixel != -1 && focal_length != -1 && aperture != -1)
1005     {
1006         FOVScopeCombo->setItemData(
1007             ISD::CCD::TELESCOPE_PRIMARY,
1008             i18nc("F-Number, Focal length, Aperture",
1009                   "<nobr>F<b>%1</b> Focal length: <b>%2</b> mm Aperture: <b>%3</b> mm<sup>2</sup></nobr>",
1010                   QString::number(primaryFL / primaryAperture, 'f', 1), QString::number(primaryFL, 'f', 2),
1011                   QString::number(primaryAperture, 'f', 2)),
1012             Qt::ToolTipRole);
1013         FOVScopeCombo->setItemData(
1014             ISD::CCD::TELESCOPE_GUIDE,
1015             i18nc("F-Number, Focal length, Aperture",
1016                   "<nobr>F<b>%1</b> Focal length: <b>%2</b> mm Aperture: <b>%3</b> mm<sup>2</sup></nobr>",
1017                   QString::number(guideFL / guideAperture, 'f', 1), QString::number(guideFL, 'f', 2),
1018                   QString::number(guideAperture, 'f', 2)),
1019             Qt::ToolTipRole);
1020 
1021         calculateFOV();
1022 
1023         //generateArgs();
1024 
1025         return true;
1026     }
1027 
1028     return false;
1029 }
1030 
setTelescopeInfo(double primaryFocalLength,double primaryAperture,double guideFocalLength,double guideAperture)1031 void Align::setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength,
1032                              double guideAperture)
1033 {
1034     if (primaryFocalLength > 0)
1035         primaryFL = primaryFocalLength;
1036     if (guideFocalLength > 0)
1037         guideFL = guideFocalLength;
1038 
1039     if (primaryAperture > 0)
1040         this->primaryAperture = primaryAperture;
1041     if (guideAperture > 0)
1042         this->guideAperture = guideAperture;
1043 
1044     focal_length = primaryFL;
1045 
1046     if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE)
1047         focal_length = guideFL;
1048 
1049     aperture = primaryAperture;
1050 
1051     if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE)
1052         aperture = guideAperture;
1053 
1054     syncTelescopeInfo();
1055 }
1056 
syncCCDInfo()1057 void Align::syncCCDInfo()
1058 {
1059     if (!currentCCD)
1060         return;
1061 
1062     auto nvp = currentCCD->getBaseDevice()->getNumber(useGuideHead ? "GUIDER_INFO" : "CCD_INFO");
1063 
1064     if (nvp)
1065     {
1066         auto np = nvp->findWidgetByName("CCD_PIXEL_SIZE_X");
1067         if (np && np->getValue() > 0)
1068             ccd_hor_pixel = ccd_ver_pixel = np->getValue();
1069 
1070         np = nvp->findWidgetByName("CCD_PIXEL_SIZE_Y");
1071         if (np && np->getValue() > 0)
1072             ccd_ver_pixel = np->getValue();
1073 
1074         np = nvp->findWidgetByName("CCD_PIXEL_SIZE_Y");
1075         if (np && np->getValue() > 0)
1076             ccd_ver_pixel = np->getValue();
1077     }
1078 
1079     ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
1080 
1081     auto svp = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL");
1082     if (svp)
1083         setWCSEnabled(Options::astrometrySolverWCS());
1084 
1085     targetChip->setImageView(alignView, FITS_ALIGN);
1086 
1087     targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &ccd_width, nullptr, &ccd_height);
1088     binningCombo->setEnabled(targetChip->canBin());
1089     if (targetChip->canBin())
1090     {
1091         binningCombo->blockSignals(true);
1092 
1093         int binx = 1, biny = 1;
1094         targetChip->getMaxBin(&binx, &biny);
1095         binningCombo->clear();
1096 
1097         for (int i = 0; i < binx; i++)
1098             binningCombo->addItem(QString("%1x%2").arg(i + 1).arg(i + 1));
1099 
1100         binningCombo->setCurrentIndex(Options::solverBinningIndex());
1101         binningCombo->blockSignals(false);
1102     }
1103 
1104     if (ccd_hor_pixel <= 0 || ccd_ver_pixel <= 0)
1105         return;
1106 
1107     if (ccd_hor_pixel > 0 && ccd_ver_pixel > 0 && focal_length > 0 && aperture > 0)
1108     {
1109         calculateFOV();
1110     }
1111 }
1112 
getFOVScale(double & fov_w,double & fov_h,double & fov_scale)1113 void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale)
1114 {
1115     fov_w     = fov_x;
1116     fov_h     = fov_y;
1117     fov_scale = fov_pixscale;
1118 }
1119 
fov()1120 QList<double> Align::fov()
1121 {
1122     QList<double> result;
1123 
1124     result << fov_x << fov_y << fov_pixscale;
1125 
1126     return result;
1127 }
1128 
cameraInfo()1129 QList<double> Align::cameraInfo()
1130 {
1131     QList<double> result;
1132 
1133     result << ccd_width << ccd_height << ccd_hor_pixel << ccd_ver_pixel;
1134 
1135     return result;
1136 }
1137 
telescopeInfo()1138 QList<double> Align::telescopeInfo()
1139 {
1140     QList<double> result;
1141 
1142     result << focal_length << aperture;
1143 
1144     return result;
1145 }
1146 
getCalculatedFOVScale(double & fov_w,double & fov_h,double & fov_scale)1147 void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale)
1148 {
1149     // FOV in arcsecs
1150     fov_w = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length;
1151     fov_h = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length;
1152 
1153     // Pix Scale
1154     fov_scale = (fov_w * (Options::solverBinningIndex() + 1)) / ccd_width;
1155 
1156     // FOV in arcmins
1157     fov_w /= 60.0;
1158     fov_h /= 60.0;
1159 }
1160 
calculateEffectiveFocalLength(double newFOVW)1161 void Align::calculateEffectiveFocalLength(double newFOVW)
1162 {
1163     if (newFOVW < 0 || newFOVW == fov_x)
1164         return;
1165 
1166     double new_focal_length = ((ccd_width * ccd_hor_pixel / 1000.0) * 206264.8062470963552) / (newFOVW * 60.0);
1167     double focal_diff = std::fabs(new_focal_length - focal_length);
1168 
1169     if (focal_diff > 1)
1170     {
1171         if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_PRIMARY)
1172             primaryEffectiveFL = new_focal_length;
1173         else
1174             guideEffectiveFL = new_focal_length;
1175         appendLogText(i18n("Effective telescope focal length is updated to %1 mm.", new_focal_length));
1176     }
1177 }
1178 
calculateFOV()1179 void Align::calculateFOV()
1180 {
1181     // Calculate FOV
1182 
1183     // FOV in arcsecs
1184     fov_x = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length;
1185     fov_y = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length;
1186 
1187     // Pix Scale
1188     fov_pixscale = (fov_x * (Options::solverBinningIndex() + 1)) / ccd_width;
1189 
1190     // FOV in arcmins
1191     fov_x /= 60.0;
1192     fov_y /= 60.0;
1193 
1194     // Put FOV upper limit as 180 degrees
1195     if (fov_x < 1 || fov_x > 60 * 180 || fov_y < 1 || fov_y > 60 * 180)
1196     {
1197         appendLogText(
1198             i18n("Warning! The calculated field of view is out of bounds. Ensure the telescope focal length and camera pixel size are correct."));
1199         return;
1200     }
1201 
1202     double calculated_fov_x = fov_x;
1203     double calculated_fov_y = fov_y;
1204 
1205     QString calculatedFOV = (QString("%1' x %2'").arg(fov_x, 0, 'f', 1).arg(fov_y, 0, 'f', 1));
1206 
1207     double effectiveFocalLength = FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_PRIMARY ? primaryEffectiveFL :
1208                                   guideEffectiveFL;
1209 
1210     FocalLengthOut->setText(QString("%1 (%2)").arg(focal_length, 0, 'f', 1).
1211                             arg(effectiveFocalLength > 0 ? effectiveFocalLength : focal_length, 0, 'f', 1));
1212     FocalRatioOut->setText(QString("%1 (%2)").arg(focal_length / aperture, 0, 'f', 1).
1213                            arg(effectiveFocalLength > 0 ? effectiveFocalLength / aperture : focal_length / aperture, 0, 'f', 1));
1214 
1215     if (effectiveFocalLength > 0)
1216     {
1217         double focal_diff = std::fabs(effectiveFocalLength  - focal_length);
1218         if (focal_diff < 5)
1219             FocalLengthOut->setStyleSheet("color:green");
1220         else if (focal_diff < 15)
1221             FocalLengthOut->setStyleSheet("color:yellow");
1222         else
1223             FocalLengthOut->setStyleSheet("color:red");
1224     }
1225 
1226     // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV
1227     // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique
1228     // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete.
1229     getEffectiveFOV();
1230 
1231     if (fov_x == 0)
1232     {
1233         //FOVOut->setReadOnly(false);
1234         FOVOut->setToolTip(
1235             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>",
1236                  calculatedFOV));
1237         fov_x = calculated_fov_x;
1238         fov_y = calculated_fov_y;
1239         m_EffectiveFOVPending = true;
1240     }
1241     else
1242     {
1243         m_EffectiveFOVPending = false;
1244         FOVOut->setToolTip(i18n("<p>Effective field of view size in arcminutes.</p>"));
1245     }
1246 
1247     solverFOV->setSize(fov_x, fov_y);
1248     sensorFOV->setSize(fov_x, fov_y);
1249     if (currentCCD)
1250         sensorFOV->setName(currentCCD->getDeviceName());
1251 
1252     FOVOut->setText(QString("%1' x %2'").arg(fov_x, 0, 'f', 1).arg(fov_y, 0, 'f', 1));
1253 
1254     // Enable or Disable PAA depending on current FOV
1255     const bool fovOK = ((fov_x + fov_y) / 2.0) > PAH_CUTOFF_FOV;
1256     if (m_PolarAlignmentAssistant != nullptr)
1257         m_PolarAlignmentAssistant->setEnabled(fovOK);
1258 
1259     if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked())
1260     {
1261         int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex();
1262 
1263         // Degrees
1264         if (unitType == 0)
1265         {
1266             double fov_low  = qMin(fov_x / 60, fov_y / 60);
1267             double fov_high = qMax(fov_x / 60, fov_y / 60);
1268             opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1269             opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1270 
1271             Options::setAstrometryImageScaleLow(fov_low);
1272             Options::setAstrometryImageScaleHigh(fov_high);
1273         }
1274         // Arcmins
1275         else if (unitType == 1)
1276         {
1277             double fov_low  = qMin(fov_x, fov_y);
1278             double fov_high = qMax(fov_x, fov_y);
1279             opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1280             opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1281 
1282             Options::setAstrometryImageScaleLow(fov_low);
1283             Options::setAstrometryImageScaleHigh(fov_high);
1284         }
1285         // Arcsec per pixel
1286         else
1287         {
1288             opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_pixscale * 0.9);
1289             opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_pixscale * 1.1);
1290 
1291             // 10% boundary
1292             Options::setAstrometryImageScaleLow(fov_pixscale * 0.9);
1293             Options::setAstrometryImageScaleHigh(fov_pixscale * 1.1);
1294         }
1295     }
1296 }
1297 
generateRemoteOptions(const QVariantMap & optionsMap)1298 QStringList Align::generateRemoteOptions(const QVariantMap &optionsMap)
1299 {
1300     QStringList solver_args;
1301 
1302     // -O overwrite
1303     // -3 Expected RA
1304     // -4 Expected DEC
1305     // -5 Radius (deg)
1306     // -L lower scale of image in arcminutes
1307     // -H upper scale of image in arcminutes
1308     // -u aw set scale to be in arcminutes
1309     // -W solution.wcs name of solution file
1310     // apog1.jpg name of target file to analyze
1311     //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1312 
1313     // Start with always-used arguments
1314     solver_args << "-O"
1315                 << "--no-plots";
1316 
1317     // Now go over boolean options
1318 
1319     // noverify
1320     if (optionsMap.contains("noverify"))
1321         solver_args << "--no-verify";
1322 
1323     // noresort
1324     if (optionsMap.contains("resort"))
1325         solver_args << "--resort";
1326 
1327     // fits2fits
1328     if (optionsMap.contains("nofits2fits"))
1329         solver_args << "--no-fits2fits";
1330 
1331     // downsample
1332     if (optionsMap.contains("downsample"))
1333         solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt());
1334 
1335     // image scale low
1336     if (optionsMap.contains("scaleL"))
1337         solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble());
1338 
1339     // image scale high
1340     if (optionsMap.contains("scaleH"))
1341         solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble());
1342 
1343     // image scale units
1344     if (optionsMap.contains("scaleUnits"))
1345         solver_args << "-u" << optionsMap.value("scaleUnits").toString();
1346 
1347     // RA
1348     if (optionsMap.contains("ra"))
1349         solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble());
1350 
1351     // DE
1352     if (optionsMap.contains("de"))
1353         solver_args << "-4" << QString::number(optionsMap.value("de").toDouble());
1354 
1355     // Radius
1356     if (optionsMap.contains("radius"))
1357         solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble());
1358 
1359     // Custom
1360     if (optionsMap.contains("custom"))
1361         solver_args << optionsMap.value("custom").toString();
1362 
1363     return solver_args;
1364 }
1365 
1366 //This will generate the high and low scale of the imager field size based on the stated units.
generateFOVBounds(double fov_w,QString & fov_low,QString & fov_high,double tolerance)1367 void Align::generateFOVBounds(double fov_w, QString &fov_low, QString &fov_high, double tolerance)
1368 {
1369     // This sets the percentage we search outside the lower and upper boundary limits
1370     // by default, we stretch the limits by 5% (tolerance = 0.05)
1371     double lower_boundary = 1.0 - tolerance;
1372     double upper_boundary = 1.0 + tolerance;
1373 
1374     // let's stretch the boundaries by 5%
1375     //    fov_lower = ((fov_h < fov_v) ? (fov_h * lower_boundary) : (fov_v * lower_boundary));
1376     //    fov_upper = ((fov_h > fov_v) ? (fov_h * upper_boundary) : (fov_v * upper_boundary));
1377 
1378     // JM 2019-10-20: The bounds consider image width only, not height.
1379     double fov_lower = fov_w * lower_boundary;
1380     double fov_upper = fov_w * upper_boundary;
1381 
1382     //No need to do anything if they are aw, since that is the default
1383     fov_low  = QString::number(fov_lower);
1384     fov_high = QString::number(fov_upper);
1385 }
1386 
1387 
generateRemoteArgs(const QSharedPointer<FITSData> & data)1388 QStringList Align::generateRemoteArgs(const QSharedPointer<FITSData> &data)
1389 {
1390     QVariantMap optionsMap;
1391 
1392     // -O overwrite
1393     // -3 Expected RA
1394     // -4 Expected DEC
1395     // -5 Radius (deg)
1396     // -L lower scale of image in arcminutes
1397     // -H upper scale of image in arcminutes
1398     // -u aw set scale to be in arcminutes
1399     // -W solution.wcs name of solution file
1400     // apog1.jpg name of target file to analyze
1401     //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1402 
1403     if (Options::astrometryUseNoVerify())
1404         optionsMap["noverify"] = true;
1405 
1406     if (Options::astrometryUseResort())
1407         optionsMap["resort"] = true;
1408 
1409     if (Options::astrometryUseNoFITS2FITS())
1410         optionsMap["nofits2fits"] = true;
1411 
1412     if (data == nullptr)
1413     {
1414         if (Options::astrometryUseDownsample())
1415         {
1416             if (Options::astrometryAutoDownsample() && ccd_width && ccd_height)
1417             {
1418                 uint8_t bin = qMax(Options::solverBinningIndex() + 1, 1u);
1419                 uint16_t w = ccd_width / bin;
1420                 optionsMap["downsample"] = getSolverDownsample(w);
1421             }
1422             else
1423                 optionsMap["downsample"] = Options::astrometryDownsample();
1424         }
1425 
1426         //Options needed for Sextractor
1427         int bin = Options::solverBinningIndex() + 1;
1428         optionsMap["image_width"] = ccd_width / bin;
1429         optionsMap["image_height"] = ccd_height / bin;
1430 
1431         if (Options::astrometryUseImageScale() && fov_x > 0 && fov_y > 0)
1432         {
1433             QString units = "dw";
1434             if (Options::astrometryImageScaleUnits() == 1)
1435                 units = "aw";
1436             else if (Options::astrometryImageScaleUnits() == 2)
1437                 units = "app";
1438             if (Options::astrometryAutoUpdateImageScale())
1439             {
1440                 QString fov_low, fov_high;
1441                 double fov_w = fov_x;
1442                 double fov_h = fov_y;
1443 
1444                 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH)
1445                 {
1446                     fov_w /= 60;
1447                     fov_h /= 60;
1448                 }
1449                 else if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
1450                 {
1451                     fov_w = fov_pixscale;
1452                     fov_h = fov_pixscale;
1453                 }
1454 
1455                 // If effective FOV is pending, let's set a wider tolerance range
1456                 generateFOVBounds(fov_w, fov_low, fov_high, m_EffectiveFOVPending ? 0.3 : 0.05);
1457 
1458                 optionsMap["scaleL"]     = fov_low;
1459                 optionsMap["scaleH"]     = fov_high;
1460                 optionsMap["scaleUnits"] = units;
1461             }
1462             else
1463             {
1464                 optionsMap["scaleL"]     = Options::astrometryImageScaleLow();
1465                 optionsMap["scaleH"]     = Options::astrometryImageScaleHigh();
1466                 optionsMap["scaleUnits"] = units;
1467             }
1468         }
1469 
1470         if (Options::astrometryUsePosition() && currentTelescope != nullptr)
1471         {
1472             double ra = 0, dec = 0;
1473             currentTelescope->getEqCoords(&ra, &dec);
1474 
1475             optionsMap["ra"]     = ra * 15.0;
1476             optionsMap["de"]     = dec;
1477             optionsMap["radius"] = Options::astrometryRadius();
1478         }
1479     }
1480     else
1481     {
1482         // Downsample
1483         QVariant width;
1484         data->getRecordValue("NAXIS1", width);
1485         if (width.isValid())
1486         {
1487             optionsMap["downsample"] = getSolverDownsample(width.toInt());
1488         }
1489         else
1490             optionsMap["downsample"] = Options::astrometryDownsample();
1491 
1492         // Pixel Scale
1493         QVariant pixscale;
1494         data->getRecordValue("SCALE", pixscale);
1495         if (pixscale.isValid())
1496         {
1497             optionsMap["scaleL"]     = 0.8 * pixscale.toDouble();
1498             optionsMap["scaleH"]     = 1.2 * pixscale.toDouble();
1499             optionsMap["scaleUnits"] = "app";
1500         }
1501 
1502         // Position
1503         QVariant ra, de;
1504         data->getRecordValue("RA", ra);
1505         data->getRecordValue("DEC", de);
1506         if (ra.isValid() && de.isValid())
1507         {
1508             optionsMap["ra"]     = ra.toDouble();
1509             optionsMap["de"]     = de.toDouble();
1510             optionsMap["radius"] = Options::astrometryRadius();
1511         }
1512     }
1513 
1514     if (Options::astrometryCustomOptions().isEmpty() == false)
1515         optionsMap["custom"] = Options::astrometryCustomOptions();
1516 
1517     return generateRemoteOptions(optionsMap);
1518 }
1519 
captureAndSolve()1520 bool Align::captureAndSolve()
1521 {
1522     m_AlignTimer.stop();
1523     m_CaptureTimer.stop();
1524 
1525     if (currentCCD == nullptr)
1526     {
1527         appendLogText(i18n("Error: No camera detected."));
1528         return false;
1529     }
1530 
1531     if (currentCCD->isConnected() == false)
1532     {
1533         appendLogText(i18n("Error: lost connection to camera."));
1534         KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::EVENT_ALERT);
1535         return false;
1536     }
1537 
1538     if (currentCCD->isBLOBEnabled() == false)
1539     {
1540         currentCCD->setBLOBEnabled(true);
1541     }
1542 
1543     // If CCD Telescope Type does not match desired scope type, change it
1544     // but remember current value so that it can be reset once capture is complete or is aborted.
1545     if (currentCCD->getTelescopeType() != FOVScopeCombo->currentIndex())
1546     {
1547         rememberTelescopeType = currentCCD->getTelescopeType();
1548         currentCCD->setTelescopeType(static_cast<ISD::CCD::TelescopeType>(FOVScopeCombo->currentIndex()));
1549     }
1550 
1551     //if (parser->init() == false)
1552     //    return false;
1553 
1554     if (focal_length == -1 || aperture == -1)
1555     {
1556         KSNotification::error(
1557             i18n("Telescope aperture and focal length are missing. Please check your driver settings and try again."));
1558         return false;
1559     }
1560 
1561     if (ccd_hor_pixel == -1 || ccd_ver_pixel == -1)
1562     {
1563         KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again."));
1564         return false;
1565     }
1566 
1567     if (currentFilter != nullptr)
1568     {
1569         if (currentFilter->isConnected() == false)
1570         {
1571             appendLogText(i18n("Error: lost connection to filter wheel."));
1572             return false;
1573         }
1574 
1575         int targetPosition = FilterPosCombo->currentIndex() + 1;
1576 
1577         if (targetPosition > 0 && targetPosition != currentFilterPosition)
1578         {
1579             filterPositionPending    = true;
1580             // Disabling the autofocus policy for align.
1581             filterManager->setFilterPosition(
1582                 targetPosition, FilterManager::NO_AUTOFOCUS_POLICY);
1583             state = ALIGN_PROGRESS;
1584             return true;
1585         }
1586     }
1587 
1588     if (currentCCD->getDriverInfo()->getClientManager()->getBLOBMode(currentCCD->getDeviceName().toLatin1().constData(),
1589             "CCD1") == B_NEVER)
1590     {
1591         if (KMessageBox::questionYesNo(
1592                     nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
1593                 KMessageBox::Yes)
1594         {
1595             currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName().toLatin1().constData(),
1596                     "CCD1");
1597             currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName().toLatin1().constData(),
1598                     "CCD2");
1599         }
1600         else
1601         {
1602             return false;
1603         }
1604     }
1605 
1606     double seqExpose = exposureIN->value();
1607 
1608     ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
1609 
1610     if (m_FocusState >= FOCUS_PROGRESS)
1611     {
1612         appendLogText(i18n("Cannot capture while focus module is busy. Retrying in %1 seconds...", CAPTURE_RETRY_DELAY / 1000));
1613         m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1614         return false;
1615     }
1616 
1617     if (targetChip->isCapturing())
1618     {
1619         appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in %1 seconds...",
1620                            CAPTURE_RETRY_DELAY / 1000));
1621         m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1622         return false;
1623     }
1624 
1625     alignView->setBaseSize(alignWidget->size());
1626 
1627     connect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Align::processData);
1628     connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress);
1629 
1630     // In case of remote solver, check if we need to update active CCD
1631     if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
1632     {
1633         if (remoteParserDevice == nullptr)
1634         {
1635             appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
1636             setSolverMode(SOLVER_LOCAL);
1637         }
1638         else
1639         {
1640             // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD
1641             auto activeDevices = remoteParserDevice->getBaseDevice()->getText("ACTIVE_DEVICES");
1642             if (activeDevices)
1643             {
1644                 auto activeCCD = activeDevices->findWidgetByName("ACTIVE_CCD");
1645                 if (QString(activeCCD->text) != CCDCaptureCombo->currentText())
1646                 {
1647                     activeCCD->setText(CCDCaptureCombo->currentText().toLatin1().data());
1648 
1649                     remoteParserDevice->getDriverInfo()->getClientManager()->sendNewText(activeDevices);
1650                 }
1651             }
1652 
1653             // Enable remote parse
1654             dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(true);
1655             dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->sendArgs(generateRemoteArgs(QSharedPointer<FITSData>()));
1656             solverTimer.start();
1657         }
1658     }
1659 
1660     // Remove temporary FITS files left before by the solver
1661     QDir dir(QDir::tempPath());
1662     dir.setNameFilters(QStringList() << "fits*"  << "tmp.*");
1663     dir.setFilter(QDir::Files);
1664     for (auto &dirFile : dir.entryList())
1665         dir.remove(dirFile);
1666 
1667     prepareCapture(targetChip);
1668 
1669     // In case we're in refresh phase of the polar alignment helper then we use capture value from there
1670     if (matchPAHStage(PAA::PAH_REFRESH))
1671         targetChip->capture(m_PolarAlignmentAssistant->getPAHExposureDuration());
1672     else
1673         targetChip->capture(seqExpose);
1674 
1675     Options::setAlignExposure(seqExpose);
1676 
1677     solveB->setEnabled(false);
1678     stopB->setEnabled(true);
1679     pi->startAnimation();
1680 
1681     differentialSlewingActivated = false;
1682 
1683     state = ALIGN_PROGRESS;
1684     emit newStatus(state);
1685     solverFOV->setProperty("visible", true);
1686 
1687     // If we're just refreshing, then we're done
1688     if (matchPAHStage(PAA::PAH_REFRESH))
1689         return true;
1690 
1691     appendLogText(i18n("Capturing image..."));
1692 
1693     //This block of code will create the row in the solution table and populate RA, DE, and object name.
1694     //It also starts the progress indicator.
1695     double ra, dec;
1696     currentTelescope->getEqCoords(&ra, &dec);
1697     if (!m_SolveFromFile)
1698     {
1699         int currentRow = solutionTable->rowCount();
1700         solutionTable->insertRow(currentRow);
1701         for (int i = 4; i < 6; i++)
1702         {
1703             QTableWidgetItem *disabledBox = new QTableWidgetItem();
1704             disabledBox->setFlags(Qt::ItemIsSelectable);
1705             solutionTable->setItem(currentRow, i, disabledBox);
1706         }
1707 
1708         QTableWidgetItem *RAReport = new QTableWidgetItem();
1709         RAReport->setText(ScopeRAOut->text());
1710         RAReport->setTextAlignment(Qt::AlignHCenter);
1711         RAReport->setFlags(Qt::ItemIsSelectable);
1712         solutionTable->setItem(currentRow, 0, RAReport);
1713 
1714         QTableWidgetItem *DECReport = new QTableWidgetItem();
1715         DECReport->setText(ScopeDecOut->text());
1716         DECReport->setTextAlignment(Qt::AlignHCenter);
1717         DECReport->setFlags(Qt::ItemIsSelectable);
1718         solutionTable->setItem(currentRow, 1, DECReport);
1719 
1720         double maxrad = 1.0;
1721         SkyObject *so =
1722             KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad);
1723         QString name;
1724         if (so)
1725         {
1726             name = so->longname();
1727         }
1728         else
1729         {
1730             name = "None";
1731         }
1732         QTableWidgetItem *ObjNameReport = new QTableWidgetItem();
1733         ObjNameReport->setText(name);
1734         ObjNameReport->setTextAlignment(Qt::AlignHCenter);
1735         ObjNameReport->setFlags(Qt::ItemIsSelectable);
1736         solutionTable->setItem(currentRow, 2, ObjNameReport);
1737 #ifdef Q_OS_OSX
1738         repaint(); //This is a band-aid for a bug in QT 5.10.0
1739 #endif
1740 
1741         QProgressIndicator *alignIndicator = new QProgressIndicator(this);
1742         solutionTable->setCellWidget(currentRow, 3, alignIndicator);
1743         alignIndicator->startAnimation();
1744 #ifdef Q_OS_OSX
1745         repaint(); //This is a band-aid for a bug in QT 5.10.0
1746 #endif
1747     }
1748 
1749     return true;
1750 }
1751 
processData(const QSharedPointer<FITSData> & data)1752 void Align::processData(const QSharedPointer<FITSData> &data)
1753 {
1754     if (data->property("chip").toInt() == ISD::CCDChip::GUIDE_CCD)
1755         return;
1756 
1757     disconnect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Align::processData);
1758     disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress);
1759 
1760     if (data)
1761         m_ImageData = data;
1762     else
1763         m_ImageData.reset();
1764 
1765     RUN_PAH(setImageData(m_ImageData));
1766 
1767     // If it's Refresh, we're done
1768     if (matchPAHStage(PAA::PAH_REFRESH))
1769     {
1770         setCaptureComplete();
1771         return;
1772     }
1773     else
1774         appendLogText(i18n("Image received."));
1775 
1776     // If Local solver, then set capture complete or perform calibration first.
1777     if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1778     {
1779         // Only perform dark image subtraction on local images.
1780         if (alignDarkFrameCheck->isChecked())
1781         {
1782             int x, y, w, h, binx = 1, biny = 1;
1783             ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
1784             targetChip->getFrame(&x, &y, &w, &h);
1785             targetChip->getBinning(&binx, &biny);
1786 
1787             uint16_t offsetX = x / binx;
1788             uint16_t offsetY = y / biny;
1789 
1790             m_DarkProcessor->denoise(targetChip, m_ImageData, exposureIN->value(), offsetX, offsetY);
1791             return;
1792         }
1793 
1794         setCaptureComplete();
1795     }
1796 }
1797 
prepareCapture(ISD::CCDChip * targetChip)1798 void Align::prepareCapture(ISD::CCDChip *targetChip)
1799 {
1800     if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL)
1801     {
1802         rememberUploadMode = ISD::CCD::UPLOAD_LOCAL;
1803         currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT);
1804     }
1805 
1806     if (currentCCD->isFastExposureEnabled())
1807     {
1808         m_RememberCameraFastExposure = true;
1809         currentCCD->setFastExposureEnabled(false);
1810     }
1811 
1812     currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS);
1813     targetChip->resetFrame();
1814     targetChip->setBatchMode(false);
1815     targetChip->setCaptureMode(FITS_ALIGN);
1816     targetChip->setFrameType(FRAME_LIGHT);
1817 
1818     int bin = Options::solverBinningIndex() + 1;
1819     targetChip->setBinning(bin, bin);
1820 
1821     // Set gain if applicable
1822     if (currentCCD->hasGain() && GainSpin->value() > GainSpinSpecialValue)
1823         currentCCD->setGain(GainSpin->value());
1824     // Set ISO if applicable
1825     if (ISOCombo->currentIndex() >= 0)
1826         targetChip->setISOIndex(ISOCombo->currentIndex());
1827 }
1828 
1829 
setCaptureComplete()1830 void Align::setCaptureComplete()
1831 {
1832     if (matchPAHStage(PAA::PAH_REFRESH))
1833     {
1834         emit newFrame(alignView);
1835         m_PolarAlignmentAssistant->processPAHRefresh();
1836         return;
1837     }
1838 
1839     emit newImage(alignView);
1840 
1841     solverFOV->setImage(alignView->getDisplayImage());
1842 
1843     startSolving();
1844 }
1845 
setSolverAction(int mode)1846 void Align::setSolverAction(int mode)
1847 {
1848     gotoModeButtonGroup->button(mode)->setChecked(true);
1849     m_CurrentGotoMode = static_cast<GotoMode>(mode);
1850 }
1851 
startSolving()1852 void Align::startSolving()
1853 {
1854     //RUN_PAH(syncStage());
1855 
1856     // This is needed because they might have directories stored in the config file.
1857     // So we can't just use the options folder list.
1858     QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs();
1859     disconnect(alignView, &FITSView::loaded, this, &Align::startSolving);
1860 
1861     if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1862     {
1863         if(Options::solverType() != SSolver::SOLVER_ASTAP) //You don't need astrometry index files to use ASTAP
1864         {
1865             bool foundAnIndex = false;
1866             for(QString dataDir : astrometryDataDirs)
1867             {
1868                 QDir dir = QDir(dataDir);
1869                 if(dir.exists())
1870                 {
1871                     dir.setNameFilters(QStringList() << "*.fits");
1872                     QStringList indexList = dir.entryList();
1873                     if(indexList.count() > 0)
1874                         foundAnIndex = true;
1875                 }
1876             }
1877             if(!foundAnIndex)
1878             {
1879                 appendLogText(
1880                     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."));
1881                 KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings");
1882                 if(alignSettings && m_IndexFilesPage)
1883                 {
1884                     alignSettings->setCurrentPage(m_IndexFilesPage);
1885                     alignSettings->show();
1886                 }
1887             }
1888         }
1889         if (m_StellarSolver)
1890         {
1891             auto *solver = m_StellarSolver.release();
1892             solver->disconnect(this);
1893             if (solver->isRunning())
1894             {
1895                 connect(solver, &StellarSolver::finished, solver, &StellarSolver::deleteLater);
1896                 solver->abort();
1897             }
1898             else
1899                 solver->deleteLater();
1900         }
1901         if (!m_ImageData)
1902             m_ImageData = alignView->imageData();
1903         m_StellarSolver.reset(new StellarSolver(SSolver::SOLVE, m_ImageData->getStatistics(), m_ImageData->getImageBuffer()));
1904         m_StellarSolver->setProperty("ExtractorType", Options::solveSextractorType());
1905         m_StellarSolver->setProperty("SolverType", Options::solverType());
1906         connect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
1907         connect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText);
1908         m_StellarSolver->setIndexFolderPaths(Options::astrometryIndexFolderList());
1909         m_StellarSolver->setParameters(m_StellarSolverProfiles.at(Options::solveOptionsProfile()));
1910 
1911         const SSolver::SolverType type = static_cast<SSolver::SolverType>(m_StellarSolver->property("SolverType").toInt());
1912         if(type == SSolver::SOLVER_LOCALASTROMETRY || type == SSolver::SOLVER_ASTAP)
1913         {
1914             QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1915                                    QRegularExpression("[-{}]")));
1916             alignView->saveImage(filename);
1917             m_StellarSolver->setProperty("FileToProcess", filename);
1918 
1919             if(Options::sextractorIsInternal())
1920                 m_StellarSolver->setProperty("SextractorBinaryPath", QString("%1/%2").arg(QCoreApplication::applicationDirPath())
1921                                              .arg("astrometry/bin/sex"));
1922             else
1923                 m_StellarSolver->setProperty("SextractorBinaryPath", Options::sextractorBinary());
1924 
1925             if (Options::astrometrySolverIsInternal())
1926                 m_StellarSolver->setProperty("SolverPath", QString("%1/%2").arg(QCoreApplication::applicationDirPath())
1927                                              .arg("astrometry/bin/solve-field"));
1928             else
1929                 m_StellarSolver->setProperty("SolverPath", Options::astrometrySolverBinary());
1930 
1931             m_StellarSolver->setProperty("ASTAPBinaryPath", Options::aSTAPExecutable());
1932             if (Options::astrometryWCSIsInternal())
1933                 m_StellarSolver->setProperty("WCSPath", QString("%1/%2").arg(QCoreApplication::applicationDirPath())
1934                                              .arg("astrometry/bin/wcsinfo"));
1935             else
1936                 m_StellarSolver->setProperty("WCSPath", Options::astrometryWCSInfo());
1937 
1938             //No need for a conf file this way.
1939             m_StellarSolver->setProperty("AutoGenerateAstroConfig", true);
1940         }
1941 
1942         if(type == SSolver::SOLVER_ONLINEASTROMETRY )
1943         {
1944             QString filename = QDir::tempPath() + QString("/solver%1.jpg").arg(QUuid::createUuid().toString().remove(
1945                                    QRegularExpression("[-{}]")));
1946             alignView->saveImage(filename);
1947 
1948             m_StellarSolver->setProperty("FileToProcess", filename);
1949             m_StellarSolver->setProperty("AstrometryAPIKey", Options::astrometryAPIKey());
1950             m_StellarSolver->setProperty("AstrometryAPIURL", Options::astrometryAPIURL());
1951         }
1952 
1953         bool useImageScale = Options::astrometryUseImageScale();
1954         if (useBlindScale == BLIND_ENGAGNED)
1955         {
1956             useImageScale = false;
1957             useBlindScale = BLIND_USED;
1958             appendLogText(i18n("Solving with blind image scale..."));
1959         }
1960 
1961         bool useImagePostion = Options::astrometryUsePosition();
1962         if (useBlindPosition == BLIND_ENGAGNED)
1963         {
1964             useImagePostion = false;
1965             useBlindPosition = BLIND_USED;
1966             appendLogText(i18n("Solving with blind image position..."));
1967         }
1968 
1969         if (m_SolveFromFile)
1970         {
1971             FITSImage::Solution solution;
1972             m_ImageData->parseSolution(solution);
1973 
1974             if (useImageScale && solution.pixscale > 0)
1975                 m_StellarSolver->setSearchScale(solution.pixscale * 0.8,
1976                                                 solution.pixscale * 1.2,
1977                                                 SSolver::ARCSEC_PER_PIX);
1978             else
1979                 m_StellarSolver->setProperty("UseScale", false);
1980 
1981             if (useImagePostion && solution.ra > 0)
1982                 m_StellarSolver->setSearchPositionInDegrees(solution.ra, solution.dec);
1983             else
1984                 m_StellarSolver->setProperty("UsePostion", false);
1985         }
1986         else
1987         {
1988             //Setting the initial search scale settings
1989             if (useImageScale)
1990             {
1991                 SSolver::ScaleUnits units = static_cast<SSolver::ScaleUnits>(Options::astrometryImageScaleUnits());
1992                 // Extend search scale from 80% to 120%
1993                 m_StellarSolver->setSearchScale(Options::astrometryImageScaleLow() * 0.8,
1994                                                 Options::astrometryImageScaleHigh() * 1.2,
1995                                                 units);
1996             }
1997             else
1998                 m_StellarSolver->setProperty("UseScale", false);
1999             //Setting the initial search location settings
2000             if(useImagePostion)
2001                 m_StellarSolver->setSearchPositionInDegrees(telescopeCoord.ra().Degrees(), telescopeCoord.dec().Degrees());
2002             else
2003                 m_StellarSolver->setProperty("UsePostion", false);
2004         }
2005 
2006         if(Options::alignmentLogging())
2007         {
2008             m_StellarSolver->setLogLevel(static_cast<SSolver::logging_level>(Options::loggerLevel()));
2009             m_StellarSolver->setSSLogLevel(SSolver::LOG_NORMAL);
2010             if(Options::astrometryLogToFile())
2011             {
2012                 m_StellarSolver->setProperty("LogToFile", true);
2013                 m_StellarSolver->setProperty("LogFileName", Options::astrometryLogFilepath());
2014             }
2015         }
2016         else
2017         {
2018             m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
2019             m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
2020         }
2021 
2022         //Unless we decide to load the WCS Coord, let's turn it off.
2023         //Be sure to set this to true instead if we want WCS from the solve.
2024         m_StellarSolver->setLoadWCS(false);
2025 
2026         // Start solving process
2027         m_StellarSolver->start();
2028     }
2029     else
2030     {
2031         // This should run only for load&slew. For regular solve, we don't get here
2032         // as the image is read and solved server-side.
2033         remoteParser->startSolver(m_ImageData->filename(), generateRemoteArgs(m_ImageData), false);
2034     }
2035 
2036     // Kick off timer
2037     solverTimer.start();
2038 
2039     state = ALIGN_PROGRESS;
2040     emit newStatus(state);
2041 }
2042 
solverComplete()2043 void Align::solverComplete()
2044 {
2045     disconnect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
2046     if(!m_StellarSolver->solvingDone() || m_StellarSolver->failed())
2047     {
2048         // If processed, we retruned. Otherwise, it is a fail
2049         if (CHECK_PAH(processSolverFailure()))
2050             return;
2051         solverFailed();
2052         return;
2053     }
2054     else
2055     {
2056         FITSImage::Solution solution = m_StellarSolver->getSolution();
2057         // Would be better if parity was a bool field instead of a QString with "pos" and "neg" as possible values.
2058         const bool eastToTheRight = solution.parity == "pos" ? false : true;
2059         solverFinished(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
2060     }
2061 }
2062 
solverFinished(double orientation,double ra,double dec,double pixscale,bool eastToTheRight)2063 void Align::solverFinished(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
2064 {
2065     pi->stopAnimation();
2066     stopB->setEnabled(false);
2067     solveB->setEnabled(true);
2068 
2069     sOrientation = orientation;
2070     sRA          = ra;
2071     sDEC         = dec;
2072 
2073     double elapsed = solverTimer.elapsed() / 1000.0;
2074     appendLogText(i18n("Solver completed after %1 seconds.", QString::number(elapsed, 'f', 2)));
2075 
2076     // Reset Telescope Type to remembered value
2077     if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN)
2078     {
2079         currentCCD->setTelescopeType(rememberTelescopeType);
2080         rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN;
2081     }
2082 
2083     m_AlignTimer.stop();
2084     if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParserDevice && remoteParser.get())
2085     {
2086         // Disable remote parse
2087         dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(false);
2088     }
2089 
2090     int binx, biny;
2091     ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
2092     targetChip->getBinning(&binx, &biny);
2093 
2094     if (Options::alignmentLogging())
2095     {
2096         QString parityString = eastToTheRight ? "neg" : "pos";
2097         appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4) Parity (%5)", QString::number(ra, 'f', 5),
2098                            QString::number(dec, 'f', 5), QString::number(orientation, 'f', 5),
2099                            QString::number(pixscale, 'f', 5), parityString));
2100     }
2101 
2102     // When solving (without Load&Slew), update effective FOV and focal length accordingly.
2103     if (!m_SolveFromFile &&
2104             (fov_x == 0 || m_EffectiveFOVPending || std::fabs(pixscale - fov_pixscale) > 0.005) &&
2105             pixscale > 0)
2106     {
2107         double newFOVW = ccd_width * pixscale / binx / 60.0;
2108         double newFOVH = ccd_height * pixscale / biny / 60.0;
2109 
2110         calculateEffectiveFocalLength(newFOVW);
2111         saveNewEffectiveFOV(newFOVW, newFOVH);
2112 
2113         m_EffectiveFOVPending = false;
2114     }
2115 
2116     alignCoord.setRA0(ra / 15.0);
2117     alignCoord.setDec0(dec);
2118     RotOut->setText(QString::number(orientation, 'f', 5));
2119 
2120     // Convert to JNow
2121     alignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
2122     // Get horizontal coords
2123     alignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
2124 
2125     // Do not update diff if we are performing load & slew.
2126     if (!m_SolveFromFile)
2127     {
2128         pixScaleOut->setText(QString::number(pixscale, 'f', 2));
2129         calculateAlignTargetDiff();
2130     }
2131 
2132     double solverPA = orientation;
2133     // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack.
2134     // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA
2135     // PA = Orientation + 180
2136     double solverFlippedPA = orientation + 180;
2137     // Limit PA to -180 to +180
2138     if (solverFlippedPA > 180)
2139         solverFlippedPA -= 360;
2140     if (solverFlippedPA < -180)
2141         solverFlippedPA += 360;
2142     solverFOV->setCenter(alignCoord);
2143     solverFOV->setPA(solverFlippedPA);
2144     solverFOV->setImageDisplay(Options::astrometrySolverOverlay());
2145     // Sensor FOV as well
2146     sensorFOV->setPA(solverFlippedPA);
2147 
2148     QString ra_dms, dec_dms;
2149     getFormattedCoords(alignCoord.ra().Hours(), alignCoord.dec().Degrees(), ra_dms, dec_dms);
2150 
2151     SolverRAOut->setText(ra_dms);
2152     SolverDecOut->setText(dec_dms);
2153 
2154 
2155     if (Options::astrometrySolverWCS())
2156     {
2157         auto ccdRotation = currentCCD->getBaseDevice()->getNumber("CCD_ROTATION");
2158         if (ccdRotation)
2159         {
2160             auto rotation = ccdRotation->findWidgetByName("CCD_ROTATION_VALUE");
2161             if (rotation)
2162             {
2163                 ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager();
2164                 rotation->setValue(orientation);
2165                 clientManager->sendNewNumber(ccdRotation);
2166 
2167                 if (m_wcsSynced == false)
2168                 {
2169                     appendLogText(
2170                         i18n("WCS information updated. Images captured from this point forward shall have valid WCS."));
2171 
2172                     // Just send telescope info in case the CCD driver did not pick up before.
2173                     auto telescopeInfo = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO");
2174                     if (telescopeInfo)
2175                         clientManager->sendNewNumber(telescopeInfo);
2176 
2177                     m_wcsSynced = true;
2178                 }
2179             }
2180         }
2181     }
2182 
2183     m_CaptureErrorCounter = 0;
2184     m_SlewErrorCounter = 0;
2185     m_CaptureTimeoutCounter = 0;
2186 
2187     appendLogText(i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4)",
2188                        alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString(), telescopeCoord.ra().toHMSString(),
2189                        telescopeCoord.dec().toDMSString()));
2190     if (!m_SolveFromFile && m_CurrentGotoMode == GOTO_SLEW)
2191     {
2192         dms diffDeg(m_TargetDiffTotal / 3600.0);
2193         appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString()));
2194     }
2195 
2196     if (rememberUploadMode != currentCCD->getUploadMode())
2197         currentCCD->setUploadMode(rememberUploadMode);
2198 
2199     // Remember to reset fast exposure
2200     if (m_RememberCameraFastExposure)
2201     {
2202         m_RememberCameraFastExposure = false;
2203         currentCCD->setFastExposureEnabled(true);
2204     }
2205 
2206     //This block of code along with some sections in the switch below will set the status report in the solution table for this item.
2207     std::unique_ptr<QTableWidgetItem> statusReport(new QTableWidgetItem());
2208     int currentRow = solutionTable->rowCount() - 1;
2209     if (!m_SolveFromFile)
2210     {
2211         stopProgressAnimation();
2212         solutionTable->setCellWidget(currentRow, 3, new QWidget());
2213         statusReport->setFlags(Qt::ItemIsSelectable);
2214     }
2215 
2216     if (m_SolveFromFile && Options::astrometryUseRotator())
2217     {
2218         loadSlewTargetPA = solverPA;
2219         qCDebug(KSTARS_EKOS_ALIGN) << "loaSlewTargetPA:" << loadSlewTargetPA;
2220     }
2221     else if (Options::astrometryUseRotator())
2222     {
2223         currentRotatorPA = solverPA;
2224 
2225         // When Load&Slew image is solved, we check if we need to rotate the rotator to match the position angle of the image
2226         if (currentRotator != nullptr)
2227         {
2228             // Update Rotator offsets
2229             auto absAngle = currentRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE");
2230             if (absAngle)
2231             {
2232                 // PA = RawAngle * Multiplier + Offset
2233                 double rawAngle = absAngle->at(0)->getValue();
2234                 double offset   = range360(solverPA - (rawAngle * Options::pAMultiplier()));
2235 
2236                 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << rawAngle << "Rotator PA:" << currentRotatorPA
2237                                            << "Rotator Offset:" << offset;
2238                 Options::setPAOffset(offset);
2239             }
2240 
2241             if (absAngle && std::isnan(loadSlewTargetPA) == false
2242                     && fabs(currentRotatorPA - loadSlewTargetPA) * 60 > Options::astrometryRotatorThreshold())
2243             {
2244                 double rawAngle = range360((loadSlewTargetPA - Options::pAOffset()) / Options::pAMultiplier());
2245                 //                if (rawAngle < 0)
2246                 //                    rawAngle += 360;
2247                 //                else if (rawAngle > 360)
2248                 //                    rawAngle -= 360;
2249                 absAngle->at(0)->setValue(rawAngle);
2250                 ClientManager *clientManager = currentRotator->getDriverInfo()->getClientManager();
2251                 clientManager->sendNewNumber(absAngle);
2252                 appendLogText(i18n("Setting position angle to %1 degrees E of N...", loadSlewTargetPA));
2253                 return;
2254             }
2255         }
2256         else if (std::isnan(loadSlewTargetPA) == false)
2257         {
2258             double current = range360(currentRotatorPA);
2259             double target = range360(loadSlewTargetPA);
2260             double targetFlipped = range360(loadSlewTargetPA + 180);
2261 
2262             double diff = current - target;
2263             if (fabs(current + 360.0 - target) < fabs(diff))
2264             {
2265                 diff = current + 360.0 - target;
2266             }
2267 
2268             if (fabs(current - targetFlipped) < fabs(diff))
2269             {
2270                 diff = current - targetFlipped;
2271                 target = targetFlipped;
2272             }
2273 
2274             if (fabs(current + 360.0 - targetFlipped) < fabs(diff))
2275             {
2276                 diff = current + 360.0 - targetFlipped;
2277                 target = targetFlipped;
2278             }
2279 
2280             double threshold = Options::astrometryRotatorThreshold() / 60.0;
2281 
2282             appendLogText(i18n("Current Rotation is %1; Target Rotation is %2; diff: %3", current, target, diff));
2283 
2284             m_ManualRotator->setRotatorDiff(current, target, diff);
2285             if (fabs(diff) > threshold)
2286             {
2287                 targetAccuracyNotMet = true;
2288                 m_ManualRotator->show();
2289                 m_ManualRotator->raise();
2290                 return;
2291             }
2292             else
2293             {
2294                 loadSlewTargetPA = std::numeric_limits<double>::quiet_NaN();
2295                 targetAccuracyNotMet = false;
2296             }
2297         }
2298     }
2299 
2300     emit newSolverResults(orientation, ra, dec, pixscale);
2301     QJsonObject solution =
2302     {
2303         {"camera", currentCCD->getDeviceName()},
2304         {"ra", SolverRAOut->text()},
2305         {"de", SolverDecOut->text()},
2306         {"dRA", m_TargetDiffRA},
2307         {"dDE", m_TargetDiffDE},
2308         {"targetDiff", m_TargetDiffTotal},
2309         {"pix", pixscale},
2310         {"rot", orientation},
2311         {"fov", FOVOut->text()},
2312     };
2313     emit newSolution(solution.toVariantMap());
2314 
2315     switch (m_CurrentGotoMode)
2316     {
2317         case GOTO_SYNC:
2318             executeGOTO();
2319 
2320             if (!m_SolveFromFile)
2321             {
2322                 stopProgressAnimation();
2323                 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2324                 solutionTable->setItem(currentRow, 3, statusReport.release());
2325             }
2326 
2327             return;
2328 
2329         case GOTO_SLEW:
2330             if (m_SolveFromFile || m_TargetDiffTotal > static_cast<double>(accuracySpin->value()))
2331             {
2332                 if (!m_SolveFromFile && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
2333                 {
2334                     appendLogText(i18n("Maximum number of iterations reached. Solver failed."));
2335 
2336                     if (!m_SolveFromFile)
2337                     {
2338                         statusReport->setIcon(QIcon(":/icons/AlignFailure.svg"));
2339                         solutionTable->setItem(currentRow, 3, statusReport.release());
2340                     }
2341 
2342                     solverFailed();
2343                     return;
2344                 }
2345 
2346                 targetAccuracyNotMet = true;
2347 
2348                 if (!m_SolveFromFile)
2349                 {
2350                     stopProgressAnimation();
2351                     statusReport->setIcon(QIcon(":/icons/AlignWarning.svg"));
2352                     solutionTable->setItem(currentRow, 3, statusReport.release());
2353                 }
2354 
2355                 executeGOTO();
2356                 return;
2357             }
2358 
2359             if (!m_SolveFromFile)
2360             {
2361                 stopProgressAnimation();
2362                 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2363                 solutionTable->setItem(currentRow, 3, statusReport.release());
2364             }
2365 
2366             appendLogText(i18n("Target is within acceptable range. Astrometric solver is successful."));
2367 
2368             //            if (mountModelRunning)
2369             //            {
2370             //                finishAlignmentPoint(true);
2371             //                if (mountModelRunning)
2372             //                    return;
2373             //            }
2374             break;
2375 
2376         case GOTO_NOTHING:
2377             if (!m_SolveFromFile)
2378             {
2379                 stopProgressAnimation();
2380                 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2381                 solutionTable->setItem(currentRow, 3, statusReport.release());
2382             }
2383             break;
2384     }
2385 
2386     KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"));
2387     state = ALIGN_COMPLETE;
2388     emit newStatus(state);
2389     solverIterations = 0;
2390 
2391     solverFOV->setProperty("visible", true);
2392 
2393     if (!matchPAHStage(PAA::PAH_IDLE))
2394         m_PolarAlignmentAssistant->processPAHStage(orientation, ra, dec, pixscale, eastToTheRight);
2395     else
2396     {
2397         solveB->setEnabled(true);
2398         loadSlewB->setEnabled(true);
2399     }
2400 }
2401 
solverFailed()2402 void Align::solverFailed()
2403 {
2404     if (state != ALIGN_ABORTED)
2405     {
2406         // Try to solve with scale turned off, if not turned off already
2407         if (Options::astrometryUseImageScale() && useBlindScale == BLIND_IDLE)
2408         {
2409             useBlindScale = BLIND_ENGAGNED;
2410             setAlignTableResult(ALIGN_RESULT_FAILED);
2411             captureAndSolve();
2412             return;
2413         }
2414 
2415         // Try to solve with the position turned off, if not turned off already
2416         if (Options::astrometryUsePosition() && useBlindPosition == BLIND_IDLE)
2417         {
2418             useBlindPosition = BLIND_ENGAGNED;
2419             setAlignTableResult(ALIGN_RESULT_FAILED);
2420             captureAndSolve();
2421             return;
2422         }
2423 
2424 
2425         appendLogText(i18n("Solver Failed."));
2426         if(!Options::alignmentLogging())
2427             appendLogText(
2428                 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."));
2429 
2430         KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"),
2431                               KSNotification::EVENT_ALERT);
2432     }
2433 
2434     pi->stopAnimation();
2435     stopB->setEnabled(false);
2436     solveB->setEnabled(true);
2437     loadSlewB->setEnabled(true);
2438 
2439     m_AlignTimer.stop();
2440 
2441     m_SolveFromFile = false;
2442     solverIterations = 0;
2443     m_CaptureErrorCounter = 0;
2444     m_CaptureTimeoutCounter = 0;
2445     m_SlewErrorCounter = 0;
2446 
2447     state = ALIGN_FAILED;
2448     emit newStatus(state);
2449 
2450     solverFOV->setProperty("visible", false);
2451 
2452     setAlignTableResult(ALIGN_RESULT_FAILED);
2453 }
2454 
stop(Ekos::AlignState mode)2455 void Align::stop(Ekos::AlignState mode)
2456 {
2457     m_CaptureTimer.stop();
2458     if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL && m_StellarSolver)
2459         m_StellarSolver->abort();
2460     else if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser)
2461         remoteParser->stopSolver();
2462     //parser->stopSolver();
2463     pi->stopAnimation();
2464     stopB->setEnabled(false);
2465     solveB->setEnabled(true);
2466     loadSlewB->setEnabled(true);
2467 
2468     // Reset Telescope Type to remembered value
2469     if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN)
2470     {
2471         currentCCD->setTelescopeType(rememberTelescopeType);
2472         rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN;
2473     }
2474 
2475     m_SolveFromFile = false;
2476     solverIterations = 0;
2477     m_CaptureErrorCounter = 0;
2478     m_CaptureTimeoutCounter = 0;
2479     m_SlewErrorCounter = 0;
2480     m_AlignTimer.stop();
2481 
2482     disconnect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Align::processData);
2483     disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress);
2484 
2485     if (rememberUploadMode != currentCCD->getUploadMode())
2486         currentCCD->setUploadMode(rememberUploadMode);
2487 
2488     // Remember to reset fast exposure
2489     if (m_RememberCameraFastExposure)
2490     {
2491         m_RememberCameraFastExposure = false;
2492         currentCCD->setFastExposureEnabled(true);
2493     }
2494 
2495     ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
2496 
2497     // If capture is still in progress, let's stop that.
2498     if (matchPAHStage(PAA::PAH_POST_REFRESH))
2499     {
2500         if (targetChip->isCapturing())
2501             targetChip->abortExposure();
2502 
2503         appendLogText(i18n("Refresh is complete."));
2504     }
2505     else
2506     {
2507         if (targetChip->isCapturing())
2508         {
2509             targetChip->abortExposure();
2510             appendLogText(i18n("Capture aborted."));
2511         }
2512         else
2513         {
2514             double elapsed = solverTimer.elapsed() / 1000.0;
2515             appendLogText(i18n("Solver aborted after %1 seconds.", QString::number(elapsed, 'f', 2)));
2516         }
2517     }
2518 
2519     state = mode;
2520     emit newStatus(state);
2521 
2522     setAlignTableResult(ALIGN_RESULT_FAILED);
2523 }
2524 
getProgressStatus()2525 QProgressIndicator * Align::getProgressStatus()
2526 {
2527     int currentRow = solutionTable->rowCount() - 1;
2528 
2529     // check if the current row indicates a progress state
2530     // 1. no row present
2531     if (currentRow < 0)
2532         return nullptr;
2533     // 2. indicator is not present or not a progress indicator
2534     QWidget *indicator = solutionTable->cellWidget(currentRow, 3);
2535     if (indicator == nullptr)
2536         return nullptr;
2537     return dynamic_cast<QProgressIndicator *>(indicator);
2538 }
2539 
stopProgressAnimation()2540 void Align::stopProgressAnimation()
2541 {
2542     QProgressIndicator *progress_indicator = getProgressStatus();
2543     if (progress_indicator != nullptr)
2544         progress_indicator->stopAnimation();
2545 }
2546 
getSolutionResult()2547 QList<double> Align::getSolutionResult()
2548 {
2549     QList<double> result;
2550 
2551     result << sOrientation << sRA << sDEC;
2552 
2553     return result;
2554 }
2555 
appendLogText(const QString & text)2556 void Align::appendLogText(const QString &text)
2557 {
2558     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
2559                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
2560 
2561     qCInfo(KSTARS_EKOS_ALIGN) << text;
2562 
2563     emit newLog(text);
2564 }
2565 
clearLog()2566 void Align::clearLog()
2567 {
2568     m_LogText.clear();
2569     emit newLog(QString());
2570 }
2571 
processSwitch(ISwitchVectorProperty * svp)2572 void Align::processSwitch(ISwitchVectorProperty *svp)
2573 {
2574     if (!strcmp(svp->name, "DOME_MOTION"))
2575     {
2576         // If dome is not ready and state is now
2577         if (domeReady == false && svp->s == IPS_OK)
2578         {
2579             domeReady = true;
2580             // trigger process number for mount so that it proceeds with normal workflow since
2581             // it was stopped by dome not being ready
2582             handleMountStatus();
2583         }
2584     }
2585     else if ((!strcmp(svp->name, "TELESCOPE_MOTION_NS") || !strcmp(svp->name, "TELESCOPE_MOTION_WE")))
2586         switch (svp->s)
2587         {
2588             case IPS_BUSY:
2589                 // react upon mount motion
2590                 handleMountMotion();
2591                 m_wasSlewStarted = true;
2592                 break;
2593             default:
2594                 qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished.";
2595                 handleMountStatus();
2596                 break;
2597         }
2598 
2599 }
2600 
processNumber(INumberVectorProperty * nvp)2601 void Align::processNumber(INumberVectorProperty *nvp)
2602 {
2603     if (!strcmp(nvp->name, "EQUATORIAL_EOD_COORD") || !strcmp(nvp->name, "EQUATORIAL_COORD"))
2604     {
2605         QString ra_dms, dec_dms;
2606 
2607         getFormattedCoords(telescopeCoord.ra().Hours(), telescopeCoord.dec().Degrees(), ra_dms, dec_dms);
2608 
2609         ScopeRAOut->setText(ra_dms);
2610         ScopeDecOut->setText(dec_dms);
2611 
2612         //        qCDebug(KSTARS_EKOS_ALIGN) << "## RA" << ra_dms << "DE" << dec_dms
2613         //                                   << "state:" << pstateStr(nvp->s) << "slewStarted?" << m_wasSlewStarted;
2614 
2615         switch (nvp->s)
2616         {
2617             // Idle --> Mount not tracking or slewing
2618             case IPS_IDLE:
2619                 m_wasSlewStarted = false;
2620                 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE";
2621                 break;
2622 
2623             // Ok --> Mount Tracking. If m_wasSlewStarted is true
2624             // then it just finished slewing
2625             case IPS_OK:
2626             {
2627                 // Update the boxes as the mount just finished slewing
2628                 if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition())
2629                 {
2630                     //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position...";
2631                     opsAstrometry->estRA->setText(ra_dms);
2632                     opsAstrometry->estDec->setText(dec_dms);
2633 
2634                     Options::setAstrometryPositionRA(nvp->np[0].value * 15);
2635                     Options::setAstrometryPositionDE(nvp->np[1].value);
2636 
2637                     //generateArgs();
2638                 }
2639 
2640                 // If dome is syncing, wait until it stops
2641                 if (currentDome && currentDome->isMoving())
2642                 {
2643                     domeReady = false;
2644                     return;
2645                 }
2646 
2647                 // If we are looking for celestial pole
2648                 if (m_wasSlewStarted && matchPAHStage(PAA::PAH_FIND_CP))
2649                 {
2650                     //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE";
2651                     m_wasSlewStarted = false;
2652                     appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify."));
2653                     setSolverAction(GOTO_NOTHING);
2654 
2655                     m_PolarAlignmentAssistant->setPAHStage(PAA::PAH_FIRST_CAPTURE);
2656                     return;
2657                 }
2658 
2659                 switch (state)
2660                 {
2661                     case ALIGN_PROGRESS:
2662                         break;
2663 
2664                     case ALIGN_SYNCING:
2665                     {
2666                         m_wasSlewStarted = false;
2667                         //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE";
2668                         if (m_CurrentGotoMode == GOTO_SLEW)
2669                         {
2670                             Slew();
2671                             return;
2672                         }
2673                         else
2674                         {
2675                             appendLogText(i18n("Mount is synced to solution coordinates. Astrometric solver is successful."));
2676                             KSNotification::event(QLatin1String("AlignSuccessful"),
2677                                                   i18n("Astrometry alignment completed successfully"));
2678                             state = ALIGN_COMPLETE;
2679                             emit newStatus(state);
2680                             solverIterations = 0;
2681                         }
2682                     }
2683                     break;
2684 
2685                     case ALIGN_SLEWING:
2686 
2687                         if (!didSlewStart())
2688                         {
2689                             // If mount has not started slewing yet, then skip
2690                             //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet...";
2691                             break;
2692                         }
2693 
2694                         //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed.";
2695                         m_wasSlewStarted = false;
2696                         if (m_SolveFromFile)
2697                         {
2698                             m_SolveFromFile = false;
2699 
2700                             state = ALIGN_PROGRESS;
2701                             emit newStatus(state);
2702 
2703                             if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY)
2704                                 appendLogText(i18n("Settling..."));
2705                             m_CaptureTimer.start(delaySpin->value());
2706                             return;
2707                         }
2708                         else if (differentialSlewingActivated)
2709                         {
2710                             appendLogText(i18n("Differential slewing complete. Astrometric solver is successful."));
2711                             KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"));
2712                             state = ALIGN_COMPLETE;
2713                             emit newStatus(state);
2714                             solverIterations = 0;
2715                         }
2716                         else if (m_CurrentGotoMode == GOTO_SLEW || (m_MountModel && m_MountModel->isRunning()))
2717                         {
2718                             if (targetAccuracyNotMet)
2719                                 appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again..."));
2720                             else
2721                                 appendLogText(i18n("Slew complete. Solving Alignment Point. . ."));
2722 
2723                             targetAccuracyNotMet = false;
2724 
2725                             state = ALIGN_PROGRESS;
2726                             emit newStatus(state);
2727 
2728                             if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY)
2729                                 appendLogText(i18n("Settling..."));
2730                             m_CaptureTimer.start(delaySpin->value());
2731                             return;
2732                         }
2733                         break;
2734 
2735                     default:
2736                     {
2737                         //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE";
2738                         m_wasSlewStarted = false;
2739                     }
2740                     break;
2741                 }
2742             }
2743             break;
2744 
2745             // Busy --> Mount Slewing or Moving (NSWE buttons)
2746             case IPS_BUSY:
2747             {
2748                 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew running.";
2749                 m_wasSlewStarted = true;
2750 
2751                 handleMountMotion();
2752             }
2753             break;
2754 
2755             // Alert --> Mount has problem moving or communicating.
2756             case IPS_ALERT:
2757             {
2758                 //qCDebug(KSTARS_EKOS_ALIGN) << "IPS_ALERT --> setting slewStarted to FALSE";
2759                 m_wasSlewStarted = false;
2760 
2761                 if (state == ALIGN_SYNCING || state == ALIGN_SLEWING)
2762                 {
2763                     if (state == ALIGN_SYNCING)
2764                         appendLogText(i18n("Syncing failed."));
2765                     else
2766                         appendLogText(i18n("Slewing failed."));
2767 
2768                     if (++m_SlewErrorCounter == 3)
2769                     {
2770                         abort();
2771                         return;
2772                     }
2773                     else
2774                     {
2775                         if (m_CurrentGotoMode == GOTO_SLEW)
2776                             Slew();
2777                         else
2778                             Sync();
2779                     }
2780                 }
2781 
2782                 return;
2783             }
2784         }
2785 
2786         RUN_PAH(processMountRotation(telescopeCoord.ra(), delaySpin->value()));
2787     }
2788     else if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE"))
2789     {
2790         // PA = RawAngle * Multiplier + Offset
2791         currentRotatorPA = (nvp->np[0].value * Options::pAMultiplier()) + Options::pAOffset();
2792         if (currentRotatorPA > 180)
2793             currentRotatorPA -= 360;
2794         if (currentRotatorPA < -180)
2795             currentRotatorPA += 360;
2796         if (std::isnan(loadSlewTargetPA) == false
2797                 && fabs(currentRotatorPA - loadSlewTargetPA) * 60 <= Options::astrometryRotatorThreshold())
2798         {
2799             appendLogText(i18n("Rotator reached target position angle."));
2800             targetAccuracyNotMet = true;
2801             loadSlewTargetPA = std::numeric_limits<double>::quiet_NaN();
2802             QTimer::singleShot(Options::settlingTime(), this, &Ekos::Align::executeGOTO);
2803         }
2804     }
2805 
2806     // N.B. Ekos::Manager already manages TELESCOPE_INFO, why here again?
2807     //if (!strcmp(coord->name, "TELESCOPE_INFO"))
2808     //syncTelescopeInfo();
2809 }
2810 
handleMountMotion()2811 void Align::handleMountMotion()
2812 {
2813     if (state == ALIGN_PROGRESS)
2814     {
2815         if (matchPAHStage(PAA::PAH_IDLE))
2816         {
2817             // whoops, mount slews during alignment
2818             appendLogText(i18n("Slew detected, suspend solving..."));
2819             suspend();
2820             // reset the state to busy so that solving restarts after slewing finishes
2821             m_SolveFromFile = true;
2822             // if mount model is running, retry the current alignment point
2823             //            if (mountModelRunning)
2824             //                appendLogText(i18n("Restarting alignment point %1", currentAlignmentPoint + 1));
2825         }
2826 
2827         state = ALIGN_SLEWING;
2828     }
2829 }
2830 
handleMountStatus()2831 void Align::handleMountStatus()
2832 {
2833     auto nvp = currentTelescope->getBaseDevice()->getNumber(currentTelescope->isJ2000() ? "EQUATORIAL_COORD" :
2834                "EQUATORIAL_EOD_COORD");
2835 
2836     if (nvp)
2837         processNumber(nvp);
2838 }
2839 
2840 
executeGOTO()2841 void Align::executeGOTO()
2842 {
2843     if (m_SolveFromFile)
2844     {
2845         m_targetCoord = alignCoord;
2846         SlewToTarget();
2847     }
2848     else if (m_CurrentGotoMode == GOTO_SYNC)
2849         Sync();
2850     else if (m_CurrentGotoMode == GOTO_SLEW)
2851         SlewToTarget();
2852 }
2853 
Sync()2854 void Align::Sync()
2855 {
2856     state = ALIGN_SYNCING;
2857 
2858     if (currentTelescope->Sync(&alignCoord))
2859     {
2860         emit newStatus(state);
2861         appendLogText(
2862             i18n("Syncing to RA (%1) DEC (%2)", alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString()));
2863     }
2864     else
2865     {
2866         state = ALIGN_IDLE;
2867         emit newStatus(state);
2868         appendLogText(i18n("Syncing failed."));
2869     }
2870 }
2871 
Slew()2872 void Align::Slew()
2873 {
2874     state = ALIGN_SLEWING;
2875     emit newStatus(state);
2876 
2877     //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2878     //m_wasSlewStarted = currentTelescope->Slew(&m_targetCoord);
2879     //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2880 
2881     // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered
2882     // from Goto
2883     currentTelescope->Slew(&m_targetCoord);
2884     slewStartTimer.start();
2885     appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", m_targetCoord.ra().toHMSString(),
2886                        m_targetCoord.dec().toDMSString()));
2887 }
2888 
SlewToTarget()2889 void Align::SlewToTarget()
2890 {
2891     if (canSync && !m_SolveFromFile)
2892     {
2893         // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided
2894 #if 0
2895         if (Ekos::Manager::Instance()->getCurrentJobName().isEmpty())
2896         {
2897             KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"),
2898                                   i18n("Ekos job (%1) - Telescope synced",
2899                                        Ekos::Manager::Instance()->getCurrentJobName()));
2900         }
2901 #endif
2902 
2903         // Do we perform a regular sync or use differential slewing?
2904         if (Options::astrometryDifferentialSlewing())
2905         {
2906             dms m_TargetDiffRA = alignCoord.ra().deltaAngle(m_targetCoord.ra());
2907             dms m_TargetDiffDE = alignCoord.dec().deltaAngle(m_targetCoord.dec());
2908 
2909             m_targetCoord.setRA(m_targetCoord.ra() - m_TargetDiffRA);
2910             m_targetCoord.setDec(m_targetCoord.dec() - m_TargetDiffDE);
2911 
2912             differentialSlewingActivated = true;
2913 
2914             qCDebug(KSTARS_EKOS_ALIGN) << "Using differential slewing...";
2915 
2916             Slew();
2917         }
2918         else
2919             Sync();
2920 
2921         return;
2922     }
2923 
2924     Slew();
2925 }
2926 
2927 
getFormattedCoords(double ra,double dec,QString & ra_str,QString & dec_str)2928 void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str)
2929 {
2930     dms ra_s, dec_s;
2931     ra_s.setH(ra);
2932     dec_s.setD(dec);
2933 
2934     ra_str = QString("%1:%2:%3")
2935              .arg(ra_s.hour(), 2, 10, QChar('0'))
2936              .arg(ra_s.minute(), 2, 10, QChar('0'))
2937              .arg(ra_s.second(), 2, 10, QChar('0'));
2938     if (dec_s.Degrees() < 0)
2939         dec_str = QString("-%1:%2:%3")
2940                   .arg(abs(dec_s.degree()), 2, 10, QChar('0'))
2941                   .arg(abs(dec_s.arcmin()), 2, 10, QChar('0'))
2942                   .arg(dec_s.arcsec(), 2, 10, QChar('0'));
2943     else
2944         dec_str = QString("%1:%2:%3")
2945                   .arg(dec_s.degree(), 2, 10, QChar('0'))
2946                   .arg(dec_s.arcmin(), 2, 10, QChar('0'))
2947                   .arg(dec_s.arcsec(), 2, 10, QChar('0'));
2948 }
2949 
loadAndSlew(QString fileURL)2950 bool Align::loadAndSlew(QString fileURL)
2951 {
2952     if (fileURL.isEmpty())
2953         fileURL = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Load Image"), dirPath,
2954                                                "Images (*.fits *.fits.fz *.fit *.fts "
2955                                                "*.jpg *.jpeg *.png *.gif *.bmp "
2956                                                "*.cr2 *.cr3 *.crw *.nef *.raf *.dng *.arw *.orf)");
2957 
2958     if (fileURL.isEmpty())
2959         return false;
2960 
2961     QFileInfo fileInfo(fileURL);
2962 
2963     dirPath = fileInfo.absolutePath();
2964 
2965     differentialSlewingActivated = false;
2966 
2967     m_SolveFromFile = true;
2968 
2969     if (m_PolarAlignmentAssistant)
2970         m_PolarAlignmentAssistant->stopPAHProcess();
2971 
2972     slewR->setChecked(true);
2973     m_CurrentGotoMode = GOTO_SLEW;
2974 
2975     solveB->setEnabled(false);
2976     stopB->setEnabled(true);
2977     pi->startAnimation();
2978 
2979     if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParserDevice == nullptr)
2980     {
2981         appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
2982         setSolverMode(SOLVER_LOCAL);
2983     }
2984 
2985     m_ImageData.clear();
2986 
2987     alignView->loadFile(fileURL, false);
2988     //m_FileToSolve = fileURL;
2989     connect(alignView, &FITSView::loaded, this, &Align::startSolving);
2990 
2991     return true;
2992 }
2993 
loadAndSlew(const QByteArray & image,const QString & extension)2994 bool Align::loadAndSlew(const QByteArray &image, const QString &extension)
2995 {
2996     differentialSlewingActivated = false;
2997     m_SolveFromFile = true;
2998     RUN_PAH(stopPAHProcess());
2999     slewR->setChecked(true);
3000     m_CurrentGotoMode = GOTO_SLEW;
3001     solveB->setEnabled(false);
3002     stopB->setEnabled(true);
3003     pi->startAnimation();
3004 
3005     QSharedPointer<FITSData> data;
3006     data.reset(new FITSData(), &QObject::deleteLater);
3007     data->loadFromBuffer(image, extension);
3008     alignView->loadData(data);
3009     startSolving();
3010     return true;
3011 }
3012 
setExposure(double value)3013 void Align::setExposure(double value)
3014 {
3015     exposureIN->setValue(value);
3016 }
3017 
setBinningIndex(int binIndex)3018 void Align::setBinningIndex(int binIndex)
3019 {
3020     syncSettings();
3021     Options::setSolverBinningIndex(binIndex);
3022 
3023     // If sender is not our combo box, then we need to update the combobox itself
3024     if (dynamic_cast<QComboBox *>(sender()) != binningCombo)
3025     {
3026         binningCombo->blockSignals(true);
3027         binningCombo->setCurrentIndex(binIndex);
3028         binningCombo->blockSignals(false);
3029     }
3030 
3031     // Need to calculate FOV and args for APP
3032     if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
3033     {
3034         calculateFOV();
3035         //generateArgs();
3036     }
3037 }
3038 
setFOVTelescopeType(int index)3039 void Align::setFOVTelescopeType(int index)
3040 {
3041     FOVScopeCombo->setCurrentIndex(index);
3042 }
3043 
addFilter(ISD::GDInterface * newFilter)3044 void Align::addFilter(ISD::GDInterface *newFilter)
3045 {
3046     for (auto filter : Filters)
3047     {
3048         if (filter->getDeviceName() == newFilter->getDeviceName())
3049             return;
3050     }
3051 
3052     FilterCaptureLabel->setEnabled(true);
3053     FilterDevicesCombo->setEnabled(true);
3054     FilterPosLabel->setEnabled(true);
3055     FilterPosCombo->setEnabled(true);
3056 
3057     FilterDevicesCombo->addItem(newFilter->getDeviceName());
3058 
3059     Filters.append(static_cast<ISD::Filter *>(newFilter));
3060 
3061     int filterWheelIndex = 1;
3062     if (Options::defaultAlignFilterWheel().isEmpty() == false)
3063         filterWheelIndex = FilterDevicesCombo->findText(Options::defaultAlignFilterWheel());
3064 
3065     if (filterWheelIndex < 1)
3066         filterWheelIndex = 1;
3067 
3068     checkFilter(filterWheelIndex);
3069     FilterDevicesCombo->setCurrentIndex(filterWheelIndex);
3070 
3071     emit settingsUpdated(getSettings());
3072 }
3073 
setFilterWheel(const QString & device)3074 bool Align::setFilterWheel(const QString &device)
3075 {
3076     bool deviceFound = false;
3077 
3078     for (int i = 1; i < FilterDevicesCombo->count(); i++)
3079         if (device == FilterDevicesCombo->itemText(i))
3080         {
3081             checkFilter(i);
3082             deviceFound = true;
3083             break;
3084         }
3085 
3086     if (deviceFound == false)
3087         return false;
3088 
3089     return true;
3090 }
3091 
filterWheel()3092 QString Align::filterWheel()
3093 {
3094     if (FilterDevicesCombo->currentIndex() >= 1)
3095         return FilterDevicesCombo->currentText();
3096 
3097     return QString();
3098 }
3099 
setFilter(const QString & filter)3100 bool Align::setFilter(const QString &filter)
3101 {
3102     if (FilterDevicesCombo->currentIndex() >= 1)
3103     {
3104         FilterPosCombo->setCurrentText(filter);
3105         return true;
3106     }
3107 
3108     return false;
3109 }
3110 
filter()3111 QString Align::filter()
3112 {
3113     return FilterPosCombo->currentText();
3114 }
3115 
3116 
checkFilter(int filterNum)3117 void Align::checkFilter(int filterNum)
3118 {
3119     if (filterNum == -1)
3120     {
3121         filterNum = FilterDevicesCombo->currentIndex();
3122         if (filterNum == -1)
3123             return;
3124     }
3125 
3126     // "--" is no filter
3127     if (filterNum == 0)
3128     {
3129         currentFilter = nullptr;
3130         currentFilterPosition = -1;
3131         FilterPosCombo->clear();
3132         return;
3133     }
3134 
3135     if (filterNum <= Filters.count())
3136         currentFilter = Filters.at(filterNum - 1);
3137 
3138     FilterPosCombo->clear();
3139 
3140     FilterPosCombo->addItems(filterManager->getFilterLabels());
3141 
3142     currentFilterPosition = filterManager->getFilterPosition();
3143 
3144     FilterPosCombo->setCurrentIndex(Options::lockAlignFilterIndex());
3145 
3146     syncSettings();
3147 }
3148 
setWCSEnabled(bool enable)3149 void Align::setWCSEnabled(bool enable)
3150 {
3151     if (!currentCCD)
3152         return;
3153 
3154     auto wcsControl = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL");
3155 
3156     auto wcs_enable  = wcsControl->findWidgetByName("WCS_ENABLE");
3157     auto wcs_disable = wcsControl->findWidgetByName("WCS_DISABLE");
3158 
3159     if (!wcs_enable || !wcs_disable)
3160         return;
3161 
3162     if ((wcs_enable->getState() == ISS_ON && enable) || (wcs_disable->getState() == ISS_ON && !enable))
3163         return;
3164 
3165     wcsControl->reset();
3166     if (enable)
3167     {
3168         appendLogText(i18n("World Coordinate System (WCS) is enabled. CCD rotation must be set either manually in the "
3169                            "CCD driver or by solving an image before proceeding to capture any further images, "
3170                            "otherwise the WCS information may be invalid."));
3171         wcs_enable->setState(ISS_ON);
3172     }
3173     else
3174     {
3175         wcs_disable->setState(ISS_ON);
3176         m_wcsSynced    = false;
3177         appendLogText(i18n("World Coordinate System (WCS) is disabled."));
3178     }
3179 
3180     ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager();
3181     if (clientManager)
3182         clientManager->sendNewSwitch(wcsControl);
3183 }
3184 
checkCCDExposureProgress(ISD::CCDChip * targetChip,double remaining,IPState state)3185 void Align::checkCCDExposureProgress(ISD::CCDChip *targetChip, double remaining, IPState state)
3186 {
3187     INDI_UNUSED(targetChip);
3188     INDI_UNUSED(remaining);
3189 
3190     if (state == IPS_ALERT)
3191     {
3192         if (++m_CaptureErrorCounter == 3 && !matchPAHStage(PolarAlignmentAssistant::PAH_REFRESH))
3193         {
3194             appendLogText(i18n("Capture error. Aborting..."));
3195             abort();
3196             return;
3197         }
3198 
3199         appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter));
3200         setAlignTableResult(ALIGN_RESULT_FAILED);
3201         captureAndSolve();
3202     }
3203 }
3204 
setAlignTableResult(AlignResult result)3205 void Align::setAlignTableResult(AlignResult result)
3206 {
3207     // Do nothing if the progress indicator is not running.
3208     // This is necessary since it could happen that a problem occurs
3209     // before #captureAndSolve() has been started and there does not
3210     // exist a table entry for the current run.
3211     QProgressIndicator *progress_indicator = getProgressStatus();
3212     if (progress_indicator == nullptr || ! progress_indicator->isAnimated())
3213         return;
3214     stopProgressAnimation();
3215 
3216     QIcon icon;
3217     switch (result)
3218     {
3219         case ALIGN_RESULT_SUCCESS:
3220             icon = QIcon(":/icons/AlignSuccess.svg");
3221             break;
3222 
3223         case ALIGN_RESULT_WARNING:
3224             icon = QIcon(":/icons/AlignWarning.svg");
3225             break;
3226 
3227         case ALIGN_RESULT_FAILED:
3228         default:
3229             icon = QIcon(":/icons/AlignFailure.svg");
3230             break;
3231     }
3232     int currentRow = solutionTable->rowCount() - 1;
3233     solutionTable->setCellWidget(currentRow, 3, new QWidget());
3234     QTableWidgetItem *statusReport = new QTableWidgetItem();
3235     statusReport->setIcon(icon);
3236     statusReport->setFlags(Qt::ItemIsSelectable);
3237     solutionTable->setItem(currentRow, 3, statusReport);
3238 }
3239 
setFocusStatus(Ekos::FocusState state)3240 void Align::setFocusStatus(Ekos::FocusState state)
3241 {
3242     m_FocusState = state;
3243 }
3244 
getSolverDownsample(uint16_t binnedW)3245 uint8_t Align::getSolverDownsample(uint16_t binnedW)
3246 {
3247     uint8_t downsample = Options::astrometryDownsample();
3248 
3249     if (!Options::astrometryAutoDownsample())
3250         return downsample;
3251 
3252     while (downsample < 8)
3253     {
3254         if (binnedW / downsample <= 1024)
3255             break;
3256 
3257         downsample += 2;
3258     }
3259 
3260     return downsample;
3261 }
3262 
saveSettleTime()3263 void Align::saveSettleTime()
3264 {
3265     Options::setSettlingTime(delaySpin->value());
3266 }
3267 
setCaptureStatus(CaptureState newState)3268 void Align::setCaptureStatus(CaptureState newState)
3269 {
3270     switch (newState)
3271     {
3272         case CAPTURE_PROGRESS:
3273         {
3274             // Only reset m_targetCoord if capture wasn't suspended then resumed due to error duing ongoing
3275             // capture
3276             if (currentTelescope && m_CaptureState != CAPTURE_SUSPENDED)
3277             {
3278                 updateTargetCoords();
3279             }
3280 
3281         }
3282         break;
3283         case CAPTURE_ALIGNING:
3284             if (currentTelescope && currentTelescope->hasAlignmentModel() && Options::resetMountModelAfterMeridian())
3285             {
3286                 qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (currentTelescope->clearAlignmentModel() ?
3287                                            "successful." : "failed.");
3288             }
3289 
3290             m_CaptureTimer.start(Options::settlingTime());
3291             break;
3292 
3293         default:
3294             break;
3295     }
3296 
3297     m_CaptureState = newState;
3298 }
3299 
showFITSViewer()3300 void Align::showFITSViewer()
3301 {
3302     static int lastFVTabID = -1;
3303     if (m_ImageData)
3304     {
3305         QUrl url = QUrl::fromLocalFile("align.fits");
3306         if (fv.isNull())
3307         {
3308             fv = KStars::Instance()->createFITSViewer();
3309             fv->loadData(m_ImageData, url, &lastFVTabID);
3310         }
3311         else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
3312             fv->loadData(m_ImageData, url, &lastFVTabID);
3313 
3314         fv->show();
3315     }
3316 }
3317 
toggleAlignWidgetFullScreen()3318 void Align::toggleAlignWidgetFullScreen()
3319 {
3320     if (alignWidget->parent() == nullptr)
3321     {
3322         alignWidget->setParent(this);
3323         rightLayout->insertWidget(0, alignWidget);
3324         alignWidget->showNormal();
3325     }
3326     else
3327     {
3328         alignWidget->setParent(nullptr);
3329         alignWidget->setWindowTitle(i18nc("@title:window", "Align Frame"));
3330         alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
3331         alignWidget->showMaximized();
3332         alignWidget->show();
3333     }
3334 }
3335 
updateTelescopeType(int index)3336 void Align::updateTelescopeType(int index)
3337 {
3338     if (currentCCD == nullptr)
3339         return;
3340 
3341     // Reset style sheet.
3342     FocalLengthOut->setStyleSheet(QString());
3343 
3344     syncSettings();
3345 
3346     focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL;
3347     aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture;
3348 
3349     Options::setSolverScopeType(index);
3350 
3351     syncTelescopeInfo();
3352 }
3353 
3354 //void Align::setMountCoords(const QString &raStr, const QString &decStr, const QString &azStr,
3355 //                           const QString &altStr, int pierSide, const QString &haStr)
3356 //{
3357 //    mountRa = dms(raStr, false);
3358 //    mountDec = dms(decStr, true);
3359 //    mountHa = dms(haStr, false);
3360 //    mountAz = dms(azStr, true);
3361 //    mountAlt = dms(altStr, true);
3362 //    mountPierSide = static_cast<ISD::Telescope::PierSide>(pierSide);
3363 //}
3364 
setMountStatus(ISD::Telescope::Status newState)3365 void Align::setMountStatus(ISD::Telescope::Status newState)
3366 {
3367     switch (newState)
3368     {
3369         case ISD::Telescope::MOUNT_PARKING:
3370         case ISD::Telescope::MOUNT_SLEWING:
3371         case ISD::Telescope::MOUNT_MOVING:
3372             solveB->setEnabled(false);
3373             loadSlewB->setEnabled(false);
3374             break;
3375 
3376         default:
3377             if (state != ALIGN_PROGRESS)
3378             {
3379                 solveB->setEnabled(true);
3380                 if (matchPAHStage(PAA::PAH_IDLE))
3381                 {
3382                     loadSlewB->setEnabled(true);
3383                 }
3384             }
3385             break;
3386     }
3387 
3388     RUN_PAH(setMountStatus(newState));
3389 }
3390 
setAstrometryDevice(ISD::GDInterface * newAstrometry)3391 void Align::setAstrometryDevice(ISD::GDInterface *newAstrometry)
3392 {
3393     remoteParserDevice = newAstrometry;
3394 
3395     remoteSolverR->setEnabled(true);
3396     if (remoteParser.get() != nullptr)
3397     {
3398         remoteParser->setAstrometryDevice(remoteParserDevice);
3399         connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
3400         connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
3401     }
3402 }
3403 
setRotator(ISD::GDInterface * newRotator)3404 void Align::setRotator(ISD::GDInterface *newRotator)
3405 {
3406     currentRotator = newRotator;
3407     connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection);
3408 }
3409 
refreshAlignOptions()3410 void Align::refreshAlignOptions()
3411 {
3412     solverFOV->setImageDisplay(Options::astrometrySolverWCS());
3413     m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
3414 }
3415 
setFilterManager(const QSharedPointer<FilterManager> & manager)3416 void Align::setFilterManager(const QSharedPointer<FilterManager> &manager)
3417 {
3418     filterManager = manager;
3419 
3420     connect(filterManager.data(), &FilterManager::ready, [this]()
3421     {
3422         if (filterPositionPending)
3423         {
3424             m_FocusState = FOCUS_IDLE;
3425             filterPositionPending = false;
3426             captureAndSolve();
3427         }
3428     }
3429            );
3430 
3431     connect(filterManager.data(), &FilterManager::failed, [this]()
3432     {
3433         appendLogText(i18n("Filter operation failed."));
3434         abort();
3435     }
3436            );
3437 
3438     connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState)
3439     {
3440         if (filterPositionPending)
3441         {
3442             switch (filterState)
3443             {
3444                 case FILTER_OFFSET:
3445                     appendLogText(i18n("Changing focus offset by %1 steps...", filterManager->getTargetFilterOffset()));
3446                     break;
3447 
3448                 case FILTER_CHANGE:
3449                 {
3450                     const int filterComboIndex = filterManager->getTargetFilterPosition() - 1;
3451                     if (filterComboIndex >= 0 && filterComboIndex < FilterPosCombo->count())
3452                         appendLogText(i18n("Changing filter to %1...", FilterPosCombo->itemText(filterComboIndex)));
3453                 }
3454                 break;
3455 
3456                 case FILTER_AUTOFOCUS:
3457                     appendLogText(i18n("Auto focus on filter change..."));
3458                     break;
3459 
3460                 default:
3461                     break;
3462             }
3463         }
3464     });
3465 
3466     connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]()
3467     {
3468         checkFilter();
3469     });
3470     connect(filterManager.data(), &FilterManager::positionChanged, this, [this]()
3471     {
3472         checkFilter();
3473     });
3474 }
3475 
getEffectiveFOV()3476 QVariantMap Align::getEffectiveFOV()
3477 {
3478     KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs);
3479 
3480     fov_x = fov_y = 0;
3481 
3482     for (auto &map : effectiveFOVs)
3483     {
3484         if (map["Profile"].toString() == m_ActiveProfile->name)
3485         {
3486             if (map["Width"].toInt() == ccd_width &&
3487                     map["Height"].toInt() == ccd_height &&
3488                     map["PixelW"].toDouble() == ccd_hor_pixel &&
3489                     map["PixelH"].toDouble() == ccd_ver_pixel &&
3490                     map["FocalLength"].toDouble() == focal_length)
3491             {
3492                 fov_x = map["FovW"].toDouble();
3493                 fov_y = map["FovH"].toDouble();
3494                 return map;
3495             }
3496         }
3497     }
3498 
3499     return QVariantMap();
3500 }
3501 
saveNewEffectiveFOV(double newFOVW,double newFOVH)3502 void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH)
3503 {
3504     if (newFOVW < 0 || newFOVH < 0 || (newFOVW == fov_x && newFOVH == fov_y))
3505         return;
3506 
3507     QVariantMap effectiveMap = getEffectiveFOV();
3508 
3509     // If ID exists, delete it first.
3510     if (effectiveMap.isEmpty() == false)
3511         KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString());
3512 
3513     // If FOV is 0x0, then we just remove existing effective FOV
3514     if (newFOVW == 0.0 && newFOVH == 0.0)
3515     {
3516         calculateFOV();
3517         return;
3518     }
3519 
3520     effectiveMap["Profile"] = m_ActiveProfile->name;
3521     effectiveMap["Width"] = ccd_width;
3522     effectiveMap["Height"] = ccd_height;
3523     effectiveMap["PixelW"] = ccd_hor_pixel;
3524     effectiveMap["PixelH"] = ccd_ver_pixel;
3525     effectiveMap["FocalLength"] = focal_length;
3526     effectiveMap["FovW"] = newFOVW;
3527     effectiveMap["FovH"] = newFOVH;
3528 
3529     KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap);
3530 
3531     calculateFOV();
3532 
3533 }
3534 
zoomAlignView()3535 void Align::zoomAlignView()
3536 {
3537     alignView->ZoomDefault();
3538 
3539     emit newFrame(alignView);
3540 }
3541 
setAlignZoom(double scale)3542 void Align::setAlignZoom(double scale)
3543 {
3544     if (scale > 1)
3545         alignView->ZoomIn();
3546     else if (scale < 1)
3547         alignView->ZoomOut();
3548 
3549     emit newFrame(alignView);
3550 }
3551 
getSettings() const3552 QJsonObject Align::getSettings() const
3553 {
3554     QJsonObject settings;
3555 
3556     double gain = -1;
3557     if (GainSpinSpecialValue > INVALID_VALUE && GainSpin->value() > GainSpinSpecialValue)
3558         gain = GainSpin->value();
3559     else if (currentCCD && currentCCD->hasGain())
3560         currentCCD->getGain(&gain);
3561 
3562     settings.insert("camera", CCDCaptureCombo->currentText());
3563     settings.insert("fw", FilterDevicesCombo->currentText());
3564     settings.insert("filter", FilterPosCombo->currentText());
3565     settings.insert("exp", exposureIN->value());
3566     settings.insert("bin", qMax(1, binningCombo->currentIndex() + 1));
3567     settings.insert("solverAction", gotoModeButtonGroup->checkedId());
3568     settings.insert("scopeType", FOVScopeCombo->currentIndex());
3569     settings.insert("gain", gain);
3570     settings.insert("iso", ISOCombo->currentIndex());
3571     settings.insert("accuracy", accuracySpin->value());
3572     settings.insert("settle", delaySpin->value());
3573     settings.insert("dark", alignDarkFrameCheck->isChecked());
3574 
3575     return settings;
3576 }
3577 
setSettings(const QJsonObject & settings)3578 void Align::setSettings(const QJsonObject &settings)
3579 {
3580     static bool init = false;
3581 
3582     auto syncControl = [settings](const QString & key, QWidget * widget)
3583     {
3584         QSpinBox *pSB = nullptr;
3585         QDoubleSpinBox *pDSB = nullptr;
3586         QCheckBox *pCB = nullptr;
3587         QComboBox *pComboBox = nullptr;
3588 
3589         if ((pSB = qobject_cast<QSpinBox *>(widget)))
3590         {
3591             const int value = settings[key].toInt(pSB->value());
3592             if (value != pSB->value())
3593             {
3594                 pSB->setValue(value);
3595                 return true;
3596             }
3597         }
3598         else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
3599         {
3600             const double value = settings[key].toDouble(pDSB->value());
3601             if (value != pDSB->value())
3602             {
3603                 pDSB->setValue(value);
3604                 return true;
3605             }
3606         }
3607         else if ((pCB = qobject_cast<QCheckBox *>(widget)))
3608         {
3609             const bool value = settings[key].toBool(pCB->isChecked());
3610             if (value != pCB->isChecked())
3611             {
3612                 pCB->setChecked(value);
3613                 return true;
3614             }
3615         }
3616         // ONLY FOR STRINGS, not INDEX
3617         else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
3618         {
3619             const QString value = settings[key].toString(pComboBox->currentText());
3620             if (value != pComboBox->currentText())
3621             {
3622                 pComboBox->setCurrentText(value);
3623                 return true;
3624             }
3625         }
3626 
3627         return false;
3628     };
3629 
3630     // Camera. If camera changed, check CCD.
3631     if (syncControl("camera", CCDCaptureCombo) || init == false)
3632         checkCCD();
3633     // Filter Wheel
3634     if (syncControl("fw", FilterDevicesCombo) || init == false)
3635         checkFilter();
3636     // Filter
3637     syncControl("filter", FilterPosCombo);
3638     Options::setLockAlignFilterIndex(FilterPosCombo->currentIndex());
3639     // Exposure
3640     syncControl("exp", exposureIN);
3641     // Binning
3642     const int bin = settings["bin"].toInt(binningCombo->currentIndex() + 1) - 1;
3643     if (bin != binningCombo->currentIndex())
3644         binningCombo->setCurrentIndex(bin);
3645     // Profiles
3646     const QString profileName = settings["sep"].toString();
3647     QStringList profiles = getStellarSolverProfiles();
3648     int profileIndex = getStellarSolverProfiles().indexOf(profileName);
3649     if (profileIndex >= 0 && profileIndex != static_cast<int>(Options::solveOptionsProfile()))
3650         Options::setSolveOptionsProfile(profileIndex);
3651 
3652     int solverAction = settings["solverAction"].toInt(gotoModeButtonGroup->checkedId());
3653     if (solverAction != gotoModeButtonGroup->checkedId())
3654     {
3655         gotoModeButtonGroup->button(solverAction)->setChecked(true);
3656         m_CurrentGotoMode = static_cast<GotoMode>(solverAction);
3657     }
3658 
3659     FOVScopeCombo->setCurrentIndex(settings["scopeType"].toInt(0));
3660 
3661     // Gain
3662     if (currentCCD->hasGain())
3663     {
3664         syncControl("gain", GainSpin);
3665     }
3666     // ISO
3667     if (ISOCombo->count() > 1)
3668     {
3669         const int iso = settings["iso"].toInt(ISOCombo->currentIndex());
3670         if (iso != ISOCombo->currentIndex())
3671             ISOCombo->setCurrentIndex(iso);
3672     }
3673     // Dark
3674     syncControl("dark", alignDarkFrameCheck);
3675     // Accuracy
3676     syncControl("accuracy", accuracySpin);
3677     // Settle
3678     syncControl("settle", delaySpin);
3679 
3680     init = true;
3681 }
3682 
syncSettings()3683 void Align::syncSettings()
3684 {
3685     emit settingsUpdated(getSettings());
3686 }
3687 
3688 
syncFOV()3689 void Align::syncFOV()
3690 {
3691     QString newFOV = FOVOut->text();
3692     QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)");
3693     QRegularExpressionMatch match = re.match(newFOV);
3694     if (match.hasMatch())
3695     {
3696         double newFOVW = match.captured(1).toDouble();
3697         double newFOVH = match.captured(2).toDouble();
3698 
3699         //if (newFOVW > 0 && newFOVH > 0)
3700         saveNewEffectiveFOV(newFOVW, newFOVH);
3701 
3702         FOVOut->setStyleSheet(QString());
3703     }
3704     else
3705     {
3706         KSNotification::error(i18n("Invalid FOV."));
3707         FOVOut->setStyleSheet("background-color:red");
3708     }
3709 }
3710 
3711 // m_wasSlewStarted can't be false for more than 10s after a slew starts.
didSlewStart()3712 bool Align::didSlewStart()
3713 {
3714     if (m_wasSlewStarted)
3715         return true;
3716     if (slewStartTimer.isValid() && slewStartTimer.elapsed() > MAX_WAIT_FOR_SLEW_START_MSEC)
3717     {
3718         qCDebug(KSTARS_EKOS_ALIGN) << "Slew timed out...waited > 10s, it must have started already.";
3719         return true;
3720     }
3721     return false;
3722 }
3723 
setTargetCoords(double ra0,double de0)3724 void Align::setTargetCoords(double ra0, double de0)
3725 {
3726     double maxrad = 1000.0 / Options::zoomFactor();
3727     SkyPoint target;
3728     target.setRA0(ra0);
3729     target.setDec0(de0);
3730     target.updateCoordsNow(KStarsData::Instance()->updateNum());
3731     setTarget(*KStarsData::Instance()->skyComposite()->objectNearest(&target, maxrad), target);
3732 }
3733 
setTarget(const SkyObject & targetObject,const SkyPoint & targetCoord)3734 void Align::setTarget(const SkyObject &targetObject, const SkyPoint &targetCoord)
3735 {
3736     Q_UNUSED(targetObject);
3737     m_targetCoord = targetCoord;
3738     m_targetCoordValid = true;
3739     qCInfo(KSTARS_EKOS_ALIGN) << "Target updated to JNow RA:" << m_targetCoord.ra().toHMSString()
3740                               << "DE:" << m_targetCoord.dec().toDMSString();
3741 }
3742 
getTargetCoords()3743 QList<double> Align::getTargetCoords()
3744 {
3745     QList<double> coord;
3746     // if a target has been set, take the target coordinates. Otherwise, take the coordinates the scope is pointing to currently
3747     if (m_targetCoordValid)
3748         coord << m_targetCoord.ra0().Hours() << m_targetCoord.dec0().Degrees();
3749     else
3750         coord << telescopeCoord.ra0().Hours() << telescopeCoord.dec0().Degrees();
3751 
3752     return coord;
3753 }
3754 
setTargetRotation(double rotation)3755 void Align::setTargetRotation(double rotation)
3756 {
3757     loadSlewTargetPA = rotation;
3758 
3759     qCDebug(KSTARS_EKOS_ALIGN) << "Target Rotation updated to: " << loadSlewTargetPA;
3760 }
3761 
calculateAlignTargetDiff()3762 void Align::calculateAlignTargetDiff()
3763 {
3764     if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) || matchPAHStage(PAA::PAH_SECOND_CAPTURE)
3765             || matchPAHStage(PAA::PAH_THIRD_CAPTURE))
3766         return;
3767     m_TargetDiffRA = (alignCoord.ra().deltaAngle(m_targetCoord.ra())).Degrees() * 3600;
3768     m_TargetDiffDE = (alignCoord.dec().deltaAngle(m_targetCoord.dec())).Degrees() * 3600;
3769 
3770     dms RADiff(fabs(m_TargetDiffRA) / 3600.0), DEDiff(m_TargetDiffDE / 3600.0);
3771     QString dRAText = QString("%1%2").arg((m_TargetDiffRA > 0 ? "+" : "-"), RADiff.toHMSString());
3772     QString dDEText = DEDiff.toDMSString(true);
3773 
3774     m_TargetDiffTotal = sqrt(m_TargetDiffRA * m_TargetDiffRA + m_TargetDiffDE * m_TargetDiffDE);
3775 
3776     errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg(
3777                         QString::number(m_TargetDiffTotal, 'f', 0),
3778                         QString::number(m_TargetDiffRA, 'f', 0),
3779                         QString::number(m_TargetDiffDE, 'f', 0)));
3780     if (m_TargetDiffTotal <= static_cast<double>(accuracySpin->value()))
3781         errOut->setStyleSheet("color:green");
3782     else if (m_TargetDiffTotal < 1.5 * accuracySpin->value())
3783         errOut->setStyleSheet("color:yellow");
3784     else
3785         errOut->setStyleSheet("color:red");
3786 
3787     //This block of code will write the result into the solution table and plot it on the graph.
3788     int currentRow = solutionTable->rowCount() - 1;
3789     QTableWidgetItem *dRAReport = new QTableWidgetItem();
3790     if (dRAReport)
3791     {
3792         dRAReport->setText(QString::number(m_TargetDiffRA, 'f', 3) + "\"");
3793         dRAReport->setTextAlignment(Qt::AlignHCenter);
3794         dRAReport->setFlags(Qt::ItemIsSelectable);
3795         solutionTable->setItem(currentRow, 4, dRAReport);
3796     }
3797 
3798     QTableWidgetItem *dDECReport = new QTableWidgetItem();
3799     if (dDECReport)
3800     {
3801         dDECReport->setText(QString::number(m_TargetDiffDE, 'f', 3) + "\"");
3802         dDECReport->setTextAlignment(Qt::AlignHCenter);
3803         dDECReport->setFlags(Qt::ItemIsSelectable);
3804         solutionTable->setItem(currentRow, 5, dDECReport);
3805     }
3806 
3807     double raPlot  = m_TargetDiffRA;
3808     double decPlot = m_TargetDiffDE;
3809     alignPlot->graph(0)->addData(raPlot, decPlot);
3810 
3811     QCPItemText *textLabel = new QCPItemText(alignPlot);
3812     textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter);
3813 
3814     textLabel->position->setType(QCPItemPosition::ptPlotCoords);
3815     textLabel->position->setCoords(raPlot, decPlot);
3816     textLabel->setColor(Qt::red);
3817     textLabel->setPadding(QMargins(0, 0, 0, 0));
3818     textLabel->setBrush(Qt::white);
3819     textLabel->setPen(Qt::NoPen);
3820     textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' ');
3821     textLabel->setFont(QFont(font().family(), 8));
3822 
3823     if (!alignPlot->xAxis->range().contains(m_TargetDiffRA))
3824     {
3825         alignPlot->graph(0)->rescaleKeyAxis(true);
3826         alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
3827     }
3828     if (!alignPlot->yAxis->range().contains(m_TargetDiffDE))
3829     {
3830         alignPlot->graph(0)->rescaleValueAxis(true);
3831         alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
3832     }
3833     alignPlot->replot();
3834 }
3835 
updateTargetCoords()3836 void Align::updateTargetCoords()
3837 {
3838     // if target has already been set, we're done
3839     if (m_targetCoordValid)
3840         return;
3841     else
3842     {
3843         // otherwise, the best guess is the position the mount reports
3844         m_targetCoord = telescopeCoord;
3845         m_targetCoordValid = true;
3846         qCDebug(KSTARS_EKOS_ALIGN) << "No target set, using mount Coordinates JNow RA:" << telescopeCoord.ra().toHMSString()
3847                                    << "DE:" << telescopeCoord.dec().toDMSString();
3848     }
3849 }
3850 
getStellarSolverProfiles()3851 QStringList Align::getStellarSolverProfiles()
3852 {
3853     QStringList profiles;
3854     for (auto param : m_StellarSolverProfiles)
3855         profiles << param.listName;
3856 
3857     return profiles;
3858 }
3859 
exportSolutionPoints()3860 void Align::exportSolutionPoints()
3861 {
3862     if (solutionTable->rowCount() == 0)
3863         return;
3864 
3865     QUrl exportFile = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Export Solution Points"),
3866                       alignURLPath,
3867                       "CSV File (*.csv)");
3868     if (exportFile.isEmpty()) // if user presses cancel
3869         return;
3870     if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false)
3871         exportFile.setPath(exportFile.toLocalFile() + ".csv");
3872 
3873     QString path = exportFile.toLocalFile();
3874 
3875     if (QFile::exists(path))
3876     {
3877         int r = KMessageBox::warningContinueCancel(nullptr,
3878                 i18n("A file named \"%1\" already exists. "
3879                      "Overwrite it?",
3880                      exportFile.fileName()),
3881                 i18n("Overwrite File?"), KStandardGuiItem::overwrite());
3882         if (r == KMessageBox::Cancel)
3883             return;
3884     }
3885 
3886     if (!exportFile.isValid())
3887     {
3888         QString message = i18n("Invalid URL: %1", exportFile.url());
3889         KSNotification::sorry(message, i18n("Invalid URL"));
3890         return;
3891     }
3892 
3893     QFile file;
3894     file.setFileName(path);
3895     if (!file.open(QIODevice::WriteOnly))
3896     {
3897         QString message = i18n("Unable to write to file %1", path);
3898         KSNotification::sorry(message, i18n("Could Not Open File"));
3899         return;
3900     }
3901 
3902     QTextStream outstream(&file);
3903 
3904     QString epoch = QString::number(KStarsDateTime::currentDateTime().epoch());
3905 
3906     outstream << "RA (J" << epoch << "),DE (J" << epoch
3907               << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << endl;
3908 
3909     for (int i = 0; i < solutionTable->rowCount(); i++)
3910     {
3911         QTableWidgetItem *raCell      = solutionTable->item(i, 0);
3912         QTableWidgetItem *deCell      = solutionTable->item(i, 1);
3913         QTableWidgetItem *objNameCell = solutionTable->item(i, 2);
3914         QTableWidgetItem *raErrorCell = solutionTable->item(i, 4);
3915         QTableWidgetItem *deErrorCell = solutionTable->item(i, 5);
3916 
3917         if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell)
3918         {
3919             KSNotification::sorry(i18n("Error in table structure."));
3920             return;
3921         }
3922         dms raDMS = dms::fromString(raCell->text(), false);
3923         dms deDMS = dms::fromString(deCell->text(), true);
3924         outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ','
3925                   << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ','
3926                   << deErrorCell->text().remove('\"') << endl;
3927     }
3928     emit newLog(i18n("Solution Points Saved as: %1", path));
3929     file.close();
3930 }
3931 
initPolarAlignmentAssistant()3932 void Align::initPolarAlignmentAssistant()
3933 {
3934     // Create PAA instance
3935     m_PolarAlignmentAssistant = new PolarAlignmentAssistant(this, alignView);
3936     connect(m_PolarAlignmentAssistant, &Ekos::PAA::captureAndSolve, this, &Ekos::Align::captureAndSolve);
3937     connect(m_PolarAlignmentAssistant, &Ekos::PAA::settleStarted, [this](double duration)
3938     {
3939         m_CaptureTimer.start(duration);
3940     });
3941     connect(m_PolarAlignmentAssistant, &Ekos::PAA::newAlignTableResult, this, &Ekos::Align::setAlignTableResult);
3942     connect(m_PolarAlignmentAssistant, &Ekos::PAA::newFrame, this, &Ekos::Align::newFrame);
3943     connect(m_PolarAlignmentAssistant, &Ekos::PAA::newPAHStage, this, &Ekos::Align::processPAHStage);
3944     connect(m_PolarAlignmentAssistant, &Ekos::PAA::newLog, this, &Ekos::Align::appendLogText);
3945 
3946     tabWidget->addTab(m_PolarAlignmentAssistant, i18n("Polar Alignment"));
3947 }
3948 
initManualRotator()3949 void Align::initManualRotator()
3950 {
3951     if (m_ManualRotator)
3952         return;
3953 
3954     m_ManualRotator = new ManualRotator(this);
3955     connect(m_ManualRotator, &Ekos::ManualRotator::captureAndSolve, this, &Ekos::Align::captureAndSolve);
3956 }
3957 
initDarkProcessor()3958 void Align::initDarkProcessor()
3959 {
3960     if (m_DarkProcessor)
3961         return;
3962 
3963     m_DarkProcessor = new DarkProcessor(this);
3964     connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Align::appendLogText);
3965     connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
3966     {
3967         alignDarkFrameCheck->setChecked(completed);
3968         if (completed)
3969         {
3970             alignView->rescale(ZOOM_KEEP_LEVEL);
3971             alignView->updateFrame();
3972         }
3973 
3974         setCaptureComplete();
3975     });
3976 }
3977 
processPAHStage(int stage)3978 void Align::processPAHStage(int stage)
3979 {
3980     switch (stage)
3981     {
3982         case PAA::PAH_POST_REFRESH:
3983         {
3984             Options::setAstrometrySolverWCS(rememberSolverWCS);
3985             Options::setAutoWCS(rememberAutoWCS);
3986             stop(ALIGN_IDLE);
3987         }
3988         break;
3989         case PAA::PAH_PRE_REFRESH:
3990             // If user stops here, we restore the settings, if not we
3991             // disable again in the refresh process
3992             // and restore when refresh is complete
3993             Options::setAstrometrySolverWCS(rememberSolverWCS);
3994             Options::setAutoWCS(rememberAutoWCS);
3995             break;
3996         case PAA::PAH_FIRST_CAPTURE:
3997             nothingR->setChecked(true);
3998             m_CurrentGotoMode = GOTO_NOTHING;
3999             loadSlewB->setEnabled(false);
4000 
4001             rememberSolverWCS = Options::astrometrySolverWCS();
4002             rememberAutoWCS   = Options::autoWCS();
4003 
4004             Options::setAutoWCS(false);
4005             Options::setAstrometrySolverWCS(true);
4006             break;
4007         case PAA::PAH_SECOND_CAPTURE:
4008         case PAA::PAH_THIRD_CAPTURE:
4009             if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY)
4010                 emit newLog(i18n("Settling..."));
4011             m_CaptureTimer.start(delaySpin->value());
4012             break;
4013 
4014         default:
4015             break;
4016     }
4017 }
4018 
matchPAHStage(uint32_t stage)4019 bool Align::matchPAHStage(uint32_t stage)
4020 {
4021     return m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->getPAHStage() == stage;
4022 }
4023 }
4024