Warning, file /education/kstars/kstars/ekos/align/align.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 SPDX-FileCopyrightText: 2013 Jasem Mutlaq <mutlaqja@ikarustech.com> 0003 SPDX-FileCopyrightText: 2013-2021 Jasem Mutlaq <mutlaqja@ikarustech.com> 0004 SPDX-FileCopyrightText: 2018-2020 Robert Lancaster <rlancaste@gmail.com> 0005 SPDX-FileCopyrightText: 2019-2021 Hy Murveit <hy@murveit.com> 0006 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 0010 #include "align.h" 0011 #include "alignadaptor.h" 0012 #include "alignview.h" 0013 #include <ekos_align_debug.h> 0014 0015 // Options 0016 #include "Options.h" 0017 #include "opsalign.h" 0018 #include "opsprograms.h" 0019 #include "opsastrometry.h" 0020 #include "opsastrometryindexfiles.h" 0021 0022 // Components 0023 #include "mountmodel.h" 0024 #include "polaralignmentassistant.h" 0025 #include "remoteastrometryparser.h" 0026 #include "manualrotator.h" 0027 0028 // FITS 0029 #include "fitsviewer/fitsdata.h" 0030 0031 // Auxiliary 0032 #include "auxiliary/QProgressIndicator.h" 0033 #include "auxiliary/ksmessagebox.h" 0034 #include "ekos/auxiliary/darkprocessor.h" 0035 #include "ekos/auxiliary/filtermanager.h" 0036 #include "ekos/auxiliary/stellarsolverprofileeditor.h" 0037 #include "ekos/auxiliary/profilesettings.h" 0038 #include "ekos/auxiliary/opticaltrainmanager.h" 0039 #include "ekos/auxiliary/opticaltrainsettings.h" 0040 #include "ksnotification.h" 0041 #include "kspaths.h" 0042 #include "ksuserdb.h" 0043 #include "fov.h" 0044 #include "kstars.h" 0045 #include "kstarsdata.h" 0046 #include "skymapcomposite.h" 0047 #include "ekos/auxiliary/solverutils.h" 0048 #include "ekos/auxiliary/rotatorutils.h" 0049 0050 // INDI 0051 #include "ekos/manager.h" 0052 #include "indi/indidome.h" 0053 #include "indi/clientmanager.h" 0054 #include "indi/driverinfo.h" 0055 #include "indi/indifilterwheel.h" 0056 #include "indi/indirotator.h" 0057 #include "profileinfo.h" 0058 0059 // System Includes 0060 #include <KActionCollection> 0061 #include <basedevice.h> 0062 #include <indicom.h> 0063 #include <memory> 0064 0065 // Qt version calming 0066 #include <qtendl.h> 0067 0068 #define MAXIMUM_SOLVER_ITERATIONS 10 0069 #define CAPTURE_RETRY_DELAY 10000 0070 #define CAPTURE_ROTATOR_DELAY 5000 // After 5 seconds estimated value should be not bad 0071 #define PAH_CUTOFF_FOV 10 // Minimum FOV width in arcminutes for PAH to work 0072 #define CHECK_PAH(x) \ 0073 m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->x 0074 #define RUN_PAH(x) \ 0075 if (m_PolarAlignmentAssistant) m_PolarAlignmentAssistant->x 0076 0077 namespace Ekos 0078 { 0079 0080 using PAA = PolarAlignmentAssistant; 0081 0082 bool isEqual(double a, double b) 0083 { 0084 return std::abs(a - b) < 0.01; 0085 } 0086 0087 Align::Align(const QSharedPointer<ProfileInfo> &activeProfile) : m_ActiveProfile(activeProfile) 0088 { 0089 setupUi(this); 0090 0091 qRegisterMetaType<Ekos::AlignState>("Ekos::AlignState"); 0092 qDBusRegisterMetaType<Ekos::AlignState>(); 0093 0094 new AlignAdaptor(this); 0095 QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this); 0096 0097 dirPath = QDir::homePath(); 0098 0099 KStarsData::Instance()->clearTransientFOVs(); 0100 0101 solverFOV.reset(new FOV()); 0102 solverFOV->setName(i18n("Solver FOV")); 0103 solverFOV->setObjectName("solver_fov"); 0104 solverFOV->setLockCelestialPole(true); 0105 solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name()); 0106 solverFOV->setProperty("visible", false); 0107 KStarsData::Instance()->addTransientFOV(solverFOV); 0108 0109 sensorFOV.reset(new FOV()); 0110 sensorFOV->setObjectName("sensor_fov"); 0111 sensorFOV->setLockCelestialPole(true); 0112 sensorFOV->setProperty("visible", Options::showSensorFOV()); 0113 KStarsData::Instance()->addTransientFOV(sensorFOV); 0114 0115 QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov"); 0116 if (a) 0117 a->setEnabled(true); 0118 0119 showFITSViewerB->setIcon( 0120 QIcon::fromTheme("kstars_fitsviewer")); 0121 showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0122 connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer); 0123 0124 toggleFullScreenB->setIcon( 0125 QIcon::fromTheme("view-fullscreen")); 0126 toggleFullScreenB->setShortcut(Qt::Key_F4); 0127 toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0128 connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen); 0129 0130 // rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem")); 0131 rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0132 0133 m_AlignView.reset(new AlignView(alignWidget, FITS_ALIGN)); 0134 m_AlignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); 0135 m_AlignView->setBaseSize(alignWidget->size()); 0136 m_AlignView->createFloatingToolBar(); 0137 QVBoxLayout *vlayout = new QVBoxLayout(); 0138 0139 vlayout->addWidget(m_AlignView.get()); 0140 alignWidget->setLayout(vlayout); 0141 0142 connect(solveB, &QPushButton::clicked, this, &Ekos::Align::captureAndSolve); 0143 connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort); 0144 0145 // Effective FOV Edit 0146 connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV); 0147 0148 connect(loadSlewB, &QPushButton::clicked, this, [this]() 0149 { 0150 loadAndSlew(); 0151 }); 0152 0153 gotoModeButtonGroup->setId(syncR, GOTO_SYNC); 0154 gotoModeButtonGroup->setId(slewR, GOTO_SLEW); 0155 gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING); 0156 0157 m_CurrentGotoMode = static_cast<GotoMode>(Options::solverGotoOption()); 0158 gotoModeButtonGroup->button(m_CurrentGotoMode)->setChecked(true); 0159 0160 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) 0161 connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, 0162 [ = ](int id, bool toggled) 0163 #else 0164 connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this, 0165 [ = ](int id, bool toggled) 0166 #endif 0167 { 0168 if (toggled) 0169 this->m_CurrentGotoMode = static_cast<GotoMode>(id); 0170 }); 0171 0172 m_CaptureTimer.setSingleShot(true); 0173 m_CaptureTimer.setInterval(CAPTURE_RETRY_DELAY); 0174 connect(&m_CaptureTimer, &QTimer::timeout, this, &Align::processCaptureTimeout); 0175 m_AlignTimer.setSingleShot(true); 0176 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000); 0177 connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout); 0178 0179 pi.reset(new QProgressIndicator(this)); 0180 stopLayout->addWidget(pi.get()); 0181 0182 rememberSolverWCS = Options::astrometrySolverWCS(); 0183 rememberAutoWCS = Options::autoWCS(); 0184 0185 solverModeButtonGroup->setId(localSolverR, SOLVER_LOCAL); 0186 solverModeButtonGroup->setId(remoteSolverR, SOLVER_REMOTE); 0187 0188 setSolverMode(Options::solverMode()); 0189 0190 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) 0191 connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, 0192 [ = ](int id, bool toggled) 0193 #else 0194 connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this, 0195 [ = ](int id, bool toggled) 0196 #endif 0197 { 0198 if (toggled) 0199 setSolverMode(id); 0200 }); 0201 0202 connect(alignAccuracyThreshold, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]() 0203 { 0204 buildTarget(); 0205 }); 0206 0207 connect(alignBinning, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, 0208 &Ekos::Align::setBinningIndex); 0209 0210 connect(this, &Align::newStatus, this, [this](AlignState state) 0211 { 0212 opticalTrainCombo->setEnabled(state < ALIGN_PROGRESS); 0213 trainB->setEnabled(state < ALIGN_PROGRESS); 0214 }); 0215 0216 connect(alignUseCurrentFilter, &QCheckBox::toggled, this, &Align::checkFilter); 0217 0218 //Note: This is to prevent a button from being called the default button 0219 //and then executing when the user hits the enter key such as when on a Text Box 0220 QList<QPushButton *> qButtons = findChildren<QPushButton *>(); 0221 for (auto &button : qButtons) 0222 button->setAutoDefault(false); 0223 0224 savedOptionsProfiles = QDir(KSPaths::writableLocation( 0225 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini"); 0226 if(QFile(savedOptionsProfiles).exists()) 0227 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles); 0228 else 0229 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles(); 0230 0231 m_StellarSolver.reset(new StellarSolver()); 0232 connect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText); 0233 0234 setupPolarAlignmentAssistant(); 0235 setupManualRotator(); 0236 setuptDarkProcessor(); 0237 setupPlot(); 0238 setupSolutionTable(); 0239 setupOptions(); 0240 0241 // Load all settings 0242 loadGlobalSettings(); 0243 connectSettings(); 0244 0245 setupOpticalTrainManager(); 0246 } 0247 0248 Align::~Align() 0249 { 0250 if (m_StellarSolver.get() != nullptr) 0251 disconnect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText); 0252 0253 if (alignWidget->parent() == nullptr) 0254 toggleAlignWidgetFullScreen(); 0255 0256 // Remove temporary FITS files left before by the solver 0257 QDir dir(QDir::tempPath()); 0258 dir.setNameFilters(QStringList() << "fits*" 0259 << "tmp.*"); 0260 dir.setFilter(QDir::Files); 0261 for (auto &dirFile : dir.entryList()) 0262 dir.remove(dirFile); 0263 } 0264 void Align::selectSolutionTableRow(int row, int column) 0265 { 0266 Q_UNUSED(column) 0267 0268 solutionTable->selectRow(row); 0269 for (int i = 0; i < alignPlot->itemCount(); i++) 0270 { 0271 QCPAbstractItem *abstractItem = alignPlot->item(i); 0272 if (abstractItem) 0273 { 0274 QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem); 0275 if (item) 0276 { 0277 if (i == row) 0278 { 0279 item->setColor(Qt::black); 0280 item->setBrush(Qt::yellow); 0281 } 0282 else 0283 { 0284 item->setColor(Qt::red); 0285 item->setBrush(Qt::white); 0286 } 0287 } 0288 } 0289 } 0290 alignPlot->replot(); 0291 } 0292 0293 void Align::handleHorizontalPlotSizeChange() 0294 { 0295 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); 0296 alignPlot->replot(); 0297 } 0298 0299 void Align::handleVerticalPlotSizeChange() 0300 { 0301 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0); 0302 alignPlot->replot(); 0303 } 0304 0305 void Align::resizeEvent(QResizeEvent *event) 0306 { 0307 if (event->oldSize().width() != -1) 0308 { 0309 if (event->oldSize().width() != size().width()) 0310 handleHorizontalPlotSizeChange(); 0311 else if (event->oldSize().height() != size().height()) 0312 handleVerticalPlotSizeChange(); 0313 } 0314 else 0315 { 0316 QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange); 0317 } 0318 } 0319 0320 void Align::handlePointTooltip(QMouseEvent *event) 0321 { 0322 QCPAbstractItem *item = alignPlot->itemAt(event->localPos()); 0323 if (item) 0324 { 0325 QCPItemText *label = qobject_cast<QCPItemText *>(item); 0326 if (label) 0327 { 0328 QString labelText = label->text(); 0329 int point = labelText.toInt() - 1; 0330 0331 if (point < 0) 0332 return; 0333 QToolTip::showText(event->globalPos(), 0334 i18n("<table>" 0335 "<tr>" 0336 "<th colspan=\"2\">Object %1: %2</th>" 0337 "</tr>" 0338 "<tr>" 0339 "<td>RA:</td><td>%3</td>" 0340 "</tr>" 0341 "<tr>" 0342 "<td>DE:</td><td>%4</td>" 0343 "</tr>" 0344 "<tr>" 0345 "<td>dRA:</td><td>%5</td>" 0346 "</tr>" 0347 "<tr>" 0348 "<td>dDE:</td><td>%6</td>" 0349 "</tr>" 0350 "</table>", 0351 point + 1, 0352 solutionTable->item(point, 2)->text(), 0353 solutionTable->item(point, 0)->text(), 0354 solutionTable->item(point, 1)->text(), 0355 solutionTable->item(point, 4)->text(), 0356 solutionTable->item(point, 5)->text()), 0357 alignPlot, alignPlot->rect()); 0358 } 0359 } 0360 } 0361 0362 void Align::buildTarget() 0363 { 0364 double accuracyRadius = alignAccuracyThreshold->value(); 0365 if (centralTarget) 0366 { 0367 concentricRings->data()->clear(); 0368 redTarget->data()->clear(); 0369 yellowTarget->data()->clear(); 0370 centralTarget->data()->clear(); 0371 } 0372 else 0373 { 0374 concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); 0375 redTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); 0376 yellowTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); 0377 centralTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); 0378 } 0379 const int pointCount = 200; 0380 QVector<QCPCurveData> circleRings( 0381 pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175% 0382 QVector<QCPCurveData> circleCentral(pointCount); 0383 QVector<QCPCurveData> circleYellow(pointCount); 0384 QVector<QCPCurveData> circleRed(pointCount); 0385 0386 int circleRingPt = 0; 0387 for (int i = 0; i < pointCount; i++) 0388 { 0389 double theta = i / static_cast<double>(pointCount) * 2 * M_PI; 0390 0391 for (double ring = 1; ring < 8; ring++) 0392 { 0393 if (ring != 4 && ring != 6) 0394 { 0395 if (i % (9 - static_cast<int>(ring)) == 0) //This causes fewer points to draw on the inner circles. 0396 { 0397 circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta), 0398 accuracyRadius * ring * 0.25 * qSin(theta)); 0399 circleRingPt++; 0400 } 0401 } 0402 } 0403 0404 circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta)); 0405 circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta)); 0406 circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta)); 0407 } 0408 0409 concentricRings->setLineStyle(QCPCurve::lsNone); 0410 concentricRings->setScatterSkip(0); 0411 concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1)); 0412 0413 concentricRings->data()->set(circleRings, true); 0414 redTarget->data()->set(circleRed, true); 0415 yellowTarget->data()->set(circleYellow, true); 0416 centralTarget->data()->set(circleCentral, true); 0417 0418 concentricRings->setPen(QPen(Qt::white)); 0419 redTarget->setPen(QPen(Qt::red)); 0420 yellowTarget->setPen(QPen(Qt::yellow)); 0421 centralTarget->setPen(QPen(Qt::green)); 0422 0423 concentricRings->setBrush(Qt::NoBrush); 0424 redTarget->setBrush(QBrush(QColor(255, 0, 0, 50))); 0425 yellowTarget->setBrush( 0426 QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity. 0427 centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50))); 0428 0429 if (alignPlot->size().width() > 0) 0430 alignPlot->replot(); 0431 } 0432 0433 void Align::slotAutoScaleGraph() 0434 { 0435 double accuracyRadius = alignAccuracyThreshold->value(); 0436 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); 0437 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); 0438 0439 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); 0440 0441 alignPlot->replot(); 0442 } 0443 0444 0445 void Align::slotClearAllSolutionPoints() 0446 { 0447 if (solutionTable->rowCount() == 0) 0448 return; 0449 0450 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() 0451 { 0452 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); 0453 KSMessageBox::Instance()->disconnect(this); 0454 0455 solutionTable->setRowCount(0); 0456 alignPlot->graph(0)->data()->clear(); 0457 alignPlot->clearItems(); 0458 buildTarget(); 0459 0460 slotAutoScaleGraph(); 0461 0462 }); 0463 0464 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"), 0465 i18n("Clear Solution Points"), 60); 0466 } 0467 0468 void Align::slotRemoveSolutionPoint() 0469 { 0470 auto abstractItem = alignPlot->item(solutionTable->currentRow()); 0471 if (abstractItem) 0472 { 0473 auto item = qobject_cast<QCPItemText *>(abstractItem); 0474 if (item) 0475 { 0476 double point = item->position->key(); 0477 alignPlot->graph(0)->data()->remove(point); 0478 } 0479 } 0480 0481 alignPlot->removeItem(solutionTable->currentRow()); 0482 0483 for (int i = 0; i < alignPlot->itemCount(); i++) 0484 { 0485 auto oneItem = alignPlot->item(i); 0486 if (oneItem) 0487 { 0488 auto item = qobject_cast<QCPItemText *>(oneItem); 0489 if (item) 0490 item->setText(QString::number(i + 1)); 0491 } 0492 } 0493 solutionTable->removeRow(solutionTable->currentRow()); 0494 alignPlot->replot(); 0495 } 0496 0497 void Align::slotMountModel() 0498 { 0499 if (!m_MountModel) 0500 { 0501 m_MountModel = new MountModel(this); 0502 connect(m_MountModel, &Ekos::MountModel::newLog, this, &Ekos::Align::appendLogText, Qt::UniqueConnection); 0503 connect(m_MountModel, &Ekos::MountModel::aborted, this, [this]() 0504 { 0505 if (m_Mount && m_Mount->isSlewing()) 0506 m_Mount->abort(); 0507 abort(); 0508 }); 0509 connect(this, &Ekos::Align::newStatus, m_MountModel, &Ekos::MountModel::setAlignStatus, Qt::UniqueConnection); 0510 } 0511 0512 m_MountModel->show(); 0513 } 0514 0515 0516 bool Align::isParserOK() 0517 { 0518 return true; //For now 0519 // Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid."); 0520 0521 // bool rc = parser->init(); 0522 0523 // if (rc) 0524 // { 0525 // connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); 0526 // connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); 0527 // } 0528 0529 // return rc; 0530 } 0531 0532 void Align::checkAlignmentTimeout() 0533 { 0534 if (m_SolveFromFile || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS) 0535 abort(); 0536 else 0537 { 0538 appendLogText(i18n("Solver timed out.")); 0539 parser->stopSolver(); 0540 0541 setAlignTableResult(ALIGN_RESULT_FAILED); 0542 captureAndSolve(); 0543 } 0544 // TODO must also account for loadAndSlew. Retain file name 0545 } 0546 0547 void Align::setSolverMode(int mode) 0548 { 0549 if (sender() == nullptr && mode >= 0 && mode <= 1) 0550 { 0551 solverModeButtonGroup->button(mode)->setChecked(true); 0552 } 0553 0554 Options::setSolverMode(mode); 0555 0556 if (mode == SOLVER_REMOTE) 0557 { 0558 if (remoteParser.get() != nullptr && m_RemoteParserDevice != nullptr) 0559 { 0560 parser = remoteParser.get(); 0561 (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(m_RemoteParserDevice); 0562 return; 0563 } 0564 0565 remoteParser.reset(new Ekos::RemoteAstrometryParser()); 0566 parser = remoteParser.get(); 0567 (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(m_RemoteParserDevice); 0568 if (m_Camera) 0569 (dynamic_cast<RemoteAstrometryParser *>(parser))->setCCD(m_Camera->getDeviceName()); 0570 0571 parser->setAlign(this); 0572 if (parser->init()) 0573 { 0574 connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); 0575 connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); 0576 } 0577 else 0578 parser->disconnect(); 0579 } 0580 } 0581 0582 QString Align::camera() 0583 { 0584 if (m_Camera) 0585 return m_Camera->getDeviceName(); 0586 0587 return QString(); 0588 } 0589 0590 void Align::checkCamera() 0591 { 0592 if (!m_Camera) 0593 return; 0594 0595 // Do NOT perform checks if align is in progress as this may result 0596 // in signals/slots getting disconnected. 0597 switch (state) 0598 { 0599 // Idle, camera change is OK. 0600 case ALIGN_IDLE: 0601 case ALIGN_COMPLETE: 0602 case ALIGN_FAILED: 0603 case ALIGN_ABORTED: 0604 case ALIGN_SUCCESSFUL: 0605 break; 0606 0607 // Busy, camera change is not OK. 0608 case ALIGN_PROGRESS: 0609 case ALIGN_SYNCING: 0610 case ALIGN_SLEWING: 0611 case ALIGN_SUSPENDED: 0612 case ALIGN_ROTATING: 0613 return; 0614 } 0615 0616 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD); 0617 if (targetChip == nullptr || (targetChip && targetChip->isCapturing())) 0618 return; 0619 0620 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) 0621 (dynamic_cast<RemoteAstrometryParser *>(remoteParser.get()))->setCCD(m_Camera->getDeviceName()); 0622 0623 syncCameraInfo(); 0624 syncCameraControls(); 0625 syncTelescopeInfo(); 0626 } 0627 0628 bool Align::setCamera(ISD::Camera *device) 0629 { 0630 if (m_Camera && m_Camera == device) 0631 { 0632 checkCamera(); 0633 return false; 0634 } 0635 0636 if (m_Camera) 0637 m_Camera->disconnect(this); 0638 0639 m_Camera = device; 0640 0641 if (m_Camera) 0642 { 0643 connect(m_Camera, &ISD::ConcreteDevice::Connected, this, [this]() 0644 { 0645 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected(); 0646 controlBox->setEnabled(isConnected); 0647 gotoBox->setEnabled(isConnected); 0648 plateSolverOptionsGroup->setEnabled(isConnected); 0649 tabWidget->setEnabled(isConnected); 0650 }); 0651 connect(m_Camera, &ISD::ConcreteDevice::Disconnected, this, [this]() 0652 { 0653 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected(); 0654 controlBox->setEnabled(isConnected); 0655 gotoBox->setEnabled(isConnected); 0656 plateSolverOptionsGroup->setEnabled(isConnected); 0657 tabWidget->setEnabled(isConnected); 0658 0659 opticalTrainCombo->setEnabled(true); 0660 trainLabel->setEnabled(true); 0661 }); 0662 } 0663 0664 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected(); 0665 controlBox->setEnabled(isConnected); 0666 gotoBox->setEnabled(isConnected); 0667 plateSolverOptionsGroup->setEnabled(isConnected); 0668 tabWidget->setEnabled(isConnected); 0669 0670 if (!m_Camera) 0671 return false; 0672 0673 checkCamera(); 0674 0675 return true; 0676 } 0677 0678 bool Align::setMount(ISD::Mount *device) 0679 { 0680 if (m_Mount && m_Mount == device) 0681 { 0682 syncTelescopeInfo(); 0683 return false; 0684 } 0685 0686 if (m_Mount) 0687 m_Mount->disconnect(this); 0688 0689 m_Mount = device; 0690 0691 if (m_Mount) 0692 { 0693 connect(m_Mount, &ISD::ConcreteDevice::Connected, this, [this]() 0694 { 0695 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected(); 0696 controlBox->setEnabled(isConnected); 0697 gotoBox->setEnabled(isConnected); 0698 plateSolverOptionsGroup->setEnabled(isConnected); 0699 tabWidget->setEnabled(isConnected); 0700 }); 0701 connect(m_Mount, &ISD::ConcreteDevice::Disconnected, this, [this]() 0702 { 0703 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected(); 0704 controlBox->setEnabled(isConnected); 0705 gotoBox->setEnabled(isConnected); 0706 plateSolverOptionsGroup->setEnabled(isConnected); 0707 tabWidget->setEnabled(isConnected); 0708 0709 opticalTrainCombo->setEnabled(true); 0710 trainLabel->setEnabled(true); 0711 }); 0712 } 0713 0714 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected(); 0715 controlBox->setEnabled(isConnected); 0716 gotoBox->setEnabled(isConnected); 0717 plateSolverOptionsGroup->setEnabled(isConnected); 0718 tabWidget->setEnabled(isConnected); 0719 0720 if (!m_Mount) 0721 return false; 0722 0723 RUN_PAH(setCurrentTelescope(m_Mount)); 0724 0725 connect(m_Mount, &ISD::Mount::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection); 0726 connect(m_Mount, &ISD::Mount::Disconnected, this, [this]() 0727 { 0728 m_isRateSynced = false; 0729 }); 0730 0731 syncTelescopeInfo(); 0732 return true; 0733 } 0734 0735 bool Align::setDome(ISD::Dome *device) 0736 { 0737 if (m_Dome && m_Dome == device) 0738 return false; 0739 0740 if (m_Dome) 0741 m_Dome->disconnect(this); 0742 0743 m_Dome = device; 0744 0745 if (!m_Dome) 0746 return false; 0747 0748 connect(m_Dome, &ISD::Dome::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection); 0749 return true; 0750 } 0751 0752 void Align::removeDevice(const QSharedPointer<ISD::GenericDevice> &device) 0753 { 0754 auto name = device->getDeviceName(); 0755 device->disconnect(this); 0756 0757 // Check mounts 0758 if (m_Mount && m_Mount->getDeviceName() == name) 0759 { 0760 m_Mount->disconnect(this); 0761 m_Mount = nullptr; 0762 } 0763 0764 // Check domes 0765 if (m_Dome && m_Dome->getDeviceName() == name) 0766 { 0767 m_Dome->disconnect(this); 0768 m_Dome = nullptr; 0769 } 0770 0771 // Check rotators 0772 if (m_Rotator && m_Rotator->getDeviceName() == name) 0773 { 0774 m_Rotator->disconnect(this); 0775 m_Rotator = nullptr; 0776 } 0777 0778 // Check cameras 0779 if (m_Camera && m_Camera->getDeviceName() == name) 0780 { 0781 m_Camera->disconnect(this); 0782 m_Camera = nullptr; 0783 0784 QTimer::singleShot(1000, this, &Align::checkCamera); 0785 } 0786 0787 // Check Remote Astrometry 0788 if (m_RemoteParserDevice && m_RemoteParserDevice->getDeviceName() == name) 0789 { 0790 m_RemoteParserDevice.clear(); 0791 } 0792 0793 // Check Filter Wheels 0794 if (m_FilterWheel && m_FilterWheel->getDeviceName() == name) 0795 { 0796 m_FilterWheel->disconnect(this); 0797 m_FilterWheel = nullptr; 0798 0799 QTimer::singleShot(1000, this, &Align::checkFilter); 0800 } 0801 0802 } 0803 0804 bool Align::syncTelescopeInfo() 0805 { 0806 if (m_Mount == nullptr || m_Mount->isConnected() == false) 0807 return false; 0808 0809 if (m_isRateSynced == false) 0810 { 0811 auto speed = m_Settings["PAHMountSpeed"]; 0812 auto slewRates = m_Mount->slewRates(); 0813 if (speed.isValid()) 0814 { 0815 RUN_PAH(syncMountSpeed(speed.toString())); 0816 } 0817 else if (!slewRates.isEmpty()) 0818 { 0819 RUN_PAH(syncMountSpeed(slewRates.last())); 0820 } 0821 0822 m_isRateSynced = !slewRates.empty(); 0823 } 0824 0825 canSync = m_Mount->canSync(); 0826 0827 if (canSync == false && syncR->isEnabled()) 0828 { 0829 slewR->setChecked(true); 0830 appendLogText(i18n("Mount does not support syncing.")); 0831 } 0832 0833 syncR->setEnabled(canSync); 0834 0835 if (m_FocalLength == -1 || m_Aperture == -1) 0836 return false; 0837 0838 if (m_CameraPixelWidth != -1 && m_CameraPixelHeight != -1) 0839 { 0840 calculateFOV(); 0841 return true; 0842 } 0843 0844 return false; 0845 } 0846 0847 void Align::syncCameraInfo() 0848 { 0849 if (!m_Camera) 0850 return; 0851 0852 auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD); 0853 Q_ASSERT(targetChip); 0854 0855 // Get Maximum resolution and pixel size 0856 uint8_t bit_depth = 8; 0857 targetChip->getImageInfo(m_CameraWidth, m_CameraHeight, m_CameraPixelWidth, m_CameraPixelHeight, bit_depth); 0858 0859 setWCSEnabled(Options::astrometrySolverWCS()); 0860 0861 int binx = 1, biny = 1; 0862 alignBinning->setEnabled(targetChip->canBin()); 0863 if (targetChip->canBin()) 0864 { 0865 alignBinning->blockSignals(true); 0866 0867 targetChip->getMaxBin(&binx, &biny); 0868 alignBinning->clear(); 0869 0870 for (int i = 0; i < binx; i++) 0871 alignBinning->addItem(QString("%1x%2").arg(i + 1).arg(i + 1)); 0872 0873 auto binning = m_Settings["alignBinning"]; 0874 if (binning.isValid()) 0875 alignBinning->setCurrentText(binning.toString()); 0876 0877 alignBinning->blockSignals(false); 0878 } 0879 0880 // In case ROI is different (smaller) than maximum resolution, let's use that. 0881 // N.B. 2022.08.14 JM: We must account for binning since this value is used for FOV calculations. 0882 int roiW = 0, roiH = 0; 0883 targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &roiW, nullptr, &roiH); 0884 roiW *= binx; 0885 roiH *= biny; 0886 if ( (roiW > 0 && roiW < m_CameraWidth) || (roiH > 0 && roiH < m_CameraHeight)) 0887 { 0888 m_CameraWidth = roiW; 0889 m_CameraHeight = roiH; 0890 } 0891 0892 if (m_CameraPixelWidth > 0 && m_CameraPixelHeight > 0 && m_FocalLength > 0 && m_Aperture > 0) 0893 { 0894 calculateFOV(); 0895 } 0896 } 0897 0898 void Align::syncCameraControls() 0899 { 0900 if (m_Camera == nullptr) 0901 return; 0902 0903 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD); 0904 if (targetChip == nullptr || (targetChip && targetChip->isCapturing())) 0905 return; 0906 0907 auto isoList = targetChip->getISOList(); 0908 alignISO->clear(); 0909 0910 if (isoList.isEmpty()) 0911 { 0912 alignISO->setEnabled(false); 0913 } 0914 else 0915 { 0916 alignISO->setEnabled(true); 0917 alignISO->addItems(isoList); 0918 alignISO->setCurrentIndex(targetChip->getISOIndex()); 0919 } 0920 0921 // Gain Check 0922 if (m_Camera->hasGain()) 0923 { 0924 double min, max, step, value; 0925 m_Camera->getGainMinMaxStep(&min, &max, &step); 0926 0927 // Allow the possibility of no gain value at all. 0928 alignGainSpecialValue = min - step; 0929 alignGain->setRange(alignGainSpecialValue, max); 0930 alignGain->setSpecialValueText(i18n("--")); 0931 alignGain->setEnabled(true); 0932 alignGain->setSingleStep(step); 0933 m_Camera->getGain(&value); 0934 0935 auto gain = m_Settings["alignGain"]; 0936 // Set the custom gain if we have one 0937 // otherwise it will not have an effect. 0938 if (gain.isValid()) 0939 TargetCustomGainValue = gain.toDouble(); 0940 if (TargetCustomGainValue > 0) 0941 alignGain->setValue(TargetCustomGainValue); 0942 else 0943 alignGain->setValue(alignGainSpecialValue); 0944 0945 alignGain->setReadOnly(m_Camera->getGainPermission() == IP_RO); 0946 0947 connect(alignGain, &QDoubleSpinBox::editingFinished, this, [this]() 0948 { 0949 if (alignGain->value() > alignGainSpecialValue) 0950 TargetCustomGainValue = alignGain->value(); 0951 }); 0952 } 0953 else 0954 alignGain->setEnabled(false); 0955 } 0956 0957 void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale) 0958 { 0959 fov_w = m_FOVWidth; 0960 fov_h = m_FOVHeight; 0961 fov_scale = m_FOVPixelScale; 0962 } 0963 0964 QList<double> Align::fov() 0965 { 0966 QList<double> result; 0967 0968 result << m_FOVWidth << m_FOVHeight << m_FOVPixelScale; 0969 0970 return result; 0971 } 0972 0973 QList<double> Align::cameraInfo() 0974 { 0975 QList<double> result; 0976 0977 result << m_CameraWidth << m_CameraHeight << m_CameraPixelWidth << m_CameraPixelHeight; 0978 0979 return result; 0980 } 0981 0982 QList<double> Align::telescopeInfo() 0983 { 0984 QList<double> result; 0985 0986 result << m_FocalLength << m_Aperture << m_Reducer; 0987 0988 return result; 0989 } 0990 0991 void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale) 0992 { 0993 // FOV in arcsecs 0994 // DSLR 0995 auto reducedFocalLength = m_Reducer * m_FocalLength; 0996 if (m_FocalRatio > 0) 0997 { 0998 // The forumla is in radians, must convert to degrees. 0999 // Then to arcsecs 1000 fov_w = 3600 * 2 * atan(m_CameraWidth * (m_CameraPixelWidth / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad; 1001 fov_h = 3600 * 2 * atan(m_CameraHeight * (m_CameraPixelHeight / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad; 1002 } 1003 // Telescope 1004 else 1005 { 1006 fov_w = 206264.8062470963552 * m_CameraWidth * m_CameraPixelWidth / 1000.0 / reducedFocalLength; 1007 fov_h = 206264.8062470963552 * m_CameraHeight * m_CameraPixelHeight / 1000.0 / reducedFocalLength; 1008 } 1009 1010 // Pix Scale 1011 fov_scale = (fov_w * (alignBinning->currentIndex() + 1)) / m_CameraWidth; 1012 1013 // FOV in arcmins 1014 fov_w /= 60.0; 1015 fov_h /= 60.0; 1016 } 1017 1018 void Align::calculateEffectiveFocalLength(double newFOVW) 1019 { 1020 if (newFOVW < 0 || isEqual(newFOVW, m_FOVWidth)) 1021 return; 1022 1023 auto reducedFocalLength = m_Reducer * m_FocalLength; 1024 double new_focal_length = 0; 1025 1026 if (m_FocalRatio > 0) 1027 new_focal_length = ((m_CameraWidth * m_CameraPixelWidth / 1000.0) / tan(newFOVW / 2)) / 2; 1028 else 1029 new_focal_length = ((m_CameraWidth * m_CameraPixelWidth / 1000.0) * 206264.8062470963552) / (newFOVW * 60.0); 1030 double focal_diff = std::fabs(new_focal_length - reducedFocalLength); 1031 1032 if (focal_diff > 1) 1033 { 1034 m_EffectiveFocalLength = new_focal_length / m_Reducer; 1035 appendLogText(i18n("Effective telescope focal length is updated to %1 mm.", m_EffectiveFocalLength)); 1036 } 1037 } 1038 1039 void Align::calculateFOV() 1040 { 1041 auto reducedFocalLength = m_Reducer * m_FocalLength; 1042 auto reducecdEffectiveFocalLength = m_Reducer * m_EffectiveFocalLength; 1043 auto reducedFocalRatio = m_Reducer * m_FocalRatio; 1044 1045 if (m_FocalRatio > 0) 1046 { 1047 // The forumla is in radians, must convert to degrees. 1048 // Then to arcsecs 1049 m_FOVWidth = 3600 * 2 * atan(m_CameraWidth * (m_CameraPixelWidth / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad; 1050 m_FOVHeight = 3600 * 2 * atan(m_CameraHeight * (m_CameraPixelHeight / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad; 1051 } 1052 // Telescope 1053 else 1054 { 1055 m_FOVWidth = 206264.8062470963552 * m_CameraWidth * m_CameraPixelWidth / 1000.0 / reducedFocalLength; 1056 m_FOVHeight = 206264.8062470963552 * m_CameraHeight * m_CameraPixelHeight / 1000.0 / reducedFocalLength; 1057 } 1058 1059 // Calculate FOV 1060 1061 // Pix Scale 1062 m_FOVPixelScale = (m_FOVWidth * (alignBinning->currentIndex() + 1)) / m_CameraWidth; 1063 1064 // FOV in arcmins 1065 m_FOVWidth /= 60.0; 1066 m_FOVHeight /= 60.0; 1067 1068 double calculated_fov_x = m_FOVWidth; 1069 double calculated_fov_y = m_FOVHeight; 1070 1071 QString calculatedFOV = (QString("%1' x %2'").arg(m_FOVWidth, 0, 'f', 1).arg(m_FOVHeight, 0, 'f', 1)); 1072 1073 // Put FOV upper limit as 180 degrees 1074 if (m_FOVWidth < 1 || m_FOVWidth > 60 * 180 || m_FOVHeight < 1 || m_FOVHeight > 60 * 180) 1075 { 1076 appendLogText( 1077 i18n("Warning! The calculated field of view (%1) is out of bounds. Ensure the telescope focal length and camera pixel size are correct.", 1078 calculatedFOV)); 1079 return; 1080 } 1081 1082 FocalLengthOut->setText(QString("%1 (%2)").arg(reducedFocalLength, 0, 'f', 1). 1083 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength : reducedFocalLength, 0, 'f', 1)); 1084 // DSLR 1085 if (m_FocalRatio > 0) 1086 FocalRatioOut->setText(QString("%1 (%2)").arg(reducedFocalRatio, 0, 'f', 1). 1087 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength / m_Aperture : reducedFocalRatio, 0, 1088 'f', 1)); 1089 // Telescope 1090 else if (m_Aperture > 0) 1091 FocalRatioOut->setText(QString("%1 (%2)").arg(reducedFocalLength / m_Aperture, 0, 'f', 1). 1092 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength / m_Aperture : reducedFocalLength / m_Aperture, 0, 1093 'f', 1)); 1094 ReducerOut->setText(QString("%1x").arg(m_Reducer, 0, 'f', 2)); 1095 1096 if (m_EffectiveFocalLength > 0) 1097 { 1098 double focal_diff = std::fabs(m_EffectiveFocalLength - m_FocalLength); 1099 if (focal_diff < 5) 1100 FocalLengthOut->setStyleSheet("color:green"); 1101 else if (focal_diff < 15) 1102 FocalLengthOut->setStyleSheet("color:yellow"); 1103 else 1104 FocalLengthOut->setStyleSheet("color:red"); 1105 } 1106 1107 // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV 1108 // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique 1109 // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete. 1110 getEffectiveFOV(); 1111 1112 if (m_FOVWidth == 0) 1113 { 1114 //FOVOut->setReadOnly(false); 1115 FOVOut->setToolTip( 1116 i18n("<p>Effective field of view size in arcminutes.</p><p>Please capture and solve once to measure the effective FOV or enter the values manually.</p><p>Calculated FOV: %1</p>", 1117 calculatedFOV)); 1118 m_FOVWidth = calculated_fov_x; 1119 m_FOVHeight = calculated_fov_y; 1120 m_EffectiveFOVPending = true; 1121 } 1122 else 1123 { 1124 m_EffectiveFOVPending = false; 1125 FOVOut->setToolTip(i18n("<p>Effective field of view size in arcminutes.</p>")); 1126 } 1127 1128 solverFOV->setSize(m_FOVWidth, m_FOVHeight); 1129 sensorFOV->setSize(m_FOVWidth, m_FOVHeight); 1130 if (m_Camera) 1131 sensorFOV->setName(m_Camera->getDeviceName()); 1132 1133 FOVOut->setText(QString("%1' x %2'").arg(m_FOVWidth, 0, 'f', 1).arg(m_FOVHeight, 0, 'f', 1)); 1134 1135 // Enable or Disable PAA depending on current FOV 1136 const bool fovOK = ((m_FOVWidth + m_FOVHeight) / 2.0) > PAH_CUTOFF_FOV; 1137 if (m_PolarAlignmentAssistant != nullptr) 1138 m_PolarAlignmentAssistant->setEnabled(fovOK); 1139 1140 if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked()) 1141 { 1142 int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex(); 1143 1144 // Degrees 1145 if (unitType == 0) 1146 { 1147 double fov_low = qMin(m_FOVWidth / 60, m_FOVHeight / 60); 1148 double fov_high = qMax(m_FOVWidth / 60, m_FOVHeight / 60); 1149 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low); 1150 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high); 1151 1152 Options::setAstrometryImageScaleLow(fov_low); 1153 Options::setAstrometryImageScaleHigh(fov_high); 1154 } 1155 // Arcmins 1156 else if (unitType == 1) 1157 { 1158 double fov_low = qMin(m_FOVWidth, m_FOVHeight); 1159 double fov_high = qMax(m_FOVWidth, m_FOVHeight); 1160 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low); 1161 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high); 1162 1163 Options::setAstrometryImageScaleLow(fov_low); 1164 Options::setAstrometryImageScaleHigh(fov_high); 1165 } 1166 // Arcsec per pixel 1167 else 1168 { 1169 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(m_FOVPixelScale * 0.9); 1170 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(m_FOVPixelScale * 1.1); 1171 1172 // 10% boundary 1173 Options::setAstrometryImageScaleLow(m_FOVPixelScale * 0.9); 1174 Options::setAstrometryImageScaleHigh(m_FOVPixelScale * 1.1); 1175 } 1176 } 1177 } 1178 1179 QStringList Align::generateRemoteOptions(const QVariantMap &optionsMap) 1180 { 1181 QStringList solver_args; 1182 1183 // -O overwrite 1184 // -3 Expected RA 1185 // -4 Expected DEC 1186 // -5 Radius (deg) 1187 // -L lower scale of image in arcminutes 1188 // -H upper scale of image in arcminutes 1189 // -u aw set scale to be in arcminutes 1190 // -W solution.wcs name of solution file 1191 // apog1.jpg name of target file to analyze 1192 //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg 1193 1194 // Start with always-used arguments 1195 solver_args << "-O" 1196 << "--no-plots"; 1197 1198 // Now go over boolean options 1199 1200 // noverify 1201 if (optionsMap.contains("noverify")) 1202 solver_args << "--no-verify"; 1203 1204 // noresort 1205 if (optionsMap.contains("resort")) 1206 solver_args << "--resort"; 1207 1208 // fits2fits 1209 if (optionsMap.contains("nofits2fits")) 1210 solver_args << "--no-fits2fits"; 1211 1212 // downsample 1213 if (optionsMap.contains("downsample")) 1214 solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt()); 1215 1216 // image scale low 1217 if (optionsMap.contains("scaleL")) 1218 solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble()); 1219 1220 // image scale high 1221 if (optionsMap.contains("scaleH")) 1222 solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble()); 1223 1224 // image scale units 1225 if (optionsMap.contains("scaleUnits")) 1226 solver_args << "-u" << optionsMap.value("scaleUnits").toString(); 1227 1228 // RA 1229 if (optionsMap.contains("ra")) 1230 solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble()); 1231 1232 // DE 1233 if (optionsMap.contains("de")) 1234 solver_args << "-4" << QString::number(optionsMap.value("de").toDouble()); 1235 1236 // Radius 1237 if (optionsMap.contains("radius")) 1238 solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble()); 1239 1240 // Custom 1241 if (optionsMap.contains("custom")) 1242 solver_args << optionsMap.value("custom").toString(); 1243 1244 return solver_args; 1245 } 1246 1247 //This will generate the high and low scale of the imager field size based on the stated units. 1248 void Align::generateFOVBounds(double fov_w, QString &fov_low, QString &fov_high, double tolerance) 1249 { 1250 // This sets the percentage we search outside the lower and upper boundary limits 1251 // by default, we stretch the limits by 5% (tolerance = 0.05) 1252 double lower_boundary = 1.0 - tolerance; 1253 double upper_boundary = 1.0 + tolerance; 1254 1255 // let's stretch the boundaries by 5% 1256 // fov_lower = ((fov_h < fov_v) ? (fov_h * lower_boundary) : (fov_v * lower_boundary)); 1257 // fov_upper = ((fov_h > fov_v) ? (fov_h * upper_boundary) : (fov_v * upper_boundary)); 1258 1259 // JM 2019-10-20: The bounds consider image width only, not height. 1260 double fov_lower = fov_w * lower_boundary; 1261 double fov_upper = fov_w * upper_boundary; 1262 1263 //No need to do anything if they are aw, since that is the default 1264 fov_low = QString::number(fov_lower); 1265 fov_high = QString::number(fov_upper); 1266 } 1267 1268 1269 QStringList Align::generateRemoteArgs(const QSharedPointer<FITSData> &data) 1270 { 1271 QVariantMap optionsMap; 1272 1273 // -O overwrite 1274 // -3 Expected RA 1275 // -4 Expected DEC 1276 // -5 Radius (deg) 1277 // -L lower scale of image in arcminutes 1278 // -H upper scale of image in arcminutes 1279 // -u aw set scale to be in arcminutes 1280 // -W solution.wcs name of solution file 1281 // apog1.jpg name of target file to analyze 1282 //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg 1283 1284 if (Options::astrometryUseNoVerify()) 1285 optionsMap["noverify"] = true; 1286 1287 if (Options::astrometryUseResort()) 1288 optionsMap["resort"] = true; 1289 1290 if (Options::astrometryUseNoFITS2FITS()) 1291 optionsMap["nofits2fits"] = true; 1292 1293 if (data == nullptr) 1294 { 1295 if (Options::astrometryUseDownsample()) 1296 { 1297 if (Options::astrometryAutoDownsample() && m_CameraWidth && m_CameraHeight) 1298 { 1299 uint16_t w = m_CameraWidth / (alignBinning->currentIndex() + 1); 1300 optionsMap["downsample"] = getSolverDownsample(w); 1301 } 1302 else 1303 optionsMap["downsample"] = Options::astrometryDownsample(); 1304 } 1305 1306 //Options needed for Sextractor 1307 optionsMap["image_width"] = m_CameraWidth / (alignBinning->currentIndex() + 1); 1308 optionsMap["image_height"] = m_CameraHeight / (alignBinning->currentIndex() + 1); 1309 1310 if (Options::astrometryUseImageScale() && m_FOVWidth > 0 && m_FOVHeight > 0) 1311 { 1312 QString units = "dw"; 1313 if (Options::astrometryImageScaleUnits() == 1) 1314 units = "aw"; 1315 else if (Options::astrometryImageScaleUnits() == 2) 1316 units = "app"; 1317 if (Options::astrometryAutoUpdateImageScale()) 1318 { 1319 QString fov_low, fov_high; 1320 double fov_w = m_FOVWidth; 1321 //double fov_h = m_FOVHeight; 1322 1323 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH) 1324 { 1325 fov_w /= 60; 1326 //fov_h /= 60; 1327 } 1328 else if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX) 1329 { 1330 fov_w = m_FOVPixelScale; 1331 //fov_h = m_FOVPixelScale; 1332 } 1333 1334 // If effective FOV is pending, let's set a wider tolerance range 1335 generateFOVBounds(fov_w, fov_low, fov_high, m_EffectiveFOVPending ? 0.3 : 0.05); 1336 1337 optionsMap["scaleL"] = fov_low; 1338 optionsMap["scaleH"] = fov_high; 1339 optionsMap["scaleUnits"] = units; 1340 } 1341 else 1342 { 1343 optionsMap["scaleL"] = Options::astrometryImageScaleLow(); 1344 optionsMap["scaleH"] = Options::astrometryImageScaleHigh(); 1345 optionsMap["scaleUnits"] = units; 1346 } 1347 } 1348 1349 if (Options::astrometryUsePosition() && m_Mount != nullptr) 1350 { 1351 double ra = 0, dec = 0; 1352 m_Mount->getEqCoords(&ra, &dec); 1353 1354 optionsMap["ra"] = ra * 15.0; 1355 optionsMap["de"] = dec; 1356 optionsMap["radius"] = Options::astrometryRadius(); 1357 } 1358 } 1359 else 1360 { 1361 // Downsample 1362 QVariant width; 1363 data->getRecordValue("NAXIS1", width); 1364 if (width.isValid()) 1365 { 1366 optionsMap["downsample"] = getSolverDownsample(width.toInt()); 1367 } 1368 else 1369 optionsMap["downsample"] = Options::astrometryDownsample(); 1370 1371 // Pixel Scale 1372 QVariant pixscale; 1373 data->getRecordValue("SCALE", pixscale); 1374 if (pixscale.isValid()) 1375 { 1376 optionsMap["scaleL"] = 0.8 * pixscale.toDouble(); 1377 optionsMap["scaleH"] = 1.2 * pixscale.toDouble(); 1378 optionsMap["scaleUnits"] = "app"; 1379 } 1380 1381 // Position 1382 QVariant ra, de; 1383 data->getRecordValue("RA", ra); 1384 data->getRecordValue("DEC", de); 1385 if (ra.isValid() && de.isValid()) 1386 { 1387 optionsMap["ra"] = ra.toDouble(); 1388 optionsMap["de"] = de.toDouble(); 1389 optionsMap["radius"] = Options::astrometryRadius(); 1390 } 1391 } 1392 1393 if (Options::astrometryCustomOptions().isEmpty() == false) 1394 optionsMap["custom"] = Options::astrometryCustomOptions(); 1395 1396 return generateRemoteOptions(optionsMap); 1397 } 1398 1399 bool Align::captureAndSolve() 1400 { 1401 m_AlignTimer.stop(); 1402 m_CaptureTimer.stop(); 1403 1404 if (m_Camera == nullptr) 1405 { 1406 appendLogText(i18n("Error: No camera detected.")); 1407 return false; 1408 } 1409 1410 if (m_Camera->isConnected() == false) 1411 { 1412 appendLogText(i18n("Error: lost connection to camera.")); 1413 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::Align, 1414 KSNotification::Alert); 1415 return false; 1416 } 1417 1418 if (m_Camera->isBLOBEnabled() == false) 1419 { 1420 m_Camera->setBLOBEnabled(true); 1421 } 1422 1423 //if (parser->init() == false) 1424 // return false; 1425 1426 if (m_FocalLength == -1 || m_Aperture == -1) 1427 { 1428 KSNotification::error( 1429 i18n("Telescope aperture and focal length are missing. Please check your optical train settings and try again.")); 1430 return false; 1431 } 1432 1433 if (m_CameraPixelWidth == -1 || m_CameraPixelHeight == -1) 1434 { 1435 KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again.")); 1436 return false; 1437 } 1438 1439 if (m_FilterWheel != nullptr) 1440 { 1441 if (m_FilterWheel->isConnected() == false) 1442 { 1443 appendLogText(i18n("Error: lost connection to filter wheel.")); 1444 return false; 1445 } 1446 1447 int targetPosition = alignFilter->currentIndex() + 1; 1448 1449 if (targetPosition > 0 && targetPosition != currentFilterPosition) 1450 { 1451 filterPositionPending = true; 1452 // Disabling the autofocus policy for align. 1453 m_FilterManager->setFilterPosition(targetPosition, FilterManager::NO_AUTOFOCUS_POLICY); 1454 setState(ALIGN_PROGRESS); 1455 return true; 1456 } 1457 } 1458 1459 auto clientManager = m_Camera->getDriverInfo()->getClientManager(); 1460 if (clientManager && clientManager->getBLOBMode(m_Camera->getDeviceName().toLatin1().constData(), "CCD1") == B_NEVER) 1461 { 1462 if (KMessageBox::questionYesNo( 1463 nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == 1464 KMessageBox::Yes) 1465 { 1466 clientManager->setBLOBMode(B_ONLY, m_Camera->getDeviceName().toLatin1().constData(), "CCD1"); 1467 clientManager->setBLOBMode(B_ONLY, m_Camera->getDeviceName().toLatin1().constData(), "CCD2"); 1468 } 1469 else 1470 { 1471 return false; 1472 } 1473 } 1474 1475 double seqExpose = alignExposure->value(); 1476 1477 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD); 1478 1479 if (m_FocusState >= FOCUS_PROGRESS) 1480 { 1481 appendLogText(i18n("Cannot capture while focus module is busy. Retrying in %1 seconds...", 1482 CAPTURE_RETRY_DELAY / 1000)); 1483 m_CaptureTimer.start(CAPTURE_RETRY_DELAY); 1484 return true; 1485 } 1486 1487 if (targetChip->isCapturing()) 1488 { 1489 appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in %1 seconds...", 1490 CAPTURE_RETRY_DELAY / 1000)); 1491 m_CaptureTimer.start(CAPTURE_RETRY_DELAY); 1492 return true; 1493 } 1494 1495 // Let rotate the camera BEFORE taking a capture in [Capture & Solve] 1496 if (!m_SolveFromFile && m_Rotator && m_Rotator->absoluteAngleState() == IPS_BUSY) 1497 { 1498 int TimeOut = CAPTURE_ROTATOR_DELAY; 1499 switch (m_CaptureTimeoutCounter) 1500 { 1501 case 0:// Set start time & start angle and estimate rotator time frame during first timeout 1502 { 1503 auto absAngle = 0; 1504 if ((absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE")->at(0)->getValue())) 1505 { 1506 RotatorUtils::Instance()->startTimeFrame(absAngle); 1507 m_estimateRotatorTimeFrame = true; 1508 appendLogText(i18n("Cannot capture while rotator is busy: Time delay estimate started...")); 1509 } 1510 m_CaptureTimer.start(TimeOut); 1511 break; 1512 } 1513 case 1:// Use estimated time frame (in updateProperty()) for second timeout 1514 { 1515 TimeOut = m_RotatorTimeFrame * 1000; 1516 [[fallthrough]]; 1517 } 1518 default: 1519 { 1520 TimeOut *= m_CaptureTimeoutCounter; // Extend Timeout in case estimated value is too short 1521 m_estimateRotatorTimeFrame = false; 1522 appendLogText(i18n("Cannot capture while rotator is busy: Retrying in %1 seconds...", TimeOut / 1000)); 1523 m_CaptureTimer.start(TimeOut); 1524 } 1525 } 1526 return true; // Return value is used in 'Scheduler::startAstrometry()' 1527 } 1528 1529 m_AlignView->setBaseSize(alignWidget->size()); 1530 m_AlignView->setProperty("suspended", (solverModeButtonGroup->checkedId() == SOLVER_LOCAL 1531 && alignDarkFrame->isChecked())); 1532 1533 connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData); 1534 connect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress); 1535 1536 // In case of remote solver, check if we need to update active CCD 1537 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) 1538 { 1539 if (m_RemoteParserDevice == nullptr) 1540 { 1541 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver.")); 1542 setSolverMode(SOLVER_LOCAL); 1543 } 1544 else 1545 { 1546 // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD 1547 auto activeDevices = m_RemoteParserDevice->getBaseDevice().getText("ACTIVE_DEVICES"); 1548 if (activeDevices) 1549 { 1550 auto activeCCD = activeDevices.findWidgetByName("ACTIVE_CCD"); 1551 if (QString(activeCCD->text) != m_Camera->getDeviceName()) 1552 { 1553 activeCCD->setText(m_Camera->getDeviceName().toLatin1().constData()); 1554 m_RemoteParserDevice->getClientManager()->sendNewProperty(activeDevices); 1555 } 1556 } 1557 1558 // Enable remote parse 1559 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(true); 1560 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->sendArgs(generateRemoteArgs(QSharedPointer<FITSData>())); 1561 solverTimer.start(); 1562 } 1563 } 1564 1565 // Remove temporary FITS files left before by the solver 1566 QDir dir(QDir::tempPath()); 1567 dir.setNameFilters(QStringList() << "fits*" << "tmp.*"); 1568 dir.setFilter(QDir::Files); 1569 for (auto &dirFile : dir.entryList()) 1570 dir.remove(dirFile); 1571 1572 prepareCapture(targetChip); 1573 1574 // In case we're in refresh phase of the polar alignment helper then we use capture value from there 1575 if (matchPAHStage(PAA::PAH_REFRESH)) 1576 targetChip->capture(m_PolarAlignmentAssistant->getPAHExposureDuration()); 1577 else 1578 targetChip->capture(seqExpose); 1579 1580 solveB->setEnabled(false); 1581 loadSlewB->setEnabled(false); 1582 stopB->setEnabled(true); 1583 pi->startAnimation(); 1584 1585 differentialSlewingActivated = false; 1586 1587 setState(ALIGN_PROGRESS); 1588 emit newStatus(state); 1589 solverFOV->setProperty("visible", true); 1590 1591 // If we're just refreshing, then we're done 1592 if (matchPAHStage(PAA::PAH_REFRESH)) 1593 return true; 1594 1595 appendLogText(i18n("Capturing image...")); 1596 1597 if (!m_Mount) 1598 return true; 1599 1600 //This block of code will create the row in the solution table and populate RA, DE, and object name. 1601 //It also starts the progress indicator. 1602 double ra, dec; 1603 m_Mount->getEqCoords(&ra, &dec); 1604 if (!m_SolveFromFile) 1605 { 1606 int currentRow = solutionTable->rowCount(); 1607 solutionTable->insertRow(currentRow); 1608 for (int i = 4; i < 6; i++) 1609 { 1610 QTableWidgetItem *disabledBox = new QTableWidgetItem(); 1611 disabledBox->setFlags(Qt::ItemIsSelectable); 1612 solutionTable->setItem(currentRow, i, disabledBox); 1613 } 1614 1615 QTableWidgetItem *RAReport = new QTableWidgetItem(); 1616 RAReport->setText(ScopeRAOut->text()); 1617 RAReport->setTextAlignment(Qt::AlignHCenter); 1618 RAReport->setFlags(Qt::ItemIsSelectable); 1619 solutionTable->setItem(currentRow, 0, RAReport); 1620 1621 QTableWidgetItem *DECReport = new QTableWidgetItem(); 1622 DECReport->setText(ScopeDecOut->text()); 1623 DECReport->setTextAlignment(Qt::AlignHCenter); 1624 DECReport->setFlags(Qt::ItemIsSelectable); 1625 solutionTable->setItem(currentRow, 1, DECReport); 1626 1627 double maxrad = 1.0; 1628 SkyObject *so = 1629 KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad); 1630 QString name; 1631 if (so) 1632 { 1633 name = so->longname(); 1634 } 1635 else 1636 { 1637 name = "None"; 1638 } 1639 QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); 1640 ObjNameReport->setText(name); 1641 ObjNameReport->setTextAlignment(Qt::AlignHCenter); 1642 ObjNameReport->setFlags(Qt::ItemIsSelectable); 1643 solutionTable->setItem(currentRow, 2, ObjNameReport); 1644 #ifdef Q_OS_OSX 1645 repaint(); //This is a band-aid for a bug in QT 5.10.0 1646 #endif 1647 1648 QProgressIndicator *alignIndicator = new QProgressIndicator(this); 1649 solutionTable->setCellWidget(currentRow, 3, alignIndicator); 1650 alignIndicator->startAnimation(); 1651 #ifdef Q_OS_OSX 1652 repaint(); //This is a band-aid for a bug in QT 5.10.0 1653 #endif 1654 } 1655 1656 return true; 1657 } 1658 1659 void Align::processData(const QSharedPointer<FITSData> &data) 1660 { 1661 auto chip = data->property("chip"); 1662 if (chip.isValid() && chip.toInt() == ISD::CameraChip::GUIDE_CCD) 1663 return; 1664 1665 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData); 1666 disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress); 1667 1668 if (data) 1669 { 1670 m_AlignView->loadData(data); 1671 m_ImageData = data; 1672 } 1673 else 1674 m_ImageData.reset(); 1675 1676 RUN_PAH(setImageData(m_ImageData)); 1677 1678 // If it's Refresh, we're done 1679 if (matchPAHStage(PAA::PAH_REFRESH)) 1680 { 1681 setCaptureComplete(); 1682 return; 1683 } 1684 else 1685 appendLogText(i18n("Image received.")); 1686 1687 // If Local solver, then set capture complete or perform calibration first. 1688 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL) 1689 { 1690 // Only perform dark image subtraction on local images. 1691 if (alignDarkFrame->isChecked()) 1692 { 1693 int x, y, w, h, binx = 1, biny = 1; 1694 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD); 1695 targetChip->getFrame(&x, &y, &w, &h); 1696 targetChip->getBinning(&binx, &biny); 1697 1698 uint16_t offsetX = x / binx; 1699 uint16_t offsetY = y / biny; 1700 1701 m_DarkProcessor->denoise(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()), 1702 targetChip, m_ImageData, alignExposure->value(), offsetX, offsetY); 1703 return; 1704 } 1705 1706 setCaptureComplete(); 1707 } 1708 } 1709 1710 void Align::prepareCapture(ISD::CameraChip *targetChip) 1711 { 1712 if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_LOCAL) 1713 { 1714 rememberUploadMode = ISD::Camera::UPLOAD_LOCAL; 1715 m_Camera->setUploadMode(ISD::Camera::UPLOAD_CLIENT); 1716 } 1717 1718 if (m_Camera->isFastExposureEnabled()) 1719 { 1720 m_RememberCameraFastExposure = true; 1721 m_Camera->setFastExposureEnabled(false); 1722 } 1723 1724 m_Camera->setEncodingFormat("FITS"); 1725 targetChip->resetFrame(); 1726 targetChip->setBatchMode(false); 1727 targetChip->setCaptureMode(FITS_ALIGN); 1728 targetChip->setFrameType(FRAME_LIGHT); 1729 1730 int bin = alignBinning->currentIndex() + 1; 1731 targetChip->setBinning(bin, bin); 1732 1733 // Set gain if applicable 1734 if (m_Camera->hasGain() && alignGain->isEnabled() && alignGain->value() > alignGainSpecialValue) 1735 m_Camera->setGain(alignGain->value()); 1736 // Set ISO if applicable 1737 if (alignISO->currentIndex() >= 0) 1738 targetChip->setISOIndex(alignISO->currentIndex()); 1739 } 1740 1741 1742 void Align::setCaptureComplete() 1743 { 1744 if (matchPAHStage(PAA::PAH_REFRESH)) 1745 { 1746 emit newFrame(m_AlignView); 1747 m_PolarAlignmentAssistant->processPAHRefresh(); 1748 return; 1749 } 1750 1751 emit newImage(m_AlignView); 1752 1753 solverFOV->setImage(m_AlignView->getDisplayImage()); 1754 1755 // If align logging is enabled, let's save the frame. 1756 if (Options::saveAlignImages()) 1757 { 1758 QDir dir; 1759 QDateTime now = KStarsData::Instance()->lt(); 1760 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/" + 1761 now.toString("yyyy-MM-dd")); 1762 dir.mkpath(path); 1763 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-' 1764 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts 1765 QString name = "align_frame_" + now.toString("HH-mm-ss") + ".fits"; 1766 QString filename = path + QStringLiteral("/") + name; 1767 if (m_ImageData) 1768 m_ImageData->saveImage(filename); 1769 } 1770 1771 startSolving(); 1772 } 1773 1774 void Align::setSolverAction(int mode) 1775 { 1776 gotoModeButtonGroup->button(mode)->setChecked(true); 1777 m_CurrentGotoMode = static_cast<GotoMode>(mode); 1778 } 1779 1780 void Align::startSolving() 1781 { 1782 //RUN_PAH(syncStage()); 1783 1784 // This is needed because they might have directories stored in the config file. 1785 // So we can't just use the options folder list. 1786 QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs(); 1787 disconnect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving); 1788 1789 m_UsedScale = false; 1790 m_UsedPosition = false; 1791 m_ScaleUsed = 0; 1792 m_RAUsed = 0; 1793 m_DECUsed = 0; 1794 1795 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL) 1796 { 1797 if(Options::solverType() != SSolver::SOLVER_ASTAP 1798 && Options::solverType() != SSolver::SOLVER_WATNEYASTROMETRY) //You don't need astrometry index files to use ASTAP or Watney 1799 { 1800 bool foundAnIndex = false; 1801 for(auto &dataDir : astrometryDataDirs) 1802 { 1803 QDir dir = QDir(dataDir); 1804 if(dir.exists()) 1805 { 1806 dir.setNameFilters(QStringList() << "*.fits"); 1807 QStringList indexList = dir.entryList(); 1808 if(indexList.count() > 0) 1809 foundAnIndex = true; 1810 } 1811 } 1812 if(!foundAnIndex) 1813 { 1814 appendLogText( 1815 i18n("No index files were found on your system in the specified index file directories. Please download some index files or add the correct directory to the list.")); 1816 KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings"); 1817 if(alignSettings && m_IndexFilesPage) 1818 { 1819 alignSettings->setCurrentPage(m_IndexFilesPage); 1820 alignSettings->show(); 1821 } 1822 } 1823 } 1824 if (m_StellarSolver->isRunning()) 1825 m_StellarSolver->abort(); 1826 if (!m_ImageData) 1827 m_ImageData = m_AlignView->imageData(); 1828 m_StellarSolver->loadNewImageBuffer(m_ImageData->getStatistics(), m_ImageData->getImageBuffer()); 1829 m_StellarSolver->setProperty("ProcessType", SSolver::SOLVE); 1830 m_StellarSolver->setProperty("ExtractorType", Options::solveSextractorType()); 1831 m_StellarSolver->setProperty("SolverType", Options::solverType()); 1832 connect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete); 1833 m_StellarSolver->setIndexFolderPaths(Options::astrometryIndexFolderList()); 1834 1835 auto params = m_StellarSolverProfiles.at(Options::solveOptionsProfile()); 1836 params.partition = Options::stellarSolverPartition(); 1837 m_StellarSolver->setParameters(params); 1838 1839 const SSolver::SolverType type = static_cast<SSolver::SolverType>(m_StellarSolver->property("SolverType").toInt()); 1840 if(type == SSolver::SOLVER_LOCALASTROMETRY || type == SSolver::SOLVER_ASTAP || type == SSolver::SOLVER_WATNEYASTROMETRY) 1841 { 1842 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove( 1843 QRegularExpression("[-{}]"))); 1844 m_AlignView->saveImage(filename); 1845 m_StellarSolver->setProperty("FileToProcess", filename); 1846 ExternalProgramPaths externalPaths; 1847 externalPaths.sextractorBinaryPath = Options::sextractorBinary(); 1848 externalPaths.solverPath = Options::astrometrySolverBinary(); 1849 externalPaths.astapBinaryPath = Options::aSTAPExecutable(); 1850 externalPaths.watneyBinaryPath = Options::watneyBinary(); 1851 externalPaths.wcsPath = Options::astrometryWCSInfo(); 1852 m_StellarSolver->setExternalFilePaths(externalPaths); 1853 1854 //No need for a conf file this way. 1855 m_StellarSolver->setProperty("AutoGenerateAstroConfig", true); 1856 } 1857 1858 if(type == SSolver::SOLVER_ONLINEASTROMETRY ) 1859 { 1860 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove( 1861 QRegularExpression("[-{}]"))); 1862 m_AlignView->saveImage(filename); 1863 1864 m_StellarSolver->setProperty("FileToProcess", filename); 1865 m_StellarSolver->setProperty("AstrometryAPIKey", Options::astrometryAPIKey()); 1866 m_StellarSolver->setProperty("AstrometryAPIURL", Options::astrometryAPIURL()); 1867 } 1868 1869 bool useImageScale = Options::astrometryUseImageScale(); 1870 if (useBlindScale == BLIND_ENGAGNED) 1871 { 1872 useImageScale = false; 1873 useBlindScale = BLIND_USED; 1874 appendLogText(i18n("Solving with blind image scale...")); 1875 } 1876 1877 bool useImagePosition = Options::astrometryUsePosition(); 1878 if (useBlindPosition == BLIND_ENGAGNED) 1879 { 1880 useImagePosition = false; 1881 useBlindPosition = BLIND_USED; 1882 appendLogText(i18n("Solving with blind image position...")); 1883 } 1884 1885 if (m_SolveFromFile) 1886 { 1887 FITSImage::Solution solution; 1888 m_ImageData->parseSolution(solution); 1889 1890 if (useImageScale && solution.pixscale > 0) 1891 { 1892 m_UsedScale = true; 1893 m_ScaleUsed = solution.pixscale; 1894 m_StellarSolver->setSearchScale(solution.pixscale * 0.8, 1895 solution.pixscale * 1.2, 1896 SSolver::ARCSEC_PER_PIX); 1897 } 1898 else 1899 m_StellarSolver->setProperty("UseScale", false); 1900 1901 if (useImagePosition && solution.ra > 0) 1902 { 1903 m_UsedPosition = true; 1904 m_RAUsed = solution.ra; 1905 m_DECUsed = solution.dec; 1906 m_StellarSolver->setSearchPositionInDegrees(solution.ra, solution.dec); 1907 } 1908 else 1909 m_StellarSolver->setProperty("UsePosition", false); 1910 1911 QVariant value = ""; 1912 if (!m_ImageData->getRecordValue("PIERSIDE", value)) 1913 { 1914 appendLogText(i18n("Loaded image does not have pierside information")); 1915 m_TargetPierside = ISD::Mount::PIER_UNKNOWN; 1916 } 1917 else 1918 { 1919 appendLogText(i18n("Loaded image was taken on pierside %1", value.toString())); 1920 (value == "WEST") ? m_TargetPierside = ISD::Mount::PIER_WEST : m_TargetPierside = ISD::Mount::PIER_EAST; 1921 } 1922 RotatorUtils::Instance()->Instance()->setImagePierside(m_TargetPierside); 1923 } 1924 else 1925 { 1926 //Setting the initial search scale settings 1927 if (useImageScale) 1928 { 1929 m_UsedScale = true; 1930 m_ScaleUsed = Options::astrometryImageScaleLow(); 1931 1932 SSolver::ScaleUnits units = static_cast<SSolver::ScaleUnits>(Options::astrometryImageScaleUnits()); 1933 // Extend search scale from 80% to 120% 1934 m_StellarSolver->setSearchScale(Options::astrometryImageScaleLow() * 0.8, 1935 Options::astrometryImageScaleHigh() * 1.2, 1936 units); 1937 } 1938 else 1939 m_StellarSolver->setProperty("UseScale", false); 1940 //Setting the initial search location settings 1941 if(useImagePosition) 1942 { 1943 m_StellarSolver->setSearchPositionInDegrees(m_TelescopeCoord.ra().Degrees(), m_TelescopeCoord.dec().Degrees()); 1944 m_UsedPosition = true; 1945 m_RAUsed = m_TelescopeCoord.ra().Degrees(); 1946 m_DECUsed = m_TelescopeCoord.dec().Degrees(); 1947 } 1948 else 1949 m_StellarSolver->setProperty("UsePosition", false); 1950 } 1951 1952 if(Options::alignmentLogging()) 1953 { 1954 // Not trusting SSolver logging right now (Hy Aug 1, 2022) 1955 // m_StellarSolver->setLogLevel(static_cast<SSolver::logging_level>(Options::loggerLevel())); 1956 // m_StellarSolver->setSSLogLevel(SSolver::LOG_NORMAL); 1957 m_StellarSolver->setLogLevel(SSolver::LOG_NONE); 1958 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF); 1959 if(Options::astrometryLogToFile()) 1960 { 1961 m_StellarSolver->setProperty("LogToFile", true); 1962 m_StellarSolver->setProperty("LogFileName", Options::astrometryLogFilepath()); 1963 } 1964 } 1965 else 1966 { 1967 m_StellarSolver->setLogLevel(SSolver::LOG_NONE); 1968 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF); 1969 } 1970 1971 SolverUtils::patchMultiAlgorithm(m_StellarSolver.get()); 1972 1973 // Start solving process 1974 m_StellarSolver->start(); 1975 } 1976 else 1977 { 1978 if (m_ImageData.isNull()) 1979 m_ImageData = m_AlignView->imageData(); 1980 // This should run only for load&slew. For regular solve, we don't get here 1981 // as the image is read and solved server-side. 1982 remoteParser->startSolver(m_ImageData->filename(), generateRemoteArgs(m_ImageData), false); 1983 } 1984 1985 // In these cases, the error box is not used, and we don't want it polluted 1986 // from some previous operation. 1987 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) || 1988 matchPAHStage(PAA::PAH_SECOND_CAPTURE) || 1989 matchPAHStage(PAA::PAH_THIRD_CAPTURE) || 1990 matchPAHStage(PAA::PAH_FIRST_SOLVE) || 1991 matchPAHStage(PAA::PAH_SECOND_SOLVE) || 1992 matchPAHStage(PAA::PAH_THIRD_SOLVE) || 1993 nothingR->isChecked() || 1994 syncR->isChecked()) 1995 errOut->clear(); 1996 1997 // Kick off timer 1998 solverTimer.start(); 1999 2000 setState(ALIGN_PROGRESS); 2001 emit newStatus(state); 2002 } 2003 2004 void Align::solverComplete() 2005 { 2006 disconnect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete); 2007 if(!m_StellarSolver->solvingDone() || m_StellarSolver->failed()) 2008 { 2009 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) || 2010 matchPAHStage(PAA::PAH_SECOND_CAPTURE) || 2011 matchPAHStage(PAA::PAH_THIRD_CAPTURE) || 2012 matchPAHStage(PAA::PAH_FIRST_SOLVE) || 2013 matchPAHStage(PAA::PAH_SECOND_SOLVE) || 2014 matchPAHStage(PAA::PAH_THIRD_SOLVE)) 2015 { 2016 if (CHECK_PAH(processSolverFailure())) 2017 return; 2018 else 2019 setState(ALIGN_ABORTED); 2020 } 2021 solverFailed(); 2022 return; 2023 } 2024 else 2025 { 2026 FITSImage::Solution solution = m_StellarSolver->getSolution(); 2027 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true; 2028 solverFinished(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight); 2029 } 2030 } 2031 2032 void Align::solverFinished(double orientation, double ra, double dec, double pixscale, bool eastToTheRight) 2033 { 2034 pi->stopAnimation(); 2035 stopB->setEnabled(false); 2036 solveB->setEnabled(true); 2037 2038 sOrientation = orientation; 2039 sRA = ra; 2040 sDEC = dec; 2041 2042 double elapsed = solverTimer.elapsed() / 1000.0; 2043 if (elapsed > 0) 2044 appendLogText(i18n("Solver completed after %1 seconds.", QString::number(elapsed, 'f', 2))); 2045 2046 m_AlignTimer.stop(); 2047 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice && remoteParser.get()) 2048 { 2049 // Disable remote parse 2050 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(false); 2051 } 2052 2053 int binx, biny; 2054 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD); 2055 targetChip->getBinning(&binx, &biny); 2056 2057 if (Options::alignmentLogging()) 2058 { 2059 QString parityString = eastToTheRight ? "neg" : "pos"; 2060 appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4) Parity (%5)", QString::number(ra, 'f', 5), 2061 QString::number(dec, 'f', 5), QString::number(orientation, 'f', 5), 2062 QString::number(pixscale, 'f', 5), parityString)); 2063 } 2064 2065 // When solving (without Load&Slew), update effective FOV and focal length accordingly. 2066 if (!m_SolveFromFile && 2067 (isEqual(m_FOVWidth, 0) || m_EffectiveFOVPending || std::fabs(pixscale - m_FOVPixelScale) > 0.005) && 2068 pixscale > 0) 2069 { 2070 double newFOVW = m_CameraWidth * pixscale / binx / 60.0; 2071 double newFOVH = m_CameraHeight * pixscale / biny / 60.0; 2072 2073 calculateEffectiveFocalLength(newFOVW); 2074 saveNewEffectiveFOV(newFOVW, newFOVH); 2075 2076 m_EffectiveFOVPending = false; 2077 } 2078 2079 m_AlignCoord.setRA0(ra / 15.0); 2080 m_AlignCoord.setDec0(dec); 2081 2082 // Convert to JNow 2083 m_AlignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd()); 2084 // Get horizontal coords 2085 m_AlignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); 2086 2087 // Do not update diff if we are performing load & slew. 2088 if (!m_SolveFromFile) 2089 { 2090 pixScaleOut->setText(QString::number(pixscale, 'f', 2)); 2091 calculateAlignTargetDiff(); 2092 } 2093 2094 // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack. 2095 // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA 2096 // PA = Orientation + 180 2097 double solverPA = KSUtils::rotationToPositionAngle(orientation); 2098 solverFOV->setCenter(m_AlignCoord); 2099 solverFOV->setPA(solverPA); 2100 solverFOV->setImageDisplay(Options::astrometrySolverOverlay()); 2101 // Sensor FOV as well 2102 sensorFOV->setPA(solverPA); 2103 2104 PAOut->setText(QString::number(solverPA, 'f', 2)); // Two decimals are reasonable 2105 2106 QString ra_dms, dec_dms; 2107 getFormattedCoords(m_AlignCoord.ra().Hours(), m_AlignCoord.dec().Degrees(), ra_dms, dec_dms); 2108 2109 SolverRAOut->setText(ra_dms); 2110 SolverDecOut->setText(dec_dms); 2111 2112 if (Options::astrometrySolverWCS()) 2113 { 2114 auto ccdRotation = m_Camera->getNumber("CCD_ROTATION"); 2115 if (ccdRotation) 2116 { 2117 auto rotation = ccdRotation->findWidgetByName("CCD_ROTATION_VALUE"); 2118 if (rotation) 2119 { 2120 auto clientManager = m_Camera->getDriverInfo()->getClientManager(); 2121 rotation->setValue(orientation); 2122 clientManager->sendNewProperty(ccdRotation); 2123 2124 if (m_wcsSynced == false) 2125 { 2126 appendLogText( 2127 i18n("WCS information updated. Images captured from this point forward shall have valid WCS.")); 2128 2129 // Just send telescope info in case the CCD driver did not pick up before. 2130 auto telescopeInfo = m_Mount->getNumber("TELESCOPE_INFO"); 2131 if (telescopeInfo) 2132 clientManager->sendNewProperty(telescopeInfo); 2133 2134 m_wcsSynced = true; 2135 } 2136 } 2137 } 2138 } 2139 2140 m_CaptureErrorCounter = 0; 2141 m_SlewErrorCounter = 0; 2142 m_CaptureTimeoutCounter = 0; 2143 2144 appendLogText( 2145 i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4) Target Coordinates: RA (%5) DEC (%6)", 2146 m_AlignCoord.ra().toHMSString(), 2147 m_AlignCoord.dec().toDMSString(), 2148 m_TelescopeCoord.ra().toHMSString(), 2149 m_TelescopeCoord.dec().toDMSString(), 2150 m_TargetCoord.ra().toHMSString(), 2151 m_TargetCoord.dec().toDMSString())); 2152 2153 if (!m_SolveFromFile && m_CurrentGotoMode == GOTO_SLEW) 2154 { 2155 dms diffDeg(m_TargetDiffTotal / 3600.0); 2156 appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString())); 2157 } 2158 2159 if (rememberUploadMode != m_Camera->getUploadMode()) 2160 m_Camera->setUploadMode(rememberUploadMode); 2161 2162 // Remember to reset fast exposure 2163 if (m_RememberCameraFastExposure) 2164 { 2165 m_RememberCameraFastExposure = false; 2166 m_Camera->setFastExposureEnabled(true); 2167 } 2168 2169 //This block of code along with some sections in the switch below will set the status report in the solution table for this item. 2170 std::unique_ptr<QTableWidgetItem> statusReport(new QTableWidgetItem()); 2171 int currentRow = solutionTable->rowCount() - 1; 2172 if (!m_SolveFromFile) // [Capture & Solve] 2173 { 2174 stopProgressAnimation(); 2175 solutionTable->setCellWidget(currentRow, 3, new QWidget()); 2176 statusReport->setFlags(Qt::ItemIsSelectable); 2177 // Calibration: determine camera offset from capture 2178 if (m_Rotator != nullptr && m_Rotator->isConnected()) 2179 { 2180 if (auto absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE")) 2181 // if (absAngle && std::isnan(m_TargetPositionAngle) == true) 2182 { 2183 sRawAngle = absAngle->at(0)->getValue(); 2184 double OffsetAngle = RotatorUtils::Instance()->calcOffsetAngle(sRawAngle, solverPA); 2185 RotatorUtils::Instance()->updateOffset(OffsetAngle); 2186 // Debug info 2187 auto reverseStatus = "Unknown"; 2188 auto reverseProperty = m_Rotator->getSwitch("ROTATOR_REVERSE"); 2189 if (reverseProperty) 2190 { 2191 if (reverseProperty->at(0)->getState() == ISS_ON) 2192 reverseStatus = "Reversed Direction"; 2193 else 2194 reverseStatus = "Normal Direction"; 2195 } 2196 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << sRawAngle << "Rotator PA:" << solverPA 2197 << "Rotator Offset:" << OffsetAngle << "Direction:" << reverseStatus; 2198 // Flow is: newSolverResults() -> capture: setAlignresult() -> RotatorSettings: refresh() 2199 emit newSolverResults(solverPA, ra, dec, pixscale); 2200 // appendLogText(i18n("Camera offset angle is %1 degrees.", OffsetAngle)); 2201 appendLogText(i18n("Camera position angle is %1 degrees.", RotatorUtils::Instance()->calcCameraAngle(sRawAngle, false))); 2202 } 2203 } 2204 } 2205 2206 QJsonObject solution = 2207 { 2208 {"camera", m_Camera->getDeviceName()}, 2209 {"ra", SolverRAOut->text()}, 2210 {"de", SolverDecOut->text()}, 2211 {"dRA", m_TargetDiffRA}, 2212 {"dDE", m_TargetDiffDE}, 2213 {"targetDiff", m_TargetDiffTotal}, 2214 {"pix", pixscale}, 2215 {"PA", solverPA}, 2216 {"fov", FOVOut->text()}, 2217 }; 2218 emit newSolution(solution.toVariantMap()); 2219 2220 setState(ALIGN_SUCCESSFUL); 2221 emit newStatus(state); 2222 solverIterations = 0; 2223 KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"), 2224 KSNotification::Align); 2225 2226 switch (m_CurrentGotoMode) 2227 { 2228 case GOTO_SYNC: 2229 executeGOTO(); 2230 2231 if (!m_SolveFromFile) 2232 { 2233 stopProgressAnimation(); 2234 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); 2235 solutionTable->setItem(currentRow, 3, statusReport.release()); 2236 } 2237 2238 return; 2239 2240 case GOTO_SLEW: 2241 if (m_SolveFromFile || m_TargetDiffTotal > static_cast<double>(alignAccuracyThreshold->value())) 2242 { 2243 if (!m_SolveFromFile && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS) 2244 { 2245 appendLogText(i18n("Maximum number of iterations reached. Solver failed.")); 2246 2247 if (!m_SolveFromFile) 2248 { 2249 statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); 2250 solutionTable->setItem(currentRow, 3, statusReport.release()); 2251 } 2252 2253 solverFailed(); 2254 return; 2255 } 2256 2257 targetAccuracyNotMet = true; 2258 2259 if (!m_SolveFromFile) 2260 { 2261 stopProgressAnimation(); 2262 statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); 2263 solutionTable->setItem(currentRow, 3, statusReport.release()); 2264 } 2265 2266 executeGOTO(); 2267 return; 2268 } 2269 2270 stopProgressAnimation(); 2271 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); 2272 solutionTable->setItem(currentRow, 3, statusReport.release()); 2273 2274 appendLogText(i18n("Target is within acceptable range.")); 2275 break; 2276 2277 case GOTO_NOTHING: 2278 if (!m_SolveFromFile) 2279 { 2280 stopProgressAnimation(); 2281 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); 2282 solutionTable->setItem(currentRow, 3, statusReport.release()); 2283 } 2284 break; 2285 } 2286 2287 solverFOV->setProperty("visible", true); 2288 2289 if (!matchPAHStage(PAA::PAH_IDLE)) 2290 m_PolarAlignmentAssistant->processPAHStage(orientation, ra, dec, pixscale, eastToTheRight, 2291 m_StellarSolver->getSolutionHealpix(), 2292 m_StellarSolver->getSolutionIndexNumber()); 2293 else 2294 { 2295 2296 if (checkIfRotationRequired()) 2297 { 2298 solveB->setEnabled(false); 2299 loadSlewB->setEnabled(false); 2300 return; 2301 } 2302 2303 // We are done! 2304 setState(ALIGN_COMPLETE); 2305 emit newStatus(state); 2306 2307 solveB->setEnabled(true); 2308 loadSlewB->setEnabled(true); 2309 } 2310 } 2311 2312 void Align::solverFailed() 2313 { 2314 2315 // If failed-align logging is enabled, let's save the frame. 2316 if (Options::saveFailedAlignImages()) 2317 { 2318 QDir dir; 2319 QDateTime now = KStarsData::Instance()->lt(); 2320 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/failed"); 2321 dir.mkpath(path); 2322 QString extraFilenameInfo; 2323 if (m_UsedScale) 2324 extraFilenameInfo.append(QString("_s%1u%2").arg(m_ScaleUsed, 0, 'f', 3) 2325 .arg(Options::astrometryImageScaleUnits())); 2326 if (m_UsedPosition) 2327 extraFilenameInfo.append(QString("_r%1_d%2").arg(m_RAUsed, 0, 'f', 5).arg(m_DECUsed, 0, 'f', 5)); 2328 2329 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-' 2330 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts 2331 QString name = "failed_align_frame_" + now.toString("yyyy-MM-dd-HH-mm-ss") + extraFilenameInfo + ".fits"; 2332 QString filename = path + QStringLiteral("/") + name; 2333 if (m_ImageData) 2334 { 2335 m_ImageData->saveImage(filename); 2336 appendLogText(i18n("Saving failed solver image to %1", filename)); 2337 } 2338 2339 } 2340 if (state != ALIGN_ABORTED) 2341 { 2342 // Try to solve with scale turned off, if not turned off already 2343 if (Options::astrometryUseImageScale() && useBlindScale == BLIND_IDLE) 2344 { 2345 appendLogText(i18n("Solver failed. Retrying without scale constraint.")); 2346 useBlindScale = BLIND_ENGAGNED; 2347 setAlignTableResult(ALIGN_RESULT_FAILED); 2348 captureAndSolve(); 2349 return; 2350 } 2351 2352 // Try to solve with the position turned off, if not turned off already 2353 if (Options::astrometryUsePosition() && useBlindPosition == BLIND_IDLE) 2354 { 2355 appendLogText(i18n("Solver failed. Retrying without position constraint.")); 2356 useBlindPosition = BLIND_ENGAGNED; 2357 setAlignTableResult(ALIGN_RESULT_FAILED); 2358 captureAndSolve(); 2359 return; 2360 } 2361 2362 2363 appendLogText(i18n("Solver Failed.")); 2364 if(!Options::alignmentLogging()) 2365 appendLogText( 2366 i18n("Please check you have sufficient stars in the image, the indicated FOV is correct, and the necessary index files are installed. Enable Alignment Logging in Setup Tab -> Logs to get detailed information on the failure.")); 2367 2368 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), 2369 KSNotification::Align, KSNotification::Alert); 2370 } 2371 2372 pi->stopAnimation(); 2373 stopB->setEnabled(false); 2374 solveB->setEnabled(true); 2375 loadSlewB->setEnabled(true); 2376 2377 m_AlignTimer.stop(); 2378 2379 m_SolveFromFile = false; 2380 solverIterations = 0; 2381 m_CaptureErrorCounter = 0; 2382 m_CaptureTimeoutCounter = 0; 2383 m_SlewErrorCounter = 0; 2384 2385 setState(ALIGN_FAILED); 2386 emit newStatus(state); 2387 2388 solverFOV->setProperty("visible", false); 2389 2390 setAlignTableResult(ALIGN_RESULT_FAILED); 2391 } 2392 2393 bool Align::checkIfRotationRequired() 2394 { 2395 // Check if we need to perform any rotations. 2396 if (Options::astrometryUseRotator()) 2397 { 2398 if (m_SolveFromFile) // [Load & Slew] Program flow never lands here!? 2399 { 2400 m_TargetPositionAngle = solverFOV->PA(); 2401 // We are not done yet. 2402 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to:" << m_TargetPositionAngle; 2403 } 2404 else // [Capture & Solve]: "direct" or within [Load & Slew] 2405 { 2406 currentRotatorPA = solverFOV->PA(); 2407 if (std::isnan(m_TargetPositionAngle) == false) // [Load & Slew] only 2408 { 2409 // If image pierside versus mount pierside is different and policy is lenient ... 2410 if (RotatorUtils::Instance()->Instance()->checkImageFlip() && (Options::astrometryFlipRotationAllowed())) 2411 { 2412 // ... calculate "flipped" PA ... 2413 sRawAngle = RotatorUtils::Instance()->calcRotatorAngle(m_TargetPositionAngle); 2414 m_TargetPositionAngle = RotatorUtils::Instance()->calcCameraAngle(sRawAngle, true); 2415 RotatorUtils::Instance()->setImagePierside(ISD::Mount::PIER_UNKNOWN); // ... once! 2416 } 2417 // Match the position angle with rotator 2418 if (m_Rotator != nullptr && m_Rotator->isConnected()) 2419 { 2420 if(fabs(KSUtils::rangePA(currentRotatorPA - m_TargetPositionAngle)) * 60 > 2421 Options::astrometryRotatorThreshold()) 2422 { 2423 // Signal flow: newSolverResults() -> capture: setAlignresult() -> RS: refresh() 2424 emit newSolverResults(m_TargetPositionAngle, 0, 0, 0); 2425 appendLogText(i18n("Setting camera position angle to %1 degrees ...", m_TargetPositionAngle)); 2426 setState(ALIGN_ROTATING); 2427 emit newStatus(state); // Evoke 'updateProperty()' (where the same check is executed again) 2428 return true; 2429 } 2430 else 2431 { 2432 appendLogText(i18n("Camera position angle is within acceptable range.")); 2433 // We're done! (Opposed to 'updateProperty()') 2434 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN(); 2435 } 2436 } 2437 // Match the position angle manually 2438 else 2439 { 2440 double current = currentRotatorPA; 2441 double target = m_TargetPositionAngle; 2442 2443 double diff = KSUtils::rangePA(current - target); 2444 double threshold = Options::astrometryRotatorThreshold() / 60.0; 2445 2446 appendLogText(i18n("Current PA is %1; Target PA is %2; diff: %3", current, target, diff)); 2447 2448 emit manualRotatorChanged(current, target, threshold); 2449 2450 m_ManualRotator->setRotatorDiff(current, target, diff); 2451 if (fabs(diff) > threshold) 2452 { 2453 targetAccuracyNotMet = true; 2454 m_ManualRotator->show(); 2455 m_ManualRotator->raise(); 2456 setState(ALIGN_ROTATING); 2457 emit newStatus(state); 2458 return true; 2459 } 2460 else 2461 { 2462 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN(); 2463 targetAccuracyNotMet = false; 2464 } 2465 } 2466 } 2467 } 2468 } 2469 return false; 2470 } 2471 2472 void Align::stop(Ekos::AlignState mode) 2473 { 2474 m_CaptureTimer.stop(); 2475 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL) 2476 m_StellarSolver->abort(); 2477 else if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser) 2478 remoteParser->stopSolver(); 2479 //parser->stopSolver(); 2480 pi->stopAnimation(); 2481 stopB->setEnabled(false); 2482 solveB->setEnabled(true); 2483 loadSlewB->setEnabled(true); 2484 2485 m_SolveFromFile = false; 2486 solverIterations = 0; 2487 m_CaptureErrorCounter = 0; 2488 m_CaptureTimeoutCounter = 0; 2489 m_SlewErrorCounter = 0; 2490 m_AlignTimer.stop(); 2491 2492 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData); 2493 disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress); 2494 2495 if (rememberUploadMode != m_Camera->getUploadMode()) 2496 m_Camera->setUploadMode(rememberUploadMode); 2497 2498 // Remember to reset fast exposure 2499 if (m_RememberCameraFastExposure) 2500 { 2501 m_RememberCameraFastExposure = false; 2502 m_Camera->setFastExposureEnabled(true); 2503 } 2504 2505 auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD); 2506 2507 // If capture is still in progress, let's stop that. 2508 if (matchPAHStage(PAA::PAH_POST_REFRESH)) 2509 { 2510 if (targetChip->isCapturing()) 2511 targetChip->abortExposure(); 2512 2513 appendLogText(i18n("Refresh is complete.")); 2514 } 2515 else 2516 { 2517 if (targetChip->isCapturing()) 2518 { 2519 targetChip->abortExposure(); 2520 appendLogText(i18n("Capture aborted.")); 2521 } 2522 else 2523 { 2524 double elapsed = solverTimer.elapsed() / 1000.0; 2525 if (elapsed > 0) 2526 appendLogText(i18n("Solver aborted after %1 seconds.", QString::number(elapsed, 'f', 2))); 2527 } 2528 } 2529 2530 setState(mode); 2531 emit newStatus(state); 2532 2533 setAlignTableResult(ALIGN_RESULT_FAILED); 2534 } 2535 2536 QProgressIndicator * Align::getProgressStatus() 2537 { 2538 int currentRow = solutionTable->rowCount() - 1; 2539 2540 // check if the current row indicates a progress state 2541 // 1. no row present 2542 if (currentRow < 0) 2543 return nullptr; 2544 // 2. indicator is not present or not a progress indicator 2545 QWidget *indicator = solutionTable->cellWidget(currentRow, 3); 2546 if (indicator == nullptr) 2547 return nullptr; 2548 return dynamic_cast<QProgressIndicator *>(indicator); 2549 } 2550 2551 void Align::stopProgressAnimation() 2552 { 2553 QProgressIndicator *progress_indicator = getProgressStatus(); 2554 if (progress_indicator != nullptr) 2555 progress_indicator->stopAnimation(); 2556 } 2557 2558 QList<double> Align::getSolutionResult() 2559 { 2560 QList<double> result; 2561 2562 result << sOrientation << sRA << sDEC; 2563 2564 return result; 2565 } 2566 2567 void Align::appendLogText(const QString &text) 2568 { 2569 m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", 2570 KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); 2571 2572 qCInfo(KSTARS_EKOS_ALIGN) << text; 2573 2574 emit newLog(text); 2575 } 2576 2577 void Align::clearLog() 2578 { 2579 m_LogText.clear(); 2580 emit newLog(QString()); 2581 } 2582 2583 void Align::updateProperty(INDI::Property prop) 2584 { 2585 if (prop.isNameMatch("EQUATORIAL_EOD_COORD") || prop.isNameMatch("EQUATORIAL_COORD")) 2586 { 2587 auto nvp = prop.getNumber(); 2588 QString ra_dms, dec_dms; 2589 2590 getFormattedCoords(m_TelescopeCoord.ra().Hours(), m_TelescopeCoord.dec().Degrees(), ra_dms, dec_dms); 2591 2592 ScopeRAOut->setText(ra_dms); 2593 ScopeDecOut->setText(dec_dms); 2594 2595 switch (nvp->s) 2596 { 2597 // Idle --> Mount not tracking or slewing 2598 case IPS_IDLE: 2599 m_wasSlewStarted = false; 2600 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE"; 2601 break; 2602 2603 // Ok --> Mount Tracking. If m_wasSlewStarted is true 2604 // then it just finished slewing 2605 case IPS_OK: 2606 { 2607 // Update the boxes as the mount just finished slewing 2608 if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition()) 2609 { 2610 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position..."; 2611 opsAstrometry->estRA->setText(ra_dms); 2612 opsAstrometry->estDec->setText(dec_dms); 2613 2614 Options::setAstrometryPositionRA(nvp->np[0].value * 15); 2615 Options::setAstrometryPositionDE(nvp->np[1].value); 2616 2617 //generateArgs(); 2618 } 2619 2620 // If dome is syncing, wait until it stops 2621 if (m_Dome && m_Dome->isMoving()) 2622 { 2623 domeReady = false; 2624 return; 2625 } 2626 2627 // If we are looking for celestial pole 2628 if (m_wasSlewStarted && matchPAHStage(PAA::PAH_FIND_CP)) 2629 { 2630 //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE"; 2631 m_wasSlewStarted = false; 2632 appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify.")); 2633 setSolverAction(GOTO_NOTHING); 2634 2635 m_PolarAlignmentAssistant->setPAHStage(PAA::PAH_FIRST_CAPTURE); 2636 return; 2637 } 2638 2639 switch (state) 2640 { 2641 case ALIGN_PROGRESS: 2642 break; 2643 2644 case ALIGN_SYNCING: 2645 { 2646 m_wasSlewStarted = false; 2647 //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE"; 2648 if (m_CurrentGotoMode == GOTO_SLEW) 2649 { 2650 Slew(); 2651 return; 2652 } 2653 else 2654 { 2655 appendLogText(i18n("Mount is synced to solution coordinates.")); 2656 2657 /* if (checkIfRotationRequired()) 2658 return; */ 2659 2660 KSNotification::event(QLatin1String("AlignSuccessful"), 2661 i18n("Astrometry alignment completed successfully"), KSNotification::Align); 2662 setState(ALIGN_COMPLETE); 2663 emit newStatus(state); 2664 solverIterations = 0; 2665 loadSlewB->setEnabled(true); 2666 } 2667 } 2668 break; 2669 2670 case ALIGN_SLEWING: 2671 2672 if (!didSlewStart()) 2673 { 2674 // If mount has not started slewing yet, then skip 2675 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet..."; 2676 break; 2677 } 2678 2679 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed."; 2680 m_wasSlewStarted = false; 2681 if (m_SolveFromFile) 2682 { 2683 m_SolveFromFile = false; 2684 m_TargetPositionAngle = solverFOV->PA(); 2685 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to" << m_TargetPositionAngle; 2686 2687 setState(ALIGN_PROGRESS); 2688 emit newStatus(state); 2689 2690 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY) 2691 appendLogText(i18n("Settling...")); 2692 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()' 2693 m_CaptureTimer.start(alignSettlingTime->value()); 2694 return; 2695 } 2696 else if (m_CurrentGotoMode == GOTO_SLEW || (m_MountModel && m_MountModel->isRunning())) 2697 { 2698 if (targetAccuracyNotMet) 2699 appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again...")); 2700 else 2701 appendLogText(i18n("Slew complete. Solving Alignment Point. . .")); 2702 2703 targetAccuracyNotMet = false; 2704 2705 setState(ALIGN_PROGRESS); 2706 emit newStatus(state); 2707 2708 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY) 2709 appendLogText(i18n("Settling...")); 2710 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()' 2711 m_CaptureTimer.start(alignSettlingTime->value()); 2712 } 2713 break; 2714 2715 default: 2716 { 2717 //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE"; 2718 m_wasSlewStarted = false; 2719 } 2720 break; 2721 } 2722 } 2723 break; 2724 2725 // Busy --> Mount Slewing or Moving (NSWE buttons) 2726 case IPS_BUSY: 2727 { 2728 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew running."; 2729 m_wasSlewStarted = true; 2730 2731 handleMountMotion(); 2732 } 2733 break; 2734 2735 // Alert --> Mount has problem moving or communicating. 2736 case IPS_ALERT: 2737 { 2738 //qCDebug(KSTARS_EKOS_ALIGN) << "IPS_ALERT --> setting slewStarted to FALSE"; 2739 m_wasSlewStarted = false; 2740 2741 if (state == ALIGN_SYNCING || state == ALIGN_SLEWING) 2742 { 2743 if (state == ALIGN_SYNCING) 2744 appendLogText(i18n("Syncing failed.")); 2745 else 2746 appendLogText(i18n("Slewing failed.")); 2747 2748 if (++m_SlewErrorCounter == 3) 2749 { 2750 abort(); 2751 return; 2752 } 2753 else 2754 { 2755 if (m_CurrentGotoMode == GOTO_SLEW) 2756 Slew(); 2757 else 2758 Sync(); 2759 } 2760 } 2761 2762 return; 2763 } 2764 } 2765 2766 RUN_PAH(processMountRotation(m_TelescopeCoord.ra(), alignSettlingTime->value())); 2767 } 2768 else if (prop.isNameMatch("ABS_ROTATOR_ANGLE")) 2769 { 2770 auto nvp = prop.getNumber(); 2771 double RAngle = nvp->np[0].value; 2772 currentRotatorPA = RotatorUtils::Instance()->calcCameraAngle(RAngle, false); 2773 /*QString logtext = "Alignstate: " + QString::number(state) 2774 + " IPSstate: " + QString::number(nvp->s) 2775 + " Raw Rotator Angle:" + QString::number(nvp->np[0].value) 2776 + " Current PA:" + QString::number(currentRotatorPA) 2777 + " Target PA:" + QString::number(m_TargetPositionAngle) 2778 + " Offset:" + QString::number(Options::pAOffset()); 2779 appendLogText(logtext);*/ 2780 // loadSlewTarget defined if activation through [Load & Slew] and rotator just reached position 2781 if (std::isnan(m_TargetPositionAngle) == false && state == ALIGN_ROTATING && nvp->s == IPS_OK) 2782 { 2783 auto diff = fabs(RotatorUtils::Instance()->DiffPA(currentRotatorPA - m_TargetPositionAngle)) * 60; 2784 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << RAngle << "Current PA:" << currentRotatorPA 2785 << "Target PA:" << m_TargetPositionAngle << "Diff (arcmin):" << diff << "Offset:" 2786 << Options::pAOffset(); 2787 2788 if (diff <= Options::astrometryRotatorThreshold()) 2789 { 2790 appendLogText(i18n("Rotator reached camera position angle.")); 2791 // Check angle once again (no slew -> no settle time) 2792 // QTimer::singleShot(alignSettlingTime->value(), this, &Ekos::Align::executeGOTO); 2793 executeGOTO(); 2794 } 2795 else 2796 { 2797 // Sometimes update of "nvp->s" is a bit slow, so check state again, if it's really ok 2798 QTimer::singleShot(300, [ = ] 2799 { 2800 if (nvp->s == IPS_OK) 2801 { 2802 appendLogText(i18n("Rotator failed to arrive at the requested position angle (Deviation %1 arcmin).", diff)); 2803 setState(ALIGN_FAILED); 2804 emit newStatus(state); 2805 solveB->setEnabled(true); 2806 loadSlewB->setEnabled(true); 2807 } 2808 }); 2809 } 2810 } 2811 else if (m_estimateRotatorTimeFrame) // Estimate time frame during first timeout 2812 { 2813 m_RotatorTimeFrame = RotatorUtils::Instance()->calcTimeFrame(RAngle); 2814 } 2815 } 2816 else if (prop.isNameMatch("DOME_MOTION")) 2817 { 2818 // If dome is not ready and state is now 2819 if (domeReady == false && prop.getState() == IPS_OK) 2820 { 2821 domeReady = true; 2822 // trigger process number for mount so that it proceeds with normal workflow since 2823 // it was stopped by dome not being ready 2824 handleMountStatus(); 2825 } 2826 } 2827 else if (prop.isNameMatch("TELESCOPE_MOTION_NS") || prop.isNameMatch("TELESCOPE_MOTION_WE")) 2828 { 2829 switch (prop.getState()) 2830 { 2831 case IPS_BUSY: 2832 // react upon mount motion 2833 handleMountMotion(); 2834 m_wasSlewStarted = true; 2835 break; 2836 default: 2837 qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished."; 2838 handleMountStatus(); 2839 break; 2840 } 2841 } 2842 } 2843 2844 void Align::handleMountMotion() 2845 { 2846 if (state == ALIGN_PROGRESS) 2847 { 2848 if (matchPAHStage(PAA::PAH_IDLE)) 2849 { 2850 // whoops, mount slews during alignment 2851 appendLogText(i18n("Slew detected, suspend solving...")); 2852 suspend(); 2853 } 2854 2855 setState(ALIGN_SLEWING); 2856 } 2857 } 2858 2859 void Align::handleMountStatus() 2860 { 2861 auto nvp = m_Mount->getNumber(m_Mount->isJ2000() ? "EQUATORIAL_COORD" : 2862 "EQUATORIAL_EOD_COORD"); 2863 2864 if (nvp) 2865 updateProperty(nvp); 2866 } 2867 2868 2869 void Align::executeGOTO() 2870 { 2871 if (m_SolveFromFile) 2872 { 2873 m_TargetCoord = m_AlignCoord; 2874 2875 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file. Setting Target Coordinates align coords. RA:" 2876 << m_TargetCoord.ra().toHMSString() 2877 << "DE:" << m_TargetCoord.dec().toDMSString(); 2878 2879 SlewToTarget(); 2880 } 2881 else if (m_CurrentGotoMode == GOTO_SYNC) 2882 Sync(); 2883 else if (m_CurrentGotoMode == GOTO_SLEW) 2884 SlewToTarget(); 2885 } 2886 2887 void Align::Sync() 2888 { 2889 setState(ALIGN_SYNCING); 2890 2891 if (m_Mount->Sync(&m_AlignCoord)) 2892 { 2893 emit newStatus(state); 2894 appendLogText( 2895 i18n("Syncing to RA (%1) DEC (%2)", m_AlignCoord.ra().toHMSString(), m_AlignCoord.dec().toDMSString())); 2896 } 2897 else 2898 { 2899 setState(ALIGN_IDLE); 2900 emit newStatus(state); 2901 appendLogText(i18n("Syncing failed.")); 2902 } 2903 } 2904 2905 void Align::Slew() 2906 { 2907 setState(ALIGN_SLEWING); 2908 emit newStatus(state); 2909 2910 //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted; 2911 //m_wasSlewStarted = currentTelescope->Slew(&m_targetCoord); 2912 //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted; 2913 2914 // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered 2915 // from Goto 2916 if (m_Mount->Slew(&m_TargetCoord)) 2917 { 2918 slewStartTimer.start(); 2919 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", m_TargetCoord.ra().toHMSString(), 2920 m_TargetCoord.dec().toDMSString())); 2921 } 2922 else // inform user about failure 2923 { 2924 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2) is rejected. (see notification)", 2925 m_TargetCoord.ra().toHMSString(), 2926 m_TargetCoord.dec().toDMSString())); 2927 setState(ALIGN_FAILED); 2928 emit newStatus(state); 2929 solveB->setEnabled(true); 2930 loadSlewB->setEnabled(true); 2931 } 2932 } 2933 2934 void Align::SlewToTarget() 2935 { 2936 if (canSync && !m_SolveFromFile) 2937 { 2938 // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided 2939 #if 0 2940 if (Ekos::Manager::Instance()->getCurrentJobName().isEmpty()) 2941 { 2942 KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"), 2943 i18n("Ekos job (%1) - Telescope synced", 2944 Ekos::Manager::Instance()->getCurrentJobName())); 2945 } 2946 #endif 2947 2948 // Do we perform a regular sync or use differential slewing? 2949 if (Options::astrometryDifferentialSlewing()) 2950 { 2951 dms m_TargetDiffRA = m_AlignCoord.ra().deltaAngle(m_TargetCoord.ra()); 2952 dms m_TargetDiffDE = m_AlignCoord.dec().deltaAngle(m_TargetCoord.dec()); 2953 m_TargetCoord.setRA(m_TargetCoord.ra() - m_TargetDiffRA); 2954 m_TargetCoord.setDec(m_TargetCoord.dec() - m_TargetDiffDE); 2955 qCDebug(KSTARS_EKOS_ALIGN) << "Using differential slewing. Setting Target Coordinates to RA:" 2956 << m_TargetCoord.ra().toHMSString() 2957 << "DE:" << m_TargetCoord.dec().toDMSString(); 2958 Slew(); 2959 } 2960 else 2961 Sync(); 2962 return; 2963 } 2964 Slew(); 2965 } 2966 2967 2968 void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str) 2969 { 2970 dms ra_s, dec_s; 2971 ra_s.setH(ra); 2972 dec_s.setD(dec); 2973 2974 ra_str = QString("%1:%2:%3") 2975 .arg(ra_s.hour(), 2, 10, QChar('0')) 2976 .arg(ra_s.minute(), 2, 10, QChar('0')) 2977 .arg(ra_s.second(), 2, 10, QChar('0')); 2978 if (dec_s.Degrees() < 0) 2979 dec_str = QString("-%1:%2:%3") 2980 .arg(abs(dec_s.degree()), 2, 10, QChar('0')) 2981 .arg(abs(dec_s.arcmin()), 2, 10, QChar('0')) 2982 .arg(dec_s.arcsec(), 2, 10, QChar('0')); 2983 else 2984 dec_str = QString("%1:%2:%3") 2985 .arg(dec_s.degree(), 2, 10, QChar('0')) 2986 .arg(dec_s.arcmin(), 2, 10, QChar('0')) 2987 .arg(dec_s.arcsec(), 2, 10, QChar('0')); 2988 } 2989 2990 bool Align::loadAndSlew(QString fileURL) 2991 { 2992 if (fileURL.isEmpty()) 2993 fileURL = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Load Image"), dirPath, 2994 "Images (*.fits *.fits.fz *.fit *.fts *.xisf " 2995 "*.jpg *.jpeg *.png *.gif *.bmp " 2996 "*.cr2 *.cr3 *.crw *.nef *.raf *.dng *.arw *.orf)"); 2997 2998 if (fileURL.isEmpty()) 2999 return false; 3000 3001 QFileInfo fileInfo(fileURL); 3002 if (fileInfo.exists() == false) 3003 return false; 3004 3005 dirPath = fileInfo.absolutePath(); 3006 3007 differentialSlewingActivated = false; 3008 3009 m_SolveFromFile = true; 3010 3011 if (m_PolarAlignmentAssistant) 3012 m_PolarAlignmentAssistant->stopPAHProcess(); 3013 3014 slewR->setChecked(true); 3015 m_CurrentGotoMode = GOTO_SLEW; 3016 3017 solveB->setEnabled(false); 3018 loadSlewB->setEnabled(false); 3019 stopB->setEnabled(true); 3020 pi->startAnimation(); 3021 3022 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice == nullptr) 3023 { 3024 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver.")); 3025 setSolverMode(SOLVER_LOCAL); 3026 } 3027 3028 m_ImageData.clear(); 3029 3030 m_AlignView->loadFile(fileURL); 3031 //m_FileToSolve = fileURL; 3032 connect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving); 3033 3034 return true; 3035 } 3036 3037 bool Align::loadAndSlew(const QByteArray &image, const QString &extension) 3038 { 3039 differentialSlewingActivated = false; 3040 m_SolveFromFile = true; 3041 RUN_PAH(stopPAHProcess()); 3042 slewR->setChecked(true); 3043 m_CurrentGotoMode = GOTO_SLEW; 3044 solveB->setEnabled(false); 3045 loadSlewB->setEnabled(false); 3046 stopB->setEnabled(true); 3047 pi->startAnimation(); 3048 3049 // Must clear image data so we are forced to read the 3050 // image data again from align view when solving begins. 3051 m_ImageData.clear(); 3052 QSharedPointer<FITSData> data; 3053 data.reset(new FITSData(), &QObject::deleteLater); 3054 data->loadFromBuffer(image, extension); 3055 m_AlignView->loadData(data); 3056 startSolving(); 3057 return true; 3058 } 3059 3060 void Align::setExposure(double value) 3061 { 3062 alignExposure->setValue(value); 3063 } 3064 3065 void Align::setBinningIndex(int binIndex) 3066 { 3067 // If sender is not our combo box, then we need to update the combobox itself 3068 if (dynamic_cast<QComboBox *>(sender()) != alignBinning) 3069 { 3070 alignBinning->blockSignals(true); 3071 alignBinning->setCurrentIndex(binIndex); 3072 alignBinning->blockSignals(false); 3073 } 3074 3075 // Need to calculate FOV and args for APP 3076 if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX) 3077 calculateFOV(); 3078 } 3079 3080 bool Align::setFilterWheel(ISD::FilterWheel * device) 3081 { 3082 if (m_FilterWheel && m_FilterWheel == device) 3083 { 3084 checkFilter(); 3085 return false; 3086 } 3087 3088 if (m_FilterWheel) 3089 m_FilterWheel->disconnect(this); 3090 3091 m_FilterWheel = device; 3092 3093 if (m_FilterWheel) 3094 { 3095 connect(m_FilterWheel, &ISD::ConcreteDevice::Connected, this, [this]() 3096 { 3097 FilterPosLabel->setEnabled(true); 3098 alignFilter->setEnabled(true); 3099 }); 3100 connect(m_FilterWheel, &ISD::ConcreteDevice::Disconnected, this, [this]() 3101 { 3102 FilterPosLabel->setEnabled(false); 3103 alignFilter->setEnabled(false); 3104 }); 3105 } 3106 3107 auto isConnected = m_FilterWheel && m_FilterWheel->isConnected(); 3108 FilterPosLabel->setEnabled(isConnected); 3109 alignFilter->setEnabled(isConnected); 3110 3111 checkFilter(); 3112 return true; 3113 } 3114 3115 QString Align::filterWheel() 3116 { 3117 if (m_FilterWheel) 3118 return m_FilterWheel->getDeviceName(); 3119 3120 return QString(); 3121 } 3122 3123 bool Align::setFilter(const QString &filter) 3124 { 3125 if (m_FilterWheel) 3126 { 3127 alignFilter->setCurrentText(filter); 3128 return true; 3129 } 3130 3131 return false; 3132 } 3133 3134 3135 QString Align::filter() 3136 { 3137 return alignFilter->currentText(); 3138 } 3139 3140 void Align::checkFilter() 3141 { 3142 alignFilter->clear(); 3143 3144 if (!m_FilterWheel) 3145 { 3146 FilterPosLabel->setEnabled(false); 3147 alignFilter->setEnabled(false); 3148 return; 3149 } 3150 3151 auto isConnected = m_FilterWheel->isConnected(); 3152 FilterPosLabel->setEnabled(isConnected); 3153 alignFilter->setEnabled(alignUseCurrentFilter->isChecked() == false); 3154 3155 setupFilterManager(); 3156 3157 alignFilter->addItems(m_FilterManager->getFilterLabels()); 3158 currentFilterPosition = m_FilterManager->getFilterPosition(); 3159 3160 if (alignUseCurrentFilter->isChecked()) 3161 { 3162 // use currently selected filter 3163 alignFilter->setCurrentIndex(currentFilterPosition - 1); 3164 } 3165 else 3166 { 3167 // use the fixed filter 3168 auto filter = m_Settings["alignFilter"]; 3169 if (filter.isValid()) 3170 alignFilter->setCurrentText(filter.toString()); 3171 } 3172 } 3173 3174 void Align::setRotator(ISD::Rotator * Device) 3175 { 3176 if ((Manager::Instance()->existRotatorController()) && (!m_Rotator || !(Device == m_Rotator))) 3177 { 3178 rotatorB->setEnabled(false); 3179 if (m_Rotator) 3180 { 3181 m_Rotator->disconnect(this); 3182 m_RotatorControlPanel->close(); 3183 } 3184 m_Rotator = Device; 3185 if (m_Rotator) 3186 { 3187 if (Manager::Instance()->getRotatorController(m_Rotator->getDeviceName(), m_RotatorControlPanel)) 3188 { 3189 connect(m_Rotator, &ISD::Rotator::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection); 3190 connect(rotatorB, &QPushButton::clicked, this, [this]() 3191 { 3192 m_RotatorControlPanel->show(); 3193 m_RotatorControlPanel->raise(); 3194 }); 3195 rotatorB->setEnabled(true); 3196 } 3197 } 3198 } 3199 } 3200 3201 void Align::setWCSEnabled(bool enable) 3202 { 3203 if (!m_Camera) 3204 return; 3205 3206 auto wcsControl = m_Camera->getSwitch("WCS_CONTROL"); 3207 3208 if (!wcsControl) 3209 return; 3210 3211 auto wcs_enable = wcsControl->findWidgetByName("WCS_ENABLE"); 3212 auto wcs_disable = wcsControl->findWidgetByName("WCS_DISABLE"); 3213 3214 if (!wcs_enable || !wcs_disable) 3215 return; 3216 3217 if ((wcs_enable->getState() == ISS_ON && enable) || (wcs_disable->getState() == ISS_ON && !enable)) 3218 return; 3219 3220 wcsControl->reset(); 3221 if (enable) 3222 { 3223 appendLogText(i18n("World Coordinate System (WCS) is enabled.")); 3224 wcs_enable->setState(ISS_ON); 3225 } 3226 else 3227 { 3228 appendLogText(i18n("World Coordinate System (WCS) is disabled.")); 3229 wcs_disable->setState(ISS_ON); 3230 m_wcsSynced = false; 3231 } 3232 3233 auto clientManager = m_Camera->getDriverInfo()->getClientManager(); 3234 if (clientManager) 3235 clientManager->sendNewProperty(wcsControl); 3236 } 3237 3238 void Align::checkCameraExposureProgress(ISD::CameraChip * targetChip, double remaining, IPState state) 3239 { 3240 INDI_UNUSED(targetChip); 3241 INDI_UNUSED(remaining); 3242 3243 if (state == IPS_ALERT) 3244 { 3245 if (++m_CaptureErrorCounter == 3 && !matchPAHStage(PolarAlignmentAssistant::PAH_REFRESH)) 3246 { 3247 appendLogText(i18n("Capture error. Aborting...")); 3248 abort(); 3249 return; 3250 } 3251 3252 appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter)); 3253 setAlignTableResult(ALIGN_RESULT_FAILED); 3254 captureAndSolve(); 3255 } 3256 } 3257 3258 void Align::setAlignTableResult(AlignResult result) 3259 { 3260 // Do nothing if the progress indicator is not running. 3261 // This is necessary since it could happen that a problem occurs 3262 // before #captureAndSolve() has been started and there does not 3263 // exist a table entry for the current run. 3264 QProgressIndicator *progress_indicator = getProgressStatus(); 3265 if (progress_indicator == nullptr || ! progress_indicator->isAnimated()) 3266 return; 3267 stopProgressAnimation(); 3268 3269 QIcon icon; 3270 switch (result) 3271 { 3272 case ALIGN_RESULT_SUCCESS: 3273 icon = QIcon(":/icons/AlignSuccess.svg"); 3274 break; 3275 3276 case ALIGN_RESULT_WARNING: 3277 icon = QIcon(":/icons/AlignWarning.svg"); 3278 break; 3279 3280 case ALIGN_RESULT_FAILED: 3281 default: 3282 icon = QIcon(":/icons/AlignFailure.svg"); 3283 break; 3284 } 3285 int currentRow = solutionTable->rowCount() - 1; 3286 solutionTable->setCellWidget(currentRow, 3, new QWidget()); 3287 QTableWidgetItem *statusReport = new QTableWidgetItem(); 3288 statusReport->setIcon(icon); 3289 statusReport->setFlags(Qt::ItemIsSelectable); 3290 solutionTable->setItem(currentRow, 3, statusReport); 3291 } 3292 3293 void Align::setFocusStatus(Ekos::FocusState state) 3294 { 3295 m_FocusState = state; 3296 } 3297 3298 uint8_t Align::getSolverDownsample(uint16_t binnedW) 3299 { 3300 uint8_t downsample = Options::astrometryDownsample(); 3301 3302 if (!Options::astrometryAutoDownsample()) 3303 return downsample; 3304 3305 while (downsample < 8) 3306 { 3307 if (binnedW / downsample <= 1024) 3308 break; 3309 3310 downsample += 2; 3311 } 3312 3313 return downsample; 3314 } 3315 3316 void Align::setCaptureStatus(CaptureState newState) 3317 { 3318 switch (newState) 3319 { 3320 case CAPTURE_ALIGNING: 3321 if (m_Mount && m_Mount->hasAlignmentModel() && Options::resetMountModelAfterMeridian()) 3322 { 3323 qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (m_Mount->clearAlignmentModel() ? 3324 "successful." : "failed."); 3325 } 3326 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY) 3327 appendLogText(i18n("Settling...")); 3328 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()' 3329 m_CaptureTimer.start(alignSettlingTime->value()); 3330 break; 3331 // Is this needed anymore with new flip policy? (sb 2023-10-20) 3332 // On meridian flip, reset Target Position Angle to fully rotated value 3333 // expected after MF so that we do not end up with reversed camera rotation 3334 case CAPTURE_MERIDIAN_FLIP: 3335 if (std::isnan(m_TargetPositionAngle) == false) 3336 m_TargetPositionAngle = KSUtils::rangePA(m_TargetPositionAngle + 180.0); 3337 break; 3338 default: 3339 break; 3340 } 3341 3342 m_CaptureState = newState; 3343 } 3344 3345 void Align::showFITSViewer() 3346 { 3347 static int lastFVTabID = -1; 3348 if (m_ImageData) 3349 { 3350 QUrl url = QUrl::fromLocalFile("align.fits"); 3351 if (fv.isNull()) 3352 { 3353 fv = KStars::Instance()->createFITSViewer(); 3354 fv->loadData(m_ImageData, url, &lastFVTabID); 3355 connect(fv.get(), &FITSViewer::terminated, this, [this]() 3356 { 3357 fv.clear(); 3358 }); 3359 } 3360 else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false) 3361 fv->loadData(m_ImageData, url, &lastFVTabID); 3362 3363 fv->show(); 3364 } 3365 } 3366 3367 void Align::toggleAlignWidgetFullScreen() 3368 { 3369 if (alignWidget->parent() == nullptr) 3370 { 3371 alignWidget->setParent(this); 3372 rightLayout->insertWidget(0, alignWidget); 3373 alignWidget->showNormal(); 3374 } 3375 else 3376 { 3377 alignWidget->setParent(nullptr); 3378 alignWidget->setWindowTitle(i18nc("@title:window", "Align Frame")); 3379 alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); 3380 alignWidget->showMaximized(); 3381 alignWidget->show(); 3382 } 3383 } 3384 3385 void Align::setMountStatus(ISD::Mount::Status newState) 3386 { 3387 switch (newState) 3388 { 3389 case ISD::Mount::MOUNT_PARKED: 3390 solveB->setEnabled(false); 3391 loadSlewB->setEnabled(false); 3392 break; 3393 case ISD::Mount::MOUNT_IDLE: 3394 solveB->setEnabled(true); 3395 loadSlewB->setEnabled(true); 3396 break; 3397 case ISD::Mount::MOUNT_PARKING: 3398 solveB->setEnabled(false); 3399 loadSlewB->setEnabled(false); 3400 break; 3401 case ISD::Mount::MOUNT_SLEWING: 3402 case ISD::Mount::MOUNT_MOVING: 3403 solveB->setEnabled(false); 3404 loadSlewB->setEnabled(false); 3405 break; 3406 3407 default: 3408 if (state != ALIGN_PROGRESS) 3409 { 3410 solveB->setEnabled(true); 3411 if (matchPAHStage(PAA::PAH_IDLE)) 3412 { 3413 loadSlewB->setEnabled(true); 3414 } 3415 } 3416 break; 3417 } 3418 3419 RUN_PAH(setMountStatus(newState)); 3420 } 3421 3422 void Align::setAstrometryDevice(const QSharedPointer<ISD::GenericDevice> &device) 3423 { 3424 m_RemoteParserDevice = device; 3425 3426 remoteSolverR->setEnabled(true); 3427 if (remoteParser.get() != nullptr) 3428 { 3429 remoteParser->setAstrometryDevice(m_RemoteParserDevice); 3430 connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); 3431 connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); 3432 } 3433 } 3434 3435 void Align::refreshAlignOptions() 3436 { 3437 solverFOV->setImageDisplay(Options::astrometrySolverWCS()); 3438 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000); 3439 if (m_Rotator) 3440 m_RotatorControlPanel->updateFlipPolicy(Options::astrometryFlipRotationAllowed()); 3441 } 3442 3443 void Align::setupOptions() 3444 { 3445 KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self()); 3446 3447 #ifdef Q_OS_OSX 3448 dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); 3449 #endif 3450 3451 opsAlign = new OpsAlign(this); 3452 connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions); 3453 KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("StellarSolver Options")); 3454 page->setIcon(QIcon(":/icons/StellarSolverIcon.png")); 3455 // connect(rotatorB, &QPushButton::clicked, dialog, &KConfigDialog::show); 3456 3457 opsPrograms = new OpsPrograms(this); 3458 page = dialog->addPage(opsPrograms, i18n("External & Online Programs")); 3459 page->setIcon(QIcon(":/icons/astrometry.svg")); 3460 3461 opsAstrometry = new OpsAstrometry(this); 3462 page = dialog->addPage(opsAstrometry, i18n("Scale & Position")); 3463 page->setIcon(QIcon(":/icons/center_telescope_red.svg")); 3464 3465 optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::AlignProfiles, dialog); 3466 page = dialog->addPage(optionsProfileEditor, i18n("Align Options Profiles Editor")); 3467 connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, [this]() 3468 { 3469 if(QFile(savedOptionsProfiles).exists()) 3470 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles); 3471 else 3472 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles(); 3473 opsAlign->reloadOptionsProfiles(); 3474 }); 3475 page->setIcon(QIcon::fromTheme("configure")); 3476 3477 connect(opsAlign, &OpsAlign::needToLoadProfile, this, [this, dialog, page](const QString & profile) 3478 { 3479 optionsProfileEditor->loadProfile(profile); 3480 dialog->setCurrentPage(page); 3481 }); 3482 3483 opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this); 3484 m_IndexFilesPage = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files")); 3485 m_IndexFilesPage->setIcon(QIcon::fromTheme("map-flat")); 3486 } 3487 3488 void Align::setupSolutionTable() 3489 { 3490 solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); 3491 3492 clearAllSolutionsB->setIcon( 3493 QIcon::fromTheme("application-exit")); 3494 clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 3495 3496 removeSolutionB->setIcon(QIcon::fromTheme("list-remove")); 3497 removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 3498 3499 exportSolutionsCSV->setIcon( 3500 QIcon::fromTheme("document-save-as")); 3501 exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect); 3502 3503 autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best")); 3504 autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 3505 3506 connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints); 3507 connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint); 3508 connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints); 3509 connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph); 3510 connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel); 3511 connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow); 3512 } 3513 3514 void Align::setupPlot() 3515 { 3516 double accuracyRadius = alignAccuracyThreshold->value(); 3517 3518 alignPlot->setBackground(QBrush(Qt::black)); 3519 alignPlot->setSelectionTolerance(10); 3520 3521 alignPlot->xAxis->setBasePen(QPen(Qt::white, 1)); 3522 alignPlot->yAxis->setBasePen(QPen(Qt::white, 1)); 3523 3524 alignPlot->xAxis->setTickPen(QPen(Qt::white, 1)); 3525 alignPlot->yAxis->setTickPen(QPen(Qt::white, 1)); 3526 3527 alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); 3528 alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); 3529 3530 alignPlot->xAxis->setTickLabelColor(Qt::white); 3531 alignPlot->yAxis->setTickLabelColor(Qt::white); 3532 3533 alignPlot->xAxis->setLabelColor(Qt::white); 3534 alignPlot->yAxis->setLabelColor(Qt::white); 3535 3536 alignPlot->xAxis->setLabelFont(QFont(font().family(), 10)); 3537 alignPlot->yAxis->setLabelFont(QFont(font().family(), 10)); 3538 3539 alignPlot->xAxis->setLabelPadding(2); 3540 alignPlot->yAxis->setLabelPadding(2); 3541 3542 alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); 3543 alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); 3544 alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); 3545 alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); 3546 alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow)); 3547 alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow)); 3548 3549 alignPlot->xAxis->setLabel(i18n("dRA (arcsec)")); 3550 alignPlot->yAxis->setLabel(i18n("dDE (arcsec)")); 3551 3552 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); 3553 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); 3554 3555 alignPlot->setInteractions(QCP::iRangeZoom); 3556 alignPlot->setInteraction(QCP::iRangeDrag, true); 3557 3558 alignPlot->addGraph(); 3559 alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone); 3560 alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15)); 3561 3562 buildTarget(); 3563 3564 connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip); 3565 connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange); 3566 connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange); 3567 3568 alignPlot->resize(190, 190); 3569 alignPlot->replot(); 3570 } 3571 3572 void Align::setupFilterManager() 3573 { 3574 // Do we have an existing filter manager? 3575 if (m_FilterManager) 3576 m_FilterManager->disconnect(this); 3577 3578 // Create new or refresh device 3579 Ekos::Manager::Instance()->createFilterManager(m_FilterWheel); 3580 3581 // Return global filter manager for this filter wheel. 3582 Ekos::Manager::Instance()->getFilterManager(m_FilterWheel->getDeviceName(), m_FilterManager); 3583 3584 connect(m_FilterManager.get(), &FilterManager::ready, this, [this]() 3585 { 3586 if (filterPositionPending) 3587 { 3588 m_FocusState = FOCUS_IDLE; 3589 filterPositionPending = false; 3590 captureAndSolve(); 3591 } 3592 }); 3593 3594 connect(m_FilterManager.get(), &FilterManager::failed, this, [this]() 3595 { 3596 appendLogText(i18n("Filter operation failed.")); 3597 abort(); 3598 } 3599 ); 3600 3601 connect(m_FilterManager.get(), &FilterManager::newStatus, this, [this](Ekos::FilterState filterState) 3602 { 3603 if (filterPositionPending) 3604 { 3605 switch (filterState) 3606 { 3607 case FILTER_OFFSET: 3608 appendLogText(i18n("Changing focus offset by %1 steps...", m_FilterManager->getTargetFilterOffset())); 3609 break; 3610 3611 case FILTER_CHANGE: 3612 { 3613 const int filterComboIndex = m_FilterManager->getTargetFilterPosition() - 1; 3614 if (filterComboIndex >= 0 && filterComboIndex < alignFilter->count()) 3615 appendLogText(i18n("Changing filter to %1...", alignFilter->itemText(filterComboIndex))); 3616 } 3617 break; 3618 3619 case FILTER_AUTOFOCUS: 3620 appendLogText(i18n("Auto focus on filter change...")); 3621 break; 3622 3623 default: 3624 break; 3625 } 3626 } 3627 }); 3628 3629 connect(m_FilterManager.get(), &FilterManager::labelsChanged, this, &Align::checkFilter); 3630 connect(m_FilterManager.get(), &FilterManager::positionChanged, this, &Align::checkFilter); 3631 } 3632 3633 QVariantMap Align::getEffectiveFOV() 3634 { 3635 KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs); 3636 3637 m_FOVWidth = m_FOVHeight = 0; 3638 3639 for (auto &map : effectiveFOVs) 3640 { 3641 if (map["Profile"].toString() == m_ActiveProfile->name && map["Train"].toString() == opticalTrain()) 3642 { 3643 if (isEqual(map["Width"].toInt(), m_CameraWidth) && 3644 isEqual(map["Height"].toInt(), m_CameraHeight) && 3645 isEqual(map["PixelW"].toDouble(), m_CameraPixelWidth) && 3646 isEqual(map["PixelH"].toDouble(), m_CameraPixelHeight) && 3647 isEqual(map["FocalLength"].toDouble(), m_FocalLength) && 3648 isEqual(map["FocalRedcuer"].toDouble(), m_Reducer) && 3649 isEqual(map["FocalRatio"].toDouble(), m_FocalRatio)) 3650 { 3651 m_FOVWidth = map["FovW"].toDouble(); 3652 m_FOVHeight = map["FovH"].toDouble(); 3653 return map; 3654 } 3655 } 3656 } 3657 3658 return QVariantMap(); 3659 } 3660 3661 void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH) 3662 { 3663 if (newFOVW < 0 || newFOVH < 0 || (isEqual(newFOVW, m_FOVWidth) && isEqual(newFOVH, m_FOVHeight))) 3664 return; 3665 3666 QVariantMap effectiveMap = getEffectiveFOV(); 3667 3668 // If ID exists, delete it first. 3669 if (effectiveMap.isEmpty() == false) 3670 KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString()); 3671 3672 // If FOV is 0x0, then we just remove existing effective FOV 3673 if (newFOVW == 0.0 && newFOVH == 0.0) 3674 { 3675 calculateFOV(); 3676 return; 3677 } 3678 3679 effectiveMap["Profile"] = m_ActiveProfile->name; 3680 effectiveMap["Train"] = opticalTrainCombo->currentText(); 3681 effectiveMap["Width"] = m_CameraWidth; 3682 effectiveMap["Height"] = m_CameraHeight; 3683 effectiveMap["PixelW"] = m_CameraPixelWidth; 3684 effectiveMap["PixelH"] = m_CameraPixelHeight; 3685 effectiveMap["FocalLength"] = m_FocalLength; 3686 effectiveMap["FocalReducer"] = m_Reducer; 3687 effectiveMap["FocalRatio"] = m_FocalRatio; 3688 effectiveMap["FovW"] = newFOVW; 3689 effectiveMap["FovH"] = newFOVH; 3690 3691 KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap); 3692 3693 calculateFOV(); 3694 3695 } 3696 3697 void Align::zoomAlignView() 3698 { 3699 m_AlignView->ZoomDefault(); 3700 3701 // Frame update is not immediate to reduce too many refreshes 3702 // So emit updated frame in 500ms 3703 QTimer::singleShot(500, this, [this]() 3704 { 3705 emit newFrame(m_AlignView); 3706 }); 3707 } 3708 3709 void Align::setAlignZoom(double scale) 3710 { 3711 if (scale > 1) 3712 m_AlignView->ZoomIn(); 3713 else if (scale < 1) 3714 m_AlignView->ZoomOut(); 3715 3716 // Frame update is not immediate to reduce too many refreshes 3717 // So emit updated frame in 500ms 3718 QTimer::singleShot(500, this, [this]() 3719 { 3720 emit newFrame(m_AlignView); 3721 }); 3722 } 3723 3724 void Align::syncFOV() 3725 { 3726 QString newFOV = FOVOut->text(); 3727 QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)"); 3728 QRegularExpressionMatch match = re.match(newFOV); 3729 if (match.hasMatch()) 3730 { 3731 double newFOVW = match.captured(1).toDouble(); 3732 double newFOVH = match.captured(2).toDouble(); 3733 3734 //if (newFOVW > 0 && newFOVH > 0) 3735 saveNewEffectiveFOV(newFOVW, newFOVH); 3736 3737 FOVOut->setStyleSheet(QString()); 3738 } 3739 else 3740 { 3741 KSNotification::error(i18n("Invalid FOV.")); 3742 FOVOut->setStyleSheet("background-color:red"); 3743 } 3744 } 3745 3746 // m_wasSlewStarted can't be false for more than 10s after a slew starts. 3747 bool Align::didSlewStart() 3748 { 3749 if (m_wasSlewStarted) 3750 return true; 3751 if (slewStartTimer.isValid() && slewStartTimer.elapsed() > MAX_WAIT_FOR_SLEW_START_MSEC) 3752 { 3753 qCDebug(KSTARS_EKOS_ALIGN) << "Slew timed out...waited > 10s, it must have started already."; 3754 return true; 3755 } 3756 return false; 3757 } 3758 3759 void Align::setTargetCoords(double ra0, double de0) 3760 { 3761 SkyPoint target; 3762 target.setRA0(ra0); 3763 target.setDec0(de0); 3764 target.updateCoordsNow(KStarsData::Instance()->updateNum()); 3765 setTarget(target); 3766 } 3767 3768 void Align::setTarget(const SkyPoint &targetCoord) 3769 { 3770 m_TargetCoord = targetCoord; 3771 qCInfo(KSTARS_EKOS_ALIGN) << "Target coordinates updated to JNow RA:" << m_TargetCoord.ra().toHMSString() 3772 << "DE:" << m_TargetCoord.dec().toDMSString(); 3773 } 3774 3775 QList<double> Align::getTargetCoords() 3776 { 3777 return QList<double>() << m_TargetCoord.ra0().Hours() << m_TargetCoord.dec0().Degrees(); 3778 } 3779 3780 void Align::setTargetPositionAngle(double value) 3781 { 3782 m_TargetPositionAngle = value; 3783 qCDebug(KSTARS_EKOS_ALIGN) << "Target PA updated to: " << m_TargetPositionAngle; 3784 } 3785 3786 void Align::calculateAlignTargetDiff() 3787 { 3788 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) || 3789 matchPAHStage(PAA::PAH_SECOND_CAPTURE) || 3790 matchPAHStage(PAA::PAH_THIRD_CAPTURE) || 3791 matchPAHStage(PAA::PAH_FIRST_SOLVE) || 3792 matchPAHStage(PAA::PAH_SECOND_SOLVE) || 3793 matchPAHStage(PAA::PAH_THIRD_SOLVE) || 3794 nothingR->isChecked() || 3795 syncR->isChecked()) 3796 return; 3797 m_TargetDiffRA = (m_AlignCoord.ra().deltaAngle(m_TargetCoord.ra())).Degrees() * 3600; 3798 m_TargetDiffDE = (m_AlignCoord.dec().deltaAngle(m_TargetCoord.dec())).Degrees() * 3600; 3799 3800 dms RADiff(fabs(m_TargetDiffRA) / 3600.0), DEDiff(m_TargetDiffDE / 3600.0); 3801 3802 m_TargetDiffTotal = sqrt(m_TargetDiffRA * m_TargetDiffRA + m_TargetDiffDE * m_TargetDiffDE); 3803 3804 errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg( 3805 QString::number(m_TargetDiffTotal, 'f', 0), 3806 QString::number(m_TargetDiffRA, 'f', 0), 3807 QString::number(m_TargetDiffDE, 'f', 0))); 3808 if (m_TargetDiffTotal <= static_cast<double>(alignAccuracyThreshold->value())) 3809 errOut->setStyleSheet("color:green"); 3810 else if (m_TargetDiffTotal < 1.5 * alignAccuracyThreshold->value()) 3811 errOut->setStyleSheet("color:yellow"); 3812 else 3813 errOut->setStyleSheet("color:red"); 3814 3815 //This block of code will write the result into the solution table and plot it on the graph. 3816 int currentRow = solutionTable->rowCount() - 1; 3817 QTableWidgetItem *dRAReport = new QTableWidgetItem(); 3818 if (dRAReport) 3819 { 3820 dRAReport->setText(QString::number(m_TargetDiffRA, 'f', 3) + "\""); 3821 dRAReport->setTextAlignment(Qt::AlignHCenter); 3822 dRAReport->setFlags(Qt::ItemIsSelectable); 3823 solutionTable->setItem(currentRow, 4, dRAReport); 3824 } 3825 3826 QTableWidgetItem *dDECReport = new QTableWidgetItem(); 3827 if (dDECReport) 3828 { 3829 dDECReport->setText(QString::number(m_TargetDiffDE, 'f', 3) + "\""); 3830 dDECReport->setTextAlignment(Qt::AlignHCenter); 3831 dDECReport->setFlags(Qt::ItemIsSelectable); 3832 solutionTable->setItem(currentRow, 5, dDECReport); 3833 } 3834 3835 double raPlot = m_TargetDiffRA; 3836 double decPlot = m_TargetDiffDE; 3837 alignPlot->graph(0)->addData(raPlot, decPlot); 3838 3839 QCPItemText *textLabel = new QCPItemText(alignPlot); 3840 textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter); 3841 3842 textLabel->position->setType(QCPItemPosition::ptPlotCoords); 3843 textLabel->position->setCoords(raPlot, decPlot); 3844 textLabel->setColor(Qt::red); 3845 textLabel->setPadding(QMargins(0, 0, 0, 0)); 3846 textLabel->setBrush(Qt::white); 3847 textLabel->setPen(Qt::NoPen); 3848 textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' '); 3849 textLabel->setFont(QFont(font().family(), 8)); 3850 3851 if (!alignPlot->xAxis->range().contains(m_TargetDiffRA)) 3852 { 3853 alignPlot->graph(0)->rescaleKeyAxis(true); 3854 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0); 3855 } 3856 if (!alignPlot->yAxis->range().contains(m_TargetDiffDE)) 3857 { 3858 alignPlot->graph(0)->rescaleValueAxis(true); 3859 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); 3860 } 3861 alignPlot->replot(); 3862 } 3863 3864 QStringList Align::getStellarSolverProfiles() 3865 { 3866 QStringList profiles; 3867 for (auto ¶m : m_StellarSolverProfiles) 3868 profiles << param.listName; 3869 3870 return profiles; 3871 } 3872 3873 void Align::exportSolutionPoints() 3874 { 3875 if (solutionTable->rowCount() == 0) 3876 return; 3877 3878 QUrl exportFile = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Export Solution Points"), 3879 alignURLPath, 3880 "CSV File (*.csv)"); 3881 if (exportFile.isEmpty()) // if user presses cancel 3882 return; 3883 if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false) 3884 exportFile.setPath(exportFile.toLocalFile() + ".csv"); 3885 3886 QString path = exportFile.toLocalFile(); 3887 3888 if (QFile::exists(path)) 3889 { 3890 int r = KMessageBox::warningContinueCancel(nullptr, 3891 i18n("A file named \"%1\" already exists. " 3892 "Overwrite it?", 3893 exportFile.fileName()), 3894 i18n("Overwrite File?"), KStandardGuiItem::overwrite()); 3895 if (r == KMessageBox::Cancel) 3896 return; 3897 } 3898 3899 if (!exportFile.isValid()) 3900 { 3901 QString message = i18n("Invalid URL: %1", exportFile.url()); 3902 KSNotification::sorry(message, i18n("Invalid URL")); 3903 return; 3904 } 3905 3906 QFile file; 3907 file.setFileName(path); 3908 if (!file.open(QIODevice::WriteOnly)) 3909 { 3910 QString message = i18n("Unable to write to file %1", path); 3911 KSNotification::sorry(message, i18n("Could Not Open File")); 3912 return; 3913 } 3914 3915 QTextStream outstream(&file); 3916 3917 QString epoch = QString::number(KStarsDateTime::currentDateTime().epoch()); 3918 3919 outstream << "RA (J" << epoch << "),DE (J" << epoch 3920 << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << Qt::endl; 3921 3922 for (int i = 0; i < solutionTable->rowCount(); i++) 3923 { 3924 QTableWidgetItem *raCell = solutionTable->item(i, 0); 3925 QTableWidgetItem *deCell = solutionTable->item(i, 1); 3926 QTableWidgetItem *objNameCell = solutionTable->item(i, 2); 3927 QTableWidgetItem *raErrorCell = solutionTable->item(i, 4); 3928 QTableWidgetItem *deErrorCell = solutionTable->item(i, 5); 3929 3930 if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell) 3931 { 3932 KSNotification::sorry(i18n("Error in table structure.")); 3933 return; 3934 } 3935 dms raDMS = dms::fromString(raCell->text(), false); 3936 dms deDMS = dms::fromString(deCell->text(), true); 3937 outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ',' 3938 << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ',' 3939 << deErrorCell->text().remove('\"') << Qt::endl; 3940 } 3941 emit newLog(i18n("Solution Points Saved as: %1", path)); 3942 file.close(); 3943 } 3944 3945 void Align::setupPolarAlignmentAssistant() 3946 { 3947 // Create PAA instance 3948 m_PolarAlignmentAssistant = new PolarAlignmentAssistant(this, m_AlignView); 3949 connect(m_PolarAlignmentAssistant, &Ekos::PAA::captureAndSolve, this, &Ekos::Align::captureAndSolve); 3950 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newAlignTableResult, this, &Ekos::Align::setAlignTableResult); 3951 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newFrame, this, &Ekos::Align::newFrame); 3952 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newPAHStage, this, &Ekos::Align::processPAHStage); 3953 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newLog, this, &Ekos::Align::appendLogText); 3954 3955 tabWidget->addTab(m_PolarAlignmentAssistant, i18n("Polar Alignment")); 3956 } 3957 3958 void Align::setupManualRotator() 3959 { 3960 if (m_ManualRotator) 3961 return; 3962 3963 m_ManualRotator = new ManualRotator(this); 3964 connect(m_ManualRotator, &Ekos::ManualRotator::captureAndSolve, this, &Ekos::Align::captureAndSolve); 3965 // If user cancel manual rotator, reset load slew target PA, otherwise it will keep popping up 3966 // for any subsequent solves. 3967 connect(m_ManualRotator, &Ekos::ManualRotator::rejected, this, [this]() 3968 { 3969 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN(); 3970 stop(ALIGN_IDLE); 3971 }); 3972 } 3973 3974 void Align::setuptDarkProcessor() 3975 { 3976 if (m_DarkProcessor) 3977 return; 3978 3979 m_DarkProcessor = new DarkProcessor(this); 3980 connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Align::appendLogText); 3981 connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed) 3982 { 3983 alignDarkFrame->setChecked(completed); 3984 m_AlignView->setProperty("suspended", false); 3985 if (completed) 3986 { 3987 m_AlignView->rescale(ZOOM_KEEP_LEVEL); 3988 m_AlignView->updateFrame(); 3989 } 3990 setCaptureComplete(); 3991 }); 3992 } 3993 3994 void Align::processPAHStage(int stage) 3995 { 3996 switch (stage) 3997 { 3998 case PAA::PAH_IDLE: 3999 // Abort any solver that might be running. 4000 // Assumes this state change won't happen randomly (e.g. in the middle of align). 4001 // Alternatively could just let the stellarsolver finish naturally. 4002 if (m_StellarSolver && m_StellarSolver->isRunning()) 4003 m_StellarSolver->abort(); 4004 break; 4005 case PAA::PAH_POST_REFRESH: 4006 { 4007 Options::setAstrometrySolverWCS(rememberSolverWCS); 4008 Options::setAutoWCS(rememberAutoWCS); 4009 stop(ALIGN_IDLE); 4010 } 4011 break; 4012 4013 case PAA::PAH_FIRST_CAPTURE: 4014 nothingR->setChecked(true); 4015 m_CurrentGotoMode = GOTO_NOTHING; 4016 loadSlewB->setEnabled(false); 4017 4018 rememberSolverWCS = Options::astrometrySolverWCS(); 4019 rememberAutoWCS = Options::autoWCS(); 4020 4021 Options::setAutoWCS(false); 4022 Options::setAstrometrySolverWCS(true); 4023 break; 4024 case PAA::PAH_SECOND_CAPTURE: 4025 case PAA::PAH_THIRD_CAPTURE: 4026 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY) 4027 emit newLog(i18n("Settling...")); 4028 m_CaptureTimer.start(alignSettlingTime->value()); 4029 break; 4030 4031 default: 4032 break; 4033 } 4034 4035 emit newPAAStage(stage); 4036 } 4037 4038 bool Align::matchPAHStage(uint32_t stage) 4039 { 4040 return m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->getPAHStage() == stage; 4041 } 4042 4043 void Align::toggleManualRotator(bool toggled) 4044 { 4045 if (toggled) 4046 { 4047 m_ManualRotator->show(); 4048 m_ManualRotator->raise(); 4049 } 4050 else 4051 m_ManualRotator->close(); 4052 } 4053 4054 void Align::setupOpticalTrainManager() 4055 { 4056 connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &Align::refreshOpticalTrain); 4057 connect(trainB, &QPushButton::clicked, this, [this]() 4058 { 4059 OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText()); 4060 }); 4061 connect(opticalTrainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) 4062 { 4063 ProfileSettings::Instance()->setOneSetting(ProfileSettings::AlignOpticalTrain, 4064 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index))); 4065 refreshOpticalTrain(); 4066 emit trainChanged(); 4067 }); 4068 } 4069 4070 void Align::refreshOpticalTrain() 4071 { 4072 opticalTrainCombo->blockSignals(true); 4073 opticalTrainCombo->clear(); 4074 opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames()); 4075 trainB->setEnabled(true); 4076 4077 QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::AlignOpticalTrain); 4078 4079 if (trainID.isValid()) 4080 { 4081 auto id = trainID.toUInt(); 4082 4083 // If train not found, select the first one available. 4084 if (OpticalTrainManager::Instance()->exists(id) == false) 4085 { 4086 qCWarning(KSTARS_EKOS_ALIGN) << "Optical train doesn't exist for id" << id; 4087 id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0)); 4088 } 4089 4090 auto name = OpticalTrainManager::Instance()->name(id); 4091 4092 opticalTrainCombo->setCurrentText(name); 4093 4094 auto scope = OpticalTrainManager::Instance()->getScope(name); 4095 m_FocalLength = scope["focal_length"].toDouble(-1); 4096 m_Aperture = scope["aperture"].toDouble(-1); 4097 m_FocalRatio = scope["focal_ratio"].toDouble(-1); 4098 m_Reducer = OpticalTrainManager::Instance()->getReducer(name); 4099 4100 // DSLR Lens Aperture 4101 if (m_Aperture < 0 && m_FocalRatio > 0) 4102 m_Aperture = m_FocalLength / m_FocalRatio; 4103 4104 auto mount = OpticalTrainManager::Instance()->getMount(name); 4105 setMount(mount); 4106 4107 auto camera = OpticalTrainManager::Instance()->getCamera(name); 4108 if (camera) 4109 { 4110 camera->setScopeInfo(m_FocalLength * m_Reducer, m_Aperture); 4111 opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString())); 4112 } 4113 setCamera(camera); 4114 4115 syncTelescopeInfo(); 4116 4117 auto filterWheel = OpticalTrainManager::Instance()->getFilterWheel(name); 4118 setFilterWheel(filterWheel); 4119 4120 auto rotator = OpticalTrainManager::Instance()->getRotator(name); 4121 setRotator(rotator); 4122 4123 // Load train settings 4124 OpticalTrainSettings::Instance()->setOpticalTrainID(id); 4125 auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::Align); 4126 if (settings.isValid()) 4127 setAllSettings(settings.toJsonObject().toVariantMap()); 4128 else 4129 m_Settings = m_GlobalSettings; 4130 4131 // Need to save information used for Mosaic planner 4132 Options::setTelescopeFocalLength(m_FocalLength); 4133 Options::setCameraPixelWidth(m_CameraPixelWidth); 4134 Options::setCameraPixelHeight(m_CameraPixelHeight); 4135 Options::setCameraWidth(m_CameraWidth); 4136 Options::setCameraHeight(m_CameraHeight); 4137 } 4138 4139 opticalTrainCombo->blockSignals(false); 4140 } 4141 4142 void Align::syncSettings() 4143 { 4144 QDoubleSpinBox *dsb = nullptr; 4145 QSpinBox *sb = nullptr; 4146 QCheckBox *cb = nullptr; 4147 QComboBox *cbox = nullptr; 4148 QRadioButton *cradio = nullptr; 4149 4150 QString key; 4151 QVariant value; 4152 4153 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender()))) 4154 { 4155 key = dsb->objectName(); 4156 value = dsb->value(); 4157 4158 } 4159 else if ( (sb = qobject_cast<QSpinBox*>(sender()))) 4160 { 4161 key = sb->objectName(); 4162 value = sb->value(); 4163 } 4164 else if ( (cb = qobject_cast<QCheckBox*>(sender()))) 4165 { 4166 key = cb->objectName(); 4167 value = cb->isChecked(); 4168 } 4169 else if ( (cbox = qobject_cast<QComboBox*>(sender()))) 4170 { 4171 key = cbox->objectName(); 4172 value = cbox->currentText(); 4173 } 4174 else if ( (cradio = qobject_cast<QRadioButton*>(sender()))) 4175 { 4176 key = cradio->objectName(); 4177 4178 // N.B. Only store CHECKED radios, do not store unchecked ones 4179 // as we only have exclusive groups in Align module. 4180 if (cradio->isChecked() == false) 4181 { 4182 // Remove from setting if it was added before 4183 if (m_Settings.contains(key)) 4184 { 4185 m_Settings.remove(key); 4186 emit settingsUpdated(getAllSettings()); 4187 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText())); 4188 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings); 4189 } 4190 return; 4191 } 4192 4193 value = true; 4194 } 4195 4196 // Save immediately 4197 Options::self()->setProperty(key.toLatin1(), value); 4198 4199 m_Settings[key] = value; 4200 m_GlobalSettings[key] = value; 4201 4202 emit settingsUpdated(getAllSettings()); 4203 4204 // Save to optical train specific settings as well 4205 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText())); 4206 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings); 4207 } 4208 4209 void Align::loadGlobalSettings() 4210 { 4211 QString key; 4212 QVariant value; 4213 4214 QVariantMap settings; 4215 // All Combo Boxes 4216 for (auto &oneWidget : findChildren<QComboBox*>()) 4217 { 4218 if (oneWidget->objectName() == "opticalTrainCombo") 4219 continue; 4220 4221 key = oneWidget->objectName(); 4222 value = Options::self()->property(key.toLatin1()); 4223 if (value.isValid()) 4224 { 4225 oneWidget->setCurrentText(value.toString()); 4226 settings[key] = value; 4227 } 4228 } 4229 4230 // All Double Spin Boxes 4231 for (auto &oneWidget : findChildren<QDoubleSpinBox*>()) 4232 { 4233 key = oneWidget->objectName(); 4234 value = Options::self()->property(key.toLatin1()); 4235 if (value.isValid()) 4236 { 4237 oneWidget->setValue(value.toDouble()); 4238 settings[key] = value; 4239 } 4240 } 4241 4242 // All Spin Boxes 4243 for (auto &oneWidget : findChildren<QSpinBox*>()) 4244 { 4245 key = oneWidget->objectName(); 4246 value = Options::self()->property(key.toLatin1()); 4247 if (value.isValid()) 4248 { 4249 oneWidget->setValue(value.toInt()); 4250 settings[key] = value; 4251 } 4252 } 4253 4254 // All Checkboxes 4255 for (auto &oneWidget : findChildren<QCheckBox*>()) 4256 { 4257 key = oneWidget->objectName(); 4258 value = Options::self()->property(key.toLatin1()); 4259 if (value.isValid()) 4260 { 4261 oneWidget->setChecked(value.toBool()); 4262 settings[key] = value; 4263 } 4264 } 4265 4266 m_GlobalSettings = m_Settings = settings; 4267 } 4268 4269 4270 void Align::connectSettings() 4271 { 4272 // All Combo Boxes 4273 for (auto &oneWidget : findChildren<QComboBox*>()) 4274 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings); 4275 4276 // All Double Spin Boxes 4277 for (auto &oneWidget : findChildren<QDoubleSpinBox*>()) 4278 connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings); 4279 4280 // All Spin Boxes 4281 for (auto &oneWidget : findChildren<QSpinBox*>()) 4282 connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings); 4283 4284 // All Checkboxes 4285 for (auto &oneWidget : findChildren<QCheckBox*>()) 4286 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings); 4287 4288 // All Radio buttons 4289 for (auto &oneWidget : findChildren<QRadioButton*>()) 4290 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings); 4291 4292 // Train combo box should NOT be synced. 4293 disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings); 4294 } 4295 4296 void Align::disconnectSettings() 4297 { 4298 // All Combo Boxes 4299 for (auto &oneWidget : findChildren<QComboBox*>()) 4300 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings); 4301 4302 // All Double Spin Boxes 4303 for (auto &oneWidget : findChildren<QDoubleSpinBox*>()) 4304 disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings); 4305 4306 // All Spin Boxes 4307 for (auto &oneWidget : findChildren<QSpinBox*>()) 4308 disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings); 4309 4310 // All Checkboxes 4311 for (auto &oneWidget : findChildren<QCheckBox*>()) 4312 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings); 4313 4314 // All Radio buttons 4315 for (auto &oneWidget : findChildren<QRadioButton*>()) 4316 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings); 4317 4318 } 4319 4320 /////////////////////////////////////////////////////////////////////////////////////////// 4321 /// 4322 /////////////////////////////////////////////////////////////////////////////////////////// 4323 QVariantMap Align::getAllSettings() const 4324 { 4325 QVariantMap settings; 4326 4327 // All Combo Boxes 4328 for (auto &oneWidget : findChildren<QComboBox*>()) 4329 settings.insert(oneWidget->objectName(), oneWidget->currentText()); 4330 4331 // All Double Spin Boxes 4332 for (auto &oneWidget : findChildren<QDoubleSpinBox*>()) 4333 settings.insert(oneWidget->objectName(), oneWidget->value()); 4334 4335 // All Spin Boxes 4336 for (auto &oneWidget : findChildren<QSpinBox*>()) 4337 settings.insert(oneWidget->objectName(), oneWidget->value()); 4338 4339 // All Checkboxes 4340 for (auto &oneWidget : findChildren<QCheckBox*>()) 4341 settings.insert(oneWidget->objectName(), oneWidget->isChecked()); 4342 4343 return settings; 4344 } 4345 4346 /////////////////////////////////////////////////////////////////////////////////////////// 4347 /// 4348 /////////////////////////////////////////////////////////////////////////////////////////// 4349 void Align::setAllSettings(const QVariantMap &settings) 4350 { 4351 // Disconnect settings that we don't end up calling syncSettings while 4352 // performing the changes. 4353 disconnectSettings(); 4354 4355 for (auto &name : settings.keys()) 4356 { 4357 // Combo 4358 auto comboBox = findChild<QComboBox*>(name); 4359 if (comboBox) 4360 { 4361 syncControl(settings, name, comboBox); 4362 continue; 4363 } 4364 4365 // Double spinbox 4366 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name); 4367 if (doubleSpinBox) 4368 { 4369 syncControl(settings, name, doubleSpinBox); 4370 continue; 4371 } 4372 4373 // spinbox 4374 auto spinBox = findChild<QSpinBox*>(name); 4375 if (spinBox) 4376 { 4377 syncControl(settings, name, spinBox); 4378 continue; 4379 } 4380 4381 // checkbox 4382 auto checkbox = findChild<QCheckBox*>(name); 4383 if (checkbox) 4384 { 4385 syncControl(settings, name, checkbox); 4386 continue; 4387 } 4388 4389 // Radio button 4390 auto radioButton = findChild<QRadioButton*>(name); 4391 if (radioButton) 4392 { 4393 syncControl(settings, name, radioButton); 4394 continue; 4395 } 4396 } 4397 4398 // Sync to options 4399 for (auto &key : settings.keys()) 4400 { 4401 auto value = settings[key]; 4402 // Save immediately 4403 Options::self()->setProperty(key.toLatin1(), value); 4404 Options::self()->save(); 4405 4406 m_Settings[key] = value; 4407 m_GlobalSettings[key] = value; 4408 } 4409 4410 emit settingsUpdated(getAllSettings()); 4411 4412 // Save to optical train specific settings as well 4413 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText())); 4414 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings); 4415 4416 // Restablish connections 4417 connectSettings(); 4418 } 4419 4420 /////////////////////////////////////////////////////////////////////////////////////////// 4421 /// 4422 /////////////////////////////////////////////////////////////////////////////////////////// 4423 bool Align::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget) 4424 { 4425 QSpinBox *pSB = nullptr; 4426 QDoubleSpinBox *pDSB = nullptr; 4427 QCheckBox *pCB = nullptr; 4428 QComboBox *pComboBox = nullptr; 4429 QRadioButton *pRadioButton = nullptr; 4430 bool ok = false; 4431 4432 if ((pSB = qobject_cast<QSpinBox *>(widget))) 4433 { 4434 const int value = settings[key].toInt(&ok); 4435 if (ok) 4436 { 4437 pSB->setValue(value); 4438 return true; 4439 } 4440 } 4441 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget))) 4442 { 4443 const double value = settings[key].toDouble(&ok); 4444 if (ok) 4445 { 4446 pDSB->setValue(value); 4447 return true; 4448 } 4449 } 4450 else if ((pCB = qobject_cast<QCheckBox *>(widget))) 4451 { 4452 const bool value = settings[key].toBool(); 4453 pCB->setChecked(value); 4454 return true; 4455 } 4456 // ONLY FOR STRINGS, not INDEX 4457 else if ((pComboBox = qobject_cast<QComboBox *>(widget))) 4458 { 4459 const QString value = settings[key].toString(); 4460 pComboBox->setCurrentText(value); 4461 return true; 4462 } 4463 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget))) 4464 { 4465 const bool value = settings[key].toBool(); 4466 pRadioButton->setChecked(value); 4467 return true; 4468 } 4469 4470 return false; 4471 } 4472 4473 void Align::setState(AlignState value) 4474 { 4475 qCDebug(KSTARS_EKOS_ALIGN) << "Align state changed to" << getAlignStatusString(value); 4476 state = value; 4477 }; 4478 4479 void Align::processCaptureTimeout() 4480 { 4481 if (m_CaptureTimeoutCounter++ > 3) 4482 { 4483 appendLogText(i18n("Capture timed out.")); 4484 m_CaptureTimer.stop(); 4485 abort(); 4486 } 4487 else 4488 { 4489 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD); 4490 if (targetChip->isCapturing()) 4491 { 4492 appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500)); 4493 targetChip->abortExposure(); 4494 m_CaptureTimer.start( m_CaptureTimer.interval() * 2); 4495 } 4496 else 4497 { 4498 setAlignTableResult(ALIGN_RESULT_FAILED); 4499 if (m_resetCaptureTimeoutCounter) 4500 { 4501 m_resetCaptureTimeoutCounter = false; 4502 m_CaptureTimeoutCounter = 0; 4503 } 4504 captureAndSolve(); 4505 } 4506 } 4507 } 4508 }