1 /*  Ekos Polar Alignment Assistant Tool
2     SPDX-FileCopyrightText: 2018-2021 Jasem Mutlaq
3     SPDX-FileCopyrightText: 2020-2021 Hy Murveit
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6  */
7 
8 #include "polaralignmentassistant.h"
9 
10 #include "align.h"
11 #include "kstars.h"
12 #include "kstarsdata.h"
13 #include "ksnotification.h"
14 #include "ksmessagebox.h"
15 #include "ekos/auxiliary/stellarsolverprofile.h"
16 
17 // Options
18 #include "Options.h"
19 
20 
21 #include "QProgressIndicator.h"
22 
23 #include <ekos_align_debug.h>
24 
25 #define PAA_VERSION "v2.3"
26 
27 namespace Ekos
28 {
29 
30 const QMap<PolarAlignmentAssistant::PAHStage, QString> PolarAlignmentAssistant::PAHStages =
31 {
32     {PAH_IDLE, I18N_NOOP("Idle")},
33     {PAH_FIRST_CAPTURE, I18N_NOOP("First Capture"}),
34     {PAH_FIND_CP, I18N_NOOP("Finding CP"}),
35     {PAH_FIRST_ROTATE, I18N_NOOP("First Rotation"}),
36     {PAH_SECOND_CAPTURE, I18N_NOOP("Second Capture"}),
37     {PAH_SECOND_ROTATE, I18N_NOOP("Second Rotation"}),
38     {PAH_THIRD_CAPTURE, I18N_NOOP("Third Capture"}),
39     {PAH_STAR_SELECT, I18N_NOOP("Select Star"}),
40     {PAH_PRE_REFRESH, I18N_NOOP("Select Refresh"}),
41     {PAH_REFRESH, I18N_NOOP("Refreshing"}),
42     {PAH_POST_REFRESH, I18N_NOOP("Refresh Complete"}),
43     {PAH_ERROR, I18N_NOOP("Error")},
44 };
45 
46 PolarAlignmentAssistant::PolarAlignmentAssistant(Align *parent, AlignView *view) : QWidget(parent)
47 {
48     setupUi(this);
49 
50     m_AlignInstance = parent;
51     alignView = view;
52 
53     connect(PAHSlewRateCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), [&](int index)
54     {
55         Options::setPAHMountSpeedIndex(index);
56     });
57 
58     PAHUpdatedErrorLine->setVisible(Options::pAHRefreshUpdateError());
59     PAHRefreshUpdateError->setChecked(Options::pAHRefreshUpdateError());
60     connect(PAHRefreshUpdateError, &QCheckBox::toggled, [this](bool toggled)
61     {
62         Options::setPAHRefreshUpdateError(toggled);
63         PAHUpdatedErrorLine->setVisible(toggled);
64     });
65 
66     // PAH Connections
67     PAHWidgets->setCurrentWidget(PAHIntroPage);
68     connect(this, &PolarAlignmentAssistant::PAHEnabled, [&](bool enabled)
69     {
70         PAHStartB->setEnabled(enabled);
71         directionLabel->setEnabled(enabled);
72         PAHDirectionCombo->setEnabled(enabled);
73         PAHRotationSpin->setEnabled(enabled);
74         PAHSlewRateCombo->setEnabled(enabled);
75         PAHManual->setEnabled(enabled);
76     });
77     connect(PAHStartB, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::startPAHProcess);
78     // PAH StopB is just a shortcut for the regular stop
79     connect(PAHStopB, &QPushButton::clicked, this, &PolarAlignmentAssistant::stopPAHProcess);
80     connect(PAHCorrectionsNextB, &QPushButton::clicked, this,
81             &Ekos::PolarAlignmentAssistant::setPAHCorrectionSelectionComplete);
82     connect(PAHRefreshB, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::startPAHRefreshProcess);
83     connect(PAHDoneB, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::setPAHRefreshComplete);
84     // done button for manual slewing during polar alignment:
85     connect(PAHManualDone, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::setPAHSlewDone);
86 
87     hemisphere = KStarsData::Instance()->geo()->lat()->Degrees() > 0 ? NORTH_HEMISPHERE : SOUTH_HEMISPHERE;
88 
89 }
90 
91 PolarAlignmentAssistant::~PolarAlignmentAssistant()
92 {
93 
94 }
95 
96 void PolarAlignmentAssistant::syncMountSpeed()
97 {
98     PAHSlewRateCombo->blockSignals(true);
99     PAHSlewRateCombo->clear();
100     PAHSlewRateCombo->addItems(m_CurrentTelescope->slewRates());
101     const uint16_t configMountSpeed = Options::pAHMountSpeedIndex();
102     if (configMountSpeed < PAHSlewRateCombo->count())
103         PAHSlewRateCombo->setCurrentIndex(configMountSpeed);
104     else
105     {
106         int currentSlewRateIndex = m_CurrentTelescope->getSlewRate();
107         if (currentSlewRateIndex >= 0)
108         {
109             PAHSlewRateCombo->setCurrentIndex(currentSlewRateIndex);
110             Options::setPAHMountSpeedIndex(currentSlewRateIndex);
111         }
112     }
113     PAHSlewRateCombo->blockSignals(false);
114 }
115 
116 void PolarAlignmentAssistant::setEnabled(bool enabled)
117 {
118     QWidget::setEnabled(enabled);
119 
120     emit PAHEnabled(enabled);
121     if (enabled)
122     {
123         PAHWidgets->setToolTip(QString());
124         FOVDisabledLabel->hide();
125     }
126     else
127     {
128         PAHWidgets->setToolTip(i18n(
129                                    "<p>Polar Alignment Helper tool requires the following:</p><p>1. German Equatorial Mount</p><p>2. FOV &gt;"
130                                    " 0.5 degrees</p><p>For small FOVs, use the Legacy Polar Alignment Tool.</p>"));
131         FOVDisabledLabel->show();
132     }
133 
134 }
135 
136 void PolarAlignmentAssistant::syncStage()
137 {
138     if (m_PAHStage == PAH_FIRST_CAPTURE)
139         PAHWidgets->setCurrentWidget(PAHFirstCapturePage);
140     else if (m_PAHStage == PAH_SECOND_CAPTURE)
141         PAHWidgets->setCurrentWidget(PAHSecondCapturePage);
142     else if (m_PAHStage == PAH_THIRD_CAPTURE)
143         PAHWidgets->setCurrentWidget(PAHThirdCapturePage);
144 
145 }
146 
147 bool PolarAlignmentAssistant::detectStarsPAHRefresh(QList<Edge> *stars, int num, int x, int y, int *starIndex)
148 {
149     stars->clear();
150     *starIndex = -1;
151 
152     // Use the solver settings from the align tab for for "polar-align refresh" star detection.
153     QVariantMap settings;
154     settings["optionsProfileIndex"] = Options::solveOptionsProfile();
155     settings["optionsProfileGroup"] = static_cast<int>(Ekos::AlignProfiles);
156     m_ImageData->setSourceExtractorSettings(settings);
157 
158     QElapsedTimer timer;
159     m_ImageData->findStars(ALGORITHM_SEP).waitForFinished();
160 
161     QString debugString = QString("PAA Refresh: Detected %1 stars (%2s)")
162                           .arg(m_ImageData->getStarCenters().size()).arg(timer.elapsed() / 1000.0, 5, 'f', 3);
163     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
164 
165     QList<Edge *> detectedStars = m_ImageData->getStarCenters();
166     // Let's sort detectedStars by flux, starting with widest
167     std::sort(detectedStars.begin(), detectedStars.end(), [](const Edge * edge1, const Edge * edge2) -> bool { return edge1->sum > edge2->sum;});
168 
169     // Find the closest star to the x,y position, which is where the user clicked on the alignView.
170     double bestDist = 1e9;
171     int bestIndex = -1;
172     for (int i = 0; i < detectedStars.size(); i++)
173     {
174         double dx = detectedStars[i]->x - x;
175         double dy = detectedStars[i]->y - y;
176         double dist = dx * dx + dy * dy;
177         if (dist < bestDist)
178         {
179             bestDist = dist;
180             bestIndex = i;
181         }
182     }
183 
184     int starCount = qMin(num, detectedStars.count());
185     for (int i = 0; i < starCount; i++)
186         stars->append(*(detectedStars[i]));
187     if (bestIndex >= starCount)
188     {
189         // If we found the star, but requested 'num' stars, and the user's star
190         // is lower down in the list, add it and return num+1 stars.
191         stars->append(*(detectedStars[bestIndex]));
192         *starIndex = starCount;
193     }
194     else
195     {
196         *starIndex = bestIndex;
197     }
198     debugString = QString("PAA Refresh: User's star(%1,%2) is index %3").arg(x).arg(y).arg(*starIndex);
199     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
200 
201     detectedStars.clear();
202 
203     return stars->count();
204 }
205 
206 void PolarAlignmentAssistant::processPAHRefresh()
207 {
208     alignView->setStarCircle();
209     PAHUpdatedErrorTotal->clear();
210     PAHUpdatedErrorAlt->clear();
211     PAHUpdatedErrorAz->clear();
212     QString debugString;
213     // Always run on the initial iteration to setup the user's star,
214     // so that if it is enabled later the star could be tracked.
215     // Flaw here is that if enough stars are not detected, iteration is not incremented,
216     // so it may repeat.
217     if (Options::pAHRefreshUpdateError() || (refreshIteration == 0))
218     {
219         constexpr int MIN_PAH_REFRESH_STARS = 10;
220 
221         QList<Edge> stars;
222         // Index of user's star in the detected stars list. In the first iteration
223         // the stars haven't moved and we can just use the location of the click.
224         // Later we'll need to find the star with starCorrespondence.
225         int clickedStarIndex;
226         detectStarsPAHRefresh(&stars, 100, correctionFrom.x(), correctionFrom.y(), &clickedStarIndex);
227         if (clickedStarIndex < 0)
228         {
229             debugString = QString("PAA Refresh(%1): Didn't find the clicked star near %2,%3")
230                           .arg(refreshIteration).arg(correctionFrom.x()).arg(correctionFrom.y());
231             qCDebug(KSTARS_EKOS_ALIGN) << debugString;
232 
233             emit newAlignTableResult(Align::ALIGN_RESULT_FAILED);
234             emit captureAndSolve();
235             return;
236         }
237 
238         debugString = QString("PAA Refresh(%1): Refresh star(%2,%3) is index %4 with offset %5 %6")
239                       .arg(refreshIteration + 1).arg(correctionFrom.x(), 4, 'f', 0)
240                       .arg(correctionFrom.y(), 4, 'f', 0).arg(clickedStarIndex)
241                       .arg(stars[clickedStarIndex].x - correctionFrom.x(), 4, 'f', 0)
242                       .arg(stars[clickedStarIndex].y - correctionFrom.y(), 4, 'f', 0);
243         qCDebug(KSTARS_EKOS_ALIGN) << debugString;
244 
245         if (stars.size() > MIN_PAH_REFRESH_STARS)
246         {
247             int dx = 0;
248             int dy = 0;
249             int starIndex = -1;
250 
251             if (refreshIteration++ == 0)
252             {
253                 // First iteration. Setup starCorrespondence so we can find the user's star.
254                 // clickedStarIndex should be the index of a detected star near where the user clicked.
255                 starCorrespondencePAH.initialize(stars, clickedStarIndex);
256                 if (clickedStarIndex >= 0)
257                 {
258                     setupCorrectionGraphics(QPointF(stars[clickedStarIndex].x, stars[clickedStarIndex].y));
259                     emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
260                     emit newFrame(alignView);
261                 }
262             }
263             else
264             {
265                 // Or, in other iterations find the movement of the "user's star".
266                 // The 0.40 means it's OK if star correspondence only finds 40% of the
267                 // reference stars (as we'd have more issues near the image edge otherwise).
268                 QVector<int> starMap;
269                 starCorrespondencePAH.find(stars, 200.0, &starMap, false, 0.40);
270 
271                 // Go through the starMap, and find the user's star, and compare its position
272                 // to its initial position.
273                 for (int i = 0; i < starMap.size(); ++i)
274                 {
275                     if (starMap[i] == starCorrespondencePAH.guideStar())
276                     {
277                         dx = stars[i].x - correctionFrom.x();
278                         dy = stars[i].y - correctionFrom.y();
279                         starIndex = i;
280                         break;
281                     }
282                 }
283                 if (starIndex == -1)
284                 {
285                     bool allOnes = true;
286                     for (int i = 0; i < starMap.size(); ++i)
287                     {
288                         if (starMap[i] != -1)
289                             allOnes = false;
290                     }
291                     debugString = QString("PAA Refresh(%1): starMap %2").arg(refreshIteration).arg(allOnes ? "ALL -1's" : "not all -1's");
292                     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
293                 }
294             }
295 
296             if (starIndex >= 0)
297             {
298                 // Annotate the user's star on the alignview.
299                 alignView->setStarCircle(QPointF(stars[starIndex].x, stars[starIndex].y));
300                 debugString = QString("PAA Refresh(%1): User's star is now at %2,%3, with movement = %4,%5").arg(refreshIteration)
301                               .arg(stars[starIndex].x, 4, 'f', 0).arg(stars[starIndex].y, 4, 'f', 0).arg(dx).arg(dy);
302                 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
303 
304                 double azE, altE;
305                 if (polarAlign.pixelError(alignView->keptImage(), QPointF(stars[starIndex].x, stars[starIndex].y),
306                                           correctionTo, &azE, &altE))
307                 {
308                     const double errDegrees = hypot(azE, altE);
309                     dms totalError(errDegrees), azError(azE), altError(altE);
310                     PAHUpdatedErrorTotal->setText(totalError.toDMSString());
311                     PAHUpdatedErrorAlt->setText(altError.toDMSString());
312                     PAHUpdatedErrorAz->setText(azError.toDMSString());
313                     constexpr double oneArcMin = 1.0 / 60.0;
314                     PAHUpdatedErrorTotal->setStyleSheet(
315                         errDegrees < oneArcMin ? "color:green" : (errDegrees < 2 * oneArcMin ? "color:yellow" : "color:red"));
316                     PAHUpdatedErrorAlt->setStyleSheet(
317                         fabs(altE) < oneArcMin ? "color:green" : (fabs(altE) < 2 * oneArcMin ? "color:yellow" : "color:red"));
318                     PAHUpdatedErrorAz->setStyleSheet(
319                         fabs(azE) < oneArcMin ? "color:green" : (fabs(azE) < 2 * oneArcMin ? "color:yellow" : "color:red"));
320 
321                     debugString = QString("PAA Refresh(%1): %2,%3 --> %4,%5 @ %6,%7. Corrected az: %8 (%9) alt: %10 (%11) total: %12 (%13)")
322                                   .arg(refreshIteration).arg(correctionFrom.x(), 4, 'f', 0).arg(correctionFrom.y(), 4, 'f', 0)
323                                   .arg(correctionTo.x(), 4, 'f', 0).arg(correctionTo.y(), 4, 'f', 0)
324                                   .arg(stars[starIndex].x, 4, 'f', 0).arg(stars[starIndex].y, 4, 'f', 0)
325                                   .arg(azError.toDMSString()).arg(azE, 5, 'f', 3)
326                                   .arg(altError.toDMSString()).arg(altE, 6, 'f', 3)
327                                   .arg(totalError.toDMSString()).arg(hypot(azE, altE), 6, 'f', 3);
328                     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
329                 }
330                 else
331                 {
332                     debugString = QString("PAA Refresh(%1): pixelError failed to estimate the remaining correction").arg(refreshIteration);
333                     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
334                 }
335             }
336             else
337             {
338                 if (refreshIteration > 1)
339                 {
340                     debugString = QString("PAA Refresh(%1): Didn't find the user's star").arg(refreshIteration);
341                     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
342                 }
343             }
344         }
345         else
346         {
347             debugString = QString("PAA Refresh(%1): Too few stars detected (%2)").arg(refreshIteration).arg(stars.size());
348             qCDebug(KSTARS_EKOS_ALIGN) << debugString;
349         }
350     }
351     // Finally start the next capture
352     emit captureAndSolve();
353 }
354 
355 bool PolarAlignmentAssistant::processSolverFailure()
356 {
357     if ((m_PAHStage == PAH_FIRST_CAPTURE || m_PAHStage == PAH_SECOND_CAPTURE || m_PAHStage == PAH_THIRD_CAPTURE)
358             && ++m_PAHRetrySolveCounter < 4)
359     {
360         emit captureAndSolve();
361         return true;
362     }
363 
364     if (m_PAHStage != PAH_IDLE)
365     {
366         emit newLog(i18n("PAA: Stopping, solver failed too many times."));
367         stopPAHProcess();
368     }
369 
370     return false;
371 }
372 
373 void PolarAlignmentAssistant::setPAHStage(PAHStage stage)
374 {
375     if (stage != m_PAHStage)
376     {
377         m_PAHStage = stage;
378         emit newPAHStage(m_PAHStage);
379     }
380 }
381 
382 void PolarAlignmentAssistant::processMountRotation(const dms &ra, double settleDuration)
383 {
384     double deltaAngle = fabs(ra.deltaAngle(targetPAH.ra()).Degrees());
385 
386     if (m_PAHStage == PAH_FIRST_ROTATE)
387     {
388         // only wait for telescope to slew to new position if manual slewing is switched off
389         if(!PAHManual->isChecked())
390         {
391             qCDebug(KSTARS_EKOS_ALIGN) << "First mount rotation remaining degrees:" << deltaAngle;
392             if (deltaAngle <= PAH_ROTATION_THRESHOLD)
393             {
394                 m_CurrentTelescope->StopWE();
395                 emit newLog(i18n("Mount first rotation is complete."));
396 
397                 m_PAHStage = PAH_SECOND_CAPTURE;
398                 emit newPAHStage(m_PAHStage);
399 
400                 PAHWidgets->setCurrentWidget(PAHSecondCapturePage);
401                 emit newPAHMessage(secondCaptureText->text());
402 
403                 if (settleDuration >= 0)
404                 {
405                     PAHWidgets->setCurrentWidget(PAHFirstSettlePage);
406                     emit newLog(i18n("Settling..."));
407                     QTimer::singleShot(settleDuration, [this]()
408                     {
409                         PAHWidgets->setCurrentWidget(PAHSecondCapturePage);
410                         emit newPAHMessage(secondCaptureText->text());
411                     });
412                 }
413 
414                 emit settleStarted(settleDuration);
415             }
416             // If for some reason we didn't stop, let's stop if we get too far
417             else if (deltaAngle > PAHRotationSpin->value() * 1.25)
418             {
419                 m_CurrentTelescope->Abort();
420                 emit newLog(i18n("Mount aborted. Please restart the process and reduce the speed."));
421                 stopPAHProcess();
422             }
423             return;
424         } // endif not manual slew
425     }
426     else if (m_PAHStage == PAH_SECOND_ROTATE)
427     {
428         // only wait for telescope to slew to new position if manual slewing is switched off
429         if(!PAHManual->isChecked())
430         {
431 
432             qCDebug(KSTARS_EKOS_ALIGN) << "Second mount rotation remaining degrees:" << deltaAngle;
433             if (deltaAngle <= PAH_ROTATION_THRESHOLD)
434             {
435                 m_CurrentTelescope->StopWE();
436                 emit newLog(i18n("Mount second rotation is complete."));
437 
438                 m_PAHStage = PAH_THIRD_CAPTURE;
439                 emit newPAHStage(m_PAHStage);
440 
441 
442                 PAHWidgets->setCurrentWidget(PAHThirdCapturePage);
443                 emit newPAHMessage(thirdCaptureText->text());
444 
445                 if (settleDuration >= 0)
446                 {
447                     PAHWidgets->setCurrentWidget(PAHSecondSettlePage);
448                     emit newLog(i18n("Settling..."));
449                     QTimer::singleShot(settleDuration, [this]()
450                     {
451                         PAHWidgets->setCurrentWidget(PAHThirdCapturePage);
452                         emit newPAHMessage(thirdCaptureText->text());
453                     });
454                 }
455 
456                 emit settleStarted(settleDuration);
457             }
458             // If for some reason we didn't stop, let's stop if we get too far
459             else if (deltaAngle > PAHRotationSpin->value() * 1.25)
460             {
461                 m_CurrentTelescope->Abort();
462                 emit newLog(i18n("Mount aborted. Please restart the process and reduce the speed."));
463                 stopPAHProcess();
464             }
465             return;
466         } // endif not manual slew
467     }
468 }
469 
470 bool PolarAlignmentAssistant::checkPAHForMeridianCrossing()
471 {
472     // Make sure using -180 to 180 for hourAngle and DEC. (Yes dec should be between -90 and 90).
473     double hourAngle = m_CurrentTelescope->hourAngle().Degrees();
474     while (hourAngle < -180)
475         hourAngle += 360;
476     while (hourAngle > 180)
477         hourAngle -= 360;
478     double ra = 0, dec = 0;
479     m_CurrentTelescope->getEqCoords(&ra, &dec);
480     while (dec < -180)
481         dec += 360;
482     while (dec > 180)
483         dec -= 360;
484 
485     // Don't do this check within 2 degrees of the poles.
486     bool nearThePole = fabs(dec) > 88;
487     if (nearThePole)
488         return false;
489 
490     double degreesPerSlew = PAHRotationSpin->value();
491     bool closeToMeridian = fabs(hourAngle) < 2.0 * degreesPerSlew;
492     bool goingWest = PAHDirectionCombo->currentIndex() == 0;
493 
494     // If the pier is on the east side (pointing west) and will slew west and is within 2 slews of the HA=0,
495     // or on the west side (pointing east) and will slew east, and is within 2 slews of HA=0
496     // then warn and give the user a chance to cancel.
497     bool wouldCrossMeridian =
498         ((m_CurrentTelescope->pierSide() == ISD::Telescope::PIER_EAST && !goingWest && closeToMeridian) ||
499          (m_CurrentTelescope->pierSide() == ISD::Telescope::PIER_WEST && goingWest && closeToMeridian) ||
500          (m_CurrentTelescope->pierSide() == ISD::Telescope::PIER_UNKNOWN && closeToMeridian));
501 
502     return wouldCrossMeridian;
503 }
504 
505 void PolarAlignmentAssistant::startPAHProcess()
506 {
507     qCInfo(KSTARS_EKOS_ALIGN) << QString("Starting Polar Alignment Assistant process %1 ...").arg(PAA_VERSION);
508 
509     auto executePAH = [ this ]()
510     {
511         m_PAHStage = PAH_FIRST_CAPTURE;
512         emit newPAHStage(m_PAHStage);
513 
514         if (Options::limitedResourcesMode())
515             emit newLog(i18n("Warning: Equatorial Grid Lines will not be drawn due to limited resources mode."));
516 
517         if (m_CurrentTelescope->hasAlignmentModel())
518         {
519             emit newLog(i18n("Clearing mount Alignment Model..."));
520             m_CurrentTelescope->clearAlignmentModel();
521         }
522 
523         // Unpark
524         m_CurrentTelescope->UnPark();
525 
526         // Set tracking ON if not already
527         if (m_CurrentTelescope->canControlTrack() && m_CurrentTelescope->isTracking() == false)
528             m_CurrentTelescope->setTrackEnabled(true);
529 
530         PAHStartB->setEnabled(false);
531         PAHStopB->setEnabled(true);
532 
533         PAHUpdatedErrorTotal->clear();
534         PAHUpdatedErrorAlt->clear();
535         PAHUpdatedErrorAz->clear();
536         PAHOrigErrorTotal->clear();
537         PAHOrigErrorAlt->clear();
538         PAHOrigErrorAz->clear();
539 
540         PAHWidgets->setCurrentWidget(PAHFirstCapturePage);
541         emit newPAHMessage(firstCaptureText->text());
542 
543         m_PAHRetrySolveCounter = 0;
544         emit captureAndSolve();
545     };
546 
547     // Right off the bat, check if this alignment might cause a pier crash.
548     // If we're crossing the meridian, warn unless within 5-degrees from the pole.
549     if (checkPAHForMeridianCrossing())
550     {
551         connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executePAH]()
552         {
553             KSMessageBox::Instance()->disconnect(this);
554             executePAH();
555         });
556 
557         emit newLog(i18n("Warning, This could cause the telescope to cross the meridian. Check your direction."));
558     }
559     else
560         executePAH();
561 }
562 
563 void PolarAlignmentAssistant::stopPAHProcess()
564 {
565     if (m_PAHStage == PAH_IDLE)
566         return;
567 
568     qCInfo(KSTARS_EKOS_ALIGN) << "Stopping Polar Alignment Assistant process...";
569 
570     // Only display dialog if user explicitly restarts
571     if ((static_cast<QPushButton *>(sender()) == PAHStopB) && KMessageBox::questionYesNo(KStars::Instance(),
572             i18n("Are you sure you want to stop the polar alignment process?"),
573             i18n("Polar Alignment Assistant"), KStandardGuiItem::yes(), KStandardGuiItem::no(),
574             "restart_PAA_process_dialog") == KMessageBox::No)
575         return;
576 
577     if (m_CurrentTelescope && m_CurrentTelescope->isInMotion())
578         m_CurrentTelescope->Abort();
579 
580     m_PAHStage = PAH_IDLE;
581     emit newPAHStage(m_PAHStage);
582 
583     PAHStartB->setEnabled(true);
584     PAHStopB->setEnabled(false);
585     PAHRefreshB->setEnabled(true);
586     PAHWidgets->setCurrentWidget(PAHIntroPage);
587     emit newPAHMessage(introText->text());
588 
589     alignView->reset();
590     alignView->setRefreshEnabled(false);
591 
592     emit newFrame(alignView);
593     disconnect(alignView, &AlignView::trackingStarSelected, this, &Ekos::PolarAlignmentAssistant::setPAHCorrectionOffset);
594     disconnect(alignView, &AlignView::newCorrectionVector, this, &Ekos::PolarAlignmentAssistant::newCorrectionVector);
595 
596     if (Options::pAHAutoPark())
597     {
598         m_CurrentTelescope->Park();
599         emit newLog(i18n("Parking the mount..."));
600     }
601 }
602 
603 void PolarAlignmentAssistant::rotatePAH()
604 {
605     double TargetDiffRA = PAHRotationSpin->value();
606     bool westMeridian = PAHDirectionCombo->currentIndex() == 0;
607 
608     // West
609     if (westMeridian)
610         TargetDiffRA *= -1;
611     // East
612     else
613         TargetDiffRA *= 1;
614 
615     // JM 2018-05-03: Hemispheres shouldn't affect rotation direction in RA
616 
617     // if Manual slewing is selected, don't move the mount
618     if (PAHManual->isChecked())
619     {
620         return;
621     }
622 
623     const SkyPoint telescopeCoord = m_CurrentTelescope->currentCoordinates();
624 
625     // TargetDiffRA is in degrees
626     dms newTelescopeRA = (telescopeCoord.ra() + dms(TargetDiffRA)).reduce();
627 
628     targetPAH.setRA(newTelescopeRA);
629     targetPAH.setDec(telescopeCoord.dec());
630 
631     //m_CurrentTelescope->Slew(&targetPAH);
632     // Set Selected Speed
633     if (PAHSlewRateCombo->currentIndex() >= 0)
634         m_CurrentTelescope->setSlewRate(PAHSlewRateCombo->currentIndex());
635     // Go to direction
636     m_CurrentTelescope->MoveWE(westMeridian ? ISD::Telescope::MOTION_WEST : ISD::Telescope::MOTION_EAST,
637                                ISD::Telescope::MOTION_START);
638 
639     emit newLog(i18n("Please wait until mount completes rotating to RA (%1) DE (%2)", targetPAH.ra().toHMSString(),
640                      targetPAH.dec().toDMSString()));
641 }
642 
643 void PolarAlignmentAssistant::setupCorrectionGraphics(const QPointF &pixel)
644 {
645     // We use the previously stored image (the 3rd PAA image)
646     // so we can continue to estimage the correction even after
647     // capturing new images during the refresh stage.
648     const QSharedPointer<FITSData> &imageData = alignView->keptImage();
649 
650     // Just the altitude correction
651     if (!polarAlign.findCorrectedPixel(imageData, pixel, &correctionAltTo, true))
652     {
653         qCInfo(KSTARS_EKOS_ALIGN) << QString(i18n("PAA: Failed to findCorrectedPixel."));
654         return;
655     }
656     // The whole correction.
657     if (!polarAlign.findCorrectedPixel(imageData, pixel, &correctionTo))
658     {
659         qCInfo(KSTARS_EKOS_ALIGN) << QString(i18n("PAA: Failed to findCorrectedPixel."));
660         return;
661     }
662     QString debugString = QString("PAA: Correction: %1,%2 --> %3,%4 (alt only %5,%6)")
663                           .arg(pixel.x(), 4, 'f', 0).arg(pixel.y(), 4, 'f', 0)
664                           .arg(correctionTo.x(), 4, 'f', 0).arg(correctionTo.y(), 4, 'f', 0)
665                           .arg(correctionAltTo.x(), 4, 'f', 0).arg(correctionAltTo.y(), 4, 'f', 0);
666     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
667     correctionFrom = pixel;
668     alignView->setCorrectionParams(correctionFrom, correctionTo, correctionAltTo);
669 
670     return;
671 }
672 
673 bool PolarAlignmentAssistant::calculatePAHError()
674 {
675     // Hold on to the imageData so we can use it during the refresh phase.
676     alignView->holdOnToImage();
677 
678     if (!polarAlign.findAxis())
679     {
680         emit newLog(i18n("PAA: Failed to find RA Axis center."));
681         stopPAHProcess();
682         return false;
683     }
684 
685     double azimuthError, altitudeError;
686     polarAlign.calculateAzAltError(&azimuthError, &altitudeError);
687     dms polarError(hypot(altitudeError, azimuthError));
688     dms azError(azimuthError), altError(altitudeError);
689 
690     if (alignView->isEQGridShown() == false && !Options::limitedResourcesMode())
691         alignView->toggleEQGrid();
692 
693     QString msg = QString("%1. Azimuth: %2  Altitude: %3")
694                   .arg(polarError.toDMSString()).arg(azError.toDMSString())
695                   .arg(altError.toDMSString());
696     emit newLog(QString("Polar Alignment Error: %1").arg(msg));
697     PAHErrorLabel->setText(msg);
698 
699     // These are viewed during the refresh phase.
700     PAHOrigErrorTotal->setText(polarError.toDMSString());
701     PAHOrigErrorAlt->setText(altError.toDMSString());
702     PAHOrigErrorAz->setText(azError.toDMSString());
703 
704     setupCorrectionGraphics(QPointF(m_ImageData->width() / 2, m_ImageData->height() / 2));
705 
706     // Find Celestial pole location and mount's RA axis
707     SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90);
708     QPointF imagePoint, celestialPolePoint;
709     m_ImageData->wcsToPixel(CP, celestialPolePoint, imagePoint);
710     if (m_ImageData->contains(celestialPolePoint))
711     {
712         alignView->setCelestialPole(celestialPolePoint);
713         QPointF raAxis;
714         if (polarAlign.findCorrectedPixel(m_ImageData, celestialPolePoint, &raAxis))
715             alignView->setRaAxis(raAxis);
716     }
717 
718     connect(alignView, &AlignView::trackingStarSelected, this, &Ekos::PolarAlignmentAssistant::setPAHCorrectionOffset);
719     emit polarResultUpdated(QLineF(correctionFrom, correctionTo), polarError.Degrees(), azError.Degrees(), altError.Degrees());
720 
721     connect(alignView, &AlignView::newCorrectionVector, this, &Ekos::PolarAlignmentAssistant::newCorrectionVector,
722             Qt::UniqueConnection);
723     syncCorrectionVector();
724     emit newFrame(alignView);
725 
726     return true;
727 }
728 
729 void PolarAlignmentAssistant::syncCorrectionVector()
730 {
731     emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
732     alignView->setCorrectionParams(correctionFrom, correctionTo, correctionAltTo);
733 }
734 
735 void PolarAlignmentAssistant::setPAHCorrectionOffsetPercentage(double dx, double dy)
736 {
737     double x = dx * alignView->zoomedWidth();
738     double y = dy * alignView->zoomedHeight();
739     setPAHCorrectionOffset(static_cast<int>(round(x)), static_cast<int>(round(y)));
740 }
741 
742 void PolarAlignmentAssistant::setPAHCorrectionOffset(int x, int y)
743 {
744     if (m_PAHStage == PAH_REFRESH || m_PAHStage == PAH_PRE_REFRESH)
745     {
746         emit newLog(i18n("Polar-alignment star cannot be updated during refresh phase as it might affect error measurements."));
747     }
748     else
749     {
750         setupCorrectionGraphics(QPointF(x, y));
751         emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
752         emit newFrame(alignView);
753     }
754 }
755 
756 void PolarAlignmentAssistant::setPAHCorrectionSelectionComplete()
757 {
758     m_PAHStage = PAH_PRE_REFRESH;
759     emit newPAHStage(m_PAHStage);
760     PAHWidgets->setCurrentWidget(PAHRefreshPage);
761     emit newPAHMessage(refreshText->text());
762 }
763 
764 void PolarAlignmentAssistant::setPAHSlewDone()
765 {
766     emit newPAHMessage("Manual slew done.");
767     switch(m_PAHStage)
768     {
769         case PAH_FIRST_ROTATE :
770             m_PAHStage = PAH_SECOND_CAPTURE;
771             emit newPAHStage(m_PAHStage);
772             PAHWidgets->setCurrentWidget(PAHSecondCapturePage);
773             emit newLog(i18n("First manual rotation done."));
774             break;
775         case PAH_SECOND_ROTATE :
776             m_PAHStage = PAH_THIRD_CAPTURE;
777             emit newPAHStage(m_PAHStage);
778             PAHWidgets->setCurrentWidget(PAHThirdCapturePage);
779             emit newLog(i18n("Second manual rotation done."));
780             break;
781         default :
782             return; // no other stage should be able to trigger this event
783     }
784 }
785 
786 
787 
788 void PolarAlignmentAssistant::startPAHRefreshProcess()
789 {
790     qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant refreshing...";
791 
792     refreshIteration = 0;
793 
794     m_PAHStage = PAH_REFRESH;
795     emit newPAHStage(m_PAHStage);
796 
797     PAHRefreshB->setEnabled(false);
798 
799     // Hide EQ Grids if shown
800     if (alignView->isEQGridShown())
801         alignView->toggleEQGrid();
802 
803     alignView->setRefreshEnabled(true);
804 
805     Options::setAstrometrySolverWCS(false);
806     Options::setAutoWCS(false);
807 
808     // We for refresh, just capture really
809     emit captureAndSolve();
810 }
811 
812 void PolarAlignmentAssistant::setPAHRefreshComplete()
813 {
814     refreshIteration = 0;
815     m_PAHStage = PAH_POST_REFRESH;
816     emit newPAHStage(m_PAHStage);
817     stopPAHProcess();
818 }
819 
820 void PolarAlignmentAssistant::processPAHStage(double orientation, double ra, double dec, double pixscale,
821         bool eastToTheRight)
822 {
823     if (m_PAHStage == PAH_FIND_CP)
824     {
825         emit newLog(
826             i18n("Mount is synced to celestial pole. You can now continue Polar Alignment Assistant procedure."));
827         m_PAHStage = PAH_FIRST_CAPTURE;
828         emit newPAHStage(m_PAHStage);
829         return;
830     }
831 
832     if (m_PAHStage == PAH_FIRST_CAPTURE || m_PAHStage == PAH_SECOND_CAPTURE || m_PAHStage == PAH_THIRD_CAPTURE)
833     {
834         bool doWcs = (m_PAHStage == PAH_THIRD_CAPTURE) || !Options::limitedResourcesMode();
835         if (doWcs)
836         {
837             emit newLog(i18n("Please wait while WCS data is processed..."));
838             PAHWidgets->setCurrentWidget(
839                 m_PAHStage == PAH_FIRST_CAPTURE
840                 ? PAHFirstWcsPage
841                 : (m_PAHStage == PAH_SECOND_CAPTURE ? PAHSecondWcsPage
842                    : PAHThirdWcsPage));
843         }
844         connect(alignView, &AlignView::wcsToggled, this, &Ekos::PolarAlignmentAssistant::setWCSToggled, Qt::UniqueConnection);
845         alignView->injectWCS(orientation, ra, dec, pixscale, eastToTheRight, doWcs);
846         return;
847     }
848 }
849 
850 QJsonObject PolarAlignmentAssistant::getPAHSettings() const
851 {
852     QJsonObject settings;
853 
854     settings.insert("mountDirection", PAHDirectionCombo->currentIndex());
855     settings.insert("mountSpeed", PAHSlewRateCombo->currentIndex());
856     settings.insert("mountRotation", PAHRotationSpin->value());
857     settings.insert("refresh", PAHExposure->value());
858     settings.insert("manualslew", PAHManual->isChecked());
859 
860     return settings;
861 }
862 
863 void PolarAlignmentAssistant::setPAHSettings(const QJsonObject &settings)
864 {
865     PAHDirectionCombo->setCurrentIndex(settings["mountDirection"].toInt(0));
866     PAHRotationSpin->setValue(settings["mountRotation"].toInt(30));
867     PAHExposure->setValue(settings["refresh"].toDouble(1));
868     if (settings.contains("mountSpeed"))
869         PAHSlewRateCombo->setCurrentIndex(settings["mountSpeed"].toInt(0));
870     PAHManual->setChecked(settings["manualslew"].toBool(false));
871 }
872 
873 void PolarAlignmentAssistant::setWCSToggled(bool result)
874 {
875     emit newLog(i18n("WCS data processing is complete."));
876 
877     disconnect(alignView, &AlignView::wcsToggled, this, &Ekos::PolarAlignmentAssistant::setWCSToggled);
878 
879     if (m_PAHStage == PAH_FIRST_CAPTURE)
880     {
881         // We need WCS to be synced first
882         if (result == false && m_AlignInstance->wcsSynced() == true)
883         {
884             emit newLog(i18n("WCS info is now valid. Capturing next frame..."));
885             emit captureAndSolve();
886             return;
887         }
888 
889         polarAlign.reset();
890         polarAlign.addPoint(m_ImageData);
891 
892         m_PAHStage = PAH_FIRST_ROTATE;
893         emit newPAHStage(m_PAHStage);
894 
895         if (PAHManual->isChecked())
896         {
897             QString msg = QString("Please rotate your mount about %1 deg in RA")
898                           .arg(PAHRotationSpin->value());
899             manualRotateText->setText(msg);
900             emit newLog(msg);
901             PAHWidgets->setCurrentWidget(PAHManualRotatePage);
902             emit newPAHMessage(manualRotateText->text());
903         }
904         else
905         {
906             PAHWidgets->setCurrentWidget(PAHFirstRotatePage);
907             emit newPAHMessage(firstRotateText->text());
908         }
909 
910         rotatePAH();
911     }
912     else if (m_PAHStage == PAH_SECOND_CAPTURE)
913     {
914         m_PAHStage = PAH_SECOND_ROTATE;
915         emit newPAHStage(m_PAHStage);
916 
917         if (PAHManual->isChecked())
918         {
919             QString msg = QString("Please rotate your mount about %1 deg in RA")
920                           .arg(PAHRotationSpin->value());
921             manualRotateText->setText(msg);
922             emit newLog(msg);
923             PAHWidgets->setCurrentWidget(PAHManualRotatePage);
924             emit newPAHMessage(manualRotateText->text());
925         }
926         else
927         {
928             PAHWidgets->setCurrentWidget(PAHSecondRotatePage);
929             emit newPAHMessage(secondRotateText->text());
930         }
931 
932         polarAlign.addPoint(m_ImageData);
933 
934         rotatePAH();
935     }
936     else if (m_PAHStage == PAH_THIRD_CAPTURE)
937     {
938         // Critical error
939         if (result == false)
940         {
941             emit newLog(i18n("Failed to process World Coordinate System: %1. Try again.", m_ImageData->getLastError()));
942             return;
943         }
944 
945         polarAlign.addPoint(m_ImageData);
946 
947         // We have 3 points which uniquely defines a circle with its center representing the RA Axis
948         // We have celestial pole location. So correction vector is just the vector between these two points
949         if (calculatePAHError())
950         {
951             m_PAHStage = PAH_STAR_SELECT;
952             emit newPAHStage(m_PAHStage);
953             PAHWidgets->setCurrentWidget(PAHCorrectionPage);
954             emit newPAHMessage(correctionText->text());
955         }
956     }
957 }
958 
959 void PolarAlignmentAssistant::setMountStatus(ISD::Telescope::Status newState)
960 {
961     switch (newState)
962     {
963         case ISD::Telescope::MOUNT_PARKING:
964         case ISD::Telescope::MOUNT_SLEWING:
965         case ISD::Telescope::MOUNT_MOVING:
966             PAHStartB->setEnabled(false);
967             break;
968 
969         default:
970             if (m_PAHStage == PAH_IDLE)
971                 PAHStartB->setEnabled(true);
972             break;
973     }
974 }
975 
976 QString PolarAlignmentAssistant::getPAHMessage() const
977 {
978     switch (m_PAHStage)
979     {
980         case PAH_IDLE:
981         case PAH_FIND_CP:
982             return introText->text();
983         case PAH_FIRST_CAPTURE:
984             return firstCaptureText->text();
985         case PAH_FIRST_ROTATE:
986             return firstRotateText->text();
987         case PAH_SECOND_CAPTURE:
988             return secondCaptureText->text();
989         case PAH_SECOND_ROTATE:
990             return secondRotateText->text();
991         case PAH_THIRD_CAPTURE:
992             return thirdCaptureText->text();
993         case PAH_STAR_SELECT:
994             return correctionText->text();
995         case PAH_PRE_REFRESH:
996         case PAH_POST_REFRESH:
997         case PAH_REFRESH:
998             return refreshText->text();
999         case PAH_ERROR:
1000             return PAHErrorDescriptionLabel->text();
1001     }
1002 
1003     return QString();
1004 }
1005 
1006 
1007 }
1008