Kstars

align.cpp
1/*
2 SPDX-FileCopyrightText: 2013 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 SPDX-FileCopyrightText: 2013-2021 Jasem Mutlaq <mutlaqja@ikarustech.com>
4 SPDX-FileCopyrightText: 2018-2020 Robert Lancaster <rlancaste@gmail.com>
5 SPDX-FileCopyrightText: 2019-2021 Hy Murveit <hy@murveit.com>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include "align.h"
11#include "alignadaptor.h"
12#include "alignview.h"
13#include <ekos_align_debug.h>
14
15// Options
16#include "Options.h"
17#include "opsalign.h"
18#include "opsprograms.h"
19#include "opsastrometry.h"
20#include "opsastrometryindexfiles.h"
21
22// Components
23#include "mountmodel.h"
24#include "polaralignmentassistant.h"
25#include "remoteastrometryparser.h"
26#include "manualrotator.h"
27
28// FITS
29#include "fitsviewer/fitsdata.h"
30
31// Auxiliary
32#include "auxiliary/QProgressIndicator.h"
33#include "auxiliary/ksmessagebox.h"
34#include "ekos/auxiliary/darkprocessor.h"
35#include "ekos/auxiliary/filtermanager.h"
36#include "ekos/auxiliary/stellarsolverprofileeditor.h"
37#include "ekos/auxiliary/profilesettings.h"
38#include "ekos/auxiliary/opticaltrainmanager.h"
39#include "ekos/auxiliary/opticaltrainsettings.h"
40#include "ksnotification.h"
41#include "kspaths.h"
42#include "ksuserdb.h"
43#include "fov.h"
44#include "kstars.h"
45#include "kstarsdata.h"
46#include "skymapcomposite.h"
47#include "ekos/auxiliary/solverutils.h"
48#include "ekos/auxiliary/rotatorutils.h"
49
50// INDI
51#include "ekos/manager.h"
52#include "indi/indidome.h"
53#include "indi/clientmanager.h"
54#include "indi/driverinfo.h"
55#include "indi/indifilterwheel.h"
56#include "indi/indirotator.h"
57#include "profileinfo.h"
58
59// System Includes
60#include <KActionCollection>
61#include <basedevice.h>
62#include <indicom.h>
63#include <memory>
64
65// Qt version calming
66#include <qtendl.h>
67
68#define MAXIMUM_SOLVER_ITERATIONS 10
69#define CAPTURE_RETRY_DELAY 10000
70#define CAPTURE_ROTATOR_DELAY 5000 // After 5 seconds estimated value should be not bad
71#define PAH_CUTOFF_FOV 10 // Minimum FOV width in arcminutes for PAH to work
72#define CHECK_PAH(x) \
73 m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->x
74#define RUN_PAH(x) \
75 if (m_PolarAlignmentAssistant) m_PolarAlignmentAssistant->x
76
77namespace Ekos
78{
79
81
82bool isEqual(double a, double b)
83{
84 return std::abs(a - b) < 0.01;
85}
86
87Align::Align(const QSharedPointer<ProfileInfo> &activeProfile) : m_ActiveProfile(activeProfile)
88{
89 setupUi(this);
90
91 qRegisterMetaType<Ekos::AlignState>("Ekos::AlignState");
93
94 new AlignAdaptor(this);
95 QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this);
96
97 dirPath = QDir::homePath();
98
99 KStarsData::Instance()->clearTransientFOVs();
100
101 solverFOV.reset(new FOV());
102 solverFOV->setName(i18n("Solver FOV"));
103 solverFOV->setObjectName("solver_fov");
104 solverFOV->setLockCelestialPole(true);
105 solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name());
106 solverFOV->setProperty("visible", false);
107 KStarsData::Instance()->addTransientFOV(solverFOV);
108
109 sensorFOV.reset(new FOV());
110 sensorFOV->setObjectName("sensor_fov");
111 sensorFOV->setLockCelestialPole(true);
112 sensorFOV->setProperty("visible", Options::showSensorFOV());
113 KStarsData::Instance()->addTransientFOV(sensorFOV);
114
115 QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov");
116 if (a)
117 a->setEnabled(true);
118
119 showFITSViewerB->setIcon(
120 QIcon::fromTheme("kstars_fitsviewer"));
122 connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer);
123
124 toggleFullScreenB->setIcon(
125 QIcon::fromTheme("view-fullscreen"));
126 toggleFullScreenB->setShortcut(Qt::Key_F4);
128 connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen);
129
130 // rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem"));
132
133 m_AlignView.reset(new AlignView(alignWidget, FITS_ALIGN));
134 m_AlignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
135 m_AlignView->setBaseSize(alignWidget->size());
136 m_AlignView->createFloatingToolBar();
138
139 vlayout->addWidget(m_AlignView.get());
140 alignWidget->setLayout(vlayout);
141
142 connect(solveB, &QPushButton::clicked, this, [this]()
143 {
144 captureAndSolve(true);
145 });
146
148
149 // Effective FOV Edit
150 connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV);
151
152 connect(loadSlewB, &QPushButton::clicked, this, [this]()
153 {
154 loadAndSlew();
155 });
156
157 gotoModeButtonGroup->setId(syncR, GOTO_SYNC);
158 gotoModeButtonGroup->setId(slewR, GOTO_SLEW);
159 gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING);
160
161 // Setup Debounce timer to limit over-activation of settings changes
162 m_DebounceTimer.setInterval(500);
163 m_DebounceTimer.setSingleShot(true);
164 connect(&m_DebounceTimer, &QTimer::timeout, this, &Align::settleSettings);
165
166 m_CurrentGotoMode = static_cast<GotoMode>(Options::solverGotoOption());
167 gotoModeButtonGroup->button(m_CurrentGotoMode)->setChecked(true);
168
169#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
170 connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this,
171 [ = ](int id, bool toggled)
172#else
173 connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this,
174 [ = ](int id, bool toggled)
175#endif
176 {
177 if (toggled)
178 this->m_CurrentGotoMode = static_cast<GotoMode>(id);
179 });
180
181 m_CaptureTimer.setSingleShot(true);
182 m_CaptureTimer.setInterval(CAPTURE_RETRY_DELAY);
183 connect(&m_CaptureTimer, &QTimer::timeout, this, &Align::processCaptureTimeout);
184 m_AlignTimer.setSingleShot(true);
185 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
186 connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout);
187
188 pi.reset(new QProgressIndicator(this));
189 stopLayout->addWidget(pi.get());
190
191 rememberSolverWCS = Options::astrometrySolverWCS();
192 rememberAutoWCS = Options::autoWCS();
193
194 solverModeButtonGroup->setId(localSolverR, SOLVER_LOCAL);
195 solverModeButtonGroup->setId(remoteSolverR, SOLVER_REMOTE);
196
197 setSolverMode(Options::solverMode());
198
199#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
200 connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this,
201 [ = ](int id, bool toggled)
202#else
203 connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this,
204 [ = ](int id, bool toggled)
205#endif
206 {
207 if (toggled)
208 setSolverMode(id);
209 });
210
211 connect(alignAccuracyThreshold, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]()
212 {
213 buildTarget();
214 });
215
216 connect(alignBinning, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
218
219 connect(this, &Align::newStatus, this, [this](AlignState state)
220 {
221 opticalTrainCombo->setEnabled(state < ALIGN_PROGRESS);
222 trainB->setEnabled(state < ALIGN_PROGRESS);
223 });
224
226
227 //Note: This is to prevent a button from being called the default button
228 //and then executing when the user hits the enter key such as when on a Text Box
230 for (auto &button : qButtons)
231 button->setAutoDefault(false);
232
233 savedOptionsProfiles = QDir(KSPaths::writableLocation(
234 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
235 if(QFile(savedOptionsProfiles).exists())
236 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
237 else
238 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
239
240 m_StellarSolver.reset(new StellarSolver());
241 connect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText);
242
243 setupPolarAlignmentAssistant();
244 setupManualRotator();
245 setuptDarkProcessor();
246 setupPlot();
247 setupSolutionTable();
248 setupOptions();
249
250 // Load all settings
251 loadGlobalSettings();
252 connectSettings();
253
254 setupOpticalTrainManager();
255}
256
257Align::~Align()
258{
259 if (m_StellarSolver.get() != nullptr)
260 disconnect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText);
261
262 if (alignWidget->parent() == nullptr)
263 toggleAlignWidgetFullScreen();
264
265 // Remove temporary FITS files left before by the solver
266 QDir dir(QDir::tempPath());
267 dir.setNameFilters(QStringList() << "fits*"
268 << "tmp.*");
269 dir.setFilter(QDir::Files);
270 for (auto &dirFile : dir.entryList())
271 dir.remove(dirFile);
272}
273void Align::selectSolutionTableRow(int row, int column)
274{
275 Q_UNUSED(column)
276
277 solutionTable->selectRow(row);
278 for (int i = 0; i < alignPlot->itemCount(); i++)
279 {
281 if (abstractItem)
282 {
284 if (item)
285 {
286 if (i == row)
287 {
288 item->setColor(Qt::black);
289 item->setBrush(Qt::yellow);
290 }
291 else
292 {
293 item->setColor(Qt::red);
294 item->setBrush(Qt::white);
295 }
296 }
297 }
298 }
299 alignPlot->replot();
300}
301
302void Align::handleHorizontalPlotSizeChange()
303{
304 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
305 alignPlot->replot();
306}
307
308void Align::handleVerticalPlotSizeChange()
309{
310 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
311 alignPlot->replot();
312}
313
314void Align::resizeEvent(QResizeEvent *event)
315{
316 if (event->oldSize().width() != -1)
317 {
318 if (event->oldSize().width() != size().width())
319 handleHorizontalPlotSizeChange();
320 else if (event->oldSize().height() != size().height())
321 handleVerticalPlotSizeChange();
322 }
323 else
324 {
325 QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange);
326 }
327}
328
329void Align::handlePointTooltip(QMouseEvent *event)
330{
331 QCPAbstractItem *item = alignPlot->itemAt(event->localPos());
332 if (item)
333 {
335 if (label)
336 {
337 QString labelText = label->text();
338 int point = labelText.toInt() - 1;
339
340 if (point < 0)
341 return;
342 QToolTip::showText(event->globalPos(),
343 i18n("<table>"
344 "<tr>"
345 "<th colspan=\"2\">Object %1: %2</th>"
346 "</tr>"
347 "<tr>"
348 "<td>RA:</td><td>%3</td>"
349 "</tr>"
350 "<tr>"
351 "<td>DE:</td><td>%4</td>"
352 "</tr>"
353 "<tr>"
354 "<td>dRA:</td><td>%5</td>"
355 "</tr>"
356 "<tr>"
357 "<td>dDE:</td><td>%6</td>"
358 "</tr>"
359 "</table>",
360 point + 1,
361 solutionTable->item(point, 2)->text(),
362 solutionTable->item(point, 0)->text(),
363 solutionTable->item(point, 1)->text(),
364 solutionTable->item(point, 4)->text(),
365 solutionTable->item(point, 5)->text()),
366 alignPlot, alignPlot->rect());
367 }
368 }
369}
370
371void Align::buildTarget()
372{
373 double accuracyRadius = alignAccuracyThreshold->value();
374 if (centralTarget)
375 {
376 concentricRings->data()->clear();
377 redTarget->data()->clear();
378 yellowTarget->data()->clear();
379 centralTarget->data()->clear();
380 }
381 else
382 {
383 concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
384 redTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
385 yellowTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
386 centralTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
387 }
388 const int pointCount = 200;
390 pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175%
394
395 int circleRingPt = 0;
396 for (int i = 0; i < pointCount; i++)
397 {
398 double theta = i / static_cast<double>(pointCount) * 2 * M_PI;
399
400 for (double ring = 1; ring < 8; ring++)
401 {
402 if (ring != 4 && ring != 6)
403 {
404 if (i % (9 - static_cast<int>(ring)) == 0) //This causes fewer points to draw on the inner circles.
405 {
407 accuracyRadius * ring * 0.25 * qSin(theta));
408 circleRingPt++;
409 }
410 }
411 }
412
414 circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta));
415 circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta));
416 }
417
418 concentricRings->setLineStyle(QCPCurve::lsNone);
419 concentricRings->setScatterSkip(0);
420 concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1));
421
422 concentricRings->data()->set(circleRings, true);
423 redTarget->data()->set(circleRed, true);
424 yellowTarget->data()->set(circleYellow, true);
425 centralTarget->data()->set(circleCentral, true);
426
427 concentricRings->setPen(QPen(Qt::white));
428 redTarget->setPen(QPen(Qt::red));
429 yellowTarget->setPen(QPen(Qt::yellow));
430 centralTarget->setPen(QPen(Qt::green));
431
432 concentricRings->setBrush(Qt::NoBrush);
433 redTarget->setBrush(QBrush(QColor(255, 0, 0, 50)));
434 yellowTarget->setBrush(
435 QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity.
436 centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50)));
437
438 if (alignPlot->size().width() > 0)
439 alignPlot->replot();
440}
441
442void Align::slotAutoScaleGraph()
443{
444 double accuracyRadius = alignAccuracyThreshold->value();
445 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
446 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
447
448 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
449
450 alignPlot->replot();
451}
452
453
454void Align::slotClearAllSolutionPoints()
455{
456 if (solutionTable->rowCount() == 0)
457 return;
458
459 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
460 {
461 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
462 KSMessageBox::Instance()->disconnect(this);
463
464 solutionTable->setRowCount(0);
465 alignPlot->graph(0)->data()->clear();
466 alignPlot->clearItems();
467 buildTarget();
468
469 slotAutoScaleGraph();
470
471 });
472
473 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"),
474 i18n("Clear Solution Points"), 60);
475}
476
477void Align::slotRemoveSolutionPoint()
478{
479 auto abstractItem = alignPlot->item(solutionTable->currentRow());
480 if (abstractItem)
481 {
483 if (item)
484 {
485 double point = item->position->key();
486 alignPlot->graph(0)->data()->remove(point);
487 }
488 }
489
490 alignPlot->removeItem(solutionTable->currentRow());
491
492 for (int i = 0; i < alignPlot->itemCount(); i++)
493 {
494 auto oneItem = alignPlot->item(i);
495 if (oneItem)
496 {
498 if (item)
499 item->setText(QString::number(i + 1));
500 }
501 }
502 solutionTable->removeRow(solutionTable->currentRow());
503 alignPlot->replot();
504}
505
506void Align::slotMountModel()
507{
508 if (!m_MountModel)
509 {
510 m_MountModel = new MountModel(this);
511 connect(m_MountModel, &Ekos::MountModel::newLog, this, &Ekos::Align::appendLogText, Qt::UniqueConnection);
512 connect(m_MountModel, &Ekos::MountModel::aborted, this, [this]()
513 {
514 if (m_Mount && m_Mount->isSlewing())
515 m_Mount->abort();
516 abort();
517 });
518 connect(this, &Ekos::Align::newStatus, m_MountModel, &Ekos::MountModel::setAlignStatus, Qt::UniqueConnection);
519 }
520
521 m_MountModel->show();
522}
523
524
526{
527 return true; //For now
528 // Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid.");
529
530 // bool rc = parser->init();
531
532 // if (rc)
533 // {
534 // connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
535 // connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
536 // }
537
538 // return rc;
539}
540
541void Align::checkAlignmentTimeout()
542{
543 if (m_SolveFromFile || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
544 abort();
545 else
546 {
547 appendLogText(i18n("Solver timed out."));
548 parser->stopSolver();
549
550 setAlignTableResult(ALIGN_RESULT_FAILED);
551 captureAndSolve(false);
552 }
553 // TODO must also account for loadAndSlew. Retain file name
554}
555
557{
558 if (sender() == nullptr && mode >= 0 && mode <= 1)
559 {
560 solverModeButtonGroup->button(mode)->setChecked(true);
561 }
562
563 Options::setSolverMode(mode);
564
565 if (mode == SOLVER_REMOTE)
566 {
567 if (remoteParser.get() != nullptr && m_RemoteParserDevice != nullptr)
568 {
569 parser = remoteParser.get();
570 (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(m_RemoteParserDevice);
571 return;
572 }
573
574 remoteParser.reset(new Ekos::RemoteAstrometryParser());
575 parser = remoteParser.get();
576 (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(m_RemoteParserDevice);
577 if (m_Camera)
578 (dynamic_cast<RemoteAstrometryParser *>(parser))->setCCD(m_Camera->getDeviceName());
579
580 parser->setAlign(this);
581 if (parser->init())
582 {
583 connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
584 connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
585 }
586 else
587 parser->disconnect();
588 }
589}
590
591QString Align::camera()
592{
593 if (m_Camera)
594 return m_Camera->getDeviceName();
595
596 return QString();
597}
598
600{
601 if (!m_Camera)
602 return;
603
604 // Do NOT perform checks if align is in progress as this may result
605 // in signals/slots getting disconnected.
606 switch (state)
607 {
608 // Idle, camera change is OK.
609 case ALIGN_IDLE:
610 case ALIGN_COMPLETE:
611 case ALIGN_FAILED:
612 case ALIGN_ABORTED:
613 case ALIGN_SUCCESSFUL:
614 break;
615
616 // Busy, camera change is not OK.
617 case ALIGN_PROGRESS:
618 case ALIGN_SYNCING:
619 case ALIGN_SLEWING:
620 case ALIGN_SUSPENDED:
621 case ALIGN_ROTATING:
622 return;
623 }
624
625 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
626 if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
627 return;
628
629 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
630 (dynamic_cast<RemoteAstrometryParser *>(remoteParser.get()))->setCCD(m_Camera->getDeviceName());
631
635}
636
638{
639 if (m_Camera && m_Camera == device)
640 {
641 checkCamera();
642 return false;
643 }
644
645 if (m_Camera)
646 m_Camera->disconnect(this);
647
648 m_Camera = device;
649
650 if (m_Camera)
651 {
652 connect(m_Camera, &ISD::ConcreteDevice::Connected, this, [this]()
653 {
654 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
655 controlBox->setEnabled(isConnected);
656 gotoBox->setEnabled(isConnected);
657 plateSolverOptionsGroup->setEnabled(isConnected);
658 tabWidget->setEnabled(isConnected);
659 });
660 connect(m_Camera, &ISD::ConcreteDevice::Disconnected, this, [this]()
661 {
662 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
663 controlBox->setEnabled(isConnected);
664 gotoBox->setEnabled(isConnected);
665 plateSolverOptionsGroup->setEnabled(isConnected);
666 tabWidget->setEnabled(isConnected);
667
668 opticalTrainCombo->setEnabled(true);
669 trainLabel->setEnabled(true);
670 });
671 }
672
673 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
674 controlBox->setEnabled(isConnected);
675 gotoBox->setEnabled(isConnected);
676 plateSolverOptionsGroup->setEnabled(isConnected);
677 tabWidget->setEnabled(isConnected);
678
679 if (!m_Camera)
680 return false;
681
682 checkCamera();
683
684 return true;
685}
686
688{
689 if (m_Mount && m_Mount == device)
690 {
692 return false;
693 }
694
695 if (m_Mount)
696 m_Mount->disconnect(this);
697
698 m_Mount = device;
699
700 if (m_Mount)
701 {
702 connect(m_Mount, &ISD::ConcreteDevice::Connected, this, [this]()
703 {
704 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
705 controlBox->setEnabled(isConnected);
706 gotoBox->setEnabled(isConnected);
707 plateSolverOptionsGroup->setEnabled(isConnected);
708 tabWidget->setEnabled(isConnected);
709 });
710 connect(m_Mount, &ISD::ConcreteDevice::Disconnected, this, [this]()
711 {
712 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
713 controlBox->setEnabled(isConnected);
714 gotoBox->setEnabled(isConnected);
715 plateSolverOptionsGroup->setEnabled(isConnected);
716 tabWidget->setEnabled(isConnected);
717
718 opticalTrainCombo->setEnabled(true);
719 trainLabel->setEnabled(true);
720 });
721 }
722
723 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
724 controlBox->setEnabled(isConnected);
725 gotoBox->setEnabled(isConnected);
726 plateSolverOptionsGroup->setEnabled(isConnected);
727 tabWidget->setEnabled(isConnected);
728
729 if (!m_Mount)
730 return false;
731
732 RUN_PAH(setCurrentTelescope(m_Mount));
733
734 connect(m_Mount, &ISD::Mount::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
735 connect(m_Mount, &ISD::Mount::Disconnected, this, [this]()
736 {
737 m_isRateSynced = false;
738 });
739
741 return true;
742}
743
745{
746 if (m_Dome && m_Dome == device)
747 return false;
748
749 if (m_Dome)
750 m_Dome->disconnect(this);
751
752 m_Dome = device;
753
754 if (!m_Dome)
755 return false;
756
757 connect(m_Dome, &ISD::Dome::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
758 return true;
759}
760
761void Align::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
762{
763 auto name = device->getDeviceName();
764 device->disconnect(this);
765
766 // Check mounts
767 if (m_Mount && m_Mount->getDeviceName() == name)
768 {
769 m_Mount->disconnect(this);
770 m_Mount = nullptr;
771 }
772
773 // Check domes
774 if (m_Dome && m_Dome->getDeviceName() == name)
775 {
776 m_Dome->disconnect(this);
777 m_Dome = nullptr;
778 }
779
780 // Check rotators
781 if (m_Rotator && m_Rotator->getDeviceName() == name)
782 {
783 m_Rotator->disconnect(this);
784 m_Rotator = nullptr;
785 }
786
787 // Check cameras
788 if (m_Camera && m_Camera->getDeviceName() == name)
789 {
790 m_Camera->disconnect(this);
791 m_Camera = nullptr;
792
794 }
795
796 // Check Remote Astrometry
797 if (m_RemoteParserDevice && m_RemoteParserDevice->getDeviceName() == name)
798 {
799 m_RemoteParserDevice.clear();
800 }
801
802 // Check Filter Wheels
803 if (m_FilterWheel && m_FilterWheel->getDeviceName() == name)
804 {
805 m_FilterWheel->disconnect(this);
806 m_FilterWheel = nullptr;
807
809 }
810
811}
812
814{
815 if (m_Mount == nullptr || m_Mount->isConnected() == false)
816 return false;
817
818 if (m_isRateSynced == false)
819 {
820 auto speed = m_Settings["PAHMountSpeed"];
821 auto slewRates = m_Mount->slewRates();
822 if (speed.isValid())
823 {
824 RUN_PAH(syncMountSpeed(speed.toString()));
825 }
826 else if (!slewRates.isEmpty())
827 {
828 RUN_PAH(syncMountSpeed(slewRates.last()));
829 }
830
831 m_isRateSynced = !slewRates.empty();
832 }
833
834 canSync = m_Mount->canSync();
835
836 if (canSync == false && syncR->isEnabled())
837 {
838 slewR->setChecked(true);
839 appendLogText(i18n("Mount does not support syncing."));
840 }
841
842 syncR->setEnabled(canSync);
843
844 if (m_FocalLength == -1 || m_Aperture == -1)
845 return false;
846
847 if (m_CameraPixelWidth != -1 && m_CameraPixelHeight != -1)
848 {
849 calculateFOV();
850 return true;
851 }
852
853 return false;
854}
855
857{
858 if (!m_Camera)
859 return;
860
861 auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
862 Q_ASSERT(targetChip);
863
864 // Get Maximum resolution and pixel size
865 uint8_t bit_depth = 8;
866 targetChip->getImageInfo(m_CameraWidth, m_CameraHeight, m_CameraPixelWidth, m_CameraPixelHeight, bit_depth);
867
868 setWCSEnabled(Options::astrometrySolverWCS());
869
870 int binx = 1, biny = 1;
871 alignBinning->setEnabled(targetChip->canBin());
872 if (targetChip->canBin())
873 {
874 alignBinning->blockSignals(true);
875
876 targetChip->getMaxBin(&binx, &biny);
877 alignBinning->clear();
878
879 for (int i = 0; i < binx; i++)
880 alignBinning->addItem(QString("%1x%2").arg(i + 1).arg(i + 1));
881
882 auto binning = m_Settings["alignBinning"];
883 if (binning.isValid())
884 alignBinning->setCurrentText(binning.toString());
885
886 alignBinning->blockSignals(false);
887 }
888
889 // In case ROI is different (smaller) than maximum resolution, let's use that.
890 // N.B. 2022.08.14 JM: We must account for binning since this value is used for FOV calculations.
891 int roiW = 0, roiH = 0;
892 targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &roiW, nullptr, &roiH);
893 roiW *= binx;
894 roiH *= biny;
895 if ( (roiW > 0 && roiW < m_CameraWidth) || (roiH > 0 && roiH < m_CameraHeight))
896 {
897 m_CameraWidth = roiW;
898 m_CameraHeight = roiH;
899 }
900
901 if (m_CameraPixelWidth > 0 && m_CameraPixelHeight > 0 && m_FocalLength > 0 && m_Aperture > 0)
902 {
903 calculateFOV();
904 }
905}
906
908{
909 if (m_Camera == nullptr)
910 return;
911
912 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
913 if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
914 return;
915
916 auto isoList = targetChip->getISOList();
917 alignISO->clear();
918
919 if (isoList.isEmpty())
920 {
921 alignISO->setEnabled(false);
922 }
923 else
924 {
925 alignISO->setEnabled(true);
926 alignISO->addItems(isoList);
927 alignISO->setCurrentIndex(targetChip->getISOIndex());
928 }
929
930 // Gain Check
931 if (m_Camera->hasGain())
932 {
933 double min, max, step, value;
934 m_Camera->getGainMinMaxStep(&min, &max, &step);
935
936 // Allow the possibility of no gain value at all.
937 alignGainSpecialValue = min - step;
938 alignGain->setRange(alignGainSpecialValue, max);
939 alignGain->setSpecialValueText(i18n("--"));
940 alignGain->setEnabled(true);
941 alignGain->setSingleStep(step);
942 m_Camera->getGain(&value);
943
944 auto gain = m_Settings["alignGain"];
945 // Set the custom gain if we have one
946 // otherwise it will not have an effect.
947 if (gain.isValid())
948 TargetCustomGainValue = gain.toDouble();
949 if (TargetCustomGainValue > 0)
950 alignGain->setValue(TargetCustomGainValue);
951 else
952 alignGain->setValue(alignGainSpecialValue);
953
954 alignGain->setReadOnly(m_Camera->getGainPermission() == IP_RO);
955
957 {
958 if (alignGain->value() > alignGainSpecialValue)
959 TargetCustomGainValue = alignGain->value();
960 });
961 }
962 else
963 alignGain->setEnabled(false);
964}
965
966void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale)
967{
968 fov_w = m_FOVWidth;
969 fov_h = m_FOVHeight;
970 fov_scale = m_FOVPixelScale;
971}
972
973QList<double> Align::fov()
974{
975 QList<double> result;
976
977 result << m_FOVWidth << m_FOVHeight << m_FOVPixelScale;
978
979 return result;
980}
981
982QList<double> Align::cameraInfo()
983{
984 QList<double> result;
985
986 result << m_CameraWidth << m_CameraHeight << m_CameraPixelWidth << m_CameraPixelHeight;
987
988 return result;
989}
990
991QList<double> Align::telescopeInfo()
992{
993 QList<double> result;
994
995 result << m_FocalLength << m_Aperture << m_Reducer;
996
997 return result;
998}
999
1000void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale)
1001{
1002 // FOV in arcsecs
1003 // DSLR
1004 auto reducedFocalLength = m_Reducer * m_FocalLength;
1005 if (m_FocalRatio > 0)
1006 {
1007 // The forumla is in radians, must convert to degrees.
1008 // Then to arcsecs
1009 fov_w = 3600 * 2 * atan(m_CameraWidth * (m_CameraPixelWidth / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1010 fov_h = 3600 * 2 * atan(m_CameraHeight * (m_CameraPixelHeight / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1011 }
1012 // Telescope
1013 else
1014 {
1015 fov_w = 206264.8062470963552 * m_CameraWidth * m_CameraPixelWidth / 1000.0 / reducedFocalLength;
1016 fov_h = 206264.8062470963552 * m_CameraHeight * m_CameraPixelHeight / 1000.0 / reducedFocalLength;
1017 }
1018
1019 // Pix Scale
1020 fov_scale = (fov_w * (alignBinning->currentIndex() + 1)) / m_CameraWidth;
1021
1022 // FOV in arcmins
1023 fov_w /= 60.0;
1024 fov_h /= 60.0;
1025}
1026
1027void Align::calculateEffectiveFocalLength(double newFOVW)
1028{
1029 if (newFOVW < 0 || isEqual(newFOVW, m_FOVWidth))
1030 return;
1031
1032 auto reducedFocalLength = m_Reducer * m_FocalLength;
1033 double new_focal_length = 0;
1034
1035 if (m_FocalRatio > 0)
1036 new_focal_length = ((m_CameraWidth * m_CameraPixelWidth / 1000.0) / tan(newFOVW / 2)) / 2;
1037 else
1038 new_focal_length = ((m_CameraWidth * m_CameraPixelWidth / 1000.0) * 206264.8062470963552) / (newFOVW * 60.0);
1039 double focal_diff = std::fabs(new_focal_length - reducedFocalLength);
1040
1041 if (focal_diff > 1)
1042 {
1043 m_EffectiveFocalLength = new_focal_length / m_Reducer;
1044 appendLogText(i18n("Effective telescope focal length is updated to %1 mm.", m_EffectiveFocalLength));
1045 }
1046}
1047
1048void Align::calculateFOV()
1049{
1050 auto reducedFocalLength = m_Reducer * m_FocalLength;
1051 auto reducecdEffectiveFocalLength = m_Reducer * m_EffectiveFocalLength;
1052 auto reducedFocalRatio = m_Reducer * m_FocalRatio;
1053
1054 if (m_FocalRatio > 0)
1055 {
1056 // The forumla is in radians, must convert to degrees.
1057 // Then to arcsecs
1058 m_FOVWidth = 3600 * 2 * atan(m_CameraWidth * (m_CameraPixelWidth / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1059 m_FOVHeight = 3600 * 2 * atan(m_CameraHeight * (m_CameraPixelHeight / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1060 }
1061 // Telescope
1062 else
1063 {
1064 m_FOVWidth = 206264.8062470963552 * m_CameraWidth * m_CameraPixelWidth / 1000.0 / reducedFocalLength;
1065 m_FOVHeight = 206264.8062470963552 * m_CameraHeight * m_CameraPixelHeight / 1000.0 / reducedFocalLength;
1066 }
1067
1068 // Calculate FOV
1069
1070 // Pix Scale
1071 m_FOVPixelScale = (m_FOVWidth * (alignBinning->currentIndex() + 1)) / m_CameraWidth;
1072
1073 // FOV in arcmins
1074 m_FOVWidth /= 60.0;
1075 m_FOVHeight /= 60.0;
1076
1077 double calculated_fov_x = m_FOVWidth;
1078 double calculated_fov_y = m_FOVHeight;
1079
1080 QString calculatedFOV = (QString("%1' x %2'").arg(m_FOVWidth, 0, 'f', 1).arg(m_FOVHeight, 0, 'f', 1));
1081
1082 // Put FOV upper limit as 180 degrees
1084 {
1085 appendLogText(
1086 i18n("Warning! The calculated field of view (%1) is out of bounds. Ensure the telescope focal length and camera pixel size are correct.",
1087 calculatedFOV));
1088 return;
1089 }
1090
1091 FocalLengthOut->setText(QString("%1 (%2)").arg(reducedFocalLength, 0, 'f', 1).
1092 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength : reducedFocalLength, 0, 'f', 1));
1093 // DSLR
1094 if (m_FocalRatio > 0)
1095 FocalRatioOut->setText(QString("%1 (%2)").arg(reducedFocalRatio, 0, 'f', 1).
1096 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength / m_Aperture : reducedFocalRatio, 0,
1097 'f', 1));
1098 // Telescope
1099 else if (m_Aperture > 0)
1100 FocalRatioOut->setText(QString("%1 (%2)").arg(reducedFocalLength / m_Aperture, 0, 'f', 1).
1101 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength / m_Aperture : reducedFocalLength / m_Aperture, 0,
1102 'f', 1));
1103 ReducerOut->setText(QString("%1x").arg(m_Reducer, 0, 'f', 2));
1104
1105 if (m_EffectiveFocalLength > 0)
1106 {
1107 double focal_diff = std::fabs(m_EffectiveFocalLength - m_FocalLength);
1108 if (focal_diff < 5)
1109 FocalLengthOut->setStyleSheet("color:green");
1110 else if (focal_diff < 15)
1111 FocalLengthOut->setStyleSheet("color:yellow");
1112 else
1113 FocalLengthOut->setStyleSheet("color:red");
1114 }
1115
1116 // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV
1117 // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique
1118 // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete.
1119 getEffectiveFOV();
1120
1121 if (m_FOVWidth == 0)
1122 {
1123 //FOVOut->setReadOnly(false);
1124 FOVOut->setToolTip(
1125 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>",
1126 calculatedFOV));
1127 m_FOVWidth = calculated_fov_x;
1128 m_FOVHeight = calculated_fov_y;
1129 m_EffectiveFOVPending = true;
1130 }
1131 else
1132 {
1133 m_EffectiveFOVPending = false;
1134 FOVOut->setToolTip(i18n("<p>Effective field of view size in arcminutes.</p>"));
1135 }
1136
1137 solverFOV->setSize(m_FOVWidth, m_FOVHeight);
1138 sensorFOV->setSize(m_FOVWidth, m_FOVHeight);
1139 if (m_Camera)
1140 sensorFOV->setName(m_Camera->getDeviceName());
1141
1142 FOVOut->setText(QString("%1' x %2'").arg(m_FOVWidth, 0, 'f', 1).arg(m_FOVHeight, 0, 'f', 1));
1143
1144 // Enable or Disable PAA depending on current FOV
1145 const bool fovOK = ((m_FOVWidth + m_FOVHeight) / 2.0) > PAH_CUTOFF_FOV;
1146 if (m_PolarAlignmentAssistant != nullptr)
1147 m_PolarAlignmentAssistant->setEnabled(fovOK);
1148
1149 if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked())
1150 {
1151 int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex();
1152
1153 // Degrees
1154 if (unitType == 0)
1155 {
1156 double fov_low = qMin(m_FOVWidth / 60, m_FOVHeight / 60);
1157 double fov_high = qMax(m_FOVWidth / 60, m_FOVHeight / 60);
1158 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1159 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1160
1161 Options::setAstrometryImageScaleLow(fov_low);
1162 Options::setAstrometryImageScaleHigh(fov_high);
1163 }
1164 // Arcmins
1165 else if (unitType == 1)
1166 {
1167 double fov_low = qMin(m_FOVWidth, m_FOVHeight);
1168 double fov_high = qMax(m_FOVWidth, m_FOVHeight);
1169 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1170 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1171
1172 Options::setAstrometryImageScaleLow(fov_low);
1173 Options::setAstrometryImageScaleHigh(fov_high);
1174 }
1175 // Arcsec per pixel
1176 else
1177 {
1178 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(m_FOVPixelScale * 0.9);
1179 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(m_FOVPixelScale * 1.1);
1180
1181 // 10% boundary
1182 Options::setAstrometryImageScaleLow(m_FOVPixelScale * 0.9);
1183 Options::setAstrometryImageScaleHigh(m_FOVPixelScale * 1.1);
1184 }
1185 }
1186}
1187
1189{
1191
1192 // -O overwrite
1193 // -3 Expected RA
1194 // -4 Expected DEC
1195 // -5 Radius (deg)
1196 // -L lower scale of image in arcminutes
1197 // -H upper scale of image in arcminutes
1198 // -u aw set scale to be in arcminutes
1199 // -W solution.wcs name of solution file
1200 // apog1.jpg name of target file to analyze
1201 //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1202
1203 // Start with always-used arguments
1204 solver_args << "-O"
1205 << "--no-plots";
1206
1207 // Now go over boolean options
1208
1209 // noverify
1210 if (optionsMap.contains("noverify"))
1211 solver_args << "--no-verify";
1212
1213 // noresort
1214 if (optionsMap.contains("resort"))
1215 solver_args << "--resort";
1216
1217 // fits2fits
1218 if (optionsMap.contains("nofits2fits"))
1219 solver_args << "--no-fits2fits";
1220
1221 // downsample
1222 if (optionsMap.contains("downsample"))
1223 solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt());
1224
1225 // image scale low
1226 if (optionsMap.contains("scaleL"))
1227 solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble());
1228
1229 // image scale high
1230 if (optionsMap.contains("scaleH"))
1231 solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble());
1232
1233 // image scale units
1234 if (optionsMap.contains("scaleUnits"))
1235 solver_args << "-u" << optionsMap.value("scaleUnits").toString();
1236
1237 // RA
1238 if (optionsMap.contains("ra"))
1239 solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble());
1240
1241 // DE
1242 if (optionsMap.contains("de"))
1243 solver_args << "-4" << QString::number(optionsMap.value("de").toDouble());
1244
1245 // Radius
1246 if (optionsMap.contains("radius"))
1247 solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble());
1248
1249 // Custom
1250 if (optionsMap.contains("custom"))
1251 solver_args << optionsMap.value("custom").toString();
1252
1253 return solver_args;
1254}
1255
1256//This will generate the high and low scale of the imager field size based on the stated units.
1257void Align::generateFOVBounds(double fov_w, QString &fov_low, QString &fov_high, double tolerance)
1258{
1259 // This sets the percentage we search outside the lower and upper boundary limits
1260 // by default, we stretch the limits by 5% (tolerance = 0.05)
1261 double lower_boundary = 1.0 - tolerance;
1262 double upper_boundary = 1.0 + tolerance;
1263
1264 // let's stretch the boundaries by 5%
1265 // fov_lower = ((fov_h < fov_v) ? (fov_h * lower_boundary) : (fov_v * lower_boundary));
1266 // fov_upper = ((fov_h > fov_v) ? (fov_h * upper_boundary) : (fov_v * upper_boundary));
1267
1268 // JM 2019-10-20: The bounds consider image width only, not height.
1269 double fov_lower = fov_w * lower_boundary;
1270 double fov_upper = fov_w * upper_boundary;
1271
1272 //No need to do anything if they are aw, since that is the default
1275}
1276
1277
1279{
1280 QVariantMap optionsMap;
1281
1282 // -O overwrite
1283 // -3 Expected RA
1284 // -4 Expected DEC
1285 // -5 Radius (deg)
1286 // -L lower scale of image in arcminutes
1287 // -H upper scale of image in arcminutes
1288 // -u aw set scale to be in arcminutes
1289 // -W solution.wcs name of solution file
1290 // apog1.jpg name of target file to analyze
1291 //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1292
1293 if (Options::astrometryUseNoVerify())
1294 optionsMap["noverify"] = true;
1295
1296 if (Options::astrometryUseResort())
1297 optionsMap["resort"] = true;
1298
1299 if (Options::astrometryUseNoFITS2FITS())
1300 optionsMap["nofits2fits"] = true;
1301
1302 if (data == nullptr)
1303 {
1304 if (Options::astrometryUseDownsample())
1305 {
1306 if (Options::astrometryAutoDownsample() && m_CameraWidth && m_CameraHeight)
1307 {
1308 uint16_t w = m_CameraWidth / (alignBinning->currentIndex() + 1);
1309 optionsMap["downsample"] = getSolverDownsample(w);
1310 }
1311 else
1312 optionsMap["downsample"] = Options::astrometryDownsample();
1313 }
1314
1315 //Options needed for Sextractor
1316 optionsMap["image_width"] = m_CameraWidth / (alignBinning->currentIndex() + 1);
1317 optionsMap["image_height"] = m_CameraHeight / (alignBinning->currentIndex() + 1);
1318
1319 if (Options::astrometryUseImageScale() && m_FOVWidth > 0 && m_FOVHeight > 0)
1320 {
1321 QString units = "dw";
1322 if (Options::astrometryImageScaleUnits() == 1)
1323 units = "aw";
1324 else if (Options::astrometryImageScaleUnits() == 2)
1325 units = "app";
1326 if (Options::astrometryAutoUpdateImageScale())
1327 {
1329 double fov_w = m_FOVWidth;
1330 //double fov_h = m_FOVHeight;
1331
1332 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH)
1333 {
1334 fov_w /= 60;
1335 //fov_h /= 60;
1336 }
1337 else if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
1338 {
1339 fov_w = m_FOVPixelScale;
1340 //fov_h = m_FOVPixelScale;
1341 }
1342
1343 // If effective FOV is pending, let's set a wider tolerance range
1344 generateFOVBounds(fov_w, fov_low, fov_high, m_EffectiveFOVPending ? 0.3 : 0.05);
1345
1346 optionsMap["scaleL"] = fov_low;
1347 optionsMap["scaleH"] = fov_high;
1348 optionsMap["scaleUnits"] = units;
1349 }
1350 else
1351 {
1352 optionsMap["scaleL"] = Options::astrometryImageScaleLow();
1353 optionsMap["scaleH"] = Options::astrometryImageScaleHigh();
1354 optionsMap["scaleUnits"] = units;
1355 }
1356 }
1357
1358 if (Options::astrometryUsePosition() && m_Mount != nullptr)
1359 {
1360 double ra = 0, dec = 0;
1361 m_Mount->getEqCoords(&ra, &dec);
1362
1363 optionsMap["ra"] = ra * 15.0;
1364 optionsMap["de"] = dec;
1365 optionsMap["radius"] = Options::astrometryRadius();
1366 }
1367 }
1368 else
1369 {
1370 // Downsample
1372 data->getRecordValue("NAXIS1", width);
1373 if (width.isValid())
1374 {
1375 optionsMap["downsample"] = getSolverDownsample(width.toInt());
1376 }
1377 else
1378 optionsMap["downsample"] = Options::astrometryDownsample();
1379
1380 // Pixel Scale
1381 QVariant pixscale;
1382 data->getRecordValue("SCALE", pixscale);
1383 if (pixscale.isValid())
1384 {
1385 optionsMap["scaleL"] = 0.8 * pixscale.toDouble();
1386 optionsMap["scaleH"] = 1.2 * pixscale.toDouble();
1387 optionsMap["scaleUnits"] = "app";
1388 }
1389
1390 // Position
1391 QVariant ra, de;
1392 data->getRecordValue("RA", ra);
1393 data->getRecordValue("DEC", de);
1394 if (ra.isValid() && de.isValid())
1395 {
1396 optionsMap["ra"] = ra.toDouble();
1397 optionsMap["de"] = de.toDouble();
1398 optionsMap["radius"] = Options::astrometryRadius();
1399 }
1400 }
1401
1402 if (Options::astrometryCustomOptions().isEmpty() == false)
1403 optionsMap["custom"] = Options::astrometryCustomOptions();
1404
1406}
1407
1408
1409// For option "differential slewing" destination coords have to be initialized, if call comes from "outside".
1410// Initialization is activated through the predefined argument "initialCall = true".
1412{
1413 // Set target to current telescope position,if no object is selected yet.
1414 if (m_TargetCoord.ra().degree() < 0) // see default constructor skypoint()
1415 {
1416 m_TargetCoord = m_TelescopeCoord;
1417 appendLogText(i18n("Setting target to RA:%1 DEC:%2",
1418 m_TargetCoord.ra().toHMSString(true), m_TargetCoord.dec().toDMSString(true)));
1419 }
1420
1421 // Target coords will move the scope and in case of differential align the destination get lost.
1422 // Thus we have to save these coords for later use (-> SlewToTarget()).
1423 // This does not affect normal align, where destination is not used.
1424 if (initialCall)
1425 m_DestinationCoord = m_TargetCoord;
1426
1427 qCDebug(KSTARS_EKOS_ALIGN) << "Capture&Solve - Target RA:" << m_TargetCoord.ra().toHMSString(true)
1428 << " DE:" << m_TargetCoord.dec().toDMSString(true);
1429 qCDebug(KSTARS_EKOS_ALIGN) << "Capture&Solve - Destination RA:" << m_DestinationCoord.ra().toHMSString(true)
1430 << " DE:" << m_DestinationCoord.dec().toDMSString(true);
1431 m_AlignTimer.stop();
1432 m_CaptureTimer.stop();
1433
1434 if (m_Camera == nullptr)
1435 {
1436 appendLogText(i18n("Error: No camera detected."));
1437 return false;
1438 }
1439
1440 if (m_Camera->isConnected() == false)
1441 {
1442 appendLogText(i18n("Error: lost connection to camera."));
1443 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::Align,
1444 KSNotification::Alert);
1445 return false;
1446 }
1447
1448 if (m_Camera->isBLOBEnabled() == false)
1449 {
1450 m_Camera->setBLOBEnabled(true);
1451 }
1452
1453 //if (parser->init() == false)
1454 // return false;
1455
1456 if (m_FocalLength == -1 || m_Aperture == -1)
1457 {
1458 KSNotification::error(
1459 i18n("Telescope aperture and focal length are missing. Please check your optical train settings and try again."));
1460 return false;
1461 }
1462
1463 if (m_CameraPixelWidth == -1 || m_CameraPixelHeight == -1)
1464 {
1465 KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again."));
1466 return false;
1467 }
1468
1469 if (m_FilterWheel != nullptr)
1470 {
1471 if (m_FilterWheel->isConnected() == false)
1472 {
1473 appendLogText(i18n("Error: lost connection to filter wheel."));
1474 return false;
1475 }
1476
1477 int targetPosition = alignFilter->currentIndex() + 1;
1478
1479 if (targetPosition > 0 && targetPosition != currentFilterPosition)
1480 {
1481 filterPositionPending = true;
1482 // Disabling the autofocus policy for align.
1483 m_FilterManager->setFilterPosition(targetPosition, FilterManager::NO_AUTOFOCUS_POLICY);
1484 setState(ALIGN_PROGRESS);
1485 return true;
1486 }
1487 }
1488
1489 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
1490 if (clientManager && clientManager->getBLOBMode(m_Camera->getDeviceName().toLatin1().constData(), "CCD1") == B_NEVER)
1491 {
1492 if (KMessageBox::questionYesNo(
1493 nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
1494 KMessageBox::Yes)
1495 {
1496 clientManager->setBLOBMode(B_ONLY, m_Camera->getDeviceName().toLatin1().constData(), "CCD1");
1497 clientManager->setBLOBMode(B_ONLY, m_Camera->getDeviceName().toLatin1().constData(), "CCD2");
1498 }
1499 else
1500 {
1501 return false;
1502 }
1503 }
1504
1505 double seqExpose = alignExposure->value();
1506
1507 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1508
1509 if (m_FocusState >= FOCUS_PROGRESS)
1510 {
1511 appendLogText(i18n("Cannot capture while focus module is busy. Retrying in %1 seconds...",
1512 CAPTURE_RETRY_DELAY / 1000));
1513 m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1514 return true;
1515 }
1516
1517 if (targetChip->isCapturing())
1518 {
1519 appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in %1 seconds...",
1520 CAPTURE_RETRY_DELAY / 1000));
1521 m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1522 return true;
1523 }
1524
1525 // Let rotate the camera BEFORE taking a capture in [Capture & Solve]
1526 if (!m_SolveFromFile && m_Rotator && m_Rotator->absoluteAngleState() == IPS_BUSY)
1527 {
1528 int TimeOut = CAPTURE_ROTATOR_DELAY;
1529 switch (m_CaptureTimeoutCounter)
1530 {
1531 case 0:// Set start time & start angle and estimate rotator time frame during first timeout
1532 {
1533 auto absAngle = 0;
1534 if ((absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE")->at(0)->getValue()))
1535 {
1536 RotatorUtils::Instance()->startTimeFrame(absAngle);
1537 m_estimateRotatorTimeFrame = true;
1538 appendLogText(i18n("Cannot capture while rotator is busy: Time delay estimate started..."));
1539 }
1540 m_CaptureTimer.start(TimeOut);
1541 break;
1542 }
1543 case 1:// Use estimated time frame (in updateProperty()) for second timeout
1544 {
1545 TimeOut = m_RotatorTimeFrame * 1000;
1546 [[fallthrough]];
1547 }
1548 default:
1549 {
1550 TimeOut *= m_CaptureTimeoutCounter; // Extend Timeout in case estimated value is too short
1551 m_estimateRotatorTimeFrame = false;
1552 appendLogText(i18n("Cannot capture while rotator is busy: Retrying in %1 seconds...", TimeOut / 1000));
1553 m_CaptureTimer.start(TimeOut);
1554 }
1555 }
1556 return true; // Return value is used in 'Scheduler::startAstrometry()'
1557 }
1558
1559 m_AlignView->setBaseSize(alignWidget->size());
1560 m_AlignView->setProperty("suspended", (solverModeButtonGroup->checkedId() == SOLVER_LOCAL
1561 && alignDarkFrame->isChecked()));
1562
1563 connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
1564 connect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
1565
1566 // In case of remote solver, check if we need to update active CCD
1567 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
1568 {
1569 if (m_RemoteParserDevice == nullptr)
1570 {
1571 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
1572 setSolverMode(SOLVER_LOCAL);
1573 }
1574 else
1575 {
1576 // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD
1577 auto activeDevices = m_RemoteParserDevice->getBaseDevice().getText("ACTIVE_DEVICES");
1578 if (activeDevices)
1579 {
1580 auto activeCCD = activeDevices.findWidgetByName("ACTIVE_CCD");
1581 if (QString(activeCCD->text) != m_Camera->getDeviceName())
1582 {
1583 activeCCD->setText(m_Camera->getDeviceName().toLatin1().constData());
1584 m_RemoteParserDevice->getClientManager()->sendNewProperty(activeDevices);
1585 }
1586 }
1587
1588 // Enable remote parse
1589 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(true);
1590 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->sendArgs(generateRemoteArgs(QSharedPointer<FITSData>()));
1591 solverTimer.start();
1592 }
1593 }
1594
1595 // Remove temporary FITS files left before by the solver
1596 QDir dir(QDir::tempPath());
1597 dir.setNameFilters(QStringList() << "fits*" << "tmp.*");
1598 dir.setFilter(QDir::Files);
1599 for (auto &dirFile : dir.entryList())
1600 dir.remove(dirFile);
1601
1602 prepareCapture(targetChip);
1603
1604 // In case we're in refresh phase of the polar alignment helper then we use capture value from there
1605 if (matchPAHStage(PAA::PAH_REFRESH))
1606 targetChip->capture(m_PolarAlignmentAssistant->getPAHExposureDuration());
1607 else
1608 targetChip->capture(seqExpose);
1609
1610 solveB->setEnabled(false);
1611 loadSlewB->setEnabled(false);
1612 stopB->setEnabled(true);
1613 pi->startAnimation();
1614
1615 RotatorGOTO = false;
1616
1617 setState(ALIGN_PROGRESS);
1618 emit newStatus(state);
1619 solverFOV->setProperty("visible", true);
1620
1621 // If we're just refreshing, then we're done
1622 if (matchPAHStage(PAA::PAH_REFRESH))
1623 return true;
1624
1625 appendLogText(i18n("Capturing image..."));
1626
1627 if (!m_Mount)
1628 return true;
1629
1630 //This block of code will create the row in the solution table and populate RA, DE, and object name.
1631 //It also starts the progress indicator.
1632 double ra, dec;
1633 m_Mount->getEqCoords(&ra, &dec);
1634 if (!m_SolveFromFile)
1635 {
1636 int currentRow = solutionTable->rowCount();
1637 solutionTable->insertRow(currentRow);
1638 for (int i = 4; i < 6; i++)
1639 {
1642 solutionTable->setItem(currentRow, i, disabledBox);
1643 }
1644
1646 RAReport->setText(ScopeRAOut->text());
1647 RAReport->setTextAlignment(Qt::AlignHCenter);
1648 RAReport->setFlags(Qt::ItemIsSelectable);
1649 solutionTable->setItem(currentRow, 0, RAReport);
1650
1652 DECReport->setText(ScopeDecOut->text());
1653 DECReport->setTextAlignment(Qt::AlignHCenter);
1655 solutionTable->setItem(currentRow, 1, DECReport);
1656
1657 double maxrad = 1.0;
1658 SkyObject *so =
1659 KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad);
1660 QString name;
1661 if (so)
1662 {
1663 name = so->longname();
1664 }
1665 else
1666 {
1667 name = "None";
1668 }
1670 ObjNameReport->setText(name);
1671 ObjNameReport->setTextAlignment(Qt::AlignHCenter);
1673 solutionTable->setItem(currentRow, 2, ObjNameReport);
1674#ifdef Q_OS_OSX
1675 repaint(); //This is a band-aid for a bug in QT 5.10.0
1676#endif
1677
1679 solutionTable->setCellWidget(currentRow, 3, alignIndicator);
1680 alignIndicator->startAnimation();
1681#ifdef Q_OS_OSX
1682 repaint(); //This is a band-aid for a bug in QT 5.10.0
1683#endif
1684 }
1685
1686 return true;
1687}
1688
1690{
1691 auto chip = data->property("chip");
1692 if (chip.isValid() && chip.toInt() == ISD::CameraChip::GUIDE_CCD)
1693 return;
1694
1695 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
1696 disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
1697
1698 if (data)
1699 {
1700 m_AlignView->loadData(data);
1701 m_ImageData = data;
1702 }
1703 else
1704 m_ImageData.reset();
1705
1706 RUN_PAH(setImageData(m_ImageData));
1707
1708 // If it's Refresh, we're done
1709 if (matchPAHStage(PAA::PAH_REFRESH))
1710 {
1711 setCaptureComplete();
1712 return;
1713 }
1714 else
1715 appendLogText(i18n("Image received."));
1716
1717 // If Local solver, then set capture complete or perform calibration first.
1718 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1719 {
1720 // Only perform dark image subtraction on local images.
1721 if (alignDarkFrame->isChecked())
1722 {
1723 int x, y, w, h, binx = 1, biny = 1;
1724 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1725 targetChip->getFrame(&x, &y, &w, &h);
1726 targetChip->getBinning(&binx, &biny);
1727
1728 uint16_t offsetX = x / binx;
1729 uint16_t offsetY = y / biny;
1730
1731 m_DarkProcessor->denoise(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()),
1732 targetChip, m_ImageData, alignExposure->value(), offsetX, offsetY);
1733 return;
1734 }
1735
1736 setCaptureComplete();
1737 }
1738}
1739
1740void Align::prepareCapture(ISD::CameraChip *targetChip)
1741{
1742 if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1743 {
1744 rememberUploadMode = ISD::Camera::UPLOAD_LOCAL;
1745 m_Camera->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
1746 }
1747
1748 if (m_Camera->isFastExposureEnabled())
1749 {
1750 m_RememberCameraFastExposure = true;
1751 m_Camera->setFastExposureEnabled(false);
1752 }
1753
1754 m_Camera->setEncodingFormat("FITS");
1755 targetChip->resetFrame();
1756 targetChip->setBatchMode(false);
1757 targetChip->setCaptureMode(FITS_ALIGN);
1758 targetChip->setFrameType(FRAME_LIGHT);
1759
1760 int bin = alignBinning->currentIndex() + 1;
1761 targetChip->setBinning(bin, bin);
1762
1763 // Set gain if applicable
1764 if (m_Camera->hasGain() && alignGain->isEnabled() && alignGain->value() > alignGainSpecialValue)
1765 m_Camera->setGain(alignGain->value());
1766 // Set ISO if applicable
1767 if (alignISO->currentIndex() >= 0)
1768 targetChip->setISOIndex(alignISO->currentIndex());
1769}
1770
1771
1772void Align::setCaptureComplete()
1773{
1774 if (matchPAHStage(PAA::PAH_REFRESH))
1775 {
1776 emit newFrame(m_AlignView);
1777 m_PolarAlignmentAssistant->processPAHRefresh();
1778 return;
1779 }
1780
1781 emit newImage(m_AlignView);
1782
1783 solverFOV->setImage(m_AlignView->getDisplayImage());
1784
1785 // If align logging is enabled, let's save the frame.
1786 if (Options::saveAlignImages())
1787 {
1788 QDir dir;
1789 QDateTime now = KStarsData::Instance()->lt();
1790 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/" +
1791 now.toString("yyyy-MM-dd"));
1792 dir.mkpath(path);
1793 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
1794 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
1795 QString name = "align_frame_" + now.toString("HH-mm-ss") + ".fits";
1796 QString filename = path + QStringLiteral("/") + name;
1797 if (m_ImageData)
1798 m_ImageData->saveImage(filename);
1799 }
1800
1801 startSolving();
1802}
1803
1805{
1806 gotoModeButtonGroup->button(mode)->setChecked(true);
1807 m_CurrentGotoMode = static_cast<GotoMode>(mode);
1808}
1809
1811{
1812 //RUN_PAH(syncStage());
1813
1814 // This is needed because they might have directories stored in the config file.
1815 // So we can't just use the options folder list.
1816 QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs();
1817 disconnect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving);
1818
1819 m_UsedScale = false;
1820 m_UsedPosition = false;
1821 m_ScaleUsed = 0;
1822 m_RAUsed = 0;
1823 m_DECUsed = 0;
1824
1825 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1826 {
1827 if(Options::solverType() != SSolver::SOLVER_ASTAP
1828 && Options::solverType() != SSolver::SOLVER_WATNEYASTROMETRY) //You don't need astrometry index files to use ASTAP or Watney
1829 {
1830 bool foundAnIndex = false;
1831 for(auto &dataDir : astrometryDataDirs)
1832 {
1833 QDir dir = QDir(dataDir);
1834 if(dir.exists())
1835 {
1836 dir.setNameFilters(QStringList() << "*.fits");
1837 QStringList indexList = dir.entryList();
1838 if(indexList.count() > 0)
1839 foundAnIndex = true;
1840 }
1841 }
1842 if(!foundAnIndex)
1843 {
1844 appendLogText(
1845 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."));
1847 if(alignSettings && m_IndexFilesPage)
1848 {
1849 alignSettings->setCurrentPage(m_IndexFilesPage);
1850 alignSettings->show();
1851 }
1852 }
1853 }
1854 if (m_StellarSolver->isRunning())
1855 m_StellarSolver->abort();
1856 if (!m_ImageData)
1857 m_ImageData = m_AlignView->imageData();
1858 m_StellarSolver->loadNewImageBuffer(m_ImageData->getStatistics(), m_ImageData->getImageBuffer());
1859 m_StellarSolver->setProperty("ProcessType", SSolver::SOLVE);
1860 m_StellarSolver->setProperty("ExtractorType", Options::solveSextractorType());
1861 m_StellarSolver->setProperty("SolverType", Options::solverType());
1862 connect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
1863 m_StellarSolver->setIndexFolderPaths(Options::astrometryIndexFolderList());
1864
1865 auto params = m_StellarSolverProfiles.at(Options::solveOptionsProfile());
1866 params.partition = Options::stellarSolverPartition();
1867 m_StellarSolver->setParameters(params);
1868
1869 const SSolver::SolverType type = static_cast<SSolver::SolverType>(m_StellarSolver->property("SolverType").toInt());
1870 if(type == SSolver::SOLVER_LOCALASTROMETRY || type == SSolver::SOLVER_ASTAP || type == SSolver::SOLVER_WATNEYASTROMETRY)
1871 {
1872 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1873 QRegularExpression("[-{}]")));
1874 m_AlignView->saveImage(filename);
1875 m_StellarSolver->setProperty("FileToProcess", filename);
1877 externalPaths.sextractorBinaryPath = Options::sextractorBinary();
1878 externalPaths.solverPath = Options::astrometrySolverBinary();
1879 externalPaths.astapBinaryPath = Options::aSTAPExecutable();
1880 externalPaths.watneyBinaryPath = Options::watneyBinary();
1881 externalPaths.wcsPath = Options::astrometryWCSInfo();
1882 m_StellarSolver->setExternalFilePaths(externalPaths);
1883
1884 //No need for a conf file this way.
1885 m_StellarSolver->setProperty("AutoGenerateAstroConfig", true);
1886 }
1887
1888 if(type == SSolver::SOLVER_ONLINEASTROMETRY )
1889 {
1890 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1891 QRegularExpression("[-{}]")));
1892 m_AlignView->saveImage(filename);
1893
1894 m_StellarSolver->setProperty("FileToProcess", filename);
1895 m_StellarSolver->setProperty("AstrometryAPIKey", Options::astrometryAPIKey());
1896 m_StellarSolver->setProperty("AstrometryAPIURL", Options::astrometryAPIURL());
1897 }
1898
1899 bool useImageScale = Options::astrometryUseImageScale();
1900 if (useBlindScale == BLIND_ENGAGNED)
1901 {
1902 useImageScale = false;
1903 useBlindScale = BLIND_USED;
1904 appendLogText(i18n("Solving with blind image scale..."));
1905 }
1906
1907 bool useImagePosition = Options::astrometryUsePosition();
1908 if (useBlindPosition == BLIND_ENGAGNED)
1909 {
1910 useImagePosition = false;
1911 useBlindPosition = BLIND_USED;
1912 appendLogText(i18n("Solving with blind image position..."));
1913 }
1914
1915 if (m_SolveFromFile)
1916 {
1917 FITSImage::Solution solution;
1918 m_ImageData->parseSolution(solution);
1919
1920 if (useImageScale && solution.pixscale > 0)
1921 {
1922 m_UsedScale = true;
1923 m_ScaleUsed = solution.pixscale;
1924 m_StellarSolver->setSearchScale(solution.pixscale * 0.8,
1925 solution.pixscale * 1.2,
1926 SSolver::ARCSEC_PER_PIX);
1927 }
1928 else
1929 m_StellarSolver->setProperty("UseScale", false);
1930
1931 if (useImagePosition && solution.ra > 0)
1932 {
1933 m_UsedPosition = true;
1934 m_RAUsed = solution.ra;
1935 m_DECUsed = solution.dec;
1936 m_StellarSolver->setSearchPositionInDegrees(solution.ra, solution.dec);
1937 }
1938 else
1939 m_StellarSolver->setProperty("UsePosition", false);
1940
1941 QVariant value = "";
1942 if (!m_ImageData->getRecordValue("PIERSIDE", value))
1943 {
1944 appendLogText(i18n("Loaded image does not have pierside information"));
1945 m_TargetPierside = ISD::Mount::PIER_UNKNOWN;
1946 }
1947 else
1948 {
1949 appendLogText(i18n("Loaded image was taken on pierside %1", value.toString()));
1950 (value == "WEST") ? m_TargetPierside = ISD::Mount::PIER_WEST : m_TargetPierside = ISD::Mount::PIER_EAST;
1951 }
1952 RotatorUtils::Instance()->Instance()->setImagePierside(m_TargetPierside);
1953 }
1954 else
1955 {
1956 //Setting the initial search scale settings
1957 if (useImageScale)
1958 {
1959 m_UsedScale = true;
1960 m_ScaleUsed = Options::astrometryImageScaleLow();
1961
1962 SSolver::ScaleUnits units = static_cast<SSolver::ScaleUnits>(Options::astrometryImageScaleUnits());
1963 // Extend search scale from 80% to 120%
1964 m_StellarSolver->setSearchScale(Options::astrometryImageScaleLow() * 0.8,
1965 Options::astrometryImageScaleHigh() * 1.2,
1966 units);
1967 }
1968 else
1969 m_StellarSolver->setProperty("UseScale", false);
1970 //Setting the initial search location settings
1972 {
1973 m_StellarSolver->setSearchPositionInDegrees(m_TelescopeCoord.ra().Degrees(), m_TelescopeCoord.dec().Degrees());
1974 m_UsedPosition = true;
1975 m_RAUsed = m_TelescopeCoord.ra().Degrees();
1976 m_DECUsed = m_TelescopeCoord.dec().Degrees();
1977 }
1978 else
1979 m_StellarSolver->setProperty("UsePosition", false);
1980 }
1981
1982 if(Options::alignmentLogging())
1983 {
1984 // Not trusting SSolver logging right now (Hy Aug 1, 2022)
1985 // m_StellarSolver->setLogLevel(static_cast<SSolver::logging_level>(Options::loggerLevel()));
1986 // m_StellarSolver->setSSLogLevel(SSolver::LOG_NORMAL);
1987 m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
1988 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
1989 if(Options::astrometryLogToFile())
1990 {
1991 m_StellarSolver->setProperty("LogToFile", true);
1992 m_StellarSolver->setProperty("LogFileName", Options::astrometryLogFilepath());
1993 }
1994 }
1995 else
1996 {
1997 m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
1998 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
1999 }
2000
2001 SolverUtils::patchMultiAlgorithm(m_StellarSolver.get());
2002
2003 // Start solving process
2004 m_StellarSolver->start();
2005 }
2006 else
2007 {
2008 if (m_ImageData.isNull())
2009 m_ImageData = m_AlignView->imageData();
2010 // This should run only for load&slew. For regular solve, we don't get here
2011 // as the image is read and solved server-side.
2012 remoteParser->startSolver(m_ImageData->filename(), generateRemoteArgs(m_ImageData), false);
2013 }
2014
2015 // In these cases, the error box is not used, and we don't want it polluted
2016 // from some previous operation.
2017 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
2018 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
2019 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
2020 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
2021 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
2022 matchPAHStage(PAA::PAH_THIRD_SOLVE) ||
2023 nothingR->isChecked() ||
2024 syncR->isChecked())
2025 errOut->clear();
2026
2027 // Kick off timer
2028 solverTimer.start();
2029
2030 setState(ALIGN_PROGRESS);
2031 emit newStatus(state);
2032}
2033
2034void Align::solverComplete()
2035{
2036 disconnect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
2037 if(!m_StellarSolver->solvingDone() || m_StellarSolver->failed())
2038 {
2039 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
2040 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
2041 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
2042 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
2043 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
2044 matchPAHStage(PAA::PAH_THIRD_SOLVE))
2045 {
2046 if (CHECK_PAH(processSolverFailure()))
2047 return;
2048 else
2049 setState(ALIGN_ABORTED);
2050 }
2051 solverFailed();
2052 return;
2053 }
2054 else
2055 {
2056 FITSImage::Solution solution = m_StellarSolver->getSolution();
2057 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
2058 solverFinished(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
2059 }
2060}
2061
2062void Align::solverFinished(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
2063{
2064 pi->stopAnimation();
2065 stopB->setEnabled(false);
2066 solveB->setEnabled(true);
2067
2068 sOrientation = orientation;
2069 sRA = ra;
2070 sDEC = dec;
2071
2072 double elapsed = solverTimer.elapsed() / 1000.0;
2073 if (elapsed > 0)
2074 appendLogText(i18n("Solver completed after %1 seconds.", QString::number(elapsed, 'f', 2)));
2075
2076 m_AlignTimer.stop();
2077 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice && remoteParser.get())
2078 {
2079 // Disable remote parse
2080 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(false);
2081 }
2082
2083 int binx, biny;
2084 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2085 targetChip->getBinning(&binx, &biny);
2086
2087 if (Options::alignmentLogging())
2088 {
2089 QString parityString = eastToTheRight ? "neg" : "pos";
2090 appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4) Parity (%5)", QString::number(ra, 'f', 5),
2091 QString::number(dec, 'f', 5), QString::number(orientation, 'f', 5),
2092 QString::number(pixscale, 'f', 5), parityString));
2093 }
2094
2095 // When solving (without Load&Slew), update effective FOV and focal length accordingly.
2096 if (!m_SolveFromFile &&
2097 (isEqual(m_FOVWidth, 0) || m_EffectiveFOVPending || std::fabs(pixscale - m_FOVPixelScale) > 0.005) &&
2098 pixscale > 0)
2099 {
2100 double newFOVW = m_CameraWidth * pixscale / binx / 60.0;
2101 double newFOVH = m_CameraHeight * pixscale / biny / 60.0;
2102
2103 calculateEffectiveFocalLength(newFOVW);
2104 saveNewEffectiveFOV(newFOVW, newFOVH);
2105
2106 m_EffectiveFOVPending = false;
2107 }
2108
2109 m_AlignCoord.setRA0(ra / 15.0); // set catalog coordinates
2110 m_AlignCoord.setDec0(dec);
2111
2112 // Convert to JNow
2113 m_AlignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
2114 // Get horizontal coords
2115 m_AlignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
2116
2117 // Do not update diff if we are performing load & slew.
2118 if (!m_SolveFromFile)
2119 {
2120 pixScaleOut->setText(QString::number(pixscale, 'f', 2));
2121 calculateAlignTargetDiff();
2122 }
2123
2124 // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack.
2125 // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA
2126 // PA = Orientation + 180
2127 double solverPA = KSUtils::rotationToPositionAngle(orientation);
2128 solverFOV->setCenter(m_AlignCoord);
2129 solverFOV->setPA(solverPA);
2130 solverFOV->setImageDisplay(Options::astrometrySolverOverlay());
2131 // Sensor FOV as well
2132 sensorFOV->setPA(solverPA);
2133
2134 PAOut->setText(QString::number(solverPA, 'f', 2)); // Two decimals are reasonable
2135
2137 getFormattedCoords(m_AlignCoord.ra().Hours(), m_AlignCoord.dec().Degrees(), ra_dms, dec_dms);
2138
2139 SolverRAOut->setText(ra_dms);
2140 SolverDecOut->setText(dec_dms);
2141
2142 if (Options::astrometrySolverWCS())
2143 {
2144 auto ccdRotation = m_Camera->getNumber("CCD_ROTATION");
2145 if (ccdRotation)
2146 {
2147 auto rotation = ccdRotation->findWidgetByName("CCD_ROTATION_VALUE");
2148 if (rotation)
2149 {
2150 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
2151 rotation->setValue(orientation);
2152 clientManager->sendNewProperty(ccdRotation);
2153
2154 if (m_wcsSynced == false)
2155 {
2156 appendLogText(
2157 i18n("WCS information updated. Images captured from this point forward shall have valid WCS."));
2158
2159 // Just send telescope info in case the CCD driver did not pick up before.
2160 auto telescopeInfo = m_Mount->getNumber("TELESCOPE_INFO");
2161 if (telescopeInfo)
2162 clientManager->sendNewProperty(telescopeInfo);
2163
2164 m_wcsSynced = true;
2165 }
2166 }
2167 }
2168 }
2169
2170 m_CaptureErrorCounter = 0;
2171 m_SlewErrorCounter = 0;
2172 m_CaptureTimeoutCounter = 0;
2173
2174 appendLogText(
2175 i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4) Target Coordinates: RA (%5) DEC (%6)",
2176 m_AlignCoord.ra().toHMSString(),
2177 m_AlignCoord.dec().toDMSString(),
2178 m_TelescopeCoord.ra().toHMSString(),
2179 m_TelescopeCoord.dec().toDMSString(),
2180 m_TargetCoord.ra().toHMSString(),
2181 m_TargetCoord.dec().toDMSString()));
2182
2183 if (!m_SolveFromFile && m_CurrentGotoMode == GOTO_SLEW)
2184 {
2185 dms diffDeg(m_TargetDiffTotal / 3600.0);
2186 appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString()));
2187 }
2188
2189 if (rememberUploadMode != m_Camera->getUploadMode())
2190 m_Camera->setUploadMode(rememberUploadMode);
2191
2192 // Remember to reset fast exposure
2193 if (m_RememberCameraFastExposure)
2194 {
2195 m_RememberCameraFastExposure = false;
2196 m_Camera->setFastExposureEnabled(true);
2197 }
2198
2199 //This block of code along with some sections in the switch below will set the status report in the solution table for this item.
2200 std::unique_ptr<QTableWidgetItem> statusReport(new QTableWidgetItem());
2201 int currentRow = solutionTable->rowCount() - 1;
2202 if (!m_SolveFromFile) // [Capture & Solve]
2203 {
2204 stopProgressAnimation();
2205 solutionTable->setCellWidget(currentRow, 3, new QWidget());
2207 // Calibration: determine camera offset from capture
2208 if (m_Rotator != nullptr && m_Rotator->isConnected())
2209 {
2210 if (auto absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE"))
2211 // if (absAngle && std::isnan(m_TargetPositionAngle) == true)
2212 {
2213 sRawAngle = absAngle->at(0)->getValue();
2214 double OffsetAngle = RotatorUtils::Instance()->calcOffsetAngle(sRawAngle, solverPA);
2215 RotatorUtils::Instance()->updateOffset(OffsetAngle);
2216 // Debug info
2217 auto reverseStatus = "Unknown";
2218 auto reverseProperty = m_Rotator->getSwitch("ROTATOR_REVERSE");
2219 if (reverseProperty)
2220 {
2221 if (reverseProperty->at(0)->getState() == ISS_ON)
2222 reverseStatus = "Reversed Direction";
2223 else
2224 reverseStatus = "Normal Direction";
2225 }
2226 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << sRawAngle << "Rotator PA:" << solverPA
2227 << "Rotator Offset:" << OffsetAngle << "Direction:" << reverseStatus;
2228 // Flow is: newSolverResults() -> capture: setAlignresult() -> RotatorSettings: refresh()
2229 emit newSolverResults(solverPA, ra, dec, pixscale);
2230 // appendLogText(i18n("Camera offset angle is %1 degrees.", OffsetAngle));
2231 appendLogText(i18n("Camera position angle is %1 degrees.", RotatorUtils::Instance()->calcCameraAngle(sRawAngle, false)));
2232 }
2233 }
2234 }
2235
2236 QJsonObject solution =
2237 {
2238 {"camera", m_Camera->getDeviceName()},
2239 {"ra", SolverRAOut->text()},
2240 {"de", SolverDecOut->text()},
2241 {"dRA", m_TargetDiffRA},
2242 {"dDE", m_TargetDiffDE},
2243 {"targetDiff", m_TargetDiffTotal},
2244 {"pix", pixscale},
2245 {"PA", solverPA},
2246 {"fov", FOVOut->text()},
2247 };
2248 emit newSolution(solution.toVariantMap());
2249
2250 setState(ALIGN_SUCCESSFUL);
2251 emit newStatus(state);
2252 solverIterations = 0;
2253 KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"),
2254 KSNotification::Align);
2255
2256 switch (m_CurrentGotoMode)
2257 {
2258 case GOTO_SYNC:
2259 executeGOTO();
2260
2261 if (!m_SolveFromFile)
2262 {
2263 stopProgressAnimation();
2264 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2265 solutionTable->setItem(currentRow, 3, statusReport.release());
2266 }
2267
2268 return;
2269
2270 case GOTO_SLEW:
2271 if (m_SolveFromFile || m_TargetDiffTotal > static_cast<double>(alignAccuracyThreshold->value()))
2272 {
2273 if (!m_SolveFromFile && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
2274 {
2275 appendLogText(i18n("Maximum number of iterations reached. Solver failed."));
2276
2277 if (!m_SolveFromFile)
2278 {
2279 statusReport->setIcon(QIcon(":/icons/AlignFailure.svg"));
2280 solutionTable->setItem(currentRow, 3, statusReport.release());
2281 }
2282
2283 solverFailed();
2284 return;
2285 }
2286
2287 targetAccuracyNotMet = true;
2288
2289 if (!m_SolveFromFile)
2290 {
2291 stopProgressAnimation();
2292 statusReport->setIcon(QIcon(":/icons/AlignWarning.svg"));
2293 solutionTable->setItem(currentRow, 3, statusReport.release());
2294 }
2295
2296 executeGOTO();
2297 return;
2298 }
2299
2300 stopProgressAnimation();
2301 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2302 solutionTable->setItem(currentRow, 3, statusReport.release());
2303
2304 appendLogText(i18n("Target is within acceptable range."));
2305 break;
2306
2307 case GOTO_NOTHING:
2308 if (!m_SolveFromFile)
2309 {
2310 stopProgressAnimation();
2311 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2312 solutionTable->setItem(currentRow, 3, statusReport.release());
2313 }
2314 break;
2315 }
2316
2317 solverFOV->setProperty("visible", true);
2318
2319 if (!matchPAHStage(PAA::PAH_IDLE))
2320 m_PolarAlignmentAssistant->processPAHStage(orientation, ra, dec, pixscale, eastToTheRight,
2321 m_StellarSolver->getSolutionHealpix(),
2322 m_StellarSolver->getSolutionIndexNumber());
2323 else
2324 {
2325
2327 {
2328 solveB->setEnabled(false);
2329 loadSlewB->setEnabled(false);
2330 return;
2331 }
2332
2333 // We are done!
2334 setState(ALIGN_COMPLETE);
2335 emit newStatus(state);
2336
2337 solveB->setEnabled(true);
2338 loadSlewB->setEnabled(true);
2339 }
2340}
2341
2343{
2344
2345 // If failed-align logging is enabled, let's save the frame.
2346 if (Options::saveFailedAlignImages())
2347 {
2348 QDir dir;
2349 QDateTime now = KStarsData::Instance()->lt();
2350 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/failed");
2351 dir.mkpath(path);
2353 if (m_UsedScale)
2354 extraFilenameInfo.append(QString("_s%1u%2").arg(m_ScaleUsed, 0, 'f', 3)
2355 .arg(Options::astrometryImageScaleUnits()));
2356 if (m_UsedPosition)
2357 extraFilenameInfo.append(QString("_r%1_d%2").arg(m_RAUsed, 0, 'f', 5).arg(m_DECUsed, 0, 'f', 5));
2358
2359 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
2360 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
2361 QString name = "failed_align_frame_" + now.toString("yyyy-MM-dd-HH-mm-ss") + extraFilenameInfo + ".fits";
2362 QString filename = path + QStringLiteral("/") + name;
2363 if (m_ImageData)
2364 {
2365 m_ImageData->saveImage(filename);
2366 appendLogText(i18n("Saving failed solver image to %1", filename));
2367 }
2368
2369 }
2370 if (state != ALIGN_ABORTED)
2371 {
2372 // Try to solve with scale turned off, if not turned off already
2373 if (Options::astrometryUseImageScale() && useBlindScale == BLIND_IDLE)
2374 {
2375 appendLogText(i18n("Solver failed. Retrying without scale constraint."));
2376 useBlindScale = BLIND_ENGAGNED;
2377 setAlignTableResult(ALIGN_RESULT_FAILED);
2378 captureAndSolve(false);
2379 return;
2380 }
2381
2382 // Try to solve with the position turned off, if not turned off already
2383 if (Options::astrometryUsePosition() && useBlindPosition == BLIND_IDLE)
2384 {
2385 appendLogText(i18n("Solver failed. Retrying without position constraint."));
2386 useBlindPosition = BLIND_ENGAGNED;
2387 setAlignTableResult(ALIGN_RESULT_FAILED);
2388 captureAndSolve(false);
2389 return;
2390 }
2391
2392
2393 appendLogText(i18n("Solver Failed."));
2394 if(!Options::alignmentLogging())
2395 appendLogText(
2396 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."));
2397
2398 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"),
2399 KSNotification::Align, KSNotification::Alert);
2400 }
2401
2402 pi->stopAnimation();
2403 stopB->setEnabled(false);
2404 solveB->setEnabled(true);
2405 loadSlewB->setEnabled(true);
2406
2407 m_AlignTimer.stop();
2408
2409 m_SolveFromFile = false;
2410 solverIterations = 0;
2411 m_CaptureErrorCounter = 0;
2412 m_CaptureTimeoutCounter = 0;
2413 m_SlewErrorCounter = 0;
2414
2415 setState(ALIGN_FAILED);
2416 emit newStatus(state);
2417
2418 solverFOV->setProperty("visible", false);
2419
2420 setAlignTableResult(ALIGN_RESULT_FAILED);
2421}
2422
2424{
2425 // Check if we need to perform any rotations.
2426 if (Options::astrometryUseRotator())
2427 {
2428 if (m_SolveFromFile) // [Load & Slew] Program flow never lands here!?
2429 {
2430 m_TargetPositionAngle = solverFOV->PA();
2431 // We are not done yet.
2432 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to:" << m_TargetPositionAngle;
2433 }
2434 else // [Capture & Solve]: "direct" or within [Load & Slew]
2435 {
2436 currentRotatorPA = solverFOV->PA();
2437 if (std::isnan(m_TargetPositionAngle) == false) // [Load & Slew] only
2438 {
2439 // If image pierside versus mount pierside is different and policy is lenient ...
2440 if (RotatorUtils::Instance()->Instance()->checkImageFlip() && (Options::astrometryFlipRotationAllowed()))
2441 {
2442 // ... calculate "flipped" PA ...
2443 sRawAngle = RotatorUtils::Instance()->calcRotatorAngle(m_TargetPositionAngle);
2444 m_TargetPositionAngle = RotatorUtils::Instance()->calcCameraAngle(sRawAngle, true);
2445 RotatorUtils::Instance()->setImagePierside(ISD::Mount::PIER_UNKNOWN); // ... once!
2446 }
2447 // Match the position angle with rotator
2448 if (m_Rotator != nullptr && m_Rotator->isConnected())
2449 {
2450 if(fabs(KSUtils::rangePA(currentRotatorPA - m_TargetPositionAngle)) * 60 >
2451 Options::astrometryRotatorThreshold())
2452 {
2453 // Signal flow: newSolverResults() -> capture: setAlignresult() -> RS: refresh()
2454 emit newSolverResults(m_TargetPositionAngle, 0, 0, 0);
2455 appendLogText(i18n("Setting camera position angle to %1 degrees ...", m_TargetPositionAngle));
2456 setState(ALIGN_ROTATING);
2457 emit newStatus(state); // Evoke 'updateProperty()' (where the same check is executed again)
2458 return true;
2459 }
2460 else
2461 {
2462 appendLogText(i18n("Camera position angle is within acceptable range."));
2463 // We're done! (Opposed to 'updateProperty()')
2464 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
2465 }
2466 }
2467 // Match the position angle manually
2468 else
2469 {
2470 double current = currentRotatorPA;
2471 double target = m_TargetPositionAngle;
2472
2473 double diff = KSUtils::rangePA(current - target);
2474 double threshold = Options::astrometryRotatorThreshold() / 60.0;
2475
2476 appendLogText(i18n("Current PA is %1; Target PA is %2; diff: %3", current, target, diff));
2477
2478 emit manualRotatorChanged(current, target, threshold);
2479
2480 m_ManualRotator->setRotatorDiff(current, target, diff);
2481 if (fabs(diff) > threshold)
2482 {
2483 targetAccuracyNotMet = true;
2484 m_ManualRotator->show();
2485 m_ManualRotator->raise();
2486 setState(ALIGN_ROTATING);
2487 emit newStatus(state);
2488 return true;
2489 }
2490 else
2491 {
2492 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
2493 targetAccuracyNotMet = false;
2494 }
2495 }
2496 }
2497 }
2498 }
2499 return false;
2500}
2501
2503{
2504 m_CaptureTimer.stop();
2505 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
2506 m_StellarSolver->abort();
2507 else if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser)
2508 remoteParser->stopSolver();
2509 //parser->stopSolver();
2510 pi->stopAnimation();
2511 stopB->setEnabled(false);
2512 solveB->setEnabled(true);
2513 loadSlewB->setEnabled(true);
2514
2515 m_SolveFromFile = false;
2516 solverIterations = 0;
2517 m_CaptureErrorCounter = 0;
2518 m_CaptureTimeoutCounter = 0;
2519 m_SlewErrorCounter = 0;
2520 m_AlignTimer.stop();
2521
2522 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
2523 disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
2524
2525 if (rememberUploadMode != m_Camera->getUploadMode())
2526 m_Camera->setUploadMode(rememberUploadMode);
2527
2528 // Remember to reset fast exposure
2529 if (m_RememberCameraFastExposure)
2530 {
2531 m_RememberCameraFastExposure = false;
2532 m_Camera->setFastExposureEnabled(true);
2533 }
2534
2535 auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2536
2537 // If capture is still in progress, let's stop that.
2538 if (matchPAHStage(PAA::PAH_POST_REFRESH))
2539 {
2540 if (targetChip->isCapturing())
2541 targetChip->abortExposure();
2542
2543 appendLogText(i18n("Refresh is complete."));
2544 }
2545 else
2546 {
2547 if (targetChip->isCapturing())
2548 {
2549 targetChip->abortExposure();
2550 appendLogText(i18n("Capture aborted."));
2551 }
2552 else
2553 {
2554 double elapsed = solverTimer.elapsed() / 1000.0;
2555 if (elapsed > 0)
2556 appendLogText(i18n("Solver aborted after %1 seconds.", QString::number(elapsed, 'f', 2)));
2557 }
2558 }
2559
2560 setState(mode);
2561 emit newStatus(state);
2562
2563 setAlignTableResult(ALIGN_RESULT_FAILED);
2564}
2565
2566QProgressIndicator * Align::getProgressStatus()
2567{
2568 int currentRow = solutionTable->rowCount() - 1;
2569
2570 // check if the current row indicates a progress state
2571 // 1. no row present
2572 if (currentRow < 0)
2573 return nullptr;
2574 // 2. indicator is not present or not a progress indicator
2575 QWidget *indicator = solutionTable->cellWidget(currentRow, 3);
2576 if (indicator == nullptr)
2577 return nullptr;
2578 return dynamic_cast<QProgressIndicator *>(indicator);
2579}
2580
2581void Align::stopProgressAnimation()
2582{
2583 QProgressIndicator *progress_indicator = getProgressStatus();
2584 if (progress_indicator != nullptr)
2585 progress_indicator->stopAnimation();
2586}
2587
2589{
2590 QList<double> result;
2591
2592 result << sOrientation << sRA << sDEC;
2593
2594 return result;
2595}
2596
2597void Align::appendLogText(const QString &text)
2598{
2599 m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
2600 KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
2601
2602 qCInfo(KSTARS_EKOS_ALIGN) << text;
2603
2604 emit newLog(text);
2605}
2606
2607void Align::clearLog()
2608{
2609 m_LogText.clear();
2610 emit newLog(QString());
2611}
2612
2613void Align::updateProperty(INDI::Property prop)
2614{
2615 if (prop.isNameMatch("EQUATORIAL_EOD_COORD") || prop.isNameMatch("EQUATORIAL_COORD"))
2616 {
2617 auto nvp = prop.getNumber();
2619
2620 getFormattedCoords(m_TelescopeCoord.ra().Hours(), m_TelescopeCoord.dec().Degrees(), ra_dms, dec_dms);
2621
2622 ScopeRAOut->setText(ra_dms);
2623 ScopeDecOut->setText(dec_dms);
2624
2625 switch (nvp->s)
2626 {
2627 // Idle --> Mount not tracking or slewing
2628 case IPS_IDLE:
2629 m_wasSlewStarted = false;
2630 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE";
2631 break;
2632
2633 // Ok --> Mount Tracking. If m_wasSlewStarted is true
2634 // then it just finished slewing
2635 case IPS_OK:
2636 {
2637 // Update the boxes as the mount just finished slewing
2638 if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition())
2639 {
2640 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position...";
2641 opsAstrometry->estRA->setText(ra_dms);
2642 opsAstrometry->estDec->setText(dec_dms);
2643
2644 Options::setAstrometryPositionRA(nvp->np[0].value * 15);
2645 Options::setAstrometryPositionDE(nvp->np[1].value);
2646
2647 //generateArgs();
2648 }
2649
2650 // If dome is syncing, wait until it stops
2651 if (m_Dome && m_Dome->isMoving())
2652 {
2653 domeReady = false;
2654 return;
2655 }
2656
2657 // If we are looking for celestial pole
2658 if (m_wasSlewStarted && matchPAHStage(PAA::PAH_FIND_CP))
2659 {
2660 //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE";
2661 m_wasSlewStarted = false;
2662 appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify."));
2663 setSolverAction(GOTO_NOTHING);
2664
2665 m_PolarAlignmentAssistant->setPAHStage(PAA::PAH_FIRST_CAPTURE);
2666 return;
2667 }
2668
2669 switch (state)
2670 {
2671 case ALIGN_PROGRESS:
2672 break;
2673
2674 case ALIGN_SYNCING:
2675 {
2676 m_wasSlewStarted = false;
2677 //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE";
2678 if (m_CurrentGotoMode == GOTO_SLEW)
2679 {
2680 Slew();
2681 return;
2682 }
2683 else
2684 {
2685 appendLogText(i18n("Mount is synced to solution coordinates."));
2686
2687 /* if (checkIfRotationRequired())
2688 return; */
2689
2690 KSNotification::event(QLatin1String("AlignSuccessful"),
2691 i18n("Astrometry alignment completed successfully"), KSNotification::Align);
2692 setState(ALIGN_COMPLETE);
2693 emit newStatus(state);
2694 solverIterations = 0;
2695 loadSlewB->setEnabled(true);
2696 }
2697 }
2698 break;
2699
2700 case ALIGN_SLEWING:
2701
2702 if (!didSlewStart())
2703 {
2704 // If mount has not started slewing yet, then skip
2705 qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet...";
2706 break;
2707 }
2708
2709 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed.";
2710 m_wasSlewStarted = false;
2711 if (m_SolveFromFile)
2712 {
2713 m_SolveFromFile = false;
2714 m_TargetPositionAngle = solverFOV->PA();
2715 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to" << m_TargetPositionAngle;
2716
2717 setState(ALIGN_PROGRESS);
2718 emit newStatus(state);
2719
2720 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
2721 appendLogText(i18n("Settling..."));
2722 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
2723 m_CaptureTimer.start(alignSettlingTime->value());
2724 return;
2725 }
2726 else if (m_CurrentGotoMode == GOTO_SLEW || (m_MountModel && m_MountModel->isRunning()))
2727 {
2728 if (targetAccuracyNotMet)
2729 appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again..."));
2730 else
2731 appendLogText(i18n("Slew complete. Solving Alignment Point. . ."));
2732
2733 targetAccuracyNotMet = false;
2734
2735 setState(ALIGN_PROGRESS);
2736 emit newStatus(state);
2737
2738 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
2739 appendLogText(i18n("Settling..."));
2740 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
2741 m_CaptureTimer.start(alignSettlingTime->value());
2742 }
2743 break;
2744
2745 default:
2746 {
2747 //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE";
2748 m_wasSlewStarted = false;
2749 }
2750 break;
2751 }
2752 }
2753 break;
2754
2755 // Busy --> Mount Slewing or Moving (NSWE buttons)
2756 case IPS_BUSY:
2757 {
2758 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew running.";
2759 m_wasSlewStarted = true;
2760
2761 handleMountMotion();
2762 }
2763 break;
2764
2765 // Alert --> Mount has problem moving or communicating.
2766 case IPS_ALERT:
2767 {
2768 //qCDebug(KSTARS_EKOS_ALIGN) << "IPS_ALERT --> setting slewStarted to FALSE";
2769 m_wasSlewStarted = false;
2770
2771 if (state == ALIGN_SYNCING || state == ALIGN_SLEWING)
2772 {
2773 if (state == ALIGN_SYNCING)
2774 appendLogText(i18n("Syncing failed."));
2775 else
2776 appendLogText(i18n("Slewing failed."));
2777
2778 if (++m_SlewErrorCounter == 3)
2779 {
2780 abort();
2781 return;
2782 }
2783 else
2784 {
2785 if (m_CurrentGotoMode == GOTO_SLEW)
2786 Slew();
2787 else
2788 Sync();
2789 }
2790 }
2791
2792 return;
2793 }
2794 }
2795
2796 RUN_PAH(processMountRotation(m_TelescopeCoord.ra(), alignSettlingTime->value()));
2797 }
2798 else if (prop.isNameMatch("ABS_ROTATOR_ANGLE"))
2799 {
2800 auto nvp = prop.getNumber();
2801 double RAngle = nvp->np[0].value;
2802 currentRotatorPA = RotatorUtils::Instance()->calcCameraAngle(RAngle, false);
2803 /*QString logtext = "Alignstate: " + QString::number(state)
2804 + " IPSstate: " + QString::number(nvp->s)
2805 + " Raw Rotator Angle:" + QString::number(nvp->np[0].value)
2806 + " Current PA:" + QString::number(currentRotatorPA)
2807 + " Target PA:" + QString::number(m_TargetPositionAngle)
2808 + " Offset:" + QString::number(Options::pAOffset());
2809 appendLogText(logtext);*/
2810 // loadSlewTarget defined if activation through [Load & Slew] and rotator just reached position
2811 if (std::isnan(m_TargetPositionAngle) == false && state == ALIGN_ROTATING && nvp->s == IPS_OK)
2812 {
2813 auto diff = fabs(RotatorUtils::Instance()->DiffPA(currentRotatorPA - m_TargetPositionAngle)) * 60;
2814 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << RAngle << "Current PA:" << currentRotatorPA
2815 << "Target PA:" << m_TargetPositionAngle << "Diff (arcmin):" << diff << "Offset:"
2816 << Options::pAOffset();
2817
2818 if (diff <= Options::astrometryRotatorThreshold())
2819 {
2820 appendLogText(i18n("Rotator reached camera position angle."));
2821 // Check angle once again (no slew -> no settle time)
2822 // QTimer::singleShot(alignSettlingTime->value(), this, &Ekos::Align::executeGOTO);
2823 RotatorGOTO = true; // Flag for SlewToTarget()
2824 executeGOTO();
2825 }
2826 else
2827 {
2828 // Sometimes update of "nvp->s" is a bit slow, so check state again, if it's really ok
2829 QTimer::singleShot(300, [ = ]
2830 {
2831 if (nvp->s == IPS_OK)
2832 {
2833 appendLogText(i18n("Rotator failed to arrive at the requested position angle (Deviation %1 arcmin).", diff));
2834 setState(ALIGN_FAILED);
2835 emit newStatus(state);
2836 solveB->setEnabled(true);
2837 loadSlewB->setEnabled(true);
2838 }
2839 });
2840 }
2841 }
2842 else if (m_estimateRotatorTimeFrame) // Estimate time frame during first timeout
2843 {
2844 m_RotatorTimeFrame = RotatorUtils::Instance()->calcTimeFrame(RAngle);
2845 }
2846 }
2847 else if (prop.isNameMatch("DOME_MOTION"))
2848 {
2849 // If dome is not ready and state is now
2850 if (domeReady == false && prop.getState() == IPS_OK)
2851 {
2852 domeReady = true;
2853 // trigger process number for mount so that it proceeds with normal workflow since
2854 // it was stopped by dome not being ready
2855 handleMountStatus();
2856 }
2857 }
2858 else if (prop.isNameMatch("TELESCOPE_MOTION_NS") || prop.isNameMatch("TELESCOPE_MOTION_WE"))
2859 {
2860 switch (prop.getState())
2861 {
2862 case IPS_BUSY:
2863 // react upon mount motion
2864 handleMountMotion();
2865 m_wasSlewStarted = true;
2866 break;
2867 default:
2868 qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished.";
2869 handleMountStatus();
2870 break;
2871 }
2872 }
2873}
2874
2875void Align::handleMountMotion()
2876{
2877 if (state == ALIGN_PROGRESS)
2878 {
2879 if (matchPAHStage(PAA::PAH_IDLE))
2880 {
2881 // whoops, mount slews during alignment
2882 appendLogText(i18n("Slew detected, suspend solving..."));
2883 suspend();
2884 }
2885
2886 setState(ALIGN_SLEWING);
2887 }
2888}
2889
2890void Align::handleMountStatus()
2891{
2892 auto nvp = m_Mount->getNumber(m_Mount->isJ2000() ? "EQUATORIAL_COORD" :
2893 "EQUATORIAL_EOD_COORD");
2894
2895 if (nvp)
2897}
2898
2899
2901{
2902 if (m_SolveFromFile)
2903 {
2904 // Target coords will move the scope and in case of differential align the destination get lost.
2905 // Thus we have to save these coords for later use (-> SlewToTarget()).
2906 // This does not affect normal align, where destination is not used.
2907 m_DestinationCoord = m_AlignCoord;
2908 m_TargetCoord = m_AlignCoord;
2909
2910 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file. Setting Target Coordinates align coords. RA:"
2911 << m_TargetCoord.ra().toHMSString()
2912 << "DE:" << m_TargetCoord.dec().toDMSString();
2913
2914 SlewToTarget();
2915 }
2916 else if (m_CurrentGotoMode == GOTO_SYNC)
2917 Sync();
2918 else if (m_CurrentGotoMode == GOTO_SLEW)
2919 SlewToTarget();
2920}
2921
2923{
2924 setState(ALIGN_SYNCING);
2925
2926 if (m_Mount->Sync(&m_AlignCoord))
2927 {
2928 emit newStatus(state);
2929 appendLogText(
2930 i18n("Syncing to RA (%1) DEC (%2)", m_AlignCoord.ra().toHMSString(), m_AlignCoord.dec().toDMSString()));
2931 }
2932 else
2933 {
2934 setState(ALIGN_IDLE);
2935 emit newStatus(state);
2936 appendLogText(i18n("Syncing failed."));
2937 }
2938}
2939
2941{
2942 setState(ALIGN_SLEWING);
2943 emit newStatus(state);
2944
2945 //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2946 //m_wasSlewStarted = currentTelescope->Slew(&m_targetCoord);
2947 //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2948
2949 // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered
2950 // from Goto
2951 if (m_Mount->Slew(&m_TargetCoord))
2952 {
2953 slewStartTimer.start();
2954 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", m_TargetCoord.ra().toHMSString(),
2955 m_TargetCoord.dec().toDMSString()));
2956 }
2957 else // inform user about failure
2958 {
2959 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2) is rejected. (see notification)",
2960 m_TargetCoord.ra().toHMSString(),
2961 m_TargetCoord.dec().toDMSString()));
2962 setState(ALIGN_FAILED);
2963 emit newStatus(state);
2964 solveB->setEnabled(true);
2965 loadSlewB->setEnabled(true);
2966 }
2967}
2968
2970{
2971 if (canSync && !m_SolveFromFile)
2972 {
2973 // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided
2974#if 0
2975 if (Ekos::Manager::Instance()->getCurrentJobName().isEmpty())
2976 {
2977 KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"),
2978 i18n("Ekos job (%1) - Telescope synced",
2979 Ekos::Manager::Instance()->getCurrentJobName()));
2980 }
2981#endif
2982
2983 if (Options::astrometryDifferentialSlewing()) // Remember: Target coords will move the telescope
2984 {
2985 if (!RotatorGOTO) // Only for mount movements
2986 {
2987 m_TargetCoord.setRA(m_TargetCoord.ra() - m_AlignCoord.ra().deltaAngle(m_DestinationCoord.ra()));
2988 m_TargetCoord.setDec(m_TargetCoord.dec() - m_AlignCoord.dec().deltaAngle(m_DestinationCoord.dec()));
2989 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Target RA:" << m_TargetCoord.ra().toHMSString()
2990 << " DE:" << m_TargetCoord.dec().toDMSString();
2991 }
2992 Slew();
2993 }
2994 else
2995 Sync();
2996 return;
2997 }
2998 Slew();
2999}
3000
3001
3002void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str)
3003{
3004 dms ra_s, dec_s;
3005 ra_s.setH(ra);
3006 dec_s.setD(dec);
3007
3008 ra_str = QString("%1:%2:%3")
3009 .arg(ra_s.hour(), 2, 10, QChar('0'))
3010 .arg(ra_s.minute(), 2, 10, QChar('0'))
3011 .arg(ra_s.second(), 2, 10, QChar('0'));
3012 if (dec_s.Degrees() < 0)
3013 dec_str = QString("-%1:%2:%3")
3014 .arg(abs(dec_s.degree()), 2, 10, QChar('0'))
3015 .arg(abs(dec_s.arcmin()), 2, 10, QChar('0'))
3016 .arg(dec_s.arcsec(), 2, 10, QChar('0'));
3017 else
3018 dec_str = QString("%1:%2:%3")
3019 .arg(dec_s.degree(), 2, 10, QChar('0'))
3020 .arg(dec_s.arcmin(), 2, 10, QChar('0'))
3021 .arg(dec_s.arcsec(), 2, 10, QChar('0'));
3022}
3023
3025{
3026 if (fileURL.isEmpty())
3027 fileURL = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Load Image"), dirPath,
3028 "Images (*.fits *.fits.fz *.fit *.fts *.xisf "
3029 "*.jpg *.jpeg *.png *.gif *.bmp "
3030 "*.cr2 *.cr3 *.crw *.nef *.raf *.dng *.arw *.orf)");
3031
3032 if (fileURL.isEmpty())
3033 return false;
3034
3035 QFileInfo fileInfo(fileURL);
3036 if (fileInfo.exists() == false)
3037 return false;
3038
3039 dirPath = fileInfo.absolutePath();
3040
3041 RotatorGOTO = false;
3042
3043 m_SolveFromFile = true;
3044
3045 if (m_PolarAlignmentAssistant)
3046 m_PolarAlignmentAssistant->stopPAHProcess();
3047
3048 slewR->setChecked(true);
3049 m_CurrentGotoMode = GOTO_SLEW;
3050
3051 solveB->setEnabled(false);
3052 loadSlewB->setEnabled(false);
3053 stopB->setEnabled(true);
3054 pi->startAnimation();
3055
3056 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice == nullptr)
3057 {
3058 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
3059 setSolverMode(SOLVER_LOCAL);
3060 }
3061
3062 m_ImageData.clear();
3063
3064 m_AlignView->loadFile(fileURL);
3065 //m_FileToSolve = fileURL;
3066 connect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving);
3067
3068 return true;
3069}
3070
3071bool Align::loadAndSlew(const QByteArray &image, const QString &extension)
3072{
3073 RotatorGOTO = false;
3074 m_SolveFromFile = true;
3075 RUN_PAH(stopPAHProcess());
3076 slewR->setChecked(true);
3077 m_CurrentGotoMode = GOTO_SLEW;
3078 solveB->setEnabled(false);
3079 loadSlewB->setEnabled(false);
3080 stopB->setEnabled(true);
3081 pi->startAnimation();
3082
3083 // Must clear image data so we are forced to read the
3084 // image data again from align view when solving begins.
3085 m_ImageData.clear();
3087 data.reset(new FITSData(), &QObject::deleteLater);
3088 data->loadFromBuffer(image, extension);
3089 m_AlignView->loadData(data);
3090 startSolving();
3091 return true;
3092}
3093
3094void Align::setExposure(double value)
3095{
3096 alignExposure->setValue(value);
3097}
3098
3100{
3101 // If sender is not our combo box, then we need to update the combobox itself
3102 if (dynamic_cast<QComboBox *>(sender()) != alignBinning)
3103 {
3104 alignBinning->blockSignals(true);
3105 alignBinning->setCurrentIndex(binIndex);
3106 alignBinning->blockSignals(false);
3107 }
3108
3109 // Need to calculate FOV and args for APP
3110 if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
3111 calculateFOV();
3112}
3113
3114bool Align::setFilterWheel(ISD::FilterWheel * device)
3115{
3116 if (m_FilterWheel && m_FilterWheel == device)
3117 {
3118 checkFilter();
3119 return false;
3120 }
3121
3122 if (m_FilterWheel)
3123 m_FilterWheel->disconnect(this);
3124
3125 m_FilterWheel = device;
3126
3127 if (m_FilterWheel)
3128 {
3129 connect(m_FilterWheel, &ISD::ConcreteDevice::Connected, this, [this]()
3130 {
3131 FilterPosLabel->setEnabled(true);
3132 alignFilter->setEnabled(true);
3133 });
3134 connect(m_FilterWheel, &ISD::ConcreteDevice::Disconnected, this, [this]()
3135 {
3136 FilterPosLabel->setEnabled(false);
3137 alignFilter->setEnabled(false);
3138 });
3139 }
3140
3141 auto isConnected = m_FilterWheel && m_FilterWheel->isConnected();
3142 FilterPosLabel->setEnabled(isConnected);
3143 alignFilter->setEnabled(isConnected);
3144
3145 checkFilter();
3146 return true;
3147}
3148
3149QString Align::filterWheel()
3150{
3151 if (m_FilterWheel)
3152 return m_FilterWheel->getDeviceName();
3153
3154 return QString();
3155}
3156
3157bool Align::setFilter(const QString &filter)
3158{
3159 if (m_FilterWheel)
3160 {
3161 alignFilter->setCurrentText(filter);
3162 return true;
3163 }
3164
3165 return false;
3166}
3167
3168
3169QString Align::filter()
3170{
3171 return alignFilter->currentText();
3172}
3173
3175{
3176 alignFilter->clear();
3177
3178 if (!m_FilterWheel)
3179 {
3180 FilterPosLabel->setEnabled(false);
3181 alignFilter->setEnabled(false);
3182 return;
3183 }
3184
3185 auto isConnected = m_FilterWheel->isConnected();
3186 FilterPosLabel->setEnabled(isConnected);
3187 alignFilter->setEnabled(alignUseCurrentFilter->isChecked() == false);
3188
3189 setupFilterManager();
3190
3191 alignFilter->addItems(m_FilterManager->getFilterLabels());
3192 currentFilterPosition = m_FilterManager->getFilterPosition();
3193
3194 if (alignUseCurrentFilter->isChecked())
3195 {
3196 // use currently selected filter
3197 alignFilter->setCurrentIndex(currentFilterPosition - 1);
3198 }
3199 else
3200 {
3201 // use the fixed filter
3202 auto filter = m_Settings["alignFilter"];
3203 if (filter.isValid())
3204 alignFilter->setCurrentText(filter.toString());
3205 }
3206}
3207
3209{
3210 if ((Manager::Instance()->existRotatorController()) && (!m_Rotator || !(Device == m_Rotator)))
3211 {
3212 rotatorB->setEnabled(false);
3213 if (m_Rotator)
3214 {
3215 m_Rotator->disconnect(this);
3216 m_RotatorControlPanel->close();
3217 }
3218 m_Rotator = Device;
3219 if (m_Rotator)
3220 {
3221 if (Manager::Instance()->getRotatorController(m_Rotator->getDeviceName(), m_RotatorControlPanel))
3222 {
3223 connect(m_Rotator, &ISD::Rotator::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
3224 connect(rotatorB, &QPushButton::clicked, this, [this]()
3225 {
3226 m_RotatorControlPanel->show();
3227 m_RotatorControlPanel->raise();
3228 });
3229 rotatorB->setEnabled(true);
3230 }
3231 }
3232 }
3233}
3234
3235void Align::setWCSEnabled(bool enable)
3236{
3237 if (!m_Camera)
3238 return;
3239
3240 auto wcsControl = m_Camera->getSwitch("WCS_CONTROL");
3241
3242 if (!wcsControl)
3243 return;
3244
3245 auto wcs_enable = wcsControl->findWidgetByName("WCS_ENABLE");
3246 auto wcs_disable = wcsControl->findWidgetByName("WCS_DISABLE");
3247
3248 if (!wcs_enable || !wcs_disable)
3249 return;
3250
3251 if ((wcs_enable->getState() == ISS_ON && enable) || (wcs_disable->getState() == ISS_ON && !enable))
3252 return;
3253
3254 wcsControl->reset();
3255 if (enable)
3256 {
3257 appendLogText(i18n("World Coordinate System (WCS) is enabled."));
3258 wcs_enable->setState(ISS_ON);
3259 }
3260 else
3261 {
3262 appendLogText(i18n("World Coordinate System (WCS) is disabled."));
3263 wcs_disable->setState(ISS_ON);
3264 m_wcsSynced = false;
3265 }
3266
3267 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
3268 if (clientManager)
3269 clientManager->sendNewProperty(wcsControl);
3270}
3271
3272void Align::checkCameraExposureProgress(ISD::CameraChip * targetChip, double remaining, IPState state)
3273{
3274 INDI_UNUSED(targetChip);
3276
3277 if (state == IPS_ALERT)
3278 {
3279 if (++m_CaptureErrorCounter == 3 && !matchPAHStage(PolarAlignmentAssistant::PAH_REFRESH))
3280 {
3281 appendLogText(i18n("Capture error. Aborting..."));
3282 abort();
3283 return;
3284 }
3285
3286 appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter));
3287 setAlignTableResult(ALIGN_RESULT_FAILED);
3288 captureAndSolve(false);
3289 }
3290}
3291
3292void Align::setAlignTableResult(AlignResult result)
3293{
3294 // Do nothing if the progress indicator is not running.
3295 // This is necessary since it could happen that a problem occurs
3296 // before #captureAndSolve() has been started and there does not
3297 // exist a table entry for the current run.
3298 QProgressIndicator *progress_indicator = getProgressStatus();
3299 if (progress_indicator == nullptr || ! progress_indicator->isAnimated())
3300 return;
3301 stopProgressAnimation();
3302
3303 QIcon icon;
3304 switch (result)
3305 {
3306 case ALIGN_RESULT_SUCCESS:
3307 icon = QIcon(":/icons/AlignSuccess.svg");
3308 break;
3309
3310 case ALIGN_RESULT_WARNING:
3311 icon = QIcon(":/icons/AlignWarning.svg");
3312 break;
3313
3314 case ALIGN_RESULT_FAILED:
3315 default:
3316 icon = QIcon(":/icons/AlignFailure.svg");
3317 break;
3318 }
3319 int currentRow = solutionTable->rowCount() - 1;
3320 solutionTable->setCellWidget(currentRow, 3, new QWidget());
3322 statusReport->setIcon(icon);
3324 solutionTable->setItem(currentRow, 3, statusReport);
3325}
3326
3327void Align::setFocusStatus(Ekos::FocusState state)
3328{
3329 m_FocusState = state;
3330}
3331
3332uint8_t Align::getSolverDownsample(uint16_t binnedW)
3333{
3334 uint8_t downsample = Options::astrometryDownsample();
3335
3336 if (!Options::astrometryAutoDownsample())
3337 return downsample;
3338
3339 while (downsample < 8)
3340 {
3341 if (binnedW / downsample <= 1024)
3342 break;
3343
3344 downsample += 2;
3345 }
3346
3347 return downsample;
3348}
3349
3350void Align::setCaptureStatus(CaptureState newState)
3351{
3352 switch (newState)
3353 {
3354 case CAPTURE_ALIGNING:
3355 if (m_Mount && m_Mount->hasAlignmentModel() && Options::resetMountModelAfterMeridian())
3356 {
3357 qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (m_Mount->clearAlignmentModel() ?
3358 "successful." : "failed.");
3359 }
3360 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
3361 appendLogText(i18n("Settling..."));
3362 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
3363 m_CaptureTimer.start(alignSettlingTime->value());
3364 break;
3365 // Is this needed anymore with new flip policy? (sb 2023-10-20)
3366 // On meridian flip, reset Target Position Angle to fully rotated value
3367 // expected after MF so that we do not end up with reversed camera rotation
3369 if (std::isnan(m_TargetPositionAngle) == false)
3370 m_TargetPositionAngle = KSUtils::rangePA(m_TargetPositionAngle + 180.0);
3371 break;
3372 default:
3373 break;
3374 }
3375
3376 m_CaptureState = newState;
3377}
3378
3379void Align::showFITSViewer()
3380{
3381 static int lastFVTabID = -1;
3382 if (m_ImageData)
3383 {
3384 QUrl url = QUrl::fromLocalFile("align.fits");
3385 if (fv.isNull())
3386 {
3387 fv = KStars::Instance()->createFITSViewer();
3388 fv->loadData(m_ImageData, url, &lastFVTabID);
3389 connect(fv.get(), &FITSViewer::terminated, this, [this]()
3390 {
3391 fv.clear();
3392 });
3393 }
3394 else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
3395 fv->loadData(m_ImageData, url, &lastFVTabID);
3396
3397 fv->show();
3398 }
3399}
3400
3401void Align::toggleAlignWidgetFullScreen()
3402{
3403 if (alignWidget->parent() == nullptr)
3404 {
3405 alignWidget->setParent(this);
3406 rightLayout->insertWidget(0, alignWidget);
3407 alignWidget->showNormal();
3408 }
3409 else
3410 {
3411 alignWidget->setParent(nullptr);
3412 alignWidget->setWindowTitle(i18nc("@title:window", "Align Frame"));
3414 alignWidget->showMaximized();
3415 alignWidget->show();
3416 }
3417}
3418
3419void Align::setMountStatus(ISD::Mount::Status newState)
3420{
3421 switch (newState)
3422 {
3423 case ISD::Mount::MOUNT_PARKED:
3424 solveB->setEnabled(false);
3425 loadSlewB->setEnabled(false);
3426 break;
3427 case ISD::Mount::MOUNT_IDLE:
3428 solveB->setEnabled(true);
3429 loadSlewB->setEnabled(true);
3430 break;
3431 case ISD::Mount::MOUNT_PARKING:
3432 solveB->setEnabled(false);
3433 loadSlewB->setEnabled(false);
3434 break;
3435 case ISD::Mount::MOUNT_SLEWING:
3436 case ISD::Mount::MOUNT_MOVING:
3437 solveB->setEnabled(false);
3438 loadSlewB->setEnabled(false);
3439 break;
3440
3441 default:
3442 if (state != ALIGN_PROGRESS)
3443 {
3444 solveB->setEnabled(true);
3445 if (matchPAHStage(PAA::PAH_IDLE))
3446 {
3447 loadSlewB->setEnabled(true);
3448 }
3449 }
3450 break;
3451 }
3452
3453 RUN_PAH(setMountStatus(newState));
3454}
3455
3457{
3458 m_RemoteParserDevice = device;
3459
3460 remoteSolverR->setEnabled(true);
3461 if (remoteParser.get() != nullptr)
3462 {
3463 remoteParser->setAstrometryDevice(m_RemoteParserDevice);
3464 connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
3465 connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
3466 }
3467}
3468
3470{
3471 solverFOV->setImageDisplay(Options::astrometrySolverWCS());
3472 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
3473 if (m_Rotator)
3474 m_RotatorControlPanel->updateFlipPolicy(Options::astrometryFlipRotationAllowed());
3475}
3476
3477void Align::setupOptions()
3478{
3479 KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self());
3480
3481#ifdef Q_OS_OSX
3483#endif
3484
3485 opsAlign = new OpsAlign(this);
3486 connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions);
3487 KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("StellarSolver Options"));
3488 page->setIcon(QIcon(":/icons/StellarSolverIcon.png"));
3489 // connect(rotatorB, &QPushButton::clicked, dialog, &KConfigDialog::show);
3490
3491 opsPrograms = new OpsPrograms(this);
3492 page = dialog->addPage(opsPrograms, i18n("External & Online Programs"));
3493 page->setIcon(QIcon(":/icons/astrometry.svg"));
3494
3495 opsAstrometry = new OpsAstrometry(this);
3496 page = dialog->addPage(opsAstrometry, i18n("Scale & Position"));
3497 page->setIcon(QIcon(":/icons/center_telescope_red.svg"));
3498
3499 optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::AlignProfiles, dialog);
3500 page = dialog->addPage(optionsProfileEditor, i18n("Align Options Profiles Editor"));
3501 connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, [this]()
3502 {
3503 if(QFile(savedOptionsProfiles).exists())
3504 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
3505 else
3506 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
3507 opsAlign->reloadOptionsProfiles();
3508 });
3509 page->setIcon(QIcon::fromTheme("configure"));
3510
3511 connect(opsAlign, &OpsAlign::needToLoadProfile, this, [this, dialog, page](const QString & profile)
3512 {
3513 optionsProfileEditor->loadProfile(profile);
3514 dialog->setCurrentPage(page);
3515 });
3516
3517 opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this);
3518 m_IndexFilesPage = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files"));
3519 m_IndexFilesPage->setIcon(QIcon::fromTheme("map-flat"));
3520}
3521
3522void Align::setupSolutionTable()
3523{
3524 solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
3525
3526 clearAllSolutionsB->setIcon(
3527 QIcon::fromTheme("application-exit"));
3529
3530 removeSolutionB->setIcon(QIcon::fromTheme("list-remove"));
3532
3533 exportSolutionsCSV->setIcon(
3534 QIcon::fromTheme("document-save-as"));
3536
3537 autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best"));
3539
3540 connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints);
3541 connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint);
3542 connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints);
3543 connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph);
3544 connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel);
3545 connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow);
3546}
3547
3548void Align::setupPlot()
3549{
3550 double accuracyRadius = alignAccuracyThreshold->value();
3551
3552 alignPlot->setBackground(QBrush(Qt::black));
3553 alignPlot->setSelectionTolerance(10);
3554
3555 alignPlot->xAxis->setBasePen(QPen(Qt::white, 1));
3556 alignPlot->yAxis->setBasePen(QPen(Qt::white, 1));
3557
3558 alignPlot->xAxis->setTickPen(QPen(Qt::white, 1));
3559 alignPlot->yAxis->setTickPen(QPen(Qt::white, 1));
3560
3561 alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
3562 alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));
3563
3564 alignPlot->xAxis->setTickLabelColor(Qt::white);
3565 alignPlot->yAxis->setTickLabelColor(Qt::white);
3566
3567 alignPlot->xAxis->setLabelColor(Qt::white);
3568 alignPlot->yAxis->setLabelColor(Qt::white);
3569
3570 alignPlot->xAxis->setLabelFont(QFont(font().family(), 10));
3571 alignPlot->yAxis->setLabelFont(QFont(font().family(), 10));
3572
3573 alignPlot->xAxis->setLabelPadding(2);
3574 alignPlot->yAxis->setLabelPadding(2);
3575
3576 alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
3577 alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
3578 alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
3579 alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
3580 alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
3581 alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
3582
3583 alignPlot->xAxis->setLabel(i18n("dRA (arcsec)"));
3584 alignPlot->yAxis->setLabel(i18n("dDE (arcsec)"));
3585
3586 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
3587 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
3588
3589 alignPlot->setInteractions(QCP::iRangeZoom);
3590 alignPlot->setInteraction(QCP::iRangeDrag, true);
3591
3592 alignPlot->addGraph();
3593 alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone);
3594 alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15));
3595
3596 buildTarget();
3597
3598 connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip);
3599 connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange);
3600 connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange);
3601
3602 alignPlot->resize(190, 190);
3603 alignPlot->replot();
3604}
3605
3606void Align::setupFilterManager()
3607{
3608 // Do we have an existing filter manager?
3609 if (m_FilterManager)
3610 m_FilterManager->disconnect(this);
3611
3612 // Create new or refresh device
3613 Ekos::Manager::Instance()->createFilterManager(m_FilterWheel);
3614
3615 // Return global filter manager for this filter wheel.
3616 Ekos::Manager::Instance()->getFilterManager(m_FilterWheel->getDeviceName(), m_FilterManager);
3617
3618 connect(m_FilterManager.get(), &FilterManager::ready, this, [this]()
3619 {
3620 if (filterPositionPending)
3621 {
3622 m_FocusState = FOCUS_IDLE;
3623 filterPositionPending = false;
3624 captureAndSolve(false);
3625 }
3626 });
3627
3628 connect(m_FilterManager.get(), &FilterManager::failed, this, [this]()
3629 {
3630 appendLogText(i18n("Filter operation failed."));
3631 abort();
3632 }
3633 );
3634
3635 connect(m_FilterManager.get(), &FilterManager::newStatus, this, [this](Ekos::FilterState filterState)
3636 {
3637 if (filterPositionPending)
3638 {
3639 switch (filterState)
3640 {
3641 case FILTER_OFFSET:
3642 appendLogText(i18n("Changing focus offset by %1 steps...", m_FilterManager->getTargetFilterOffset()));
3643 break;
3644
3645 case FILTER_CHANGE:
3646 {
3647 const int filterComboIndex = m_FilterManager->getTargetFilterPosition() - 1;
3648 if (filterComboIndex >= 0 && filterComboIndex < alignFilter->count())
3649 appendLogText(i18n("Changing filter to %1...", alignFilter->itemText(filterComboIndex)));
3650 }
3651 break;
3652
3653 case FILTER_AUTOFOCUS:
3654 appendLogText(i18n("Auto focus on filter change..."));
3655 break;
3656
3657 default:
3658 break;
3659 }
3660 }
3661 });
3662
3663 connect(m_FilterManager.get(), &FilterManager::labelsChanged, this, &Align::checkFilter);
3664 connect(m_FilterManager.get(), &FilterManager::positionChanged, this, &Align::checkFilter);
3665}
3666
3667QVariantMap Align::getEffectiveFOV()
3668{
3669 KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs);
3670
3671 m_FOVWidth = m_FOVHeight = 0;
3672
3673 for (auto &map : effectiveFOVs)
3674 {
3675 if (map["Profile"].toString() == m_ActiveProfile->name && map["Train"].toString() == opticalTrain())
3676 {
3677 if (isEqual(map["Width"].toInt(), m_CameraWidth) &&
3678 isEqual(map["Height"].toInt(), m_CameraHeight) &&
3679 isEqual(map["PixelW"].toDouble(), m_CameraPixelWidth) &&
3680 isEqual(map["PixelH"].toDouble(), m_CameraPixelHeight) &&
3681 isEqual(map["FocalLength"].toDouble(), m_FocalLength) &&
3682 isEqual(map["FocalRedcuer"].toDouble(), m_Reducer) &&
3683 isEqual(map["FocalRatio"].toDouble(), m_FocalRatio))
3684 {
3685 m_FOVWidth = map["FovW"].toDouble();
3686 m_FOVHeight = map["FovH"].toDouble();
3687 return map;
3688 }
3689 }
3690 }
3691
3692 return QVariantMap();
3693}
3694
3695void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH)
3696{
3697 if (newFOVW < 0 || newFOVH < 0 || (isEqual(newFOVW, m_FOVWidth) && isEqual(newFOVH, m_FOVHeight)))
3698 return;
3699
3700 QVariantMap effectiveMap = getEffectiveFOV();
3701
3702 // If ID exists, delete it first.
3703 if (effectiveMap.isEmpty() == false)
3704 KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString());
3705
3706 // If FOV is 0x0, then we just remove existing effective FOV
3707 if (newFOVW == 0.0 && newFOVH == 0.0)
3708 {
3709 calculateFOV();
3710 return;
3711 }
3712
3713 effectiveMap["Profile"] = m_ActiveProfile->name;
3714 effectiveMap["Train"] = opticalTrainCombo->currentText();
3715 effectiveMap["Width"] = m_CameraWidth;
3716 effectiveMap["Height"] = m_CameraHeight;
3717 effectiveMap["PixelW"] = m_CameraPixelWidth;
3718 effectiveMap["PixelH"] = m_CameraPixelHeight;
3719 effectiveMap["FocalLength"] = m_FocalLength;
3720 effectiveMap["FocalReducer"] = m_Reducer;
3721 effectiveMap["FocalRatio"] = m_FocalRatio;
3722 effectiveMap["FovW"] = newFOVW;
3723 effectiveMap["FovH"] = newFOVH;
3724
3725 KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap);
3726
3727 calculateFOV();
3728
3729}
3730
3731void Align::zoomAlignView()
3732{
3733 m_AlignView->ZoomDefault();
3734
3735 // Frame update is not immediate to reduce too many refreshes
3736 // So emit updated frame in 500ms
3737 QTimer::singleShot(500, this, [this]()
3738 {
3739 emit newFrame(m_AlignView);
3740 });
3741}
3742
3743void Align::setAlignZoom(double scale)
3744{
3745 if (scale > 1)
3746 m_AlignView->ZoomIn();
3747 else if (scale < 1)
3748 m_AlignView->ZoomOut();
3749
3750 // Frame update is not immediate to reduce too many refreshes
3751 // So emit updated frame in 500ms
3752 QTimer::singleShot(500, this, [this]()
3753 {
3754 emit newFrame(m_AlignView);
3755 });
3756}
3757
3758void Align::syncFOV()
3759{
3760 QString newFOV = FOVOut->text();
3761 QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)");
3763 if (match.hasMatch())
3764 {
3765 double newFOVW = match.captured(1).toDouble();
3766 double newFOVH = match.captured(2).toDouble();
3767
3768 //if (newFOVW > 0 && newFOVH > 0)
3769 saveNewEffectiveFOV(newFOVW, newFOVH);
3770
3771 FOVOut->setStyleSheet(QString());
3772 }
3773 else
3774 {
3775 KSNotification::error(i18n("Invalid FOV."));
3776 FOVOut->setStyleSheet("background-color:red");
3777 }
3778}
3779
3780// m_wasSlewStarted can't be false for more than 10s after a slew starts.
3781bool Align::didSlewStart()
3782{
3783 if (m_wasSlewStarted)
3784 return true;
3785 if (slewStartTimer.isValid() && slewStartTimer.elapsed() > MAX_WAIT_FOR_SLEW_START_MSEC)
3786 {
3787 qCDebug(KSTARS_EKOS_ALIGN) << "Slew timed out...waited > 10s, it must have started already.";
3788 return true;
3789 }
3790 return false;
3791}
3792
3793void Align::setTargetCoords(double ra0, double de0)
3794{
3795 SkyPoint target;
3796 target.setRA0(ra0);
3797 target.setDec0(de0);
3798 target.updateCoordsNow(KStarsData::Instance()->updateNum());
3799 setTarget(target);
3800}
3801
3802void Align::setTarget(const SkyPoint &targetCoord)
3803{
3804 m_TargetCoord = targetCoord;
3805 qCInfo(KSTARS_EKOS_ALIGN) << "Target coordinates updated to JNow RA:" << m_TargetCoord.ra().toHMSString()
3806 << "DE:" << m_TargetCoord.dec().toDMSString();
3807}
3808
3809QList<double> Align::getTargetCoords()
3810{
3811 return QList<double>() << m_TargetCoord.ra0().Hours() << m_TargetCoord.dec0().Degrees();
3812}
3813
3814void Align::setTargetPositionAngle(double value)
3815{
3816 m_TargetPositionAngle = value;
3817 qCDebug(KSTARS_EKOS_ALIGN) << "Target PA updated to: " << m_TargetPositionAngle;
3818}
3819
3820void Align::calculateAlignTargetDiff()
3821{
3822 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
3823 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
3824 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
3825 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
3826 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
3827 matchPAHStage(PAA::PAH_THIRD_SOLVE) ||
3828 nothingR->isChecked() ||
3829 syncR->isChecked())
3830 return;
3831
3832 if (!Options::astrometryDifferentialSlewing()) // Normal align: Target coords are destinations coords
3833 {
3834 m_TargetDiffRA = (m_AlignCoord.ra().deltaAngle(m_TargetCoord.ra())).Degrees() * 3600; // arcsec
3835 m_TargetDiffDE = (m_AlignCoord.dec().deltaAngle(m_TargetCoord.dec())).Degrees() * 3600; // arcsec
3836 }
3837 else // Differential slewing: Target coords are new position coords
3838 {
3839 m_TargetDiffRA = (m_AlignCoord.ra().deltaAngle(m_DestinationCoord.ra())).Degrees() * 3600; // arcsec
3840 m_TargetDiffDE = (m_AlignCoord.dec().deltaAngle(m_DestinationCoord.dec())).Degrees() * 3600; // arcsec
3841 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Solution RA:" << m_AlignCoord.ra().toHMSString()
3842 << " DE:" << m_AlignCoord.dec().toDMSString();
3843 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Destination RA:" << m_DestinationCoord.ra().toHMSString()
3844 << " DE:" << m_DestinationCoord.dec().toDMSString();
3845 }
3846
3847 m_TargetDiffTotal = sqrt(m_TargetDiffRA * m_TargetDiffRA + m_TargetDiffDE * m_TargetDiffDE);
3848
3849 errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg(
3850 QString::number(m_TargetDiffTotal, 'f', 0),
3851 QString::number(m_TargetDiffRA, 'f', 0),
3852 QString::number(m_TargetDiffDE, 'f', 0)));
3853 if (m_TargetDiffTotal <= static_cast<double>(alignAccuracyThreshold->value()))
3854 errOut->setStyleSheet("color:green");
3856 errOut->setStyleSheet("color:yellow");
3857 else
3858 errOut->setStyleSheet("color:red");
3859
3860 //This block of code will write the result into the solution table and plot it on the graph.
3861 int currentRow = solutionTable->rowCount() - 1;
3863 if (dRAReport)
3864 {
3865 dRAReport->setText(QString::number(m_TargetDiffRA, 'f', 3) + "\"");
3866 dRAReport->setTextAlignment(Qt::AlignHCenter);
3868 solutionTable->setItem(currentRow, 4, dRAReport);
3869 }
3870
3872 if (dDECReport)
3873 {
3874 dDECReport->setText(QString::number(m_TargetDiffDE, 'f', 3) + "\"");
3875 dDECReport->setTextAlignment(Qt::AlignHCenter);
3877 solutionTable->setItem(currentRow, 5, dDECReport);
3878 }
3879
3880 double raPlot = m_TargetDiffRA;
3881 double decPlot = m_TargetDiffDE;
3882 alignPlot->graph(0)->addData(raPlot, decPlot);
3883
3885 textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter);
3886
3887 textLabel->position->setType(QCPItemPosition::ptPlotCoords);
3888 textLabel->position->setCoords(raPlot, decPlot);
3889 textLabel->setColor(Qt::red);
3890 textLabel->setPadding(QMargins(0, 0, 0, 0));
3891 textLabel->setBrush(Qt::white);
3892 textLabel->setPen(Qt::NoPen);
3893 textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' ');
3894 textLabel->setFont(QFont(font().family(), 8));
3895
3896 if (!alignPlot->xAxis->range().contains(m_TargetDiffRA))
3897 {
3898 alignPlot->graph(0)->rescaleKeyAxis(true);
3899 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
3900 }
3901 if (!alignPlot->yAxis->range().contains(m_TargetDiffDE))
3902 {
3903 alignPlot->graph(0)->rescaleValueAxis(true);
3904 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
3905 }
3906 alignPlot->replot();
3907}
3908
3909QStringList Align::getStellarSolverProfiles()
3910{
3911 QStringList profiles;
3912 for (auto &param : m_StellarSolverProfiles)
3913 profiles << param.listName;
3914
3915 return profiles;
3916}
3917
3918void Align::exportSolutionPoints()
3919{
3920 if (solutionTable->rowCount() == 0)
3921 return;
3922
3923 QUrl exportFile = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Export Solution Points"),
3924 alignURLPath,
3925 "CSV File (*.csv)");
3926 if (exportFile.isEmpty()) // if user presses cancel
3927 return;
3928 if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false)
3929 exportFile.setPath(exportFile.toLocalFile() + ".csv");
3930
3931 QString path = exportFile.toLocalFile();
3932
3933 if (QFile::exists(path))
3934 {
3935 int r = KMessageBox::warningContinueCancel(nullptr,
3936 i18n("A file named \"%1\" already exists. "
3937 "Overwrite it?",
3938 exportFile.fileName()),
3939 i18n("Overwrite File?"), KStandardGuiItem::overwrite());
3940 if (r == KMessageBox::Cancel)
3941 return;
3942 }
3943
3944 if (!exportFile.isValid())
3945 {
3946 QString message = i18n("Invalid URL: %1", exportFile.url());
3947 KSNotification::sorry(message, i18n("Invalid URL"));
3948 return;
3949 }
3950
3951 QFile file;
3952 file.setFileName(path);
3953 if (!file.open(QIODevice::WriteOnly))
3954 {
3955 QString message = i18n("Unable to write to file %1", path);
3956 KSNotification::sorry(message, i18n("Could Not Open File"));
3957 return;
3958 }
3959
3960 QTextStream outstream(&file);
3961
3963
3964 outstream << "RA (J" << epoch << "),DE (J" << epoch
3965 << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << Qt::endl;
3966
3967 for (int i = 0; i < solutionTable->rowCount(); i++)
3968 {
3969 QTableWidgetItem *raCell = solutionTable->item(i, 0);
3970 QTableWidgetItem *deCell = solutionTable->item(i, 1);
3974
3975 if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell)
3976 {
3977 KSNotification::sorry(i18n("Error in table structure."));
3978 return;
3979 }
3980 dms raDMS = dms::fromString(raCell->text(), false);
3981 dms deDMS = dms::fromString(deCell->text(), true);
3982 outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ','
3983 << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ','
3984 << deErrorCell->text().remove('\"') << Qt::endl;
3985 }
3986 emit newLog(i18n("Solution Points Saved as: %1", path));
3987 file.close();
3988}
3989
3990void Align::setupPolarAlignmentAssistant()
3991{
3992 // Create PAA instance
3993 m_PolarAlignmentAssistant = new PolarAlignmentAssistant(this, m_AlignView);
3994 connect(m_PolarAlignmentAssistant, &Ekos::PAA::captureAndSolve, this, [this]()
3995 {
3996 captureAndSolve(true);
3997 });
3998 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newAlignTableResult, this, &Ekos::Align::setAlignTableResult);
3999 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newFrame, this, &Ekos::Align::newFrame);
4000 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newPAHStage, this, &Ekos::Align::processPAHStage);
4001 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newLog, this, &Ekos::Align::appendLogText);
4002
4003 tabWidget->addTab(m_PolarAlignmentAssistant, i18n("Polar Alignment"));
4004}
4005
4006void Align::setupManualRotator()
4007{
4008 if (m_ManualRotator)
4009 return;
4010
4011 m_ManualRotator = new ManualRotator(this);
4012 connect(m_ManualRotator, &Ekos::ManualRotator::captureAndSolve, this, [this]()
4013 {
4014 captureAndSolve(false);
4015 });
4016 // If user cancel manual rotator, reset load slew target PA, otherwise it will keep popping up
4017 // for any subsequent solves.
4018 connect(m_ManualRotator, &Ekos::ManualRotator::rejected, this, [this]()
4019 {
4020 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
4021 stop(ALIGN_IDLE);
4022 });
4023}
4024
4025void Align::setuptDarkProcessor()
4026{
4027 if (m_DarkProcessor)
4028 return;
4029
4030 m_DarkProcessor = new DarkProcessor(this);
4031 connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Align::appendLogText);
4032 connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
4033 {
4034 alignDarkFrame->setChecked(completed);
4035 m_AlignView->setProperty("suspended", false);
4036 if (completed)
4037 {
4038 m_AlignView->rescale(ZOOM_KEEP_LEVEL);
4039 m_AlignView->updateFrame();
4040 }
4041 setCaptureComplete();
4042 });
4043}
4044
4045void Align::processPAHStage(int stage)
4046{
4047 switch (stage)
4048 {
4049 case PAA::PAH_IDLE:
4050 // Abort any solver that might be running.
4051 // Assumes this state change won't happen randomly (e.g. in the middle of align).
4052 // Alternatively could just let the stellarsolver finish naturally.
4053 if (m_StellarSolver && m_StellarSolver->isRunning())
4054 m_StellarSolver->abort();
4055 break;
4056 case PAA::PAH_POST_REFRESH:
4057 {
4058 Options::setAstrometrySolverWCS(rememberSolverWCS);
4059 Options::setAutoWCS(rememberAutoWCS);
4060 stop(ALIGN_IDLE);
4061 }
4062 break;
4063
4064 case PAA::PAH_FIRST_CAPTURE:
4065 nothingR->setChecked(true);
4066 m_CurrentGotoMode = GOTO_NOTHING;
4067 loadSlewB->setEnabled(false);
4068
4069 rememberSolverWCS = Options::astrometrySolverWCS();
4070 rememberAutoWCS = Options::autoWCS();
4071
4072 Options::setAutoWCS(false);
4073 Options::setAstrometrySolverWCS(true);
4074 break;
4075 case PAA::PAH_SECOND_CAPTURE:
4076 case PAA::PAH_THIRD_CAPTURE:
4077 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
4078 emit newLog(i18n("Settling..."));
4079 m_CaptureTimer.start(alignSettlingTime->value());
4080 break;
4081
4082 default:
4083 break;
4084 }
4085
4086 emit newPAAStage(stage);
4087}
4088
4089bool Align::matchPAHStage(uint32_t stage)
4090{
4091 return m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->getPAHStage() == stage;
4092}
4093
4094void Align::toggleManualRotator(bool toggled)
4095{
4096 if (toggled)
4097 {
4098 m_ManualRotator->show();
4099 m_ManualRotator->raise();
4100 }
4101 else
4102 m_ManualRotator->close();
4103}
4104
4105void Align::setupOpticalTrainManager()
4106{
4107 connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &Align::refreshOpticalTrain);
4108 connect(trainB, &QPushButton::clicked, this, [this]()
4109 {
4110 OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
4111 });
4113 {
4114 ProfileSettings::Instance()->setOneSetting(ProfileSettings::AlignOpticalTrain,
4115 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
4116 refreshOpticalTrain();
4117 emit trainChanged();
4118 });
4119}
4120
4121void Align::refreshOpticalTrain()
4122{
4123 opticalTrainCombo->blockSignals(true);
4124 opticalTrainCombo->clear();
4125 opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
4126 trainB->setEnabled(true);
4127
4128 QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::AlignOpticalTrain);
4129
4130 if (trainID.isValid())
4131 {
4132 auto id = trainID.toUInt();
4133
4134 // If train not found, select the first one available.
4135 if (OpticalTrainManager::Instance()->exists(id) == false)
4136 {
4137 qCWarning(KSTARS_EKOS_ALIGN) << "Optical train doesn't exist for id" << id;
4138 id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
4139 }
4140
4141 auto name = OpticalTrainManager::Instance()->name(id);
4142
4143 opticalTrainCombo->setCurrentText(name);
4144
4145 auto scope = OpticalTrainManager::Instance()->getScope(name);
4146 m_FocalLength = scope["focal_length"].toDouble(-1);
4147 m_Aperture = scope["aperture"].toDouble(-1);
4148 m_FocalRatio = scope["focal_ratio"].toDouble(-1);
4149 m_Reducer = OpticalTrainManager::Instance()->getReducer(name);
4150
4151 // DSLR Lens Aperture
4153 m_Aperture = m_FocalLength / m_FocalRatio;
4154
4155 auto mount = OpticalTrainManager::Instance()->getMount(name);
4156 setMount(mount);
4157
4158 auto camera = OpticalTrainManager::Instance()->getCamera(name);
4159 if (camera)
4160 {
4161 camera->setScopeInfo(m_FocalLength * m_Reducer, m_Aperture);
4162 opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
4163 }
4164 setCamera(camera);
4165
4166 syncTelescopeInfo();
4167
4168 auto filterWheel = OpticalTrainManager::Instance()->getFilterWheel(name);
4169 setFilterWheel(filterWheel);
4170
4171 auto rotator = OpticalTrainManager::Instance()->getRotator(name);
4172 setRotator(rotator);
4173
4174 // Load train settings
4175 OpticalTrainSettings::Instance()->setOpticalTrainID(id);
4176 auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::Align);
4177 if (settings.isValid())
4178 {
4179 auto map = settings.toJsonObject().toVariantMap();
4180 if (map != m_Settings)
4181 setAllSettings(map);
4182 }
4183 else
4184 m_Settings = m_GlobalSettings;
4185
4186 // Need to save information used for Mosaic planner
4187 Options::setTelescopeFocalLength(m_FocalLength);
4188 Options::setCameraPixelWidth(m_CameraPixelWidth);
4189 Options::setCameraPixelHeight(m_CameraPixelHeight);
4190 Options::setCameraWidth(m_CameraWidth);
4191 Options::setCameraHeight(m_CameraHeight);
4192 }
4193
4194 opticalTrainCombo->blockSignals(false);
4195}
4196
4197void Align::syncSettings()
4198{
4199 QDoubleSpinBox *dsb = nullptr;
4200 QSpinBox *sb = nullptr;
4201 QCheckBox *cb = nullptr;
4202 QComboBox *cbox = nullptr;
4203 QRadioButton *cradio = nullptr;
4204
4205 QString key;
4206 QVariant value;
4207
4208 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
4209 {
4210 key = dsb->objectName();
4211 value = dsb->value();
4212
4213 }
4214 else if ( (sb = qobject_cast<QSpinBox*>(sender())))
4215 {
4216 key = sb->objectName();
4217 value = sb->value();
4218 }
4219 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
4220 {
4221 key = cb->objectName();
4222 value = cb->isChecked();
4223 }
4224 else if ( (cbox = qobject_cast<QComboBox*>(sender())))
4225 {
4226 key = cbox->objectName();
4227 value = cbox->currentText();
4228 }
4229 else if ( (cradio = qobject_cast<QRadioButton*>(sender())))
4230 {
4231 key = cradio->objectName();
4232 value = true;
4233 }
4234
4235 // Save immediately
4236 Options::self()->setProperty(key.toLatin1(), value);
4237
4238 m_Settings[key] = value;
4239 m_GlobalSettings[key] = value;
4240 m_DebounceTimer.start();
4241}
4242
4243///////////////////////////////////////////////////////////////////////////////////////////
4244///
4245///////////////////////////////////////////////////////////////////////////////////////////
4246void Align::settleSettings()
4247{
4248 emit settingsUpdated(getAllSettings());
4249 // Save to optical train specific settings as well
4250 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4251 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4252}
4253
4254void Align::loadGlobalSettings()
4255{
4256 QString key;
4257 QVariant value;
4258
4259 QVariantMap settings;
4260 // All Combo Boxes
4261 for (auto &oneWidget : findChildren<QComboBox*>())
4262 {
4263 if (oneWidget->objectName() == "opticalTrainCombo")
4264 continue;
4265
4266 key = oneWidget->objectName();
4267 value = Options::self()->property(key.toLatin1());
4268 if (value.isValid())
4269 {
4270 oneWidget->setCurrentText(value.toString());
4271 settings[key] = value;
4272 }
4273 }
4274
4275 // All Double Spin Boxes
4276 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4277 {
4278 key = oneWidget->objectName();
4279 value = Options::self()->property(key.toLatin1());
4280 if (value.isValid())
4281 {
4282 oneWidget->setValue(value.toDouble());
4283 settings[key] = value;
4284 }
4285 }
4286
4287 // All Spin Boxes
4288 for (auto &oneWidget : findChildren<QSpinBox*>())
4289 {
4290 key = oneWidget->objectName();
4291 value = Options::self()->property(key.toLatin1());
4292 if (value.isValid())
4293 {
4294 oneWidget->setValue(value.toInt());
4295 settings[key] = value;
4296 }
4297 }
4298
4299 // All Checkboxes
4300 for (auto &oneWidget : findChildren<QCheckBox*>())
4301 {
4302 key = oneWidget->objectName();
4303 value = Options::self()->property(key.toLatin1());
4304 if (value.isValid())
4305 {
4306 oneWidget->setChecked(value.toBool());
4307 settings[key] = value;
4308 }
4309 }
4310
4311 m_GlobalSettings = m_Settings = settings;
4312}
4313
4314
4315void Align::connectSettings()
4316{
4317 // All Combo Boxes
4318 for (auto &oneWidget : findChildren<QComboBox*>())
4319 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4320
4321 // All Double Spin Boxes
4322 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4323 connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4324
4325 // All Spin Boxes
4326 for (auto &oneWidget : findChildren<QSpinBox*>())
4327 connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4328
4329 // All Checkboxes
4330 for (auto &oneWidget : findChildren<QCheckBox*>())
4331 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4332
4333 // All Radio buttons
4334 for (auto &oneWidget : findChildren<QRadioButton*>())
4335 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4336
4337 // Train combo box should NOT be synced.
4338 disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4339}
4340
4341void Align::disconnectSettings()
4342{
4343 // All Combo Boxes
4344 for (auto &oneWidget : findChildren<QComboBox*>())
4345 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4346
4347 // All Double Spin Boxes
4348 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4349 disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4350
4351 // All Spin Boxes
4352 for (auto &oneWidget : findChildren<QSpinBox*>())
4353 disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4354
4355 // All Checkboxes
4356 for (auto &oneWidget : findChildren<QCheckBox*>())
4357 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4358
4359 // All Radio buttons
4360 for (auto &oneWidget : findChildren<QRadioButton*>())
4361 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4362
4363}
4364
4365///////////////////////////////////////////////////////////////////////////////////////////
4366///
4367///////////////////////////////////////////////////////////////////////////////////////////
4368QVariantMap Align::getAllSettings() const
4369{
4370 QVariantMap settings;
4371
4372 // All Combo Boxes
4373 for (auto &oneWidget : findChildren<QComboBox*>())
4374 settings.insert(oneWidget->objectName(), oneWidget->currentText());
4375
4376 // All Double Spin Boxes
4377 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4378 settings.insert(oneWidget->objectName(), oneWidget->value());
4379
4380 // All Spin Boxes
4381 for (auto &oneWidget : findChildren<QSpinBox*>())
4382 settings.insert(oneWidget->objectName(), oneWidget->value());
4383
4384 // All Checkboxes
4385 for (auto &oneWidget : findChildren<QCheckBox*>())
4386 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
4387
4388 return settings;
4389}
4390
4391///////////////////////////////////////////////////////////////////////////////////////////
4392///
4393///////////////////////////////////////////////////////////////////////////////////////////
4394void Align::setAllSettings(const QVariantMap &settings)
4395{
4396 // Disconnect settings that we don't end up calling syncSettings while
4397 // performing the changes.
4398 disconnectSettings();
4399
4400 for (auto &name : settings.keys())
4401 {
4402 // Combo
4403 auto comboBox = findChild<QComboBox*>(name);
4404 if (comboBox)
4405 {
4406 syncControl(settings, name, comboBox);
4407 continue;
4408 }
4409
4410 // Double spinbox
4411 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
4412 if (doubleSpinBox)
4413 {
4414 syncControl(settings, name, doubleSpinBox);
4415 continue;
4416 }
4417
4418 // spinbox
4419 auto spinBox = findChild<QSpinBox*>(name);
4420 if (spinBox)
4421 {
4422 syncControl(settings, name, spinBox);
4423 continue;
4424 }
4425
4426 // checkbox
4427 auto checkbox = findChild<QCheckBox*>(name);
4428 if (checkbox)
4429 {
4430 syncControl(settings, name, checkbox);
4431 continue;
4432 }
4433
4434 // Radio button
4436 if (radioButton)
4437 {
4438 syncControl(settings, name, radioButton);
4439 continue;
4440 }
4441 }
4442
4443 // Sync to options
4444 for (auto &key : settings.keys())
4445 {
4446 auto value = settings[key];
4447 // Save immediately
4448 Options::self()->setProperty(key.toLatin1(), value);
4449 Options::self()->save();
4450
4451 m_Settings[key] = value;
4452 m_GlobalSettings[key] = value;
4453 }
4454
4455 emit settingsUpdated(getAllSettings());
4456
4457 // Save to optical train specific settings as well
4458 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4459 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4460
4461 // Restablish connections
4462 connectSettings();
4463}
4464
4465///////////////////////////////////////////////////////////////////////////////////////////
4466///
4467///////////////////////////////////////////////////////////////////////////////////////////
4468bool Align::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
4469{
4470 QSpinBox *pSB = nullptr;
4471 QDoubleSpinBox *pDSB = nullptr;
4472 QCheckBox *pCB = nullptr;
4473 QComboBox *pComboBox = nullptr;
4474 QRadioButton *pRadioButton = nullptr;
4475 bool ok = false;
4476
4477 if ((pSB = qobject_cast<QSpinBox *>(widget)))
4478 {
4479 const int value = settings[key].toInt(&ok);
4480 if (ok)
4481 {
4482 pSB->setValue(value);
4483 return true;
4484 }
4485 }
4486 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
4487 {
4488 const double value = settings[key].toDouble(&ok);
4489 if (ok)
4490 {
4491 pDSB->setValue(value);
4492 return true;
4493 }
4494 }
4495 else if ((pCB = qobject_cast<QCheckBox *>(widget)))
4496 {
4497 const bool value = settings[key].toBool();
4498 if (value != pCB->isChecked())
4499 pCB->click();
4500 return true;
4501 }
4502 // ONLY FOR STRINGS, not INDEX
4503 else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
4504 {
4505 const QString value = settings[key].toString();
4506 pComboBox->setCurrentText(value);
4507 return true;
4508 }
4509 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
4510 {
4511 const bool value = settings[key].toBool();
4512 if (value)
4513 pRadioButton->click();
4514 return true;
4515 }
4516
4517 return false;
4518}
4519
4520void Align::setState(AlignState value)
4521{
4522 qCDebug(KSTARS_EKOS_ALIGN) << "Align state changed to" << getAlignStatusString(value);
4523 state = value;
4524};
4525
4526void Align::processCaptureTimeout()
4527{
4528 if (m_CaptureTimeoutCounter++ > 3)
4529 {
4530 appendLogText(i18n("Capture timed out."));
4531 m_CaptureTimer.stop();
4532 abort();
4533 }
4534 else
4535 {
4536 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
4537 if (targetChip->isCapturing())
4538 {
4539 appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500));
4540 targetChip->abortExposure();
4541 m_CaptureTimer.start( m_CaptureTimer.interval() * 2);
4542 }
4543 else
4544 {
4545 setAlignTableResult(ALIGN_RESULT_FAILED);
4546 if (m_resetCaptureTimeoutCounter)
4547 {
4548 m_resetCaptureTimeoutCounter = false;
4549 m_CaptureTimeoutCounter = 0;
4550 }
4551 captureAndSolve(false);
4552 }
4553 }
4554}
4555}
void Slew()
Slew the telescope to the solved alignment coordinate.
Definition align.cpp:2940
static QStringList generateRemoteOptions(const QVariantMap &optionsMap)
generateOptions Generate astrometry.net option given the supplied map
Definition align.cpp:1188
void checkCameraExposureProgress(ISD::CameraChip *targetChip, double remaining, IPState state)
checkCameraExposureProgress Track the progress of CCD exposure
Definition align.cpp:3272
QStringList generateRemoteArgs(const QSharedPointer< FITSData > &imageData)
Generate arguments we pass to the remote solver.
Definition align.cpp:1278
Q_SCRIPTABLE bool setFilter(const QString &filter)
DBUS interface function.
Definition align.cpp:3157
void syncCameraInfo()
CCD information is updated, sync them.
Definition align.cpp:856
bool setCamera(ISD::Camera *device)
Add Camera to the list of available Cameras.
Definition align.cpp:637
bool setDome(ISD::Dome *device)
Add new Dome.
Definition align.cpp:744
bool setMount(ISD::Mount *device)
Add new mount.
Definition align.cpp:687
void syncCameraControls()
syncCCDControls Update camera controls like gain, offset, ISO..etc.
Definition align.cpp:907
void checkCamera()
Check CCD and make sure information is updated and FOV is re-calculated.
Definition align.cpp:599
bool isParserOK()
Does our parser exist in the system?
Definition align.cpp:525
Q_SCRIPTABLE QList< double > getSolutionResult()
DBUS interface function.
Definition align.cpp:2588
Q_SCRIPTABLE Q_NOREPLY void startSolving()
DBUS interface function.
Definition align.cpp:1810
void refreshAlignOptions()
refreshAlignOptions is called when settings are updated in OpsAlign.
Definition align.cpp:3469
void getFOVScale(double &fov_w, double &fov_h, double &fov_scale)
getFOVScale Returns calculated FOV values
Definition align.cpp:966
void executeGOTO()
After a solver process is completed successfully, sync, slew to target, or do nothing as set by the u...
Definition align.cpp:2900
bool loadAndSlew(const QByteArray &image, const QString &extension)
DBUS interface function.
Definition align.cpp:3071
void setAstrometryDevice(const QSharedPointer< ISD::GenericDevice > &device)
setAstrometryDevice
Definition align.cpp:3456
void solverFinished(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
Solver finished successfully, process the data and execute the required actions depending on the mode...
Definition align.cpp:2062
bool checkIfRotationRequired()
checkIfRotationRequired Check whether we need to perform an ALIGN_ROTATING action,...
Definition align.cpp:2423
bool setFilterWheel(ISD::FilterWheel *device)
addFilterWheel Add new filter wheel filter device.
Definition align.cpp:3114
void Sync()
Sync the telescope to the solved alignment coordinate.
Definition align.cpp:2922
void checkFilter()
Check Filter and make sure information is updated accordingly.
Definition align.cpp:3174
void setRotator(ISD::Rotator *device)
Add new Rotator.
Definition align.cpp:3208
void processData(const QSharedPointer< FITSData > &data)
Process new FITS received from CCD.
Definition align.cpp:1689
void getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale)
getCalculatedFOVScale Get calculated FOV scales from the current CCD+Telescope combination.
Definition align.cpp:1000
Q_SCRIPTABLE Q_NOREPLY void setExposure(double value)
DBUS interface function.
Definition align.cpp:3094
void settleSettings()
settleSettings Run this function after timeout from debounce timer to update database and emit settin...
Definition align.cpp:4246
Q_SCRIPTABLE Q_NOREPLY void setSolverAction(int mode)
DBUS interface function.
Definition align.cpp:1804
bool syncTelescopeInfo()
We received new telescope info, process them and update FOV.
Definition align.cpp:813
void updateProperty(INDI::Property prop)
Process updated device properties.
Definition align.cpp:2613
void solverFailed()
Process solver failure.
Definition align.cpp:2342
void SlewToTarget()
Sync the telescope to the solved alignment coordinate, and then slew to the target coordinate.
Definition align.cpp:2969
The PolarAlignmentAssistant class.
RemoteAstrometryParser invokes the remote astrometry.net solver in the remote CCD driver to solve the...
A simple class encapsulating a Field-of-View symbol.
Definition fov.h:28
CameraChip class controls a particular chip in camera.
Camera class controls an INDI Camera device.
Definition indicamera.h:47
INDI::PropertyView< ISwitch > * getSwitch(const QString &name) const
Class handles control of INDI dome devices.
Definition indidome.h:23
device handle controlling Mounts.
Definition indimount.h:27
Rotator class handles control of INDI Rotator devices.
Definition indirotator.h:20
KPageWidgetItem * addPage(QWidget *page, const QString &itemName, const QString &pixmapName=QString(), const QString &header=QString(), bool manage=true)
static KConfigDialog * exists(const QString &name)
void setCurrentPage(KPageWidgetItem *item)
void setIcon(const QIcon &icon)
static KStarsDateTime currentDateTime()
static KStars * Instance()
Definition kstars.h:123
The abstract base class for all items in a plot.
void setBrush(const QBrush &brush)
void setPen(const QPen &pen)
Holds the data of one single data point for QCPCurve.
A plottable representing a parametric curve in a plot.
@ lsNone
No line is drawn between data points (e.g. only scatters)
void setLineStyle(LineStyle style)
void setScatterStyle(const QCPScatterStyle &style)
QSharedPointer< QCPCurveDataContainer > data() const
void setScatterSkip(int skip)
@ lsNone
data points are not connected with any lines (e.g.
@ ptPlotCoords
Dynamic positioning at a plot coordinate defined by two axes (see setAxes).
A text label.
void setBrush(const QBrush &brush)
void setColor(const QColor &color)
Represents the visual appearance of scatter points.
@ ssDisc
\enumimage{ssDisc.png} a circle which is filled with the pen's color (not the brush as with ssCircle)
void mouseMove(QMouseEvent *event)
The QProgressIndicator class lets an application display a progress indicator to show that a long tas...
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:42
The sky coordinates of a point in the sky.
Definition skypoint.h:45
void apparentCoord(long double jd0, long double jdf)
Computes the apparent coordinates for this SkyPoint for any epoch, accounting for the effects of prec...
Definition skypoint.cpp:700
const CachingDms & dec() const
Definition skypoint.h:269
void setDec(dms d)
Sets Dec, the current Declination.
Definition skypoint.h:169
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:382
void setRA(dms &r)
Sets RA, the current Right Ascension.
Definition skypoint.h:144
const CachingDms & ra() const
Definition skypoint.h:263
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition skypoint.cpp:77
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition dms.cpp:429
static constexpr double DegToRad
DegToRad is a const static member equal to the number of radians in one degree (dms::PI/180....
Definition dms.h:390
Q_SCRIPTABLE Q_NOREPLY void setSolverMode(int mode)
DBUS interface function.
Definition align.cpp:556
void stop(Ekos::AlignState mode)
Stop aligning.
Definition align.cpp:2502
Q_SCRIPTABLE Q_NOREPLY void setBinningIndex(int binIndex)
DBUS interface function.
Definition align.cpp:3099
Q_SCRIPTABLE bool captureAndSolve(bool initialCall=true)
DBUS interface function.
Definition align.cpp:1411
void suspend()
Suspend aligning, recovery handled by the align module itself.
Definition align.h:407
Q_SCRIPTABLE Q_NOREPLY void abort()
DBUS interface function.
Definition align.h:399
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:78
AlignState
Definition ekos.h:145
@ ALIGN_FAILED
Alignment failed.
Definition ekos.h:148
@ ALIGN_PROGRESS
Alignment operation in progress.
Definition ekos.h:150
@ ALIGN_SUCCESSFUL
Alignment Astrometry solver successfully solved the image.
Definition ekos.h:151
@ ALIGN_SLEWING
Slewing mount to target coordinates.
Definition ekos.h:153
@ ALIGN_ABORTED
Alignment aborted by user or agent.
Definition ekos.h:149
@ ALIGN_SYNCING
Syncing mount to solution coordinates.
Definition ekos.h:152
@ ALIGN_IDLE
No ongoing operations.
Definition ekos.h:146
@ ALIGN_COMPLETE
Alignment successfully completed.
Definition ekos.h:147
@ ALIGN_SUSPENDED
Alignment operations suspended.
Definition ekos.h:155
@ ALIGN_ROTATING
Rotating (Automatic or Manual) to target position angle.
Definition ekos.h:154
@ CAPTURE_MERIDIAN_FLIP
Definition ekos.h:111
@ CAPTURE_ALIGNING
Definition ekos.h:109
ISD is a collection of INDI Standard Devices.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT SimpleJob * mount(bool ro, const QByteArray &fstype, const QString &dev, const QString &point, JobFlags flags=DefaultFlags)
QString path(const QString &relativePath)
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KGuiItem overwrite()
KGuiItem insert()
KGuiItem stop()
QString name(StandardShortcut id)
@ iRangeDrag
0x001 Axis ranges are draggable (see QCPAxisRect::setRangeDrag, QCPAxisRect::setRangeDragAxes)
@ iRangeZoom
0x002 Axis ranges are zoomable with the mouse wheel (see QCPAxisRect::setRangeZoom,...
bool isChecked() const const
void clicked(bool checked)
void toggled(bool checked)
void editingFinished()
void setEnabled(bool)
void buttonToggled(QAbstractButton *button, bool checked)
void idToggled(int id, bool checked)
void activated(int index)
void currentIndexChanged(int index)
bool registerObject(const QString &path, QObject *object, RegisterOptions options)
QDBusConnection sessionBus()
void accepted()
void rejected()
QString filePath(const QString &fileName) const const
QString homePath()
QString tempPath()
qint64 elapsed() const const
bool exists() const const
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
void setFileName(const QString &name)
virtual void close() override
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
QString absolutePath() const const
bool exists(const QString &path)
Int toInt() const const
QIcon fromTheme(const QString &name)
QVariantMap toVariantMap() const const
void editingFinished()
const_reference at(qsizetype i) const const
void clear()
iterator insert(const_iterator before, parameter_type value)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
QObject * sender() const const
T * get() const const
bool isNull() const const
void valueChanged(int i)
void splitterMoved(int pos, int index)
QString arg(Args &&... args) const const
QString number(double n, char format, int precision)
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
AlignHCenter
UniqueConnection
ItemIsSelectable
WA_LayoutUsesWidgetRect
QTextStream & bin(QTextStream &stream)
QTextStream & endl(QTextStream &stream)
void cellClicked(int row, int column)
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void setInterval(int msec)
void start()
void stop()
void timeout()
void showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
QUrl fromLocalFile(const QString &localFile)
QUuid createUuid()
bool isValid() const const
bool toBool() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
QString toString() const const
uint toUInt(bool *ok) const const
QWidget(QWidget *parent, Qt::WindowFlags f)
void setEnabled(bool)
virtual bool event(QEvent *event) override
void raise()
void repaint()
void show()
void setWindowFlags(Qt::WindowFlags type)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:02 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.