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