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

0001 /*  Ekos Polar Alignment Assistant Tool
0002     SPDX-FileCopyrightText: 2018-2021 Jasem Mutlaq
0003     SPDX-FileCopyrightText: 2020-2021 Hy Murveit
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "polaralignmentassistant.h"
0009 
0010 #include "align.h"
0011 #include "alignview.h"
0012 #include "kstars.h"
0013 #include "kstarsdata.h"
0014 #include "ksmessagebox.h"
0015 #include "ekos/auxiliary/stellarsolverprofile.h"
0016 #include "ekos/auxiliary/solverutils.h"
0017 #include "Options.h"
0018 #include "polaralignwidget.h"
0019 #include <ekos_align_debug.h>
0020 
0021 #define PAA_VERSION "v3.0"
0022 
0023 namespace Ekos
0024 {
0025 
0026 const QMap<PolarAlignmentAssistant::Stage, const char *> PolarAlignmentAssistant::PAHStages =
0027 {
0028     {PAH_IDLE, I18N_NOOP("Idle")},
0029     {PAH_FIRST_CAPTURE, I18N_NOOP("First Capture")},
0030     {PAH_FIRST_SOLVE, I18N_NOOP("First Solve")},
0031     {PAH_FIND_CP, I18N_NOOP("Finding CP")},
0032     {PAH_FIRST_ROTATE, I18N_NOOP("First Rotation")},
0033     {PAH_FIRST_SETTLE, I18N_NOOP("First Settle")},
0034     {PAH_SECOND_CAPTURE, I18N_NOOP("Second Capture")},
0035     {PAH_SECOND_SOLVE, I18N_NOOP("Second Solve")},
0036     {PAH_SECOND_ROTATE, I18N_NOOP("Second Rotation")},
0037     {PAH_SECOND_SETTLE, I18N_NOOP("Second Settle")},
0038     {PAH_THIRD_CAPTURE, I18N_NOOP("Third Capture")},
0039     {PAH_THIRD_SOLVE, I18N_NOOP("Third Solve")},
0040     {PAH_STAR_SELECT, I18N_NOOP("Select Star")},
0041     {PAH_REFRESH, I18N_NOOP("Refreshing")},
0042     {PAH_POST_REFRESH, I18N_NOOP("Refresh Complete")},
0043 };
0044 
0045 PolarAlignmentAssistant::PolarAlignmentAssistant(Align *parent, const QSharedPointer<AlignView> &view) : QWidget(parent)
0046 {
0047     setupUi(this);
0048     polarAlignWidget = new PolarAlignWidget();
0049     mainPALayout->insertWidget(0, polarAlignWidget);
0050 
0051     m_AlignInstance = parent;
0052     m_AlignView = view;
0053 
0054     showUpdatedError((pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM) ||
0055                      (pAHRefreshAlgorithm->currentIndex() == MOVE_STAR_UPDATE_ERR_ALGORITHM));
0056 
0057     connect(pAHRefreshAlgorithm, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
0058     {
0059         setPAHRefreshAlgorithm(static_cast<RefreshAlgorithm>(index));
0060     });
0061     starCorrespondencePAH.reset();
0062 
0063     // PAH Connections
0064     PAHWidgets->setCurrentWidget(PAHIntroPage);
0065     connect(this, &PolarAlignmentAssistant::PAHEnabled, [&](bool enabled)
0066     {
0067         PAHStartB->setEnabled(enabled);
0068         directionLabel->setEnabled(enabled);
0069         pAHDirection->setEnabled(enabled);
0070         pAHRotation->setEnabled(enabled);
0071         pAHMountSpeed->setEnabled(enabled);
0072         pAHManualSlew->setEnabled(enabled);
0073     });
0074     connect(PAHStartB, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::startPAHProcess);
0075     // PAH StopB is just a shortcut for the regular stop
0076     connect(PAHStopB, &QPushButton::clicked, this, &PolarAlignmentAssistant::stopPAHProcess);
0077     connect(PAHRefreshB, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::startPAHRefreshProcess);
0078     // done button for manual slewing during polar alignment:
0079     connect(PAHManualDone, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::setPAHSlewDone);
0080 
0081     hemisphere = KStarsData::Instance()->geo()->lat()->Degrees() > 0 ? NORTH_HEMISPHERE : SOUTH_HEMISPHERE;
0082 
0083     label_PAHOrigErrorAz->setText("Az: ");
0084     label_PAHOrigErrorAlt->setText("Alt: ");
0085 }
0086 
0087 PolarAlignmentAssistant::~PolarAlignmentAssistant()
0088 {
0089 }
0090 
0091 void PolarAlignmentAssistant::showUpdatedError(bool show)
0092 {
0093     label_PAHUpdatedErrorTotal->setVisible(show);
0094     PAHUpdatedErrorTotal->setVisible(show);
0095     label_PAHUpdatedErrorAlt->setVisible(show);
0096     PAHUpdatedErrorAlt->setVisible(show);
0097     label_PAHUpdatedErrorAz->setVisible(show);
0098     PAHUpdatedErrorAz->setVisible(show);
0099 }
0100 
0101 void PolarAlignmentAssistant::syncMountSpeed(const QString &speed)
0102 {
0103     pAHMountSpeed->blockSignals(true);
0104     pAHMountSpeed->clear();
0105     pAHMountSpeed->addItems(m_CurrentTelescope->slewRates());
0106     pAHMountSpeed->setCurrentText(speed);
0107     pAHMountSpeed->blockSignals(false);
0108 }
0109 
0110 void PolarAlignmentAssistant::setEnabled(bool enabled)
0111 {
0112     QWidget::setEnabled(enabled);
0113 
0114     emit PAHEnabled(enabled);
0115     if (enabled)
0116     {
0117         PAHWidgets->setToolTip(QString());
0118         FOVDisabledLabel->hide();
0119     }
0120     else
0121     {
0122         PAHWidgets->setToolTip(i18n("<p>Polar Alignment tool requires a German Equatorial Mount.</p>"));
0123         FOVDisabledLabel->show();
0124     }
0125 
0126 }
0127 
0128 // This solver is only used by the refresh plate-solving scheme.
0129 void PolarAlignmentAssistant::startSolver()
0130 {
0131     auto profiles = getDefaultAlignOptionsProfiles();
0132     auto parameters = profiles.at(Options::solveOptionsProfile());
0133     // Double search radius
0134     parameters.search_radius = parameters.search_radius * 2;
0135     constexpr double solverTimeout = 10.0;
0136 
0137     m_Solver.reset(new SolverUtils(parameters, solverTimeout), &QObject::deleteLater);
0138     connect(m_Solver.get(), &SolverUtils::done, this, &PolarAlignmentAssistant::solverDone, Qt::UniqueConnection);
0139 
0140     // Use the scale and position from the most recent solve.
0141     m_Solver->useScale(Options::astrometryUseImageScale(), m_LastPixscale * 0.9, m_LastPixscale * 1.1);
0142     m_Solver->usePosition(true, m_LastRa, m_LastDec);
0143     m_Solver->setHealpix(m_IndexToUse, m_HealpixToUse);
0144     m_Solver->runSolver(m_ImageData);
0145 }
0146 
0147 void PolarAlignmentAssistant::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution,
0148         double elapsedSeconds)
0149 {
0150     disconnect(m_Solver.get(), &SolverUtils::done, this, &PolarAlignmentAssistant::solverDone);
0151 
0152     if (m_PAHStage != PAH_REFRESH)
0153         return;
0154 
0155     if (timedOut || !success)
0156     {
0157         // I'm torn between 1 and 2 for this constant.
0158         // 1: If a solve failed, open up the search next time.
0159         // 2: A common reason for a solve to fail is because a knob was adjusted during the exposure.
0160         //    Let it try one more time.
0161         constexpr int MAX_NUM_HEALPIX_FAILURES = 2;
0162         if (++m_NumHealpixFailures >= MAX_NUM_HEALPIX_FAILURES)
0163         {
0164             // Don't use the previous index and healpix next time we solve.
0165             m_IndexToUse = -1;
0166             m_HealpixToUse = -1;
0167         }
0168     }
0169     else
0170     {
0171         // Get the index and healpix from the successful solve.
0172         m_Solver->getSolutionHealpix(&m_IndexToUse, &m_HealpixToUse);
0173     }
0174 
0175     if (timedOut)
0176     {
0177         emit newLog(i18n("Refresh solver timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1)));
0178         emit captureAndSolve();
0179     }
0180     else if (!success)
0181     {
0182         emit newLog(i18n("Refresh solver failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1)));
0183         emit updatedErrorsChanged(-1, -1, -1);
0184         emit captureAndSolve();
0185     }
0186     else
0187     {
0188         m_NumHealpixFailures = 0;
0189         refreshIteration++;
0190         const double ra = solution.ra;
0191         const double dec = solution.dec;
0192         m_LastRa = solution.ra;
0193         m_LastDec = solution.dec;
0194         m_LastOrientation = solution.orientation;
0195         m_LastPixscale = solution.pixscale;
0196 
0197         emit newLog(QString("Refresh solver success %1s: ra %2 dec %3 scale %4")
0198                     .arg(elapsedSeconds, 0, 'f', 1).arg(ra, 0, 'f', 3)
0199                     .arg(dec, 0, 'f', 3).arg(solution.pixscale));
0200 
0201         // RA is input in hours, not degrees!
0202         SkyPoint refreshCoords(ra / 15.0, dec);
0203         double azError = 0, altError = 0;
0204         if (polarAlign.processRefreshCoords(refreshCoords, m_ImageData->getDateTime(), &azError, &altError))
0205         {
0206             updateRefreshDisplay(azError, altError);
0207 
0208             const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
0209             // The 2nd false means don't block. The code below doesn't work if we block
0210             // because wcsToPixel in updateTriangle() depends on the injectWCS being finished.
0211             m_AlignView->injectWCS(solution.orientation, ra, dec, solution.pixscale, eastToTheRight, false);
0212             updatePlateSolveTriangle(m_ImageData);
0213         }
0214         else
0215             emit newLog(QString("Could not estimate mount rotation"));
0216     }
0217     // Start the next refresh capture.
0218     emit captureAndSolve();
0219 }
0220 
0221 void PolarAlignmentAssistant::updatePlateSolveTriangle(const QSharedPointer<FITSData> &image)
0222 {
0223     if (image.isNull())
0224         return;
0225 
0226     // To render the triangle for the plate-solve refresh, we need image coordinates for
0227     // - the original (3rd image) center point (originalPoint)
0228     // - the solution point (solutionPoint),
0229     // - the alt-only solution point (altOnlyPoint),
0230     // - the current center of the image (centerPixel),
0231     // The triangle is made from the first 3 above.
0232     const SkyPoint &originalCoords = polarAlign.getPoint(2);
0233     QPointF originalPixel, solutionPixel, altOnlyPixel, dummy;
0234     QPointF centerPixel(image->width() / 2, image->height() / 2);
0235     if (image->wcsToPixel(originalCoords, originalPixel, dummy) &&
0236             image->wcsToPixel(refreshSolution, solutionPixel, dummy) &&
0237             image->wcsToPixel(altOnlyRefreshSolution, altOnlyPixel, dummy))
0238     {
0239         m_AlignView->setCorrectionParams(originalPixel, solutionPixel, altOnlyPixel);
0240         m_AlignView->setStarCircle(centerPixel);
0241     }
0242     else
0243     {
0244         qCDebug(KSTARS_EKOS_ALIGN) << "wcs failed";
0245     }
0246 }
0247 
0248 namespace
0249 {
0250 
0251 // These functions paint up/down/left/right-pointing arrows in a box of size w x h.
0252 
0253 void upArrow(QPainter *painter, int w, int h)
0254 {
0255     const double wCenter = w / 2, lineTop = 0.38, lineBottom = 0.9;
0256     const double lineLength = h * (lineBottom - lineTop);
0257     painter->drawRect(wCenter - w * .1, h * lineTop, w * .2, lineLength);
0258     QPolygonF arrowHead;
0259     arrowHead << QPointF(wCenter, h * .1) << QPointF(w * .2, h * .4) << QPointF(w * .8, h * .4);
0260     painter->drawConvexPolygon(arrowHead);
0261 }
0262 void downArrow(QPainter *painter, int w, int h)
0263 {
0264     const double wCenter = w / 2, lineBottom = 0.62, lineTop = 0.1;
0265     const double lineLength = h * (lineBottom - lineTop);
0266     painter->drawRect(wCenter - w * .1, h * lineTop, w * .2, lineLength);
0267     QPolygonF arrowHead;
0268     arrowHead << QPointF(wCenter, h * .9) << QPointF(w * .2, h * .6) << QPointF(w * .8, h * .6);
0269     painter->drawConvexPolygon(arrowHead);
0270 }
0271 void leftArrow(QPainter *painter, int w, int h)
0272 {
0273     const double hCenter = h / 2, lineLeft = 0.38, lineRight = 0.9;
0274     const double lineLength = w * (lineRight - lineLeft);
0275     painter->drawRect(h * lineLeft, hCenter - h * .1, lineLength, h * .2);
0276     QPolygonF arrowHead;
0277     arrowHead << QPointF(w * .1, hCenter) << QPointF(w * .4, h * .2) << QPointF(w * .4, h * .8);
0278     painter->drawConvexPolygon(arrowHead);
0279 }
0280 void rightArrow(QPainter *painter, int w, int h)
0281 {
0282     const double hCenter = h / 2, lineLeft = .1, lineRight = .62;
0283     const double lineLength = w * (lineRight - lineLeft);
0284     painter->drawRect(h * lineLeft, hCenter - h * .1, lineLength, h * .2);
0285     QPolygonF arrowHead;
0286     arrowHead << QPointF(w * .9, hCenter) << QPointF(w * .6, h * .2) << QPointF(w * .6, h * .8);
0287     painter->drawConvexPolygon(arrowHead);
0288 }
0289 }  // namespace
0290 
0291 // Draw arrows that give the user a hint of how he/she needs to adjust the azimuth and altitude
0292 // knobs. The arrows' size changes depending on the azimuth and altitude error.
0293 void PolarAlignmentAssistant::drawArrows(double azError, double altError)
0294 {
0295     constexpr double minError = 20.0 / 3600.0;  // 20 arcsec
0296     double absError = fabs(altError);
0297 
0298     // these constants are worked out so a 10' error gives a size of 100
0299     // and a 1' error gives a size of 20.
0300     constexpr double largeErr = 10.0 / 60.0, smallErr = 1.0 / 60.0, largeSize = 100, smallSize = 20, c1 = 533.33, c2 = 11.111;
0301 
0302     int size = 0;
0303     if (absError > largeErr)
0304         size = largeSize;
0305     else if (absError < smallErr)
0306         size = smallSize;
0307     else size = absError * c1 + c2;
0308 
0309     QPixmap altPixmap(size, size);
0310     altPixmap.fill(QColor("transparent"));
0311     QPainter altPainter(&altPixmap);
0312     altPainter.setBrush(arrowAlt->palette().color(QPalette::WindowText));
0313 
0314     if (altError > minError)
0315         downArrow(&altPainter, size, size);
0316     else if (altError < -minError)
0317         upArrow(&altPainter, size, size);
0318     arrowAlt->setPixmap(altPixmap);
0319 
0320     absError = fabs(azError);
0321     if (absError > largeErr)
0322         size = largeSize;
0323     else if (absError < smallErr)
0324         size = smallSize;
0325     else size = absError * c1 + c2;
0326 
0327     QPixmap azPixmap(size, size);
0328     azPixmap.fill(QColor("transparent"));
0329     QPainter azPainter(&azPixmap);
0330     azPainter.setBrush(arrowAz->palette().color(QPalette::WindowText));
0331 
0332     if (azError > minError)
0333         leftArrow(&azPainter, size, size);
0334     else if (azError < -minError)
0335         rightArrow(&azPainter, size, size);
0336     arrowAz->setPixmap(azPixmap);
0337 }
0338 
0339 bool PolarAlignmentAssistant::detectStarsPAHRefresh(QList<Edge> *stars, int num, int x, int y, int *starIndex)
0340 {
0341     stars->clear();
0342     *starIndex = -1;
0343 
0344     // Use the solver settings from the align tab for for "polar-align refresh" star detection.
0345     QVariantMap settings;
0346     settings["optionsProfileIndex"] = Options::solveOptionsProfile();
0347     settings["optionsProfileGroup"] = static_cast<int>(Ekos::AlignProfiles);
0348     m_ImageData->setSourceExtractorSettings(settings);
0349 
0350     QElapsedTimer timer;
0351     m_ImageData->findStars(ALGORITHM_SEP).waitForFinished();
0352 
0353     QString debugString = QString("PAA Refresh: Detected %1 stars (%2s)")
0354                           .arg(m_ImageData->getStarCenters().size()).arg(timer.elapsed() / 1000.0, 5, 'f', 3);
0355     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0356 
0357     QList<Edge *> detectedStars = m_ImageData->getStarCenters();
0358     // Let's sort detectedStars by flux, starting with widest
0359     std::sort(detectedStars.begin(), detectedStars.end(), [](const Edge * edge1, const Edge * edge2) -> bool { return edge1->sum > edge2->sum;});
0360 
0361     // Find the closest star to the x,y position, which is where the user clicked on the alignView.
0362     double bestDist = 1e9;
0363     int bestIndex = -1;
0364     for (int i = 0; i < detectedStars.size(); i++)
0365     {
0366         double dx = detectedStars[i]->x - x;
0367         double dy = detectedStars[i]->y - y;
0368         double dist = dx * dx + dy * dy;
0369         if (dist < bestDist)
0370         {
0371             bestDist = dist;
0372             bestIndex = i;
0373         }
0374     }
0375 
0376     int starCount = qMin(num, detectedStars.count());
0377     for (int i = 0; i < starCount; i++)
0378         stars->append(*(detectedStars[i]));
0379     if (bestIndex >= starCount)
0380     {
0381         // If we found the star, but requested 'num' stars, and the user's star
0382         // is lower down in the list, add it and return num+1 stars.
0383         stars->append(*(detectedStars[bestIndex]));
0384         *starIndex = starCount;
0385     }
0386     else
0387     {
0388         *starIndex = bestIndex;
0389     }
0390     debugString = QString("PAA Refresh: User's star(%1,%2) is index %3").arg(x).arg(y).arg(*starIndex);
0391     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0392 
0393     detectedStars.clear();
0394 
0395     return stars->count();
0396 }
0397 
0398 void PolarAlignmentAssistant::updateRefreshDisplay(double azE, double altE)
0399 {
0400     drawArrows(azE, altE);
0401     const double errDegrees = hypot(azE, altE);
0402     dms totalError(errDegrees), azError(azE), altError(altE);
0403     PAHUpdatedErrorTotal->setText(totalError.toDMSString());
0404     PAHUpdatedErrorAlt->setText(altError.toDMSString());
0405     PAHUpdatedErrorAz->setText(azError.toDMSString());
0406 
0407     QString debugString = QString("PAA Refresh(%1): Corrected az: %2 alt: %4 total: %6")
0408                           .arg(refreshIteration).arg(azError.toDMSString())
0409                           .arg(altError.toDMSString()).arg(totalError.toDMSString());
0410     emit newLog(debugString);
0411     emit updatedErrorsChanged(totalError.Degrees(), azError.Degrees(), altError.Degrees());
0412 }
0413 
0414 void PolarAlignmentAssistant::processPAHRefresh()
0415 {
0416     m_AlignView->setStarCircle();
0417     PAHUpdatedErrorTotal->clear();
0418     PAHIteration->clear();
0419     PAHUpdatedErrorAlt->clear();
0420     PAHUpdatedErrorAz->clear();
0421     QString debugString;
0422     imageNumber++;
0423     PAHIteration->setText(QString("Image %1").arg(imageNumber));
0424 
0425     if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
0426     {
0427         startSolver();
0428         return;
0429     }
0430 
0431     // Always run on the initial iteration to setup the user's star,
0432     // so that if it is enabled later the star could be tracked.
0433     // Flaw here is that if enough stars are not detected, iteration is not incremented,
0434     // so it may repeat.
0435     if ((pAHRefreshAlgorithm->currentIndex() == MOVE_STAR_UPDATE_ERR_ALGORITHM) || (refreshIteration == 0))
0436     {
0437         constexpr int MIN_PAH_REFRESH_STARS = 10;
0438 
0439         QList<Edge> stars;
0440         // Index of user's star in the detected stars list. In the first iteration
0441         // the stars haven't moved and we can just use the location of the click.
0442         // Later we'll need to find the star with starCorrespondence.
0443         int clickedStarIndex;
0444         detectStarsPAHRefresh(&stars, 100, correctionFrom.x(), correctionFrom.y(), &clickedStarIndex);
0445         if (clickedStarIndex < 0)
0446         {
0447             debugString = QString("PAA Refresh(%1): Didn't find the clicked star near %2,%3")
0448                           .arg(refreshIteration).arg(correctionFrom.x()).arg(correctionFrom.y());
0449             qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0450 
0451             emit newAlignTableResult(Align::ALIGN_RESULT_FAILED);
0452             emit captureAndSolve();
0453             return;
0454         }
0455 
0456         debugString = QString("PAA Refresh(%1): Refresh star(%2,%3) is index %4 with offset %5 %6")
0457                       .arg(refreshIteration + 1).arg(correctionFrom.x(), 4, 'f', 0)
0458                       .arg(correctionFrom.y(), 4, 'f', 0).arg(clickedStarIndex)
0459                       .arg(stars[clickedStarIndex].x - correctionFrom.x(), 4, 'f', 0)
0460                       .arg(stars[clickedStarIndex].y - correctionFrom.y(), 4, 'f', 0);
0461         qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0462 
0463         if (stars.size() > MIN_PAH_REFRESH_STARS)
0464         {
0465             int dx = 0;
0466             int dy = 0;
0467             int starIndex = -1;
0468 
0469             if (refreshIteration++ == 0)
0470             {
0471                 // First iteration. Setup starCorrespondence so we can find the user's star.
0472                 // clickedStarIndex should be the index of a detected star near where the user clicked.
0473                 starCorrespondencePAH.initialize(stars, clickedStarIndex);
0474                 if (clickedStarIndex >= 0)
0475                 {
0476                     setupCorrectionGraphics(QPointF(stars[clickedStarIndex].x, stars[clickedStarIndex].y));
0477                     emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
0478                     emit newFrame(m_AlignView);
0479                 }
0480             }
0481             else
0482             {
0483                 // Or, in other iterations find the movement of the "user's star".
0484                 // The 0.40 means it's OK if star correspondence only finds 40% of the
0485                 // reference stars (as we'd have more issues near the image edge otherwise).
0486                 QVector<int> starMap;
0487                 starCorrespondencePAH.find(stars, 200.0, &starMap, false, 0.40);
0488 
0489                 // Go through the starMap, and find the user's star, and compare its position
0490                 // to its initial position.
0491                 for (int i = 0; i < starMap.size(); ++i)
0492                 {
0493                     if (starMap[i] == starCorrespondencePAH.guideStar())
0494                     {
0495                         dx = stars[i].x - correctionFrom.x();
0496                         dy = stars[i].y - correctionFrom.y();
0497                         starIndex = i;
0498                         break;
0499                     }
0500                 }
0501                 if (starIndex == -1)
0502                 {
0503                     bool allOnes = true;
0504                     for (int i = 0; i < starMap.size(); ++i)
0505                     {
0506                         if (starMap[i] != -1)
0507                             allOnes = false;
0508                     }
0509                     debugString = QString("PAA Refresh(%1): starMap %2").arg(refreshIteration).arg(allOnes ? "ALL -1's" : "not all -1's");
0510                     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0511                 }
0512             }
0513 
0514             if (starIndex >= 0)
0515             {
0516                 // Annotate the user's star on the alignview.
0517                 m_AlignView->setStarCircle(QPointF(stars[starIndex].x, stars[starIndex].y));
0518                 debugString = QString("PAA Refresh(%1): User's star is now at %2,%3, with movement = %4,%5").arg(refreshIteration)
0519                               .arg(stars[starIndex].x, 4, 'f', 0).arg(stars[starIndex].y, 4, 'f', 0).arg(dx).arg(dy);
0520                 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0521 
0522                 double azE, altE;
0523                 if (polarAlign.pixelError(m_AlignView->keptImage(), QPointF(stars[starIndex].x, stars[starIndex].y),
0524                                           correctionTo, &azE, &altE))
0525                 {
0526                     updateRefreshDisplay(azE, altE);
0527                     debugString = QString("PAA Refresh(%1): %2,%3 --> %4,%5 @ %6,%7")
0528                                   .arg(refreshIteration).arg(correctionFrom.x(), 4, 'f', 0).arg(correctionFrom.y(), 4, 'f', 0)
0529                                   .arg(correctionTo.x(), 4, 'f', 0).arg(correctionTo.y(), 4, 'f', 0)
0530                                   .arg(stars[starIndex].x, 4, 'f', 0).arg(stars[starIndex].y, 4, 'f', 0);
0531                     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0532                 }
0533                 else
0534                 {
0535                     debugString = QString("PAA Refresh(%1): pixelError failed to estimate the remaining correction").arg(refreshIteration);
0536                     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0537                 }
0538             }
0539             else
0540             {
0541                 if (refreshIteration > 1)
0542                 {
0543                     debugString = QString("PAA Refresh(%1): Didn't find the user's star").arg(refreshIteration);
0544                     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0545                 }
0546             }
0547         }
0548         else
0549         {
0550             debugString = QString("PAA Refresh(%1): Too few stars detected (%2)").arg(refreshIteration).arg(stars.size());
0551             qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0552             emit updatedErrorsChanged(-1, -1, -1);
0553         }
0554     }
0555     // Finally start the next capture
0556     emit captureAndSolve();
0557 }
0558 
0559 bool PolarAlignmentAssistant::processSolverFailure()
0560 {
0561     if ((m_PAHStage == PAH_FIRST_CAPTURE ||
0562             m_PAHStage == PAH_SECOND_CAPTURE ||
0563             m_PAHStage == PAH_THIRD_CAPTURE ||
0564             m_PAHStage == PAH_FIRST_SOLVE ||
0565             m_PAHStage == PAH_SECOND_SOLVE ||
0566             m_PAHStage == PAH_THIRD_SOLVE)
0567             && ++m_PAHRetrySolveCounter < 4)
0568     {
0569         emit newLog(i18n("PAA: Solver failed, retrying."));
0570         emit captureAndSolve();
0571         return true;
0572     }
0573 
0574     if (m_PAHStage != PAH_IDLE)
0575     {
0576         emit newLog(i18n("PAA: Stopping, solver failed too many times."));
0577         stopPAHProcess();
0578     }
0579 
0580     return false;
0581 }
0582 
0583 void PolarAlignmentAssistant::setPAHStage(Stage stage)
0584 {
0585     if (stage != m_PAHStage)
0586     {
0587         m_PAHStage = stage;
0588         polarAlignWidget->updatePAHStage(stage);
0589         emit newPAHStage(m_PAHStage);
0590     }
0591 }
0592 
0593 void PolarAlignmentAssistant::processMountRotation(const dms &ra, double settleDuration)
0594 {
0595     double deltaAngle = fabs(ra.deltaAngle(targetPAH.ra()).Degrees());
0596 
0597     QString rotProgressMessage;
0598     QString rotDoneMessage;
0599     Stage nextCapture;
0600     Stage nextSettle;
0601 
0602     if (m_PAHStage == PAH_FIRST_ROTATE)
0603     {
0604         rotProgressMessage = "First mount rotation remaining degrees:";
0605         rotDoneMessage = i18n("Mount first rotation is complete.");
0606         nextCapture = PAH_SECOND_CAPTURE;
0607         nextSettle = PAH_FIRST_SETTLE;
0608     }
0609     else if (m_PAHStage == PAH_SECOND_ROTATE)
0610     {
0611         rotProgressMessage = "Second mount rotation remaining degrees:";
0612         rotDoneMessage = i18n("Mount second rotation is complete.");
0613         nextCapture = PAH_THIRD_CAPTURE;
0614         nextSettle = PAH_SECOND_SETTLE;
0615     }
0616     else return;
0617 
0618     if (m_PAHStage == PAH_FIRST_ROTATE || m_PAHStage == PAH_SECOND_ROTATE)
0619     {
0620         // only wait for telescope to slew to new position if manual slewing is switched off
0621         if(!pAHManualSlew->isChecked())
0622         {
0623             qCDebug(KSTARS_EKOS_ALIGN) << rotProgressMessage << deltaAngle;
0624             if (deltaAngle <= PAH_ROTATION_THRESHOLD)
0625             {
0626                 m_CurrentTelescope->StopWE();
0627                 emit newLog(rotDoneMessage);
0628 
0629                 if (settleDuration <= 0)
0630                 {
0631                     setPAHStage(nextCapture);
0632                     updateDisplay(m_PAHStage, getPAHMessage());
0633                 }
0634                 else
0635                 {
0636                     setPAHStage(nextSettle);
0637                     updateDisplay(m_PAHStage, getPAHMessage());
0638 
0639                     emit newLog(i18n("Settling..."));
0640                     QTimer::singleShot(settleDuration, [nextCapture, this]()
0641                     {
0642                         setPAHStage(nextCapture);
0643                         updateDisplay(m_PAHStage, getPAHMessage());
0644                     });
0645                 }
0646             }
0647             // If for some reason we didn't stop, let's stop if we get too far
0648             else if (deltaAngle > pAHRotation->value() * 1.25)
0649             {
0650                 m_CurrentTelescope->abort();
0651                 emit newLog(i18n("Mount aborted. Reverse RA axis direction and try again."));
0652                 stopPAHProcess();
0653             }
0654             return;
0655         } // endif not manual slew
0656     }
0657 }
0658 
0659 bool PolarAlignmentAssistant::checkPAHForMeridianCrossing()
0660 {
0661     // Make sure using -180 to 180 for hourAngle and DEC. (Yes dec should be between -90 and 90).
0662     double hourAngle = m_CurrentTelescope->hourAngle().Degrees();
0663     while (hourAngle < -180)
0664         hourAngle += 360;
0665     while (hourAngle > 180)
0666         hourAngle -= 360;
0667     double ra = 0, dec = 0;
0668     m_CurrentTelescope->getEqCoords(&ra, &dec);
0669     while (dec < -180)
0670         dec += 360;
0671     while (dec > 180)
0672         dec -= 360;
0673 
0674     // Don't do this check within 2 degrees of the poles.
0675     bool nearThePole = fabs(dec) > 88;
0676     if (nearThePole)
0677         return false;
0678 
0679     double degreesPerSlew = pAHRotation->value();
0680     bool closeToMeridian = fabs(hourAngle) < 2.0 * degreesPerSlew;
0681     bool goingWest = pAHDirection->currentIndex() == 0;
0682 
0683     // If the pier is on the east side (pointing west) and will slew west and is within 2 slews of the HA=0,
0684     // or on the west side (pointing east) and will slew east, and is within 2 slews of HA=0
0685     // then warn and give the user a chance to cancel.
0686     bool wouldCrossMeridian =
0687         ((m_CurrentTelescope->pierSide() == ISD::Mount::PIER_EAST && !goingWest && closeToMeridian) ||
0688          (m_CurrentTelescope->pierSide() == ISD::Mount::PIER_WEST && goingWest && closeToMeridian) ||
0689          (m_CurrentTelescope->pierSide() == ISD::Mount::PIER_UNKNOWN && closeToMeridian));
0690 
0691     return wouldCrossMeridian;
0692 }
0693 
0694 void PolarAlignmentAssistant::updateDisplay(Stage stage, const QString &message)
0695 {
0696     switch(stage)
0697     {
0698         case PAH_FIRST_ROTATE:
0699         case PAH_SECOND_ROTATE:
0700             if (pAHManualSlew->isChecked())
0701             {
0702                 polarAlignWidget->updatePAHStage(stage);
0703                 PAHWidgets->setCurrentWidget(PAHManualRotatePage);
0704                 manualRotateText->setText(message);
0705                 emit newPAHMessage(message);
0706                 return;
0707             }
0708         // fall through
0709         case PAH_FIRST_CAPTURE:
0710         case PAH_SECOND_CAPTURE:
0711         case PAH_THIRD_CAPTURE:
0712         case PAH_FIRST_SOLVE:
0713         case PAH_SECOND_SOLVE:
0714         case PAH_THIRD_SOLVE:
0715             polarAlignWidget->updatePAHStage(stage);
0716             PAHWidgets->setCurrentWidget(PAHMessagePage);
0717             PAHMessageText->setText(message);
0718             emit newPAHMessage(message);
0719             break;
0720 
0721         default:
0722             break;
0723 
0724     }
0725 }
0726 
0727 void PolarAlignmentAssistant::startPAHProcess()
0728 {
0729     qCInfo(KSTARS_EKOS_ALIGN) << QString("Starting Polar Alignment Assistant process %1 ...").arg(PAA_VERSION);
0730 
0731     auto executePAH = [ this ]()
0732     {
0733         setPAHStage(PAH_FIRST_CAPTURE);
0734 
0735         if (Options::limitedResourcesMode())
0736             emit newLog(i18n("Warning: Equatorial Grid Lines will not be drawn due to limited resources mode."));
0737 
0738         if (m_CurrentTelescope->hasAlignmentModel())
0739         {
0740             emit newLog(i18n("Clearing mount Alignment Model..."));
0741             m_CurrentTelescope->clearAlignmentModel();
0742         }
0743 
0744         // Unpark
0745         m_CurrentTelescope->unpark();
0746 
0747         // Set tracking ON if not already
0748         if (m_CurrentTelescope->canControlTrack() && m_CurrentTelescope->isTracking() == false)
0749             m_CurrentTelescope->setTrackEnabled(true);
0750 
0751         PAHStartB->setEnabled(false);
0752         PAHStopB->setEnabled(true);
0753 
0754         PAHUpdatedErrorTotal->clear();
0755         PAHUpdatedErrorAlt->clear();
0756         PAHUpdatedErrorAz->clear();
0757         PAHOrigErrorTotal->clear();
0758         PAHOrigErrorAlt->clear();
0759         PAHOrigErrorAz->clear();
0760         PAHIteration->setText("");
0761 
0762         updateDisplay(m_PAHStage, getPAHMessage());
0763 
0764         m_AlignView->setCorrectionParams(QPointF(), QPointF(), QPointF());
0765 
0766         m_PAHRetrySolveCounter = 0;
0767         emit captureAndSolve();
0768     };
0769 
0770     // Right off the bat, check if this alignment might cause a pier crash.
0771     // If we're crossing the meridian, warn unless within 5-degrees from the pole.
0772     if (checkPAHForMeridianCrossing())
0773     {
0774         // Continue
0775         connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executePAH]()
0776         {
0777             KSMessageBox::Instance()->disconnect(this);
0778             executePAH();
0779         });
0780 
0781         KSMessageBox::Instance()->warningContinueCancel(i18n("This could cause the telescope to cross the meridian."),
0782                 i18n("Warning"), 15);
0783     }
0784     else
0785         executePAH();
0786 }
0787 
0788 void PolarAlignmentAssistant::stopPAHProcess()
0789 {
0790     if (m_PAHStage == PAH_IDLE)
0791         return;
0792 
0793     // Only display dialog if user clicked.
0794     //    if ((static_cast<QPushButton *>(sender()) == PAHStopB) && KMessageBox::questionYesNo(KStars::Instance(),
0795     //            i18n("Are you sure you want to stop the polar alignment process?"),
0796     //            i18n("Polar Alignment Assistant"), KStandardGuiItem::yes(), KStandardGuiItem::no(),
0797     //            "restart_PAA_process_dialog") == KMessageBox::No)
0798     //        return;
0799 
0800     if (m_PAHStage == PAH_REFRESH)
0801     {
0802         setPAHStage(PAH_POST_REFRESH);
0803         polarAlignWidget->updatePAHStage(m_PAHStage);
0804     }
0805     qCInfo(KSTARS_EKOS_ALIGN) << "Stopping Polar Alignment Assistant process...";
0806 
0807 
0808     if (m_CurrentTelescope && m_CurrentTelescope->isInMotion())
0809         m_CurrentTelescope->abort();
0810 
0811     setPAHStage(PAH_IDLE);
0812     polarAlignWidget->updatePAHStage(m_PAHStage);
0813 
0814     PAHStartB->setEnabled(true);
0815     PAHStopB->setEnabled(false);
0816     PAHRefreshB->setEnabled(true);
0817     PAHWidgets->setCurrentWidget(PAHIntroPage);
0818     emit newPAHMessage(introText->text());
0819 
0820     m_AlignView->reset();
0821     m_AlignView->setRefreshEnabled(false);
0822 
0823     emit newFrame(m_AlignView);
0824     disconnect(m_AlignView.get(), &AlignView::trackingStarSelected, this,
0825                &Ekos::PolarAlignmentAssistant::setPAHCorrectionOffset);
0826     disconnect(m_AlignView.get(), &AlignView::newCorrectionVector, this, &Ekos::PolarAlignmentAssistant::newCorrectionVector);
0827 
0828     if (Options::pAHAutoPark())
0829     {
0830         m_CurrentTelescope->park();
0831         emit newLog(i18n("Parking the mount..."));
0832     }
0833 }
0834 
0835 void PolarAlignmentAssistant::rotatePAH()
0836 {
0837     double TargetDiffRA = pAHRotation->value();
0838     bool westMeridian = pAHDirection->currentIndex() == 0;
0839 
0840     // West
0841     if (westMeridian)
0842         TargetDiffRA *= -1;
0843     // East
0844     else
0845         TargetDiffRA *= 1;
0846 
0847     // JM 2018-05-03: Hemispheres shouldn't affect rotation direction in RA
0848 
0849     // if Manual slewing is selected, don't move the mount
0850     if (pAHManualSlew->isChecked())
0851     {
0852         return;
0853     }
0854 
0855     const SkyPoint telescopeCoord = m_CurrentTelescope->currentCoordinates();
0856 
0857     // TargetDiffRA is in degrees
0858     dms newTelescopeRA = (telescopeCoord.ra() + dms(TargetDiffRA)).reduce();
0859 
0860     targetPAH.setRA(newTelescopeRA);
0861     targetPAH.setDec(telescopeCoord.dec());
0862 
0863     //m_CurrentTelescope->Slew(&targetPAH);
0864     // Set Selected Speed
0865     if (pAHMountSpeed->currentIndex() >= 0)
0866         m_CurrentTelescope->setSlewRate(pAHMountSpeed->currentIndex());
0867     // Go to direction
0868     m_CurrentTelescope->MoveWE(westMeridian ? ISD::Mount::MOTION_WEST : ISD::Mount::MOTION_EAST,
0869                                ISD::Mount::MOTION_START);
0870 
0871     emit newLog(i18n("Please wait until mount completes rotating to RA (%1) DE (%2)", targetPAH.ra().toHMSString(),
0872                      targetPAH.dec().toDMSString()));
0873 }
0874 
0875 void PolarAlignmentAssistant::setupCorrectionGraphics(const QPointF &pixel)
0876 {
0877     polarAlign.refreshSolution(&refreshSolution, &altOnlyRefreshSolution);
0878 
0879     // We use the previously stored image (the 3rd PAA image)
0880     // so we can continue to estimate the correction even after
0881     // capturing new images during the refresh stage.
0882     const QSharedPointer<FITSData> &imageData = m_AlignView->keptImage();
0883 
0884     // Just the altitude correction
0885     if (!polarAlign.findCorrectedPixel(imageData, pixel, &correctionAltTo, true))
0886     {
0887         qCInfo(KSTARS_EKOS_ALIGN) << QString(i18n("PAA: Failed to findCorrectedPixel."));
0888         return;
0889     }
0890     // The whole correction.
0891     if (!polarAlign.findCorrectedPixel(imageData, pixel, &correctionTo))
0892     {
0893         qCInfo(KSTARS_EKOS_ALIGN) << QString(i18n("PAA: Failed to findCorrectedPixel."));
0894         return;
0895     }
0896     QString debugString = QString("PAA: Correction: %1,%2 --> %3,%4 (alt only %5,%6)")
0897                           .arg(pixel.x(), 4, 'f', 0).arg(pixel.y(), 4, 'f', 0)
0898                           .arg(correctionTo.x(), 4, 'f', 0).arg(correctionTo.y(), 4, 'f', 0)
0899                           .arg(correctionAltTo.x(), 4, 'f', 0).arg(correctionAltTo.y(), 4, 'f', 0);
0900     qCDebug(KSTARS_EKOS_ALIGN) << debugString;
0901     correctionFrom = pixel;
0902 
0903     if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
0904         updatePlateSolveTriangle(imageData);
0905     else
0906         m_AlignView->setCorrectionParams(correctionFrom, correctionTo, correctionAltTo);
0907 
0908     return;
0909 }
0910 
0911 
0912 bool PolarAlignmentAssistant::calculatePAHError()
0913 {
0914     // Hold on to the imageData so we can use it during the refresh phase.
0915     m_AlignView->holdOnToImage();
0916 
0917     if (!polarAlign.findAxis())
0918     {
0919         emit newLog(i18n("PAA: Failed to find RA Axis center."));
0920         stopPAHProcess();
0921         return false;
0922     }
0923 
0924     double azimuthError, altitudeError;
0925     polarAlign.calculateAzAltError(&azimuthError, &altitudeError);
0926     drawArrows(azimuthError, altitudeError);
0927     dms polarError(hypot(altitudeError, azimuthError));
0928     dms azError(azimuthError), altError(altitudeError);
0929 
0930     // No need to toggle EQGrid
0931     //    if (m_AlignView->isEQGridShown() == false && !Options::limitedResourcesMode())
0932     //        m_AlignView->toggleEQGrid();
0933 
0934     QString msg = QString("%1. Azimuth: %2  Altitude: %3")
0935                   .arg(polarError.toDMSString()).arg(azError.toDMSString())
0936                   .arg(altError.toDMSString());
0937     emit newLog(QString("Polar Alignment Error: %1").arg(msg));
0938 
0939     polarAlign.setMaxPixelSearchRange(polarError.Degrees() + 1);
0940 
0941     // These are viewed during the refresh phase.
0942     PAHOrigErrorTotal->setText(polarError.toDMSString());
0943     PAHOrigErrorAlt->setText(altError.toDMSString());
0944     PAHOrigErrorAz->setText(azError.toDMSString());
0945 
0946     setupCorrectionGraphics(QPointF(m_ImageData->width() / 2, m_ImageData->height() / 2));
0947 
0948     // Find Celestial pole location and mount's RA axis
0949     SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90);
0950     QPointF imagePoint, celestialPolePoint;
0951     m_ImageData->wcsToPixel(CP, celestialPolePoint, imagePoint);
0952     if (m_ImageData->contains(celestialPolePoint))
0953     {
0954         m_AlignView->setCelestialPole(celestialPolePoint);
0955         QPointF raAxis;
0956         if (polarAlign.findCorrectedPixel(m_ImageData, celestialPolePoint, &raAxis))
0957             m_AlignView->setRaAxis(raAxis);
0958     }
0959 
0960     connect(m_AlignView.get(), &AlignView::trackingStarSelected, this, &Ekos::PolarAlignmentAssistant::setPAHCorrectionOffset);
0961     emit polarResultUpdated(QLineF(correctionFrom, correctionTo), polarError.Degrees(), azError.Degrees(), altError.Degrees());
0962 
0963     connect(m_AlignView.get(), &AlignView::newCorrectionVector, this, &Ekos::PolarAlignmentAssistant::newCorrectionVector,
0964             Qt::UniqueConnection);
0965     syncCorrectionVector();
0966     emit newFrame(m_AlignView);
0967 
0968     return true;
0969 }
0970 
0971 void PolarAlignmentAssistant::syncCorrectionVector()
0972 {
0973     if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
0974         return;
0975     emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
0976     m_AlignView->setCorrectionParams(correctionFrom, correctionTo, correctionAltTo);
0977 }
0978 
0979 void PolarAlignmentAssistant::setPAHCorrectionOffsetPercentage(double dx, double dy)
0980 {
0981     double x = dx * m_AlignView->zoomedWidth();
0982     double y = dy * m_AlignView->zoomedHeight();
0983     setPAHCorrectionOffset(static_cast<int>(round(x)), static_cast<int>(round(y)));
0984 }
0985 
0986 void PolarAlignmentAssistant::setPAHCorrectionOffset(int x, int y)
0987 {
0988     if (m_PAHStage == PAH_REFRESH)
0989     {
0990         emit newLog(i18n("Polar-alignment star cannot be updated during refresh phase as it might affect error measurements."));
0991     }
0992     else
0993     {
0994         setupCorrectionGraphics(QPointF(x, y));
0995         emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
0996         emit newFrame(m_AlignView);
0997     }
0998 }
0999 
1000 void PolarAlignmentAssistant::setPAHSlewDone()
1001 {
1002     emit newPAHMessage("Manual slew done.");
1003     switch(m_PAHStage)
1004     {
1005         case PAH_FIRST_ROTATE :
1006             setPAHStage(PAH_SECOND_CAPTURE);
1007             emit newLog(i18n("First manual rotation done."));
1008             updateDisplay(m_PAHStage, getPAHMessage());
1009             break;
1010         case PAH_SECOND_ROTATE :
1011             setPAHStage(PAH_THIRD_CAPTURE);
1012             emit newLog(i18n("Second manual rotation done."));
1013             updateDisplay(m_PAHStage, getPAHMessage());
1014             break;
1015         default :
1016             return; // no other stage should be able to trigger this event
1017     }
1018 }
1019 
1020 
1021 
1022 void PolarAlignmentAssistant::startPAHRefreshProcess()
1023 {
1024     qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant refreshing...";
1025 
1026     refreshIteration = 0;
1027     imageNumber = 0;
1028     m_NumHealpixFailures = 0;
1029 
1030     setPAHStage(PAH_REFRESH);
1031     polarAlignWidget->updatePAHStage(m_PAHStage);
1032     auto message = getPAHMessage();
1033     refreshText->setText(message);
1034     emit newPAHMessage(message);
1035 
1036     PAHRefreshB->setEnabled(false);
1037 
1038     // Hide EQ Grids if shown
1039     if (m_AlignView->isEQGridShown())
1040         m_AlignView->toggleEQGrid();
1041 
1042     m_AlignView->setRefreshEnabled(true);
1043 
1044     Options::setAstrometrySolverWCS(false);
1045     Options::setAutoWCS(false);
1046 
1047     // We for refresh, just capture really
1048     emit captureAndSolve();
1049 }
1050 
1051 void PolarAlignmentAssistant::processPAHStage(double orientation, double ra, double dec, double pixscale,
1052         bool eastToTheRight, short healpix, short index)
1053 {
1054     if (m_PAHStage == PAH_FIND_CP)
1055     {
1056         emit newLog(
1057             i18n("Mount is synced to celestial pole. You can now continue Polar Alignment Assistant procedure."));
1058         setPAHStage(PAH_FIRST_CAPTURE);
1059         polarAlignWidget->updatePAHStage(m_PAHStage);
1060         return;
1061     }
1062 
1063     if (m_PAHStage == PAH_FIRST_SOLVE || m_PAHStage == PAH_SECOND_SOLVE || m_PAHStage == PAH_THIRD_SOLVE)
1064     {
1065         // Used by refresh, when looking at the 3rd image.
1066         m_LastRa = ra;
1067         m_LastDec = dec;
1068         m_LastOrientation = orientation;
1069         m_LastPixscale = pixscale;
1070         m_HealpixToUse = healpix;
1071         m_IndexToUse = index;
1072 
1073         bool doWcs = (m_PAHStage == PAH_THIRD_SOLVE) || !Options::limitedResourcesMode();
1074         if (doWcs)
1075         {
1076             emit newLog(i18n("Please wait while WCS data is processed..."));
1077             PAHMessageText->setText(
1078                 m_PAHStage == PAH_FIRST_SOLVE
1079                 ? "Calculating WCS for the first image...</p>"
1080                 : (m_PAHStage == PAH_SECOND_SOLVE ? "Calculating WCS for the second image...</p>"
1081                    : "Calculating WCS for the third image...</p>"));
1082         }
1083         connect(m_AlignView.get(), &AlignView::wcsToggled, this, &Ekos::PolarAlignmentAssistant::setWCSToggled,
1084                 Qt::UniqueConnection);
1085         m_AlignView->injectWCS(orientation, ra, dec, pixscale, eastToTheRight);
1086         return;
1087     }
1088 }
1089 
1090 void PolarAlignmentAssistant::setImageData(const QSharedPointer<FITSData> &image)
1091 {
1092     m_ImageData = image;
1093 
1094     if (m_PAHStage == PAH_FIRST_CAPTURE)
1095         setPAHStage(PAH_FIRST_SOLVE);
1096     else if (m_PAHStage == PAH_SECOND_CAPTURE)
1097         setPAHStage(PAH_SECOND_SOLVE);
1098     else if (m_PAHStage == PAH_THIRD_CAPTURE)
1099         setPAHStage(PAH_THIRD_SOLVE);
1100     else
1101         return;
1102     updateDisplay(m_PAHStage, getPAHMessage());
1103 }
1104 
1105 void PolarAlignmentAssistant::setWCSToggled(bool result)
1106 {
1107     emit newLog(i18n("WCS data processing is complete."));
1108 
1109     disconnect(m_AlignView.get(), &AlignView::wcsToggled, this, &Ekos::PolarAlignmentAssistant::setWCSToggled);
1110 
1111     if (m_PAHStage == PAH_FIRST_CAPTURE || m_PAHStage == PAH_FIRST_SOLVE)
1112     {
1113         // We need WCS to be synced first
1114         if (result == false && m_AlignInstance->wcsSynced() == true)
1115         {
1116             emit newLog(i18n("WCS info is now valid. Capturing next frame..."));
1117             emit captureAndSolve();
1118             return;
1119         }
1120 
1121         polarAlign.reset();
1122         polarAlign.addPoint(m_ImageData);
1123 
1124         setPAHStage(PAH_FIRST_ROTATE);
1125         auto msg = getPAHMessage();
1126         if (pAHManualSlew->isChecked())
1127         {
1128             msg = QString("Please rotate your mount about %1 deg in RA")
1129                   .arg(pAHRotation->value());
1130             emit newLog(msg);
1131         }
1132         updateDisplay(m_PAHStage, msg);
1133 
1134         rotatePAH();
1135     }
1136     else if (m_PAHStage == PAH_SECOND_CAPTURE || m_PAHStage == PAH_SECOND_SOLVE)
1137     {
1138         setPAHStage(PAH_SECOND_ROTATE);
1139         auto msg = getPAHMessage();
1140 
1141         if (pAHManualSlew->isChecked())
1142         {
1143             msg = QString("Please rotate your mount about %1 deg in RA")
1144                   .arg(pAHRotation->value());
1145             emit newLog(msg);
1146         }
1147         updateDisplay(m_PAHStage, msg);
1148 
1149         polarAlign.addPoint(m_ImageData);
1150 
1151         rotatePAH();
1152     }
1153     else if (m_PAHStage == PAH_THIRD_CAPTURE || m_PAHStage == PAH_THIRD_SOLVE)
1154     {
1155         // Critical error
1156         if (result == false)
1157         {
1158             emit newLog(i18n("Failed to process World Coordinate System: %1. Try again.", m_ImageData->getLastError()));
1159             return;
1160         }
1161 
1162         polarAlign.addPoint(m_ImageData);
1163 
1164         // We have 3 points which uniquely defines a circle with its center representing the RA Axis
1165         // We have celestial pole location. So correction vector is just the vector between these two points
1166         if (calculatePAHError())
1167         {
1168             setPAHStage(PAH_STAR_SELECT);
1169             polarAlignWidget->updatePAHStage(m_PAHStage);
1170             PAHWidgets->setCurrentWidget(PAHRefreshPage);
1171             refreshText->setText(getPAHMessage());
1172             emit newPAHMessage(getPAHMessage());
1173         }
1174         else
1175         {
1176             emit newLog(i18n("PAA: Failed to find the RA axis. Quitting."));
1177             stopPAHProcess();
1178         }
1179     }
1180 }
1181 
1182 void PolarAlignmentAssistant::setMountStatus(ISD::Mount::Status newState)
1183 {
1184     switch (newState)
1185     {
1186         case ISD::Mount::MOUNT_PARKING:
1187         case ISD::Mount::MOUNT_SLEWING:
1188         case ISD::Mount::MOUNT_MOVING:
1189             PAHStartB->setEnabled(false);
1190             break;
1191 
1192         default:
1193             if (m_PAHStage == PAH_IDLE)
1194                 PAHStartB->setEnabled(true);
1195             break;
1196     }
1197 }
1198 
1199 QString PolarAlignmentAssistant::getPAHMessage() const
1200 {
1201     switch (m_PAHStage)
1202     {
1203         case PAH_IDLE:
1204         case PAH_FIND_CP:
1205             return introText->text();
1206         case PAH_FIRST_CAPTURE:
1207             return i18n("<p>The assistant requires three images to find a solution.  Ekos is now capturing the first image...</p>");
1208         case PAH_FIRST_SOLVE:
1209             return i18n("<p>Solving the <i>first</i> image...</p>");
1210         case PAH_FIRST_ROTATE:
1211             return i18n("<p>Executing the <i>first</i> mount rotation...</p>");
1212         case PAH_FIRST_SETTLE:
1213             return i18n("<p>Settling after the <i>first</i> mount rotation.</p>");
1214         case PAH_SECOND_SETTLE:
1215             return i18n("<p>Settling after the <i>second</i> mount rotation.</p>");
1216         case PAH_SECOND_CAPTURE:
1217             return i18n("<p>Capturing the second image...</p>");
1218         case PAH_SECOND_SOLVE:
1219             return i18n("<p>Solving the <i>second</i> image...</p>");
1220         case PAH_SECOND_ROTATE:
1221             return i18n("<p>Executing the <i>second</i> mount rotation...</p>");
1222         case PAH_THIRD_CAPTURE:
1223             return i18n("<p>Capturing the <i>third</i> and final image...</p>");
1224         case PAH_THIRD_SOLVE:
1225             return i18n("<p>Solving the <i>third</i> image...</p>");
1226         case PAH_STAR_SELECT:
1227             if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
1228                 return i18n("<p>Choose your exposure time & select an adjustment method. Then click <i>refresh</i> to begin adjustments.</p>");
1229             else
1230                 return i18n("<p>Choose your exposure time & select an adjustment method. Click <i>Refresh</i> to begin.</p><p>Correction triangle is plotted above. <i>Zoom in and select a bright star</i> to reposition the correction vector. Use the <i>MoveStar & Calc Error</i> method to estimate the remaining error.</p>");
1231         case PAH_REFRESH:
1232             if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
1233                 return i18n("<p>Adjust mount's <i>Altitude and Azimuth knobs</i> to reduce the polar alignment error.</p><p>Be patient, plate solving can be affected by knob movement. Consider using results after 2 images.  Click <i>Stop</i> when you're finished.</p>");
1234             else
1235                 return i18n("<p>Adjust mount's <i>Altitude knob</i> to move the star along the yellow line, then adjust the <i>Azimuth knob</i> to move it along the Green line until the selected star is centered within the crosshair.</p><p>Click <i>Stop</i> when the star is centered.</p>");
1236         default:
1237             break;
1238     }
1239 
1240     return QString();
1241 }
1242 
1243 void PolarAlignmentAssistant::setPAHRefreshAlgorithm(RefreshAlgorithm value)
1244 {
1245     // If the star-correspondence method of tracking polar alignment error wasn't initialized,
1246     // at the start, it can't be turned on mid process.
1247     if ((m_PAHStage == PAH_REFRESH) && refreshIteration > 0 && (value != PLATE_SOLVE_ALGORITHM)
1248             && !starCorrespondencePAH.size())
1249     {
1250         pAHRefreshAlgorithm->setCurrentIndex(PLATE_SOLVE_ALGORITHM);
1251         emit newLog(i18n("Cannot change to MoveStar algorithm once refresh has begun"));
1252         return;
1253     }
1254     if (m_PAHStage == PAH_REFRESH || m_PAHStage == PAH_STAR_SELECT)
1255     {
1256         refreshText->setText(getPAHMessage());
1257         emit newPAHMessage(getPAHMessage());
1258     }
1259 
1260     showUpdatedError((value == PLATE_SOLVE_ALGORITHM) ||
1261                      (value == MOVE_STAR_UPDATE_ERR_ALGORITHM));
1262     if (value == PLATE_SOLVE_ALGORITHM)
1263         updatePlateSolveTriangle(m_ImageData);
1264     else
1265         m_AlignView->setCorrectionParams(correctionFrom, correctionTo, correctionAltTo);
1266 
1267 }
1268 
1269 }