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

KDE's Doxygen guidelines are available online.