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

KDE's Doxygen guidelines are available online.