Kstars

polaralignmentassistant.cpp
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 "alignview.h"
12#include "kstars.h"
13#include "kstarsdata.h"
14#include "ksmessagebox.h"
15#include "ekos/auxiliary/stellarsolverprofile.h"
16#include "ekos/auxiliary/solverutils.h"
17#include "Options.h"
18#include "polaralignwidget.h"
19#include <ekos_align_debug.h>
20
21#define PAA_VERSION "v3.0"
22
23namespace Ekos
24{
25
26const QMap<PolarAlignmentAssistant::Stage, KLocalizedString> PolarAlignmentAssistant::PAHStages =
27{
28 {PAH_IDLE, ki18n("Idle")},
29 {PAH_FIRST_CAPTURE, ki18n("First Capture")},
30 {PAH_FIRST_SOLVE, ki18n("First Solve")},
31 {PAH_FIND_CP, ki18n("Finding CP")},
32 {PAH_FIRST_ROTATE, ki18n("First Rotation")},
33 {PAH_FIRST_SETTLE, ki18n("First Settle")},
34 {PAH_SECOND_CAPTURE, ki18n("Second Capture")},
35 {PAH_SECOND_SOLVE, ki18n("Second Solve")},
36 {PAH_SECOND_ROTATE, ki18n("Second Rotation")},
37 {PAH_SECOND_SETTLE, ki18n("Second Settle")},
38 {PAH_THIRD_CAPTURE, ki18n("Third Capture")},
39 {PAH_THIRD_SOLVE, ki18n("Third Solve")},
40 {PAH_STAR_SELECT, ki18n("Select Star")},
41 {PAH_REFRESH, ki18n("Refreshing")},
42 {PAH_POST_REFRESH, ki18n("Refresh Complete")},
43};
44
45PolarAlignmentAssistant::PolarAlignmentAssistant(Align *parent, const QSharedPointer<AlignView> &view) : QWidget(parent)
46{
47 setupUi(this);
48 polarAlignWidget = new PolarAlignWidget();
49 mainPALayout->insertWidget(0, polarAlignWidget);
50
51 m_AlignInstance = parent;
52 m_AlignView = view;
53
54 showUpdatedError((pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM) ||
55 (pAHRefreshAlgorithm->currentIndex() == MOVE_STAR_UPDATE_ERR_ALGORITHM));
56
57 connect(pAHRefreshAlgorithm, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
58 {
59 setPAHRefreshAlgorithm(static_cast<RefreshAlgorithm>(index));
60 });
61 starCorrespondencePAH.reset();
62
63 // PAH Connections
64 PAHWidgets->setCurrentWidget(PAHIntroPage);
65 connect(this, &PolarAlignmentAssistant::PAHEnabled, [&](bool enabled)
66 {
67 PAHStartB->setEnabled(enabled);
68 directionLabel->setEnabled(enabled);
69 pAHDirection->setEnabled(enabled);
70 pAHRotation->setEnabled(enabled);
71 pAHMountSpeed->setEnabled(enabled);
72 pAHManualSlew->setEnabled(enabled);
73 });
74 connect(PAHStartB, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::startPAHProcess);
75 // PAH StopB is just a shortcut for the regular stop
76 connect(PAHStopB, &QPushButton::clicked, this, &PolarAlignmentAssistant::stopPAHProcess);
77 connect(PAHRefreshB, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::startPAHRefreshProcess);
78 // done button for manual slewing during polar alignment:
79 connect(PAHManualDone, &QPushButton::clicked, this, &Ekos::PolarAlignmentAssistant::setPAHSlewDone);
80
81 hemisphere = KStarsData::Instance()->geo()->lat()->Degrees() > 0 ? NORTH_HEMISPHERE : SOUTH_HEMISPHERE;
82
83 label_PAHOrigErrorAz->setText("Az: ");
84 label_PAHOrigErrorAlt->setText("Alt: ");
85}
86
87PolarAlignmentAssistant::~PolarAlignmentAssistant()
88{
89}
90
91void PolarAlignmentAssistant::showUpdatedError(bool show)
92{
93 label_PAHUpdatedErrorTotal->setVisible(show);
94 PAHUpdatedErrorTotal->setVisible(show);
95 label_PAHUpdatedErrorAlt->setVisible(show);
96 PAHUpdatedErrorAlt->setVisible(show);
97 label_PAHUpdatedErrorAz->setVisible(show);
98 PAHUpdatedErrorAz->setVisible(show);
99}
100
101void PolarAlignmentAssistant::syncMountSpeed(const QString &speed)
102{
103 pAHMountSpeed->blockSignals(true);
104 pAHMountSpeed->clear();
105 pAHMountSpeed->addItems(m_CurrentTelescope->slewRates());
106 pAHMountSpeed->setCurrentText(speed);
107 pAHMountSpeed->blockSignals(false);
108}
109
110void PolarAlignmentAssistant::setEnabled(bool enabled)
111{
112 QWidget::setEnabled(enabled);
113
114 emit PAHEnabled(enabled);
115 if (enabled)
116 {
117 PAHWidgets->setToolTip(QString());
118 FOVDisabledLabel->hide();
119 }
120 else
121 {
122 PAHWidgets->setToolTip(i18n("<p>Polar Alignment tool requires a German Equatorial Mount.</p>"));
123 FOVDisabledLabel->show();
124 }
125
126}
127
128// This solver is only used by the refresh plate-solving scheme.
129void PolarAlignmentAssistant::startSolver()
130{
131 auto profiles = getDefaultAlignOptionsProfiles();
132 SSolver::Parameters parameters;
133 // Get solver parameters
134 // In case of exception, use first profile
135 try
136 {
137 parameters = profiles.at(Options::solveOptionsProfile());
138 }
139 catch (std::out_of_range const &)
140 {
141 parameters = profiles[0];
142 }
143
144 // Double search radius
145 parameters.search_radius = parameters.search_radius * 2;
146 constexpr double solverTimeout = 10.0;
147
148 m_Solver.reset(new SolverUtils(parameters, solverTimeout), &QObject::deleteLater);
149 connect(m_Solver.get(), &SolverUtils::done, this, &PolarAlignmentAssistant::solverDone, Qt::UniqueConnection);
150
151 // Use the scale and position from the most recent solve.
152 m_Solver->useScale(Options::astrometryUseImageScale(), m_LastPixscale * 0.9, m_LastPixscale * 1.1);
153 m_Solver->usePosition(true, m_LastRa, m_LastDec);
154 m_Solver->setHealpix(m_IndexToUse, m_HealpixToUse);
155 m_Solver->runSolver(m_ImageData);
156}
157
158void PolarAlignmentAssistant::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution,
159 double elapsedSeconds)
160{
161 disconnect(m_Solver.get(), &SolverUtils::done, this, &PolarAlignmentAssistant::solverDone);
162
163 if (m_PAHStage != PAH_REFRESH)
164 return;
165
166 if (timedOut || !success)
167 {
168 // I'm torn between 1 and 2 for this constant.
169 // 1: If a solve failed, open up the search next time.
170 // 2: A common reason for a solve to fail is because a knob was adjusted during the exposure.
171 // Let it try one more time.
172 constexpr int MAX_NUM_HEALPIX_FAILURES = 2;
173 if (++m_NumHealpixFailures >= MAX_NUM_HEALPIX_FAILURES)
174 {
175 // Don't use the previous index and healpix next time we solve.
176 m_IndexToUse = -1;
177 m_HealpixToUse = -1;
178 }
179 }
180 else
181 {
182 // Get the index and healpix from the successful solve.
183 m_Solver->getSolutionHealpix(&m_IndexToUse, &m_HealpixToUse);
184 }
185
186 if (timedOut)
187 {
188 emit newLog(i18n("Refresh solver timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1)));
189 emit captureAndSolve();
190 }
191 else if (!success)
192 {
193 emit newLog(i18n("Refresh solver failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1)));
194 emit updatedErrorsChanged(-1, -1, -1);
195 emit captureAndSolve();
196 }
197 else
198 {
199 m_NumHealpixFailures = 0;
200 refreshIteration++;
201 const double ra = solution.ra;
202 const double dec = solution.dec;
203 m_LastRa = solution.ra;
204 m_LastDec = solution.dec;
205 m_LastOrientation = solution.orientation;
206 m_LastPixscale = solution.pixscale;
207
208 emit newLog(QString("Refresh solver success %1s: ra %2 dec %3 scale %4")
209 .arg(elapsedSeconds, 0, 'f', 1).arg(ra, 0, 'f', 3)
210 .arg(dec, 0, 'f', 3).arg(solution.pixscale));
211
212 // RA is input in hours, not degrees!
213 SkyPoint refreshCoords(ra / 15.0, dec);
214 double azError = 0, altError = 0;
215 if (polarAlign.processRefreshCoords(refreshCoords, m_ImageData->getDateTime(), &azError, &altError))
216 {
217 updateRefreshDisplay(azError, altError);
218
219 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
220 // The 2nd false means don't block. The code below doesn't work if we block
221 // because wcsToPixel in updateTriangle() depends on the injectWCS being finished.
222 m_AlignView->injectWCS(solution.orientation, ra, dec, solution.pixscale, eastToTheRight, false);
223 updatePlateSolveTriangle(m_ImageData);
224 }
225 else
226 emit newLog(QString("Could not estimate mount rotation"));
227 }
228 // Start the next refresh capture.
229 emit captureAndSolve();
230}
231
232void PolarAlignmentAssistant::updatePlateSolveTriangle(const QSharedPointer<FITSData> &image)
233{
234 if (image.isNull())
235 return;
236
237 // To render the triangle for the plate-solve refresh, we need image coordinates for
238 // - the original (3rd image) center point (originalPoint)
239 // - the solution point (solutionPoint),
240 // - the alt-only solution point (altOnlyPoint),
241 // - the current center of the image (centerPixel),
242 // The triangle is made from the first 3 above.
243 const SkyPoint &originalCoords = polarAlign.getPoint(2);
244 QPointF originalPixel, solutionPixel, altOnlyPixel, dummy;
245 QPointF centerPixel(image->width() / 2, image->height() / 2);
246 if (image->wcsToPixel(originalCoords, originalPixel, dummy) &&
247 image->wcsToPixel(refreshSolution, solutionPixel, dummy) &&
248 image->wcsToPixel(altOnlyRefreshSolution, altOnlyPixel, dummy))
249 {
250 m_AlignView->setCorrectionParams(originalPixel, solutionPixel, altOnlyPixel);
251 m_AlignView->setStarCircle(centerPixel);
252 }
253 else
254 {
255 qCDebug(KSTARS_EKOS_ALIGN) << "wcs failed";
256 }
257}
258
259namespace
260{
261
262// These functions paint up/down/left/right-pointing arrows in a box of size w x h.
263
264void upArrow(QPainter *painter, int w, int h)
265{
266 const double wCenter = w / 2, lineTop = 0.38, lineBottom = 0.9;
267 const double lineLength = h * (lineBottom - lineTop);
268 painter->drawRect(wCenter - w * .1, h * lineTop, w * .2, lineLength);
269 QPolygonF arrowHead;
270 arrowHead << QPointF(wCenter, h * .1) << QPointF(w * .2, h * .4) << QPointF(w * .8, h * .4);
271 painter->drawConvexPolygon(arrowHead);
272}
273void downArrow(QPainter *painter, int w, int h)
274{
275 const double wCenter = w / 2, lineBottom = 0.62, lineTop = 0.1;
276 const double lineLength = h * (lineBottom - lineTop);
277 painter->drawRect(wCenter - w * .1, h * lineTop, w * .2, lineLength);
278 QPolygonF arrowHead;
279 arrowHead << QPointF(wCenter, h * .9) << QPointF(w * .2, h * .6) << QPointF(w * .8, h * .6);
280 painter->drawConvexPolygon(arrowHead);
281}
282void leftArrow(QPainter *painter, int w, int h)
283{
284 const double hCenter = h / 2, lineLeft = 0.38, lineRight = 0.9;
285 const double lineLength = w * (lineRight - lineLeft);
286 painter->drawRect(h * lineLeft, hCenter - h * .1, lineLength, h * .2);
287 QPolygonF arrowHead;
288 arrowHead << QPointF(w * .1, hCenter) << QPointF(w * .4, h * .2) << QPointF(w * .4, h * .8);
289 painter->drawConvexPolygon(arrowHead);
290}
291void rightArrow(QPainter *painter, int w, int h)
292{
293 const double hCenter = h / 2, lineLeft = .1, lineRight = .62;
294 const double lineLength = w * (lineRight - lineLeft);
295 painter->drawRect(h * lineLeft, hCenter - h * .1, lineLength, h * .2);
296 QPolygonF arrowHead;
297 arrowHead << QPointF(w * .9, hCenter) << QPointF(w * .6, h * .2) << QPointF(w * .6, h * .8);
298 painter->drawConvexPolygon(arrowHead);
299}
300} // namespace
301
302// Draw arrows that give the user a hint of how he/she needs to adjust the azimuth and altitude
303// knobs. The arrows' size changes depending on the azimuth and altitude error.
304void PolarAlignmentAssistant::drawArrows(double azError, double altError)
305{
306 constexpr double minError = 20.0 / 3600.0; // 20 arcsec
307 double absError = fabs(altError);
308
309 // these constants are worked out so a 10' error gives a size of 100
310 // and a 1' error gives a size of 20.
311 constexpr double largeErr = 10.0 / 60.0, smallErr = 1.0 / 60.0, largeSize = 100, smallSize = 20, c1 = 533.33, c2 = 11.111;
312
313 int size = 0;
314 if (absError > largeErr)
315 size = largeSize;
316 else if (absError < smallErr)
317 size = smallSize;
318 else size = absError * c1 + c2;
319
320 QPixmap altPixmap(size, size);
321 altPixmap.fill(QColor("transparent"));
322 QPainter altPainter(&altPixmap);
323 altPainter.setBrush(arrowAlt->palette().color(QPalette::WindowText));
324
325 if (altError > minError)
326 downArrow(&altPainter, size, size);
327 else if (altError < -minError)
328 upArrow(&altPainter, size, size);
329 arrowAlt->setPixmap(altPixmap);
330
331 absError = fabs(azError);
332 if (absError > largeErr)
333 size = largeSize;
334 else if (absError < smallErr)
335 size = smallSize;
336 else size = absError * c1 + c2;
337
338 QPixmap azPixmap(size, size);
339 azPixmap.fill(QColor("transparent"));
340 QPainter azPainter(&azPixmap);
341 azPainter.setBrush(arrowAz->palette().color(QPalette::WindowText));
342
343 if (azError > minError)
344 leftArrow(&azPainter, size, size);
345 else if (azError < -minError)
346 rightArrow(&azPainter, size, size);
347 arrowAz->setPixmap(azPixmap);
348}
349
350bool PolarAlignmentAssistant::detectStarsPAHRefresh(QList<Edge> *stars, int num, int x, int y, int *starIndex)
351{
352 stars->clear();
353 *starIndex = -1;
354
355 // Use the solver settings from the align tab for for "polar-align refresh" star detection.
356 QVariantMap settings;
357 settings["optionsProfileIndex"] = Options::solveOptionsProfile();
358 settings["optionsProfileGroup"] = static_cast<int>(Ekos::AlignProfiles);
359 m_ImageData->setSourceExtractorSettings(settings);
360
361 QElapsedTimer timer;
362 m_ImageData->findStars(ALGORITHM_SEP).waitForFinished();
363
364 QString debugString = QString("PAA Refresh: Detected %1 stars (%2s)")
365 .arg(m_ImageData->getStarCenters().size()).arg(timer.elapsed() / 1000.0, 5, 'f', 3);
366 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
367
368 QList<Edge *> detectedStars = m_ImageData->getStarCenters();
369 // Let's sort detectedStars by flux, starting with widest
370 std::sort(detectedStars.begin(), detectedStars.end(), [](const Edge * edge1, const Edge * edge2) -> bool { return edge1->sum > edge2->sum;});
371
372 // Find the closest star to the x,y position, which is where the user clicked on the alignView.
373 double bestDist = 1e9;
374 int bestIndex = -1;
375 for (int i = 0; i < detectedStars.size(); i++)
376 {
377 double dx = detectedStars[i]->x - x;
378 double dy = detectedStars[i]->y - y;
379 double dist = dx * dx + dy * dy;
380 if (dist < bestDist)
381 {
382 bestDist = dist;
383 bestIndex = i;
384 }
385 }
386
387 int starCount = qMin(num, detectedStars.count());
388 for (int i = 0; i < starCount; i++)
389 stars->append(*(detectedStars[i]));
390 if (bestIndex >= starCount)
391 {
392 // If we found the star, but requested 'num' stars, and the user's star
393 // is lower down in the list, add it and return num+1 stars.
394 stars->append(*(detectedStars[bestIndex]));
395 *starIndex = starCount;
396 }
397 else
398 {
399 *starIndex = bestIndex;
400 }
401 debugString = QString("PAA Refresh: User's star(%1,%2) is index %3").arg(x).arg(y).arg(*starIndex);
402 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
403
404 detectedStars.clear();
405
406 return stars->count();
407}
408
409void PolarAlignmentAssistant::updateRefreshDisplay(double azE, double altE)
410{
411 drawArrows(azE, altE);
412 const double errDegrees = hypot(azE, altE);
413 dms totalError(errDegrees), azError(azE), altError(altE);
414 PAHUpdatedErrorTotal->setText(totalError.toDMSString());
415 PAHUpdatedErrorAlt->setText(altError.toDMSString());
416 PAHUpdatedErrorAz->setText(azError.toDMSString());
417
418 QString debugString = QString("PAA Refresh(%1): Corrected az: %2 alt: %4 total: %6")
419 .arg(refreshIteration).arg(azError.toDMSString())
420 .arg(altError.toDMSString()).arg(totalError.toDMSString());
421 emit newLog(debugString);
422 emit updatedErrorsChanged(totalError.Degrees(), azError.Degrees(), altError.Degrees());
423}
424
425void PolarAlignmentAssistant::processPAHRefresh()
426{
427 m_AlignView->setStarCircle();
428 PAHUpdatedErrorTotal->clear();
429 PAHIteration->clear();
430 PAHUpdatedErrorAlt->clear();
431 PAHUpdatedErrorAz->clear();
432 QString debugString;
433 imageNumber++;
434 PAHIteration->setText(QString("Image %1").arg(imageNumber));
435
436 if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
437 {
438 startSolver();
439 return;
440 }
441
442 // Always run on the initial iteration to setup the user's star,
443 // so that if it is enabled later the star could be tracked.
444 // Flaw here is that if enough stars are not detected, iteration is not incremented,
445 // so it may repeat.
446 if ((pAHRefreshAlgorithm->currentIndex() == MOVE_STAR_UPDATE_ERR_ALGORITHM) || (refreshIteration == 0))
447 {
448 constexpr int MIN_PAH_REFRESH_STARS = 10;
449
450 QList<Edge> stars;
451 // Index of user's star in the detected stars list. In the first iteration
452 // the stars haven't moved and we can just use the location of the click.
453 // Later we'll need to find the star with starCorrespondence.
454 int clickedStarIndex;
455 detectStarsPAHRefresh(&stars, 100, correctionFrom.x(), correctionFrom.y(), &clickedStarIndex);
456 if (clickedStarIndex < 0)
457 {
458 debugString = QString("PAA Refresh(%1): Didn't find the clicked star near %2,%3")
459 .arg(refreshIteration).arg(correctionFrom.x()).arg(correctionFrom.y());
460 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
461
462 emit newAlignTableResult(Align::ALIGN_RESULT_FAILED);
463 emit captureAndSolve();
464 return;
465 }
466
467 debugString = QString("PAA Refresh(%1): Refresh star(%2,%3) is index %4 with offset %5 %6")
468 .arg(refreshIteration + 1).arg(correctionFrom.x(), 4, 'f', 0)
469 .arg(correctionFrom.y(), 4, 'f', 0).arg(clickedStarIndex)
470 .arg(stars[clickedStarIndex].x - correctionFrom.x(), 4, 'f', 0)
471 .arg(stars[clickedStarIndex].y - correctionFrom.y(), 4, 'f', 0);
472 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
473
474 if (stars.size() > MIN_PAH_REFRESH_STARS)
475 {
476 int dx = 0;
477 int dy = 0;
478 int starIndex = -1;
479
480 if (refreshIteration++ == 0)
481 {
482 // First iteration. Setup starCorrespondence so we can find the user's star.
483 // clickedStarIndex should be the index of a detected star near where the user clicked.
484 starCorrespondencePAH.initialize(stars, clickedStarIndex);
485 if (clickedStarIndex >= 0)
486 {
487 setupCorrectionGraphics(QPointF(stars[clickedStarIndex].x, stars[clickedStarIndex].y));
488 emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
489 emit newFrame(m_AlignView);
490 }
491 }
492 else
493 {
494 // Or, in other iterations find the movement of the "user's star".
495 // The 0.40 means it's OK if star correspondence only finds 40% of the
496 // reference stars (as we'd have more issues near the image edge otherwise).
497 QVector<int> starMap;
498 starCorrespondencePAH.find(stars, 200.0, &starMap, false, 0.40);
499
500 // Go through the starMap, and find the user's star, and compare its position
501 // to its initial position.
502 for (int i = 0; i < starMap.size(); ++i)
503 {
504 if (starMap[i] == starCorrespondencePAH.guideStar())
505 {
506 dx = stars[i].x - correctionFrom.x();
507 dy = stars[i].y - correctionFrom.y();
508 starIndex = i;
509 break;
510 }
511 }
512 if (starIndex == -1)
513 {
514 bool allOnes = true;
515 for (int i = 0; i < starMap.size(); ++i)
516 {
517 if (starMap[i] != -1)
518 allOnes = false;
519 }
520 debugString = QString("PAA Refresh(%1): starMap %2").arg(refreshIteration).arg(allOnes ? "ALL -1's" : "not all -1's");
521 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
522 }
523 }
524
525 if (starIndex >= 0)
526 {
527 // Annotate the user's star on the alignview.
528 m_AlignView->setStarCircle(QPointF(stars[starIndex].x, stars[starIndex].y));
529 debugString = QString("PAA Refresh(%1): User's star is now at %2,%3, with movement = %4,%5").arg(refreshIteration)
530 .arg(stars[starIndex].x, 4, 'f', 0).arg(stars[starIndex].y, 4, 'f', 0).arg(dx).arg(dy);
531 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
532
533 double azE, altE;
534 if (polarAlign.pixelError(m_AlignView->keptImage(), QPointF(stars[starIndex].x, stars[starIndex].y),
535 correctionTo, &azE, &altE))
536 {
537 updateRefreshDisplay(azE, altE);
538 debugString = QString("PAA Refresh(%1): %2,%3 --> %4,%5 @ %6,%7")
539 .arg(refreshIteration).arg(correctionFrom.x(), 4, 'f', 0).arg(correctionFrom.y(), 4, 'f', 0)
540 .arg(correctionTo.x(), 4, 'f', 0).arg(correctionTo.y(), 4, 'f', 0)
541 .arg(stars[starIndex].x, 4, 'f', 0).arg(stars[starIndex].y, 4, 'f', 0);
542 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
543 }
544 else
545 {
546 debugString = QString("PAA Refresh(%1): pixelError failed to estimate the remaining correction").arg(refreshIteration);
547 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
548 }
549 }
550 else
551 {
552 if (refreshIteration > 1)
553 {
554 debugString = QString("PAA Refresh(%1): Didn't find the user's star").arg(refreshIteration);
555 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
556 }
557 }
558 }
559 else
560 {
561 debugString = QString("PAA Refresh(%1): Too few stars detected (%2)").arg(refreshIteration).arg(stars.size());
562 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
563 emit updatedErrorsChanged(-1, -1, -1);
564 }
565 }
566 // Finally start the next capture
567 emit captureAndSolve();
568}
569
570bool PolarAlignmentAssistant::processSolverFailure()
571{
572 if ((m_PAHStage == PAH_FIRST_CAPTURE ||
573 m_PAHStage == PAH_SECOND_CAPTURE ||
574 m_PAHStage == PAH_THIRD_CAPTURE ||
575 m_PAHStage == PAH_FIRST_SOLVE ||
576 m_PAHStage == PAH_SECOND_SOLVE ||
577 m_PAHStage == PAH_THIRD_SOLVE)
578 && ++m_PAHRetrySolveCounter < 4)
579 {
580 emit newLog(i18n("PAA: Solver failed, retrying."));
581 emit captureAndSolve();
582 return true;
583 }
584
585 if (m_PAHStage != PAH_IDLE)
586 {
587 emit newLog(i18n("PAA: Stopping, solver failed too many times."));
588 stopPAHProcess();
589 }
590
591 return false;
592}
593
594void PolarAlignmentAssistant::setPAHStage(Stage stage)
595{
596 if (stage != m_PAHStage)
597 {
598 m_PAHStage = stage;
599 polarAlignWidget->updatePAHStage(stage);
600 emit newPAHStage(m_PAHStage);
601 }
602}
603
604void PolarAlignmentAssistant::processMountRotation(const dms &ra, double settleDuration)
605{
606 // Check how many degrees travelled so far from starting point
607 double traveledAngle = ra.deltaAngle(m_StartCoord.ra()).Degrees();
608 bool goingWest = pAHDirection->currentIndex() == 0;
609
610 // Handle RA wrap-around
611 // When going west: If current RA is much larger than start RA, we crossed the boundary
612 // When going east: If current RA is much smaller than start RA, we crossed the boundary
613 if (goingWest && (ra.Hours() - m_StartCoord.ra().Hours() > 12))
614 traveledAngle = (ra.Hours() - 24 - m_StartCoord.ra().Hours()) * 15;
615 else if (!goingWest && (m_StartCoord.ra().Hours() - ra.Hours() > 12))
616 traveledAngle = ((ra.Hours() + 24) - m_StartCoord.ra().Hours()) * 15;
617
618 QString rotProgressMessage;
619 QString rotDoneMessage;
620 Stage nextCapture;
621 Stage nextSettle;
622
623 if (m_PAHStage == PAH_FIRST_ROTATE)
624 {
625 rotProgressMessage = "First mount rotation completed degrees:";
626 rotDoneMessage = i18n("Mount first rotation is complete.");
627 nextCapture = PAH_SECOND_CAPTURE;
628 nextSettle = PAH_FIRST_SETTLE;
629 }
630 else if (m_PAHStage == PAH_SECOND_ROTATE)
631 {
632 rotProgressMessage = "Second mount rotation completed degrees:";
633 rotDoneMessage = i18n("Mount second rotation is complete.");
634 nextCapture = PAH_THIRD_CAPTURE;
635 nextSettle = PAH_SECOND_SETTLE;
636 }
637 else return;
638
639 auto settle = [this, rotDoneMessage, settleDuration, nextCapture, nextSettle]()
640 {
641 m_CurrentTelescope->StopWE();
642 emit newLog(rotDoneMessage);
643
644 int settleDurationMsec = settleDuration;
645
646 // Always settle at least a second!
647 // It is not obvious to users that the align settle field is also used for polar alignment.
648 if (settleDurationMsec <= 1000)
649 settleDurationMsec = 2000;
650
651 setPAHStage(nextSettle);
652 updateDisplay(m_PAHStage, getPAHMessage());
653
654 emit newLog(i18n("Settling..."));
655 QTimer::singleShot(settleDurationMsec, [nextCapture, this]()
656 {
657 setPAHStage(nextCapture);
658 updateDisplay(m_PAHStage, getPAHMessage());
659 });
660 };
661
662 if (m_PAHStage == PAH_FIRST_ROTATE || m_PAHStage == PAH_SECOND_ROTATE)
663 {
664 // only wait for telescope to slew to new position if manual slewing is switched off
665 if(!pAHManualSlew->isChecked())
666 {
667 qCDebug(KSTARS_EKOS_ALIGN) << rotProgressMessage << traveledAngle;
668
669 // Allow for some travel angle to get started first
670 if (std::abs(traveledAngle) > 0.5)
671 {
672 bool goingWest = pAHDirection->currentIndex() == 0;
673 // traveledAngle < 0 --> Going West
674 // traveledAngle > 0 --> Going East
675 // #1 Check for reversed direction
676 if ( (traveledAngle < 0 && !goingWest) || (traveledAngle > 0 && goingWest))
677 {
678 m_CurrentTelescope->abort();
679 emit newLog(i18n("Mount aborted. Reverse RA axis direction and try again."));
680 stopPAHProcess();
681 }
682 // #2 Check if we travelled almost 90% of the desired rotation or more
683 // then stop
684 else if (std::abs(traveledAngle) > pAHRotation->value() * 0.90)
685 settle();
686 }
687 }
688 }
689}
690
691bool PolarAlignmentAssistant::checkPAHForMeridianCrossing()
692{
693 // Make sure using -180 to 180 for hourAngle and DEC. (Yes dec should be between -90 and 90).
694 double hourAngle = m_CurrentTelescope->hourAngle().Degrees();
695 while (hourAngle < -180)
696 hourAngle += 360;
697 while (hourAngle > 180)
698 hourAngle -= 360;
699 double ra = 0, dec = 0;
700 m_CurrentTelescope->getEqCoords(&ra, &dec);
701 while (dec < -180)
702 dec += 360;
703 while (dec > 180)
704 dec -= 360;
705
706 // Don't do this check within 2 degrees of the poles.
707 bool nearThePole = fabs(dec) > 88;
708 if (nearThePole)
709 return false;
710
711 double degreesPerSlew = pAHRotation->value();
712 bool closeToMeridian = fabs(hourAngle) < 2.0 * degreesPerSlew;
713 bool goingWest = pAHDirection->currentIndex() == 0;
714
715 // If the pier is on the east side (pointing west) and will slew west and is within 2 slews of the HA=0,
716 // or on the west side (pointing east) and will slew east, and is within 2 slews of HA=0
717 // then warn and give the user a chance to cancel.
718 bool wouldCrossMeridian =
719 ((m_CurrentTelescope->pierSide() == ISD::Mount::PIER_EAST && !goingWest && closeToMeridian) ||
720 (m_CurrentTelescope->pierSide() == ISD::Mount::PIER_WEST && goingWest && closeToMeridian) ||
721 (m_CurrentTelescope->pierSide() == ISD::Mount::PIER_UNKNOWN && closeToMeridian));
722
723 return wouldCrossMeridian;
724}
725
726void PolarAlignmentAssistant::updateDisplay(Stage stage, const QString &message)
727{
728 switch(stage)
729 {
730 case PAH_FIRST_ROTATE:
731 case PAH_SECOND_ROTATE:
732 if (pAHManualSlew->isChecked())
733 {
734 polarAlignWidget->updatePAHStage(stage);
735 PAHWidgets->setCurrentWidget(PAHManualRotatePage);
736 manualRotateText->setText(message);
737 emit newPAHMessage(message);
738 return;
739 }
740 // fall through
741 case PAH_FIRST_CAPTURE:
742 case PAH_SECOND_CAPTURE:
743 case PAH_THIRD_CAPTURE:
744 case PAH_FIRST_SOLVE:
745 case PAH_SECOND_SOLVE:
746 case PAH_THIRD_SOLVE:
747 polarAlignWidget->updatePAHStage(stage);
748 PAHWidgets->setCurrentWidget(PAHMessagePage);
749 PAHMessageText->setText(message);
750 emit newPAHMessage(message);
751 break;
752
753 default:
754 break;
755
756 }
757}
758
759void PolarAlignmentAssistant::startPAHProcess()
760{
761 qCInfo(KSTARS_EKOS_ALIGN) << QString("Starting Polar Alignment Assistant process %1 ...").arg(PAA_VERSION);
762
763 auto executePAH = [ this ]()
764 {
765 setPAHStage(PAH_FIRST_CAPTURE);
766
767 if (Options::limitedResourcesMode())
768 emit newLog(i18n("Warning: Equatorial Grid Lines will not be drawn due to limited resources mode."));
769
770 if (m_CurrentTelescope->hasAlignmentModel())
771 {
772 emit newLog(i18n("Clearing mount Alignment Model..."));
773 m_CurrentTelescope->clearAlignmentModel();
774 }
775
776 // Unpark
777 m_CurrentTelescope->unpark();
778
779 // Set tracking ON if not already
780 if (m_CurrentTelescope->canControlTrack() && m_CurrentTelescope->isTracking() == false)
781 m_CurrentTelescope->setTrackEnabled(true);
782
783 PAHStartB->setEnabled(false);
784 PAHStopB->setEnabled(true);
785
786 PAHUpdatedErrorTotal->clear();
787 PAHUpdatedErrorAlt->clear();
788 PAHUpdatedErrorAz->clear();
789 PAHOrigErrorTotal->clear();
790 PAHOrigErrorAlt->clear();
791 PAHOrigErrorAz->clear();
792 PAHIteration->setText("");
793
794 updateDisplay(m_PAHStage, getPAHMessage());
795
796 m_AlignView->setCorrectionParams(QPointF(), QPointF(), QPointF());
797
798 m_PAHRetrySolveCounter = 0;
799 emit captureAndSolve();
800 };
801
802 // Right off the bat, check if this alignment might cause a pier crash.
803 // If we're crossing the meridian, warn unless within 5-degrees from the pole.
804 if (checkPAHForMeridianCrossing())
805 {
806 // Continue
807 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executePAH]()
808 {
809 KSMessageBox::Instance()->disconnect(this);
810 executePAH();
811 });
812
813 KSMessageBox::Instance()->warningContinueCancel(i18n("This could cause the telescope to cross the meridian."),
814 i18n("Warning"), 15);
815 }
816 else
817 executePAH();
818}
819
820void PolarAlignmentAssistant::stopPAHProcess()
821{
822 if (m_PAHStage == PAH_IDLE)
823 return;
824
825 // Only display dialog if user clicked.
826 // if ((static_cast<QPushButton *>(sender()) == PAHStopB) && KMessageBox::questionYesNo(KStars::Instance(),
827 // i18n("Are you sure you want to stop the polar alignment process?"),
828 // i18n("Polar Alignment Assistant"), KStandardGuiItem::yes(), KStandardGuiItem::no(),
829 // "restart_PAA_process_dialog") == KMessageBox::No)
830 // return;
831
832 if (m_PAHStage == PAH_REFRESH)
833 {
834 setPAHStage(PAH_POST_REFRESH);
835 polarAlignWidget->updatePAHStage(m_PAHStage);
836 }
837 qCInfo(KSTARS_EKOS_ALIGN) << "Stopping Polar Alignment Assistant process...";
838
839
840 if (m_CurrentTelescope && m_CurrentTelescope->isInMotion())
841 m_CurrentTelescope->abort();
842
843 setPAHStage(PAH_IDLE);
844 polarAlignWidget->updatePAHStage(m_PAHStage);
845
846 PAHStartB->setEnabled(true);
847 PAHStopB->setEnabled(false);
848 PAHRefreshB->setEnabled(true);
849 PAHWidgets->setCurrentWidget(PAHIntroPage);
850 emit newPAHMessage(introText->text());
851
852 m_AlignView->reset();
853 m_AlignView->setRefreshEnabled(false);
854
855 emit newFrame(m_AlignView);
856 disconnect(m_AlignView.get(), &AlignView::trackingStarSelected, this,
857 &Ekos::PolarAlignmentAssistant::setPAHCorrectionOffset);
858 disconnect(m_AlignView.get(), &AlignView::newCorrectionVector, this, &Ekos::PolarAlignmentAssistant::newCorrectionVector);
859
860 if (Options::pAHAutoPark())
861 {
862 m_CurrentTelescope->park();
863 emit newLog(i18n("Parking the mount..."));
864 }
865}
866
867void PolarAlignmentAssistant::rotatePAH()
868{
869 double TargetDiffRA = pAHRotation->value();
870 bool westMeridian = pAHDirection->currentIndex() == 0;
871
872 // West
873 if (westMeridian)
874 TargetDiffRA *= -1;
875 // East
876 else
877 TargetDiffRA *= 1;
878
879 // JM 2018-05-03: Hemispheres shouldn't affect rotation direction in RA
880
881 // if Manual slewing is selected, don't move the mount
882 if (pAHManualSlew->isChecked())
883 {
884 return;
885 }
886
887 const SkyPoint telescopeCoord = m_CurrentTelescope->currentCoordinates();
888
889 // Record starting point
890 m_StartCoord = telescopeCoord;
891
892 // TargetDiffRA is in degrees
893 dms newTelescopeRA = (telescopeCoord.ra() + dms(TargetDiffRA)).reduce();
894
895 targetPAH.setRA(newTelescopeRA);
896 targetPAH.setDec(telescopeCoord.dec());
897
898 //m_CurrentTelescope->Slew(&targetPAH);
899 // Set Selected Speed
900 if (pAHMountSpeed->currentIndex() >= 0)
901 m_CurrentTelescope->setSlewRate(pAHMountSpeed->currentIndex());
902 // Go to direction
903 m_CurrentTelescope->MoveWE(westMeridian ? ISD::Mount::MOTION_WEST : ISD::Mount::MOTION_EAST,
904 ISD::Mount::MOTION_START);
905
906 emit newLog(i18n("Please wait until mount completes rotating to RA (%1) DE (%2)", targetPAH.ra().toHMSString(),
907 targetPAH.dec().toDMSString()));
908}
909
910void PolarAlignmentAssistant::setupCorrectionGraphics(const QPointF &pixel)
911{
912 polarAlign.refreshSolution(&refreshSolution, &altOnlyRefreshSolution);
913
914 // We use the previously stored image (the 3rd PAA image)
915 // so we can continue to estimate the correction even after
916 // capturing new images during the refresh stage.
917 const QSharedPointer<FITSData> &imageData = m_AlignView->keptImage();
918
919 // Just the altitude correction
920 if (!polarAlign.findCorrectedPixel(imageData, pixel, &correctionAltTo, true))
921 {
922 qCInfo(KSTARS_EKOS_ALIGN) << QString(i18n("PAA: Failed to findCorrectedPixel."));
923 return;
924 }
925 // The whole correction.
926 if (!polarAlign.findCorrectedPixel(imageData, pixel, &correctionTo))
927 {
928 qCInfo(KSTARS_EKOS_ALIGN) << QString(i18n("PAA: Failed to findCorrectedPixel."));
929 return;
930 }
931 QString debugString = QString("PAA: Correction: %1,%2 --> %3,%4 (alt only %5,%6)")
932 .arg(pixel.x(), 4, 'f', 0).arg(pixel.y(), 4, 'f', 0)
933 .arg(correctionTo.x(), 4, 'f', 0).arg(correctionTo.y(), 4, 'f', 0)
934 .arg(correctionAltTo.x(), 4, 'f', 0).arg(correctionAltTo.y(), 4, 'f', 0);
935 qCDebug(KSTARS_EKOS_ALIGN) << debugString;
936 correctionFrom = pixel;
937
938 if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
939 updatePlateSolveTriangle(imageData);
940 else
941 m_AlignView->setCorrectionParams(correctionFrom, correctionTo, correctionAltTo);
942
943 return;
944}
945
946
947bool PolarAlignmentAssistant::calculatePAHError()
948{
949 // Hold on to the imageData so we can use it during the refresh phase.
950 m_AlignView->holdOnToImage();
951
952 if (!polarAlign.findAxis())
953 {
954 emit newLog(i18n("PAA: Failed to find RA Axis center."));
955 stopPAHProcess();
956 return false;
957 }
958
959 double azimuthError, altitudeError;
960 polarAlign.calculateAzAltError(&azimuthError, &altitudeError);
961 drawArrows(azimuthError, altitudeError);
962 dms polarError(hypot(altitudeError, azimuthError));
963 dms azError(azimuthError), altError(altitudeError);
964
965 // No need to toggle EQGrid
966 // if (m_AlignView->isEQGridShown() == false && !Options::limitedResourcesMode())
967 // m_AlignView->toggleEQGrid();
968
969 QString msg = QString("%1. Azimuth: %2 Altitude: %3")
970 .arg(polarError.toDMSString()).arg(azError.toDMSString())
971 .arg(altError.toDMSString());
972 emit newLog(QString("Polar Alignment Error: %1").arg(msg));
973
974 polarAlign.setMaxPixelSearchRange(polarError.Degrees() + 1);
975
976 // These are viewed during the refresh phase.
977 PAHOrigErrorTotal->setText(polarError.toDMSString());
978 PAHOrigErrorAlt->setText(altError.toDMSString());
979 PAHOrigErrorAz->setText(azError.toDMSString());
980
981 setupCorrectionGraphics(QPointF(m_ImageData->width() / 2, m_ImageData->height() / 2));
982
983 // Find Celestial pole location and mount's RA axis
984 SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90);
985 QPointF imagePoint, celestialPolePoint;
986 m_ImageData->wcsToPixel(CP, celestialPolePoint, imagePoint);
987 if (m_ImageData->contains(celestialPolePoint))
988 {
989 m_AlignView->setCelestialPole(celestialPolePoint);
990 QPointF raAxis;
991 if (polarAlign.findCorrectedPixel(m_ImageData, celestialPolePoint, &raAxis))
992 m_AlignView->setRaAxis(raAxis);
993 }
994
995 connect(m_AlignView.get(), &AlignView::trackingStarSelected, this, &Ekos::PolarAlignmentAssistant::setPAHCorrectionOffset);
996 emit polarResultUpdated(QLineF(correctionFrom, correctionTo), polarError.Degrees(), azError.Degrees(), altError.Degrees());
997
998 connect(m_AlignView.get(), &AlignView::newCorrectionVector, this, &Ekos::PolarAlignmentAssistant::newCorrectionVector,
1000 syncCorrectionVector();
1001 emit newFrame(m_AlignView);
1002
1003 return true;
1004}
1005
1006void PolarAlignmentAssistant::syncCorrectionVector()
1007{
1008 if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
1009 return;
1010 emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
1011 m_AlignView->setCorrectionParams(correctionFrom, correctionTo, correctionAltTo);
1012}
1013
1014void PolarAlignmentAssistant::setPAHCorrectionOffsetPercentage(double dx, double dy)
1015{
1016 double x = dx * m_AlignView->zoomedWidth();
1017 double y = dy * m_AlignView->zoomedHeight();
1018 setPAHCorrectionOffset(static_cast<int>(round(x)), static_cast<int>(round(y)));
1019}
1020
1021void PolarAlignmentAssistant::setPAHCorrectionOffset(int x, int y)
1022{
1023 if (m_PAHStage == PAH_REFRESH)
1024 {
1025 emit newLog(i18n("Polar-alignment star cannot be updated during refresh phase as it might affect error measurements."));
1026 }
1027 else
1028 {
1029 setupCorrectionGraphics(QPointF(x, y));
1030 emit newCorrectionVector(QLineF(correctionFrom, correctionTo));
1031 emit newFrame(m_AlignView);
1032 }
1033}
1034
1035void PolarAlignmentAssistant::setPAHSlewDone()
1036{
1037 emit newPAHMessage("Manual slew done.");
1038 switch(m_PAHStage)
1039 {
1040 case PAH_FIRST_ROTATE :
1041 setPAHStage(PAH_SECOND_CAPTURE);
1042 emit newLog(i18n("First manual rotation done."));
1043 updateDisplay(m_PAHStage, getPAHMessage());
1044 break;
1045 case PAH_SECOND_ROTATE :
1046 setPAHStage(PAH_THIRD_CAPTURE);
1047 emit newLog(i18n("Second manual rotation done."));
1048 updateDisplay(m_PAHStage, getPAHMessage());
1049 break;
1050 default :
1051 return; // no other stage should be able to trigger this event
1052 }
1053}
1054
1055
1056
1057void PolarAlignmentAssistant::startPAHRefreshProcess()
1058{
1059 qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant refreshing...";
1060
1061 refreshIteration = 0;
1062 imageNumber = 0;
1063 m_NumHealpixFailures = 0;
1064
1065 setPAHStage(PAH_REFRESH);
1066 polarAlignWidget->updatePAHStage(m_PAHStage);
1067 auto message = getPAHMessage();
1068 refreshText->setText(message);
1069 emit newPAHMessage(message);
1070
1071 PAHRefreshB->setEnabled(false);
1072
1073 // Hide EQ Grids if shown
1074 if (m_AlignView->isEQGridShown())
1075 m_AlignView->toggleEQGrid();
1076
1077 m_AlignView->setRefreshEnabled(true);
1078
1079 Options::setAstrometrySolverWCS(false);
1080 Options::setAutoWCS(false);
1081
1082 // We for refresh, just capture really
1083 emit captureAndSolve();
1084}
1085
1086void PolarAlignmentAssistant::processPAHStage(double orientation, double ra, double dec, double pixscale,
1087 bool eastToTheRight, short healpix, short index)
1088{
1089 if (m_PAHStage == PAH_FIND_CP)
1090 {
1091 emit newLog(
1092 i18n("Mount is synced to celestial pole. You can now continue Polar Alignment Assistant procedure."));
1093 setPAHStage(PAH_FIRST_CAPTURE);
1094 polarAlignWidget->updatePAHStage(m_PAHStage);
1095 return;
1096 }
1097
1098 if (m_PAHStage == PAH_FIRST_SOLVE || m_PAHStage == PAH_SECOND_SOLVE || m_PAHStage == PAH_THIRD_SOLVE)
1099 {
1100 // Used by refresh, when looking at the 3rd image.
1101 m_LastRa = ra;
1102 m_LastDec = dec;
1103 m_LastOrientation = orientation;
1104 m_LastPixscale = pixscale;
1105 m_HealpixToUse = healpix;
1106 m_IndexToUse = index;
1107
1108 bool doWcs = (m_PAHStage == PAH_THIRD_SOLVE) || !Options::limitedResourcesMode();
1109 if (doWcs)
1110 {
1111 emit newLog(i18n("Please wait while WCS data is processed..."));
1112 PAHMessageText->setText(
1113 m_PAHStage == PAH_FIRST_SOLVE
1114 ? "Calculating WCS for the first image...</p>"
1115 : (m_PAHStage == PAH_SECOND_SOLVE ? "Calculating WCS for the second image...</p>"
1116 : "Calculating WCS for the third image...</p>"));
1117 }
1118 connect(m_AlignView.get(), &AlignView::wcsToggled, this, &Ekos::PolarAlignmentAssistant::setWCSToggled,
1120 m_AlignView->injectWCS(orientation, ra, dec, pixscale, eastToTheRight);
1121 return;
1122 }
1123}
1124
1125void PolarAlignmentAssistant::setImageData(const QSharedPointer<FITSData> &image)
1126{
1127 m_ImageData = image;
1128
1129 if (m_PAHStage == PAH_FIRST_CAPTURE)
1130 setPAHStage(PAH_FIRST_SOLVE);
1131 else if (m_PAHStage == PAH_SECOND_CAPTURE)
1132 setPAHStage(PAH_SECOND_SOLVE);
1133 else if (m_PAHStage == PAH_THIRD_CAPTURE)
1134 setPAHStage(PAH_THIRD_SOLVE);
1135 else
1136 return;
1137 updateDisplay(m_PAHStage, getPAHMessage());
1138}
1139
1140void PolarAlignmentAssistant::setWCSToggled(bool result)
1141{
1142 emit newLog(i18n("WCS data processing is complete."));
1143
1144 disconnect(m_AlignView.get(), &AlignView::wcsToggled, this, &Ekos::PolarAlignmentAssistant::setWCSToggled);
1145
1146 if (m_PAHStage == PAH_FIRST_CAPTURE || m_PAHStage == PAH_FIRST_SOLVE)
1147 {
1148 // We need WCS to be synced first
1149 if (result == false && m_AlignInstance->wcsSynced() == true)
1150 {
1151 emit newLog(i18n("WCS info is now valid. Capturing next frame..."));
1152 emit captureAndSolve();
1153 return;
1154 }
1155
1156 polarAlign.reset();
1157 polarAlign.addPoint(m_ImageData);
1158
1159 setPAHStage(PAH_FIRST_ROTATE);
1160 auto msg = getPAHMessage();
1161 if (pAHManualSlew->isChecked())
1162 {
1163 msg = QString("Please rotate your mount about %1 deg in RA")
1164 .arg(pAHRotation->value());
1165 emit newLog(msg);
1166 }
1167 updateDisplay(m_PAHStage, msg);
1168
1169 rotatePAH();
1170 }
1171 else if (m_PAHStage == PAH_SECOND_CAPTURE || m_PAHStage == PAH_SECOND_SOLVE)
1172 {
1173 setPAHStage(PAH_SECOND_ROTATE);
1174 auto msg = getPAHMessage();
1175
1176 if (pAHManualSlew->isChecked())
1177 {
1178 msg = QString("Please rotate your mount about %1 deg in RA")
1179 .arg(pAHRotation->value());
1180 emit newLog(msg);
1181 }
1182 updateDisplay(m_PAHStage, msg);
1183
1184 polarAlign.addPoint(m_ImageData);
1185
1186 rotatePAH();
1187 }
1188 else if (m_PAHStage == PAH_THIRD_CAPTURE || m_PAHStage == PAH_THIRD_SOLVE)
1189 {
1190 // Critical error
1191 if (result == false)
1192 {
1193 emit newLog(i18n("Failed to process World Coordinate System: %1. Try again.", m_ImageData->getLastError()));
1194 return;
1195 }
1196
1197 polarAlign.addPoint(m_ImageData);
1198
1199 // We have 3 points which uniquely defines a circle with its center representing the RA Axis
1200 // We have celestial pole location. So correction vector is just the vector between these two points
1201 if (calculatePAHError())
1202 {
1203 setPAHStage(PAH_STAR_SELECT);
1204 polarAlignWidget->updatePAHStage(m_PAHStage);
1205 PAHWidgets->setCurrentWidget(PAHRefreshPage);
1206 refreshText->setText(getPAHMessage());
1207 emit newPAHMessage(getPAHMessage());
1208 }
1209 else
1210 {
1211 emit newLog(i18n("PAA: Failed to find the RA axis. Quitting."));
1212 stopPAHProcess();
1213 }
1214 }
1215}
1216
1217void PolarAlignmentAssistant::setMountStatus(ISD::Mount::Status newState)
1218{
1219 switch (newState)
1220 {
1221 case ISD::Mount::MOUNT_PARKING:
1222 case ISD::Mount::MOUNT_SLEWING:
1223 case ISD::Mount::MOUNT_MOVING:
1224 PAHStartB->setEnabled(false);
1225 break;
1226
1227 default:
1228 if (m_PAHStage == PAH_IDLE)
1229 PAHStartB->setEnabled(true);
1230 break;
1231 }
1232}
1233
1234QString PolarAlignmentAssistant::getPAHMessage() const
1235{
1236 switch (m_PAHStage)
1237 {
1238 case PAH_IDLE:
1239 case PAH_FIND_CP:
1240 return introText->text();
1241 case PAH_FIRST_CAPTURE:
1242 return i18n("<p>The assistant requires three images to find a solution. Ekos is now capturing the first image...</p>");
1243 case PAH_FIRST_SOLVE:
1244 return i18n("<p>Solving the <i>first</i> image...</p>");
1245 case PAH_FIRST_ROTATE:
1246 return i18n("<p>Executing the <i>first</i> mount rotation...</p>");
1247 case PAH_FIRST_SETTLE:
1248 return i18n("<p>Settling after the <i>first</i> mount rotation.</p>");
1249 case PAH_SECOND_SETTLE:
1250 return i18n("<p>Settling after the <i>second</i> mount rotation.</p>");
1251 case PAH_SECOND_CAPTURE:
1252 return i18n("<p>Capturing the second image...</p>");
1253 case PAH_SECOND_SOLVE:
1254 return i18n("<p>Solving the <i>second</i> image...</p>");
1255 case PAH_SECOND_ROTATE:
1256 return i18n("<p>Executing the <i>second</i> mount rotation...</p>");
1257 case PAH_THIRD_CAPTURE:
1258 return i18n("<p>Capturing the <i>third</i> and final image...</p>");
1259 case PAH_THIRD_SOLVE:
1260 return i18n("<p>Solving the <i>third</i> image...</p>");
1261 case PAH_STAR_SELECT:
1262 if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
1263 return i18n("<p>Choose your exposure time & select an adjustment method. Then click <i>refresh</i> to begin adjustments.</p>");
1264 else
1265 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>");
1266 case PAH_REFRESH:
1267 if (pAHRefreshAlgorithm->currentIndex() == PLATE_SOLVE_ALGORITHM)
1268 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>");
1269 else
1270 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>");
1271 default:
1272 break;
1273 }
1274
1275 return QString();
1276}
1277
1278void PolarAlignmentAssistant::setPAHRefreshAlgorithm(RefreshAlgorithm value)
1279{
1280 // If the star-correspondence method of tracking polar alignment error wasn't initialized,
1281 // at the start, it can't be turned on mid process.
1282 if ((m_PAHStage == PAH_REFRESH) && refreshIteration > 0 && (value != PLATE_SOLVE_ALGORITHM)
1283 && !starCorrespondencePAH.size())
1284 {
1285 pAHRefreshAlgorithm->setCurrentIndex(PLATE_SOLVE_ALGORITHM);
1286 emit newLog(i18n("Cannot change to MoveStar algorithm once refresh has begun"));
1287 return;
1288 }
1289 if (m_PAHStage == PAH_REFRESH || m_PAHStage == PAH_STAR_SELECT)
1290 {
1291 refreshText->setText(getPAHMessage());
1292 emit newPAHMessage(getPAHMessage());
1293 }
1294
1295 showUpdatedError((value == PLATE_SOLVE_ALGORITHM) ||
1296 (value == MOVE_STAR_UPDATE_ERR_ALGORITHM));
1297 if (value == PLATE_SOLVE_ALGORITHM)
1298 updatePlateSolveTriangle(m_ImageData);
1299 else
1300 m_AlignView->setCorrectionParams(correctionFrom, correctionTo, correctionAltTo);
1301
1302}
1303
1304}
Align class handles plate-solving and polar alignment measurement and correction using astrometry....
Definition align.h:77
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra() const
Definition skypoint.h:263
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
double Hours() const
Definition dms.h:168
const dms reduce() const
return the equivalent angle between 0 and 360 degrees.
Definition dms.cpp:251
const dms deltaAngle(dms angle) const
deltaAngle Return the shortest difference (path) between this angle and the supplied angle.
Definition dms.cpp:267
const double & Degrees() const
Definition dms.h:141
KLocalizedString KI18N_EXPORT ki18n(const char *text)
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
void clicked(bool checked)
void currentIndexChanged(int index)
void accepted()
void append(QList< T > &&value)
iterator begin()
void clear()
qsizetype count() const const
iterator end()
qsizetype size() const const
void deleteLater()
void drawConvexPolygon(const QPoint *points, int pointCount)
void drawRect(const QRect &rectangle)
qreal x() const const
qreal y() const const
bool isNull() const const
QString arg(Args &&... args) const const
UniqueConnection
QTextStream & dec(QTextStream &stream)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void setEnabled(bool)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:55:58 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.