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
85using PAA = PolarAlignmentAssistant;
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 (Options::astrometryUseImageScale())
1155 {
1156 auto unitType = Options::astrometryImageScaleUnits();
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 auto 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 if (m_Dome && m_Dome->isMoving())
1539 {
1540 qCWarning(KSTARS_EKOS_ALIGN) << "Cannot capture while dome is in motion. Retrying in" << CAPTURE_RETRY_DELAY / 1000 <<
1541 "seconds...";
1542 m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1543 return true;
1544 }
1545
1546 // Let rotate the camera BEFORE taking a capture in [Capture & Solve]
1547 if (!m_SolveFromFile && m_Rotator && m_Rotator->absoluteAngleState() == IPS_BUSY)
1548 {
1549 int TimeOut = CAPTURE_ROTATOR_DELAY;
1550 switch (m_CaptureTimeoutCounter)
1551 {
1552 case 0:// Set start time & start angle and estimate rotator time frame during first timeout
1553 {
1554 auto absAngle = 0;
1555 if ((absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE")->at(0)->getValue()))
1556 {
1557 RotatorUtils::Instance()->startTimeFrame(absAngle);
1558 m_estimateRotatorTimeFrame = true;
1559 appendLogText(i18n("Cannot capture while rotator is busy: Time delay estimate started..."));
1560 }
1561 m_CaptureTimer.start(TimeOut);
1562 break;
1563 }
1564 case 1:// Use estimated time frame (in updateProperty()) for second timeout
1565 {
1566 TimeOut = m_RotatorTimeFrame * 1000;
1567 [[fallthrough]];
1568 }
1569 default:
1570 {
1571 TimeOut *= m_CaptureTimeoutCounter; // Extend Timeout in case estimated value is too short
1572 m_estimateRotatorTimeFrame = false;
1573 appendLogText(i18n("Cannot capture while rotator is busy: Retrying in %1 seconds...", TimeOut / 1000));
1574 m_CaptureTimer.start(TimeOut);
1575 }
1576 }
1577 return true; // Return value is used in 'Scheduler::startAstrometry()'
1578 }
1579
1580 m_AlignView->setBaseSize(alignWidget->size());
1581 m_AlignView->setProperty("suspended", (solverModeButtonGroup->checkedId() == SOLVER_LOCAL
1582 && alignDarkFrame->isChecked()));
1583
1584 connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
1585 connect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
1586
1587 // In case of remote solver, check if we need to update active CCD
1588 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
1589 {
1590 if (m_RemoteParserDevice == nullptr)
1591 {
1592 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
1593 setSolverMode(SOLVER_LOCAL);
1594 }
1595 else
1596 {
1597 // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD
1598 auto activeDevices = m_RemoteParserDevice->getBaseDevice().getText("ACTIVE_DEVICES");
1599 if (activeDevices)
1600 {
1601 auto activeCCD = activeDevices.findWidgetByName("ACTIVE_CCD");
1602 if (QString(activeCCD->text) != m_Camera->getDeviceName())
1603 {
1604 activeCCD->setText(m_Camera->getDeviceName().toLatin1().constData());
1605 m_RemoteParserDevice->getClientManager()->sendNewProperty(activeDevices);
1606 }
1607 }
1608
1609 // Enable remote parse
1610 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(true);
1611 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->sendArgs(generateRemoteArgs(QSharedPointer<FITSData>()));
1612 solverTimer.start();
1613 }
1614 }
1615
1616 // Remove temporary FITS files left before by the solver
1617 QDir dir(QDir::tempPath());
1618 dir.setNameFilters(QStringList() << "fits*" << "tmp.*");
1619 dir.setFilter(QDir::Files);
1620 for (auto &dirFile : dir.entryList())
1621 dir.remove(dirFile);
1622
1623 prepareCapture(targetChip);
1624
1625 // In case we're in refresh phase of the polar alignment helper then we use capture value from there
1626 if (matchPAHStage(PAA::PAH_REFRESH))
1627 targetChip->capture(m_PolarAlignmentAssistant->getPAHExposureDuration());
1628 else
1629 targetChip->capture(seqExpose);
1630
1631 solveB->setEnabled(false);
1632 loadSlewB->setEnabled(false);
1633 stopB->setEnabled(true);
1634 pi->startAnimation();
1635
1636 RotatorGOTO = false;
1637
1638 setState(ALIGN_PROGRESS);
1639 emit newStatus(state);
1640 solverFOV->setProperty("visible", true);
1641
1642 // If we're just refreshing, then we're done
1643 if (matchPAHStage(PAA::PAH_REFRESH))
1644 return true;
1645
1646 appendLogText(i18n("Capturing image..."));
1647
1648 if (!m_Mount)
1649 return true;
1650
1651 //This block of code will create the row in the solution table and populate RA, DE, and object name.
1652 //It also starts the progress indicator.
1653 double ra, dec;
1654 m_Mount->getEqCoords(&ra, &dec);
1655 if (!m_SolveFromFile)
1656 {
1657 int currentRow = solutionTable->rowCount();
1658 solutionTable->insertRow(currentRow);
1659 for (int i = 4; i < 6; i++)
1660 {
1661 QTableWidgetItem *disabledBox = new QTableWidgetItem();
1662 disabledBox->setFlags(Qt::ItemIsSelectable);
1663 solutionTable->setItem(currentRow, i, disabledBox);
1664 }
1665
1666 QTableWidgetItem *RAReport = new QTableWidgetItem();
1667 RAReport->setText(ScopeRAOut->text());
1669 RAReport->setFlags(Qt::ItemIsSelectable);
1670 solutionTable->setItem(currentRow, 0, RAReport);
1671
1672 QTableWidgetItem *DECReport = new QTableWidgetItem();
1673 DECReport->setText(ScopeDecOut->text());
1675 DECReport->setFlags(Qt::ItemIsSelectable);
1676 solutionTable->setItem(currentRow, 1, DECReport);
1677
1678 double maxrad = 1.0;
1679 SkyObject *so =
1680 KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad);
1681 QString name;
1682 if (so)
1683 {
1684 name = so->longname();
1685 }
1686 else
1687 {
1688 name = "None";
1689 }
1690 QTableWidgetItem *ObjNameReport = new QTableWidgetItem();
1691 ObjNameReport->setText(name);
1692 ObjNameReport->setTextAlignment(Qt::AlignHCenter);
1693 ObjNameReport->setFlags(Qt::ItemIsSelectable);
1694 solutionTable->setItem(currentRow, 2, ObjNameReport);
1695#ifdef Q_OS_MACOS
1696 repaint(); //This is a band-aid for a bug in QT 5.10.0
1697#endif
1698
1699 QProgressIndicator *alignIndicator = new QProgressIndicator(this);
1700 solutionTable->setCellWidget(currentRow, 3, alignIndicator);
1701 alignIndicator->startAnimation();
1702#ifdef Q_OS_MACOS
1703 repaint(); //This is a band-aid for a bug in QT 5.10.0
1704#endif
1705 }
1706
1707 return true;
1708}
1709
1711{
1712 auto chip = data->property("chip");
1713 if (chip.isValid() && chip.toInt() == ISD::CameraChip::GUIDE_CCD)
1714 return;
1715
1716 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
1717 disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
1718
1719 if (data)
1720 {
1721 m_AlignView->loadData(data);
1722 m_ImageData = data;
1723 }
1724 else
1725 m_ImageData.reset();
1726
1727 RUN_PAH(setImageData(m_ImageData));
1728
1729 // If it's Refresh, we're done
1730 if (matchPAHStage(PAA::PAH_REFRESH))
1731 {
1732 setCaptureComplete();
1733 return;
1734 }
1735 else
1736 appendLogText(i18n("Image received."));
1737
1738 // If Local solver, then set capture complete or perform calibration first.
1739 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1740 {
1741 // Only perform dark image subtraction on local images.
1742 if (alignDarkFrame->isChecked())
1743 {
1744 int x, y, w, h, binx = 1, biny = 1;
1745 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1746 targetChip->getFrame(&x, &y, &w, &h);
1747 targetChip->getBinning(&binx, &biny);
1748
1749 uint16_t offsetX = x / binx;
1750 uint16_t offsetY = y / biny;
1751
1752 m_DarkProcessor->denoise(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()),
1753 targetChip, m_ImageData, alignExposure->value(), offsetX, offsetY);
1754 return;
1755 }
1756
1757 setCaptureComplete();
1758 }
1759}
1760
1761void Align::prepareCapture(ISD::CameraChip *targetChip)
1762{
1763 if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_REMOTE)
1764 {
1765 rememberUploadMode = ISD::Camera::UPLOAD_REMOTE;
1766 m_Camera->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
1767 }
1768
1769 if (m_Camera->isFastExposureEnabled())
1770 {
1771 m_RememberCameraFastExposure = true;
1772 m_Camera->setFastExposureEnabled(false);
1773 }
1774
1775 m_Camera->setEncodingFormat("FITS");
1776 targetChip->resetFrame();
1777 targetChip->setBatchMode(false);
1778 targetChip->setCaptureMode(FITS_ALIGN);
1779 targetChip->setFrameType(FRAME_LIGHT);
1780
1781 int bin = alignBinning->currentIndex() + 1;
1782 targetChip->setBinning(bin, bin);
1783
1784 // Set gain if applicable
1785 if (m_Camera->hasGain() && alignGain->isEnabled() && alignGain->value() > alignGainSpecialValue)
1786 m_Camera->setGain(alignGain->value());
1787 // Set ISO if applicable
1788 if (alignISO->currentIndex() >= 0)
1789 targetChip->setISOIndex(alignISO->currentIndex());
1790}
1791
1792
1793void Align::setCaptureComplete()
1794{
1795 if (matchPAHStage(PAA::PAH_REFRESH))
1796 {
1797 emit newFrame(m_AlignView);
1798 m_PolarAlignmentAssistant->processPAHRefresh();
1799 return;
1800 }
1801
1802 emit newImage(m_AlignView);
1803
1804 solverFOV->setImage(m_AlignView->getDisplayImage());
1805
1806 // If align logging is enabled, let's save the frame.
1807 if (Options::saveAlignImages())
1808 {
1809 QDir dir;
1810 QDateTime now = KStarsData::Instance()->lt();
1811 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/" +
1812 now.toString("yyyy-MM-dd"));
1813 dir.mkpath(path);
1814 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
1815 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
1816 QString name = "align_frame_" + now.toString("HH-mm-ss") + ".fits";
1817 QString filename = path + QStringLiteral("/") + name;
1818 if (m_ImageData)
1819 m_ImageData->saveImage(filename);
1820 }
1821
1822 startSolving();
1823}
1824
1826{
1827 gotoModeButtonGroup->button(mode)->setChecked(true);
1828 m_CurrentGotoMode = static_cast<GotoMode>(mode);
1829}
1830
1832{
1833 //RUN_PAH(syncStage());
1834
1835 // This is needed because they might have directories stored in the config file.
1836 // So we can't just use the options folder list.
1837 QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs();
1838 disconnect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving);
1839
1840 m_UsedScale = false;
1841 m_UsedPosition = false;
1842 m_ScaleUsed = 0;
1843 m_RAUsed = 0;
1844 m_DECUsed = 0;
1845
1846 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1847 {
1848 if(Options::solverType() != SSolver::SOLVER_ASTAP
1849 && Options::solverType() != SSolver::SOLVER_WATNEYASTROMETRY) //You don't need astrometry index files to use ASTAP or Watney
1850 {
1851 bool foundAnIndex = false;
1852 for(auto &dataDir : astrometryDataDirs)
1853 {
1854 QDir dir = QDir(dataDir);
1855 if(dir.exists())
1856 {
1857 dir.setNameFilters(QStringList() << "*.fits");
1858 QStringList indexList = dir.entryList();
1859 if(indexList.count() > 0)
1860 foundAnIndex = true;
1861 }
1862 }
1863 if(!foundAnIndex)
1864 {
1865 appendLogText(
1866 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."));
1867 KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings");
1868 if(alignSettings && m_IndexFilesPage)
1869 {
1870 alignSettings->setCurrentPage(m_IndexFilesPage);
1871 alignSettings->show();
1872 }
1873 }
1874 }
1875 if (m_StellarSolver->isRunning())
1876 m_StellarSolver->abort();
1877 if (!m_ImageData)
1878 m_ImageData = m_AlignView->imageData();
1879 m_StellarSolver->loadNewImageBuffer(m_ImageData->getStatistics(), m_ImageData->getImageBuffer());
1880 m_StellarSolver->setProperty("ProcessType", SSolver::SOLVE);
1881 m_StellarSolver->setProperty("ExtractorType", Options::solveSextractorType());
1882 m_StellarSolver->setProperty("SolverType", Options::solverType());
1883 connect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
1884 m_StellarSolver->setIndexFolderPaths(Options::astrometryIndexFolderList());
1885
1886 SSolver::Parameters params;
1887 // Get solver parameters
1888 // In case of exception, use first profile
1889 try
1890 {
1891 params = m_StellarSolverProfiles.at(Options::solveOptionsProfile());
1892 }
1893 catch (std::out_of_range const &)
1894 {
1895 params = m_StellarSolverProfiles[0];
1896 }
1897
1898 params.partition = Options::stellarSolverPartition();
1899 m_StellarSolver->setParameters(params);
1900
1901 const SSolver::SolverType type = static_cast<SSolver::SolverType>(m_StellarSolver->property("SolverType").toInt());
1902 if(type == SSolver::SOLVER_LOCALASTROMETRY || type == SSolver::SOLVER_ASTAP || type == SSolver::SOLVER_WATNEYASTROMETRY)
1903 {
1904 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1905 QRegularExpression("[-{}]")));
1906 m_AlignView->saveImage(filename);
1907 m_StellarSolver->setProperty("FileToProcess", filename);
1908 ExternalProgramPaths externalPaths;
1909 externalPaths.sextractorBinaryPath = Options::sextractorBinary();
1910 externalPaths.solverPath = Options::astrometrySolverBinary();
1911 externalPaths.astapBinaryPath = Options::aSTAPExecutable();
1912 externalPaths.watneyBinaryPath = Options::watneyBinary();
1913 externalPaths.wcsPath = Options::astrometryWCSInfo();
1914 m_StellarSolver->setExternalFilePaths(externalPaths);
1915
1916 //No need for a conf file this way.
1917 m_StellarSolver->setProperty("AutoGenerateAstroConfig", true);
1918 }
1919
1920 if(type == SSolver::SOLVER_ONLINEASTROMETRY )
1921 {
1922 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1923 QRegularExpression("[-{}]")));
1924 m_AlignView->saveImage(filename);
1925
1926 m_StellarSolver->setProperty("FileToProcess", filename);
1927 m_StellarSolver->setProperty("AstrometryAPIKey", Options::astrometryAPIKey());
1928 m_StellarSolver->setProperty("AstrometryAPIURL", Options::astrometryAPIURL());
1929 }
1930
1931 bool useImageScale = Options::astrometryUseImageScale();
1932 if (useBlindScale == BLIND_ENGAGNED)
1933 {
1934 useImageScale = false;
1935 useBlindScale = BLIND_USED;
1936 appendLogText(i18n("Solving with blind image scale..."));
1937 }
1938
1939 bool useImagePosition = Options::astrometryUsePosition();
1940 if (useBlindPosition == BLIND_ENGAGNED)
1941 {
1942 useImagePosition = false;
1943 useBlindPosition = BLIND_USED;
1944 appendLogText(i18n("Solving with blind image position..."));
1945 }
1946
1947 if (m_SolveFromFile)
1948 {
1949 FITSImage::Solution solution;
1950 m_ImageData->parseSolution(solution);
1951
1952 if (useImageScale && solution.pixscale > 0)
1953 {
1954 m_UsedScale = true;
1955 m_ScaleUsed = solution.pixscale;
1956 m_StellarSolver->setSearchScale(solution.pixscale * 0.8,
1957 solution.pixscale * 1.2,
1958 SSolver::ARCSEC_PER_PIX);
1959 }
1960 else
1961 m_StellarSolver->setProperty("UseScale", false);
1962
1963 if (useImagePosition && solution.ra > 0)
1964 {
1965 m_UsedPosition = true;
1966 m_RAUsed = solution.ra;
1967 m_DECUsed = solution.dec;
1968 m_StellarSolver->setSearchPositionInDegrees(solution.ra, solution.dec);
1969 }
1970 else
1971 m_StellarSolver->setProperty("UsePosition", false);
1972
1973 QVariant value = "";
1974 if (!m_ImageData->getRecordValue("PIERSIDE", value))
1975 {
1976 appendLogText(i18n("Loaded image does not have pierside information"));
1977 m_TargetPierside = ISD::Mount::PIER_UNKNOWN;
1978 }
1979 else
1980 {
1981 appendLogText(i18n("Loaded image was taken on pierside %1", value.toString()));
1982 (value == "WEST") ? m_TargetPierside = ISD::Mount::PIER_WEST : m_TargetPierside = ISD::Mount::PIER_EAST;
1983 }
1984 RotatorUtils::Instance()->Instance()->setImagePierside(m_TargetPierside);
1985 }
1986 else
1987 {
1988 //Setting the initial search scale settings
1989 if (useImageScale)
1990 {
1991 m_UsedScale = true;
1992 m_ScaleUsed = Options::astrometryImageScaleLow();
1993
1994 SSolver::ScaleUnits units = static_cast<SSolver::ScaleUnits>(Options::astrometryImageScaleUnits());
1995 // Extend search scale from 80% to 120%
1996 m_StellarSolver->setSearchScale(Options::astrometryImageScaleLow() * 0.8,
1997 Options::astrometryImageScaleHigh() * 1.2,
1998 units);
1999 }
2000 else
2001 m_StellarSolver->setProperty("UseScale", false);
2002 //Setting the initial search location settings
2003 if(useImagePosition)
2004 {
2005 m_StellarSolver->setSearchPositionInDegrees(m_TelescopeCoord.ra().Degrees(), m_TelescopeCoord.dec().Degrees());
2006 m_UsedPosition = true;
2007 m_RAUsed = m_TelescopeCoord.ra().Degrees();
2008 m_DECUsed = m_TelescopeCoord.dec().Degrees();
2009 }
2010 else
2011 m_StellarSolver->setProperty("UsePosition", false);
2012 }
2013
2014 if(Options::alignmentLogging())
2015 {
2016 // Not trusting SSolver logging right now (Hy Aug 1, 2022)
2017 // m_StellarSolver->setLogLevel(static_cast<SSolver::logging_level>(Options::loggerLevel()));
2018 // m_StellarSolver->setSSLogLevel(SSolver::LOG_NORMAL);
2019 m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
2020 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
2021 if(Options::astrometryLogToFile())
2022 {
2023 m_StellarSolver->setProperty("LogToFile", true);
2024 m_StellarSolver->setProperty("LogFileName", Options::astrometryLogFilepath());
2025 }
2026 }
2027 else
2028 {
2029 m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
2030 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
2031 }
2032
2033 SolverUtils::patchMultiAlgorithm(m_StellarSolver.get());
2034
2035 // Start solving process
2036 m_StellarSolver->start();
2037 }
2038 else
2039 {
2040 if (m_ImageData.isNull())
2041 m_ImageData = m_AlignView->imageData();
2042 // This should run only for load&slew. For regular solve, we don't get here
2043 // as the image is read and solved server-side.
2044 remoteParser->startSolver(m_ImageData->filename(), generateRemoteArgs(m_ImageData), false);
2045 }
2046
2047 // In these cases, the error box is not used, and we don't want it polluted
2048 // from some previous operation.
2049 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
2050 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
2051 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
2052 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
2053 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
2054 matchPAHStage(PAA::PAH_THIRD_SOLVE) ||
2055 nothingR->isChecked() ||
2056 syncR->isChecked())
2057 errOut->clear();
2058
2059 // Kick off timer
2060 solverTimer.start();
2061
2062 setState(ALIGN_PROGRESS);
2063 emit newStatus(state);
2064}
2065
2066void Align::solverComplete()
2067{
2068 disconnect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
2069 if(!m_StellarSolver->solvingDone() || m_StellarSolver->failed())
2070 {
2071 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
2072 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
2073 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
2074 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
2075 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
2076 matchPAHStage(PAA::PAH_THIRD_SOLVE))
2077 {
2078 if (CHECK_PAH(processSolverFailure()))
2079 return;
2080 else
2081 setState(ALIGN_ABORTED);
2082 }
2083 solverFailed();
2084 return;
2085 }
2086 else
2087 {
2088 FITSImage::Solution solution = m_StellarSolver->getSolution();
2089 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
2090 solverFinished(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
2091 }
2092}
2093
2094void Align::solverFinished(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
2095{
2096 pi->stopAnimation();
2097 stopB->setEnabled(false);
2098 solveB->setEnabled(true);
2099
2100 sOrientation = orientation;
2101 sRA = ra;
2102 sDEC = dec;
2103
2104 double elapsed = solverTimer.elapsed() / 1000.0;
2105 if (elapsed > 0)
2106 appendLogText(i18n("Solver completed after %1 seconds.", QString::number(elapsed, 'f', 2)));
2107
2108 m_AlignTimer.stop();
2109 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice && remoteParser.get())
2110 {
2111 // Disable remote parse
2112 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(false);
2113 }
2114
2115 int binx, biny;
2116 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2117 targetChip->getBinning(&binx, &biny);
2118
2119 if (Options::alignmentLogging())
2120 {
2121 QString parityString = eastToTheRight ? "neg" : "pos";
2122 appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4) Parity (%5)", QString::number(ra, 'f', 5),
2123 QString::number(dec, 'f', 5), QString::number(orientation, 'f', 5),
2124 QString::number(pixscale, 'f', 5), parityString));
2125 }
2126
2127 // When solving (without Load&Slew), update effective FOV and focal length accordingly.
2128 if (!m_SolveFromFile &&
2129 (isEqual(m_FOVWidth, 0) || m_EffectiveFOVPending || std::fabs(pixscale - m_FOVPixelScale) > 0.005) &&
2130 pixscale > 0)
2131 {
2132 double newFOVW = m_CameraWidth * pixscale / binx / 60.0;
2133 double newFOVH = m_CameraHeight * pixscale / biny / 60.0;
2134
2135 calculateEffectiveFocalLength(newFOVW);
2136 saveNewEffectiveFOV(newFOVW, newFOVH);
2137
2138 m_EffectiveFOVPending = false;
2139 }
2140
2141 m_AlignCoord.setRA0(ra / 15.0); // set catalog coordinates
2142 m_AlignCoord.setDec0(dec);
2143
2144 // Convert to JNow
2145 m_AlignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
2146 // Get horizontal coords
2147 m_AlignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
2148
2149 // Do not update diff if we are performing load & slew.
2150 if (!m_SolveFromFile)
2151 {
2152 pixScaleOut->setText(QString::number(pixscale, 'f', 2));
2153 calculateAlignTargetDiff();
2154 }
2155
2156 // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack.
2157 // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA
2158 // PA = Orientation + 180
2159 double solverPA = KSUtils::rotationToPositionAngle(orientation);
2160 solverFOV->setCenter(m_AlignCoord);
2161 solverFOV->setPA(solverPA);
2162 solverFOV->setImageDisplay(Options::astrometrySolverOverlay());
2163 // Sensor FOV as well
2164 sensorFOV->setPA(solverPA);
2165
2166 PAOut->setText(QString::number(solverPA, 'f', 2)); // Two decimals are reasonable
2167
2168 QString ra_dms, dec_dms;
2169 getFormattedCoords(m_AlignCoord.ra().Hours(), m_AlignCoord.dec().Degrees(), ra_dms, dec_dms);
2170
2171 SolverRAOut->setText(ra_dms);
2172 SolverDecOut->setText(dec_dms);
2173
2174 if (Options::astrometrySolverWCS())
2175 {
2176 auto ccdRotation = m_Camera->getNumber("CCD_ROTATION");
2177 if (ccdRotation)
2178 {
2179 auto rotation = ccdRotation->findWidgetByName("CCD_ROTATION_VALUE");
2180 if (rotation)
2181 {
2182 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
2183 rotation->setValue(orientation);
2184 clientManager->sendNewProperty(ccdRotation);
2185
2186 if (m_wcsSynced == false)
2187 {
2188 appendLogText(
2189 i18n("WCS information updated. Images captured from this point forward shall have valid WCS."));
2190
2191 // Just send telescope info in case the CCD driver did not pick up before.
2192 auto telescopeInfo = m_Mount->getNumber("TELESCOPE_INFO");
2193 if (telescopeInfo)
2194 clientManager->sendNewProperty(telescopeInfo);
2195
2196 m_wcsSynced = true;
2197 }
2198 }
2199 }
2200 }
2201
2202 m_CaptureErrorCounter = 0;
2203 m_SlewErrorCounter = 0;
2204 m_CaptureTimeoutCounter = 0;
2205
2206 appendLogText(
2207 i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4) Target Coordinates: RA (%5) DEC (%6)",
2208 m_AlignCoord.ra().toHMSString(),
2209 m_AlignCoord.dec().toDMSString(),
2210 m_TelescopeCoord.ra().toHMSString(),
2211 m_TelescopeCoord.dec().toDMSString(),
2212 m_TargetCoord.ra().toHMSString(),
2213 m_TargetCoord.dec().toDMSString()));
2214
2215 if (!m_SolveFromFile && m_CurrentGotoMode == GOTO_SLEW)
2216 {
2217 dms diffDeg(m_TargetDiffTotal / 3600.0);
2218 appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString()));
2219 }
2220
2221 if (rememberUploadMode != m_Camera->getUploadMode())
2222 m_Camera->setUploadMode(rememberUploadMode);
2223
2224 // Remember to reset fast exposure
2225 if (m_RememberCameraFastExposure)
2226 {
2227 m_RememberCameraFastExposure = false;
2228 m_Camera->setFastExposureEnabled(true);
2229 }
2230
2231 //This block of code along with some sections in the switch below will set the status report in the solution table for this item.
2232 std::unique_ptr<QTableWidgetItem> statusReport(new QTableWidgetItem());
2233 int currentRow = solutionTable->rowCount() - 1;
2234 if (!m_SolveFromFile) // [Capture & Solve]
2235 {
2236 stopProgressAnimation();
2237 solutionTable->setCellWidget(currentRow, 3, new QWidget());
2238 statusReport->setFlags(Qt::ItemIsSelectable);
2239 // Calibration: determine camera offset from capture
2240 if (m_Rotator != nullptr && m_Rotator->isConnected())
2241 {
2242 if (auto absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE"))
2243 // if (absAngle && std::isnan(m_TargetPositionAngle) == true)
2244 {
2245 sRawAngle = absAngle->at(0)->getValue();
2246 double OffsetAngle = RotatorUtils::Instance()->calcOffsetAngle(sRawAngle, solverPA);
2247 RotatorUtils::Instance()->updateOffset(OffsetAngle);
2248 // Debug info
2249 auto reverseStatus = "Unknown";
2250 auto reverseProperty = m_Rotator->getSwitch("ROTATOR_REVERSE");
2251 if (reverseProperty)
2252 {
2253 if (reverseProperty->at(0)->getState() == ISS_ON)
2254 reverseStatus = "Reversed Direction";
2255 else
2256 reverseStatus = "Normal Direction";
2257 }
2258 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << sRawAngle << "Rotator PA:" << solverPA
2259 << "Rotator Offset:" << OffsetAngle << "Direction:" << reverseStatus;
2260 // Flow is: newSolverResults() -> capture: setAlignresult() -> RotatorSettings: refresh()
2261 emit newSolverResults(solverPA, ra, dec, pixscale);
2262 // appendLogText(i18n("Camera offset angle is %1 degrees.", OffsetAngle));
2263 appendLogText(i18n("Camera position angle is %1 degrees.", RotatorUtils::Instance()->calcCameraAngle(sRawAngle, false)));
2264 }
2265 }
2266 }
2267
2268 QJsonObject solution =
2269 {
2270 {"camera", m_Camera->getDeviceName()},
2271 {"ra", SolverRAOut->text()},
2272 {"de", SolverDecOut->text()},
2273 {"dRA", m_TargetDiffRA},
2274 {"dDE", m_TargetDiffDE},
2275 {"targetDiff", m_TargetDiffTotal},
2276 {"pix", pixscale},
2277 {"PA", solverPA},
2278 {"fov", FOVOut->text()},
2279 };
2280 emit newSolution(solution.toVariantMap());
2281
2282 setState(ALIGN_SUCCESSFUL);
2283 emit newStatus(state);
2284 solverIterations = 0;
2285 KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"),
2286 KSNotification::Align);
2287
2288 switch (m_CurrentGotoMode)
2289 {
2290 case GOTO_SYNC:
2291 executeGOTO();
2292
2293 if (!m_SolveFromFile)
2294 {
2295 stopProgressAnimation();
2296 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2297 solutionTable->setItem(currentRow, 3, statusReport.release());
2298 }
2299
2300 return;
2301
2302 case GOTO_SLEW:
2303 if (m_SolveFromFile || m_TargetDiffTotal > static_cast<double>(alignAccuracyThreshold->value()))
2304 {
2305 if (!m_SolveFromFile && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
2306 {
2307 appendLogText(i18n("Maximum number of iterations reached. Solver failed."));
2308
2309 if (!m_SolveFromFile)
2310 {
2311 statusReport->setIcon(QIcon(":/icons/AlignFailure.svg"));
2312 solutionTable->setItem(currentRow, 3, statusReport.release());
2313 }
2314
2315 solverFailed();
2316 return;
2317 }
2318
2319 targetAccuracyNotMet = true;
2320
2321 if (!m_SolveFromFile)
2322 {
2323 stopProgressAnimation();
2324 statusReport->setIcon(QIcon(":/icons/AlignWarning.svg"));
2325 solutionTable->setItem(currentRow, 3, statusReport.release());
2326 }
2327
2328 executeGOTO();
2329 return;
2330 }
2331
2332 stopProgressAnimation();
2333 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2334 solutionTable->setItem(currentRow, 3, statusReport.release());
2335
2336 appendLogText(i18n("Target is within acceptable range."));
2337 break;
2338
2339 case GOTO_NOTHING:
2340 if (!m_SolveFromFile)
2341 {
2342 stopProgressAnimation();
2343 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2344 solutionTable->setItem(currentRow, 3, statusReport.release());
2345 }
2346 break;
2347 }
2348
2349 solverFOV->setProperty("visible", true);
2350
2351 if (!matchPAHStage(PAA::PAH_IDLE))
2352 m_PolarAlignmentAssistant->processPAHStage(orientation, ra, dec, pixscale, eastToTheRight,
2353 m_StellarSolver->getSolutionHealpix(),
2354 m_StellarSolver->getSolutionIndexNumber());
2355 else
2356 {
2357
2359 {
2360 solveB->setEnabled(false);
2361 loadSlewB->setEnabled(false);
2362 return;
2363 }
2364
2365 // We are done!
2366 setState(ALIGN_COMPLETE);
2367 emit newStatus(state);
2368
2369 solveB->setEnabled(true);
2370 loadSlewB->setEnabled(true);
2371 }
2372}
2373
2375{
2376
2377 // If failed-align logging is enabled, let's save the frame.
2378 if (Options::saveFailedAlignImages())
2379 {
2380 QDir dir;
2381 QDateTime now = KStarsData::Instance()->lt();
2382 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/failed");
2383 dir.mkpath(path);
2384 QString extraFilenameInfo;
2385 if (m_UsedScale)
2386 extraFilenameInfo.append(QString("_s%1u%2").arg(m_ScaleUsed, 0, 'f', 3)
2387 .arg(Options::astrometryImageScaleUnits()));
2388 if (m_UsedPosition)
2389 extraFilenameInfo.append(QString("_r%1_d%2").arg(m_RAUsed, 0, 'f', 5).arg(m_DECUsed, 0, 'f', 5));
2390
2391 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
2392 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
2393 QString name = "failed_align_frame_" + now.toString("yyyy-MM-dd-HH-mm-ss") + extraFilenameInfo + ".fits";
2394 QString filename = path + QStringLiteral("/") + name;
2395 if (m_ImageData)
2396 {
2397 m_ImageData->saveImage(filename);
2398 appendLogText(i18n("Saving failed solver image to %1", filename));
2399 }
2400
2401 }
2402 if (state != ALIGN_ABORTED)
2403 {
2404 // Try to solve with scale turned off, if not turned off already
2405 if (Options::astrometryUseImageScale() && useBlindScale == BLIND_IDLE)
2406 {
2407 appendLogText(i18n("Solver failed. Retrying without scale constraint."));
2408 useBlindScale = BLIND_ENGAGNED;
2409 setAlignTableResult(ALIGN_RESULT_FAILED);
2410 captureAndSolve(false);
2411 return;
2412 }
2413
2414 // Try to solve with the position turned off, if not turned off already
2415 if (Options::astrometryUsePosition() && useBlindPosition == BLIND_IDLE)
2416 {
2417 appendLogText(i18n("Solver failed. Retrying without position constraint."));
2418 useBlindPosition = BLIND_ENGAGNED;
2419 setAlignTableResult(ALIGN_RESULT_FAILED);
2420 captureAndSolve(false);
2421 return;
2422 }
2423
2424
2425 appendLogText(i18n("Solver Failed."));
2426 if(!Options::alignmentLogging())
2427 appendLogText(
2428 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."));
2429
2430 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"),
2431 KSNotification::Align, KSNotification::Alert);
2432 }
2433
2434 pi->stopAnimation();
2435 stopB->setEnabled(false);
2436 solveB->setEnabled(true);
2437 loadSlewB->setEnabled(true);
2438
2439 m_AlignTimer.stop();
2440
2441 m_SolveFromFile = false;
2442 solverIterations = 0;
2443 m_CaptureErrorCounter = 0;
2444 m_CaptureTimeoutCounter = 0;
2445 m_SlewErrorCounter = 0;
2446
2447 setState(ALIGN_FAILED);
2448 emit newStatus(state);
2449
2450 solverFOV->setProperty("visible", false);
2451
2452 setAlignTableResult(ALIGN_RESULT_FAILED);
2453}
2454
2456{
2457 // Check if we need to perform any rotations.
2458 if (Options::astrometryUseRotator())
2459 {
2460 if (m_SolveFromFile) // [Load & Slew] Program flow never lands here!?
2461 {
2462 m_TargetPositionAngle = solverFOV->PA();
2463 // We are not done yet.
2464 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to:" << m_TargetPositionAngle;
2465 }
2466 else // [Capture & Solve]: "direct" or within [Load & Slew]
2467 {
2468 currentRotatorPA = solverFOV->PA();
2469 if (std::isnan(m_TargetPositionAngle) == false) // [Load & Slew] only
2470 {
2471 // If image pierside versus mount pierside is different and policy is lenient ...
2472 if (RotatorUtils::Instance()->Instance()->checkImageFlip() && (Options::astrometryFlipRotationAllowed()))
2473 {
2474 // ... calculate "flipped" PA ...
2475 sRawAngle = RotatorUtils::Instance()->calcRotatorAngle(m_TargetPositionAngle);
2476 m_TargetPositionAngle = RotatorUtils::Instance()->calcCameraAngle(sRawAngle, true);
2477 RotatorUtils::Instance()->setImagePierside(ISD::Mount::PIER_UNKNOWN); // ... once!
2478 }
2479 // Match the position angle with rotator
2480 if (m_Rotator != nullptr && m_Rotator->isConnected())
2481 {
2482 if(fabs(KSUtils::rangePA(currentRotatorPA - m_TargetPositionAngle)) * 60 >
2483 Options::astrometryRotatorThreshold())
2484 {
2485 // Signal flow: newSolverResults() -> capture: setAlignresult() -> RS: refresh()
2486 emit newSolverResults(m_TargetPositionAngle, 0, 0, 0);
2487 appendLogText(i18n("Setting camera position angle to %1 degrees ...", m_TargetPositionAngle));
2488 setState(ALIGN_ROTATING);
2489 emit newStatus(state); // Evoke 'updateProperty()' (where the same check is executed again)
2490 return true;
2491 }
2492 else
2493 {
2494 appendLogText(i18n("Camera position angle is within acceptable range."));
2495 // We're done! (Opposed to 'updateProperty()')
2496 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
2497 }
2498 }
2499 // Match the position angle manually
2500 else
2501 {
2502 double current = currentRotatorPA;
2503 double target = m_TargetPositionAngle;
2504
2505 double diff = KSUtils::rangePA(current - target);
2506 double threshold = Options::astrometryRotatorThreshold() / 60.0;
2507
2508 appendLogText(i18n("Current PA is %1; Target PA is %2; diff: %3", current, target, diff));
2509
2510 emit manualRotatorChanged(current, target, threshold);
2511
2512 m_ManualRotator->setRotatorDiff(current, target, diff);
2513 if (fabs(diff) > threshold)
2514 {
2515 targetAccuracyNotMet = true;
2516 m_ManualRotator->show();
2517 m_ManualRotator->raise();
2518 setState(ALIGN_ROTATING);
2519 emit newStatus(state);
2520 return true;
2521 }
2522 else
2523 {
2524 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
2525 targetAccuracyNotMet = false;
2526 }
2527 }
2528 }
2529 }
2530 }
2531 return false;
2532}
2533
2535{
2536 m_CaptureTimer.stop();
2537 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
2538 m_StellarSolver->abort();
2539 else if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser)
2540 remoteParser->stopSolver();
2541 //parser->stopSolver();
2542 pi->stopAnimation();
2543 stopB->setEnabled(false);
2544 solveB->setEnabled(true);
2545 loadSlewB->setEnabled(true);
2546
2547 m_SolveFromFile = false;
2548 solverIterations = 0;
2549 m_CaptureErrorCounter = 0;
2550 m_CaptureTimeoutCounter = 0;
2551 m_SlewErrorCounter = 0;
2552 m_AlignTimer.stop();
2553
2554 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
2555 disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
2556
2557 if (rememberUploadMode != m_Camera->getUploadMode())
2558 m_Camera->setUploadMode(rememberUploadMode);
2559
2560 // Remember to reset fast exposure
2561 if (m_RememberCameraFastExposure)
2562 {
2563 m_RememberCameraFastExposure = false;
2564 m_Camera->setFastExposureEnabled(true);
2565 }
2566
2567 auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2568
2569 // If capture is still in progress, let's stop that.
2570 if (matchPAHStage(PAA::PAH_POST_REFRESH))
2571 {
2572 if (targetChip->isCapturing())
2573 targetChip->abortExposure();
2574
2575 appendLogText(i18n("Refresh is complete."));
2576 }
2577 else
2578 {
2579 if (targetChip->isCapturing())
2580 {
2581 targetChip->abortExposure();
2582 appendLogText(i18n("Capture aborted."));
2583 }
2584 else
2585 {
2586 double elapsed = solverTimer.elapsed() / 1000.0;
2587 if (elapsed > 0)
2588 appendLogText(i18n("Solver aborted after %1 seconds.", QString::number(elapsed, 'f', 2)));
2589 }
2590 }
2591
2592 setState(mode);
2593 emit newStatus(state);
2594
2595 setAlignTableResult(ALIGN_RESULT_FAILED);
2596}
2597
2598QProgressIndicator * Align::getProgressStatus()
2599{
2600 int currentRow = solutionTable->rowCount() - 1;
2601
2602 // check if the current row indicates a progress state
2603 // 1. no row present
2604 if (currentRow < 0)
2605 return nullptr;
2606 // 2. indicator is not present or not a progress indicator
2607 QWidget *indicator = solutionTable->cellWidget(currentRow, 3);
2608 if (indicator == nullptr)
2609 return nullptr;
2610 return dynamic_cast<QProgressIndicator *>(indicator);
2611}
2612
2613void Align::stopProgressAnimation()
2614{
2615 QProgressIndicator *progress_indicator = getProgressStatus();
2616 if (progress_indicator != nullptr)
2617 progress_indicator->stopAnimation();
2618}
2619
2621{
2622 QList<double> result;
2623
2624 result << sOrientation << sRA << sDEC;
2625
2626 return result;
2627}
2628
2629void Align::appendLogText(const QString &text)
2630{
2631 m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
2632 KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
2633
2634 qCInfo(KSTARS_EKOS_ALIGN) << text;
2635
2636 emit newLog(text);
2637}
2638
2639void Align::clearLog()
2640{
2641 m_LogText.clear();
2642 emit newLog(QString());
2643}
2644
2645void Align::updateProperty(INDI::Property prop)
2646{
2647 if (prop.isNameMatch("EQUATORIAL_EOD_COORD") || prop.isNameMatch("EQUATORIAL_COORD"))
2648 {
2649 auto nvp = prop.getNumber();
2650 QString ra_dms, dec_dms;
2651
2652 getFormattedCoords(m_TelescopeCoord.ra().Hours(), m_TelescopeCoord.dec().Degrees(), ra_dms, dec_dms);
2653
2654 ScopeRAOut->setText(ra_dms);
2655 ScopeDecOut->setText(dec_dms);
2656
2657 switch (nvp->s)
2658 {
2659 // Idle --> Mount not tracking or slewing
2660 case IPS_IDLE:
2661 m_wasSlewStarted = false;
2662 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE";
2663 break;
2664
2665 // Ok --> Mount Tracking. If m_wasSlewStarted is true
2666 // then it just finished slewing
2667 case IPS_OK:
2668 {
2669 // Update the boxes as the mount just finished slewing
2670 if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition())
2671 {
2672 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position...";
2673 opsAstrometry->estRA->setText(ra_dms);
2674 opsAstrometry->estDec->setText(dec_dms);
2675
2676 Options::setAstrometryPositionRA(nvp->np[0].value * 15);
2677 Options::setAstrometryPositionDE(nvp->np[1].value);
2678
2679 //generateArgs();
2680 }
2681
2682 // If we are looking for celestial pole
2683 if (m_wasSlewStarted && matchPAHStage(PAA::PAH_FIND_CP))
2684 {
2685 //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE";
2686 m_wasSlewStarted = false;
2687 appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify."));
2688 setSolverAction(GOTO_NOTHING);
2689
2690 m_PolarAlignmentAssistant->setPAHStage(PAA::PAH_FIRST_CAPTURE);
2691 return;
2692 }
2693
2694 switch (state)
2695 {
2696 case ALIGN_PROGRESS:
2697 break;
2698
2699 case ALIGN_SYNCING:
2700 {
2701 m_wasSlewStarted = false;
2702 //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE";
2703 if (m_CurrentGotoMode == GOTO_SLEW)
2704 {
2705 Slew();
2706 return;
2707 }
2708 else
2709 {
2710 appendLogText(i18n("Mount is synced to solution coordinates."));
2711
2712 /* if (checkIfRotationRequired())
2713 return; */
2714
2715 KSNotification::event(QLatin1String("AlignSuccessful"),
2716 i18n("Astrometry alignment completed successfully"), KSNotification::Align);
2717 setState(ALIGN_COMPLETE);
2718 emit newStatus(state);
2719 solverIterations = 0;
2720 loadSlewB->setEnabled(true);
2721 }
2722 }
2723 break;
2724
2725 case ALIGN_SLEWING:
2726
2727 if (!didSlewStart())
2728 {
2729 // If mount has not started slewing yet, then skip
2730 qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet...";
2731 break;
2732 }
2733
2734 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed.";
2735 m_wasSlewStarted = false;
2736 if (m_SolveFromFile)
2737 {
2738 m_SolveFromFile = false;
2739 m_TargetPositionAngle = solverFOV->PA();
2740 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to" << m_TargetPositionAngle;
2741
2742 setState(ALIGN_PROGRESS);
2743 emit newStatus(state);
2744
2745 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
2746 appendLogText(i18n("Settling..."));
2747 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
2748 m_CaptureTimer.start(alignSettlingTime->value());
2749 return;
2750 }
2751 else if (m_CurrentGotoMode == GOTO_SLEW || (m_MountModel && m_MountModel->isRunning()))
2752 {
2753 if (targetAccuracyNotMet)
2754 appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again..."));
2755 else
2756 appendLogText(i18n("Slew complete. Solving Alignment Point. . ."));
2757
2758 targetAccuracyNotMet = false;
2759
2760 setState(ALIGN_PROGRESS);
2761 emit newStatus(state);
2762
2763 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
2764 appendLogText(i18n("Settling..."));
2765 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
2766 m_CaptureTimer.start(alignSettlingTime->value());
2767 }
2768 break;
2769
2770 default:
2771 {
2772 //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE";
2773 m_wasSlewStarted = false;
2774 }
2775 break;
2776 }
2777 }
2778 break;
2779
2780 // Busy --> Mount Slewing or Moving (NSWE buttons)
2781 case IPS_BUSY:
2782 {
2783 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew running.";
2784 m_wasSlewStarted = true;
2785
2786 handleMountMotion();
2787 }
2788 break;
2789
2790 // Alert --> Mount has problem moving or communicating.
2791 case IPS_ALERT:
2792 {
2793 //qCDebug(KSTARS_EKOS_ALIGN) << "IPS_ALERT --> setting slewStarted to FALSE";
2794 m_wasSlewStarted = false;
2795
2796 if (state == ALIGN_SYNCING || state == ALIGN_SLEWING)
2797 {
2798 if (state == ALIGN_SYNCING)
2799 appendLogText(i18n("Syncing failed."));
2800 else
2801 appendLogText(i18n("Slewing failed."));
2802
2803 if (++m_SlewErrorCounter == 3)
2804 {
2805 abort();
2806 return;
2807 }
2808 else
2809 {
2810 if (m_CurrentGotoMode == GOTO_SLEW)
2811 Slew();
2812 else
2813 Sync();
2814 }
2815 }
2816
2817 return;
2818 }
2819 }
2820
2821 RUN_PAH(processMountRotation(m_TelescopeCoord.ra(), alignSettlingTime->value()));
2822 }
2823 else if (prop.isNameMatch("ABS_ROTATOR_ANGLE"))
2824 {
2825 auto nvp = prop.getNumber();
2826 double RAngle = nvp->np[0].value;
2827 currentRotatorPA = RotatorUtils::Instance()->calcCameraAngle(RAngle, false);
2828 /*QString logtext = "Alignstate: " + QString::number(state)
2829 + " IPSstate: " + QString::number(nvp->s)
2830 + " Raw Rotator Angle:" + QString::number(nvp->np[0].value)
2831 + " Current PA:" + QString::number(currentRotatorPA)
2832 + " Target PA:" + QString::number(m_TargetPositionAngle)
2833 + " Offset:" + QString::number(Options::pAOffset());
2834 appendLogText(logtext);*/
2835 // loadSlewTarget defined if activation through [Load & Slew] and rotator just reached position
2836 if (std::isnan(m_TargetPositionAngle) == false && state == ALIGN_ROTATING && nvp->s == IPS_OK)
2837 {
2838 auto diff = fabs(RotatorUtils::Instance()->DiffPA(currentRotatorPA - m_TargetPositionAngle)) * 60;
2839 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << RAngle << "Current PA:" << currentRotatorPA
2840 << "Target PA:" << m_TargetPositionAngle << "Diff (arcmin):" << diff << "Offset:"
2841 << Options::pAOffset();
2842
2843 if (diff <= Options::astrometryRotatorThreshold())
2844 {
2845 appendLogText(i18n("Rotator reached camera position angle."));
2846 // Check angle once again (no slew -> no settle time)
2847 // QTimer::singleShot(alignSettlingTime->value(), this, &Ekos::Align::executeGOTO);
2848 RotatorGOTO = true; // Flag for SlewToTarget()
2849 executeGOTO();
2850 }
2851 else
2852 {
2853 // Sometimes update of "nvp->s" is a bit slow, so check state again, if it's really ok
2854 QTimer::singleShot(300, [ = ]
2855 {
2856 if (nvp->s == IPS_OK)
2857 {
2858 appendLogText(i18n("Rotator failed to arrive at the requested position angle (Deviation %1 arcmin).", diff));
2859 setState(ALIGN_FAILED);
2860 emit newStatus(state);
2861 solveB->setEnabled(true);
2862 loadSlewB->setEnabled(true);
2863 }
2864 });
2865 }
2866 }
2867 else if (m_estimateRotatorTimeFrame) // Estimate time frame during first timeout
2868 {
2869 m_RotatorTimeFrame = RotatorUtils::Instance()->calcTimeFrame(RAngle);
2870 }
2871 }
2872 else if (prop.isNameMatch("TELESCOPE_MOTION_NS") || prop.isNameMatch("TELESCOPE_MOTION_WE"))
2873 {
2874 switch (prop.getState())
2875 {
2876 case IPS_BUSY:
2877 // react upon mount motion
2878 handleMountMotion();
2879 m_wasSlewStarted = true;
2880 break;
2881 default:
2882 qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished.";
2883 handleMountStatus();
2884 break;
2885 }
2886 }
2887}
2888
2889void Align::handleMountMotion()
2890{
2891 if (state == ALIGN_PROGRESS)
2892 {
2893 if (matchPAHStage(PAA::PAH_IDLE))
2894 {
2895 // whoops, mount slews during alignment
2896 appendLogText(i18n("Slew detected, suspend solving..."));
2897 suspend();
2898 }
2899
2900 setState(ALIGN_SLEWING);
2901 }
2902}
2903
2904void Align::handleMountStatus()
2905{
2906 auto nvp = m_Mount->getNumber(m_Mount->isJ2000() ? "EQUATORIAL_COORD" :
2907 "EQUATORIAL_EOD_COORD");
2908
2909 if (nvp)
2910 updateProperty(nvp);
2911}
2912
2913
2915{
2916 if (m_SolveFromFile)
2917 {
2918 // Target coords will move the scope and in case of differential align the destination get lost.
2919 // Thus we have to save these coords for later use (-> SlewToTarget()).
2920 // This does not affect normal align, where destination is not used.
2921 m_DestinationCoord = m_AlignCoord;
2922 m_TargetCoord = m_AlignCoord;
2923
2924 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file. Setting Target Coordinates align coords. RA:"
2925 << m_TargetCoord.ra().toHMSString()
2926 << "DE:" << m_TargetCoord.dec().toDMSString();
2927
2928 SlewToTarget();
2929 }
2930 else if (m_CurrentGotoMode == GOTO_SYNC)
2931 Sync();
2932 else if (m_CurrentGotoMode == GOTO_SLEW)
2933 SlewToTarget();
2934}
2935
2937{
2938 setState(ALIGN_SYNCING);
2939
2940 if (m_Mount->Sync(&m_AlignCoord))
2941 {
2942 emit newStatus(state);
2943 appendLogText(
2944 i18n("Syncing to RA (%1) DEC (%2)", m_AlignCoord.ra().toHMSString(), m_AlignCoord.dec().toDMSString()));
2945 }
2946 else
2947 {
2948 setState(ALIGN_IDLE);
2949 emit newStatus(state);
2950 appendLogText(i18n("Syncing failed."));
2951 }
2952}
2953
2955{
2956 setState(ALIGN_SLEWING);
2957 emit newStatus(state);
2958
2959 //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2960 //m_wasSlewStarted = currentTelescope->Slew(&m_targetCoord);
2961 //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2962
2963 // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered
2964 // from Goto
2965 if (m_Mount->Slew(&m_TargetCoord))
2966 {
2967 slewStartTimer.start();
2968 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", m_TargetCoord.ra().toHMSString(),
2969 m_TargetCoord.dec().toDMSString()));
2970 }
2971 else // inform user about failure
2972 {
2973 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2) is rejected. (see notification)",
2974 m_TargetCoord.ra().toHMSString(),
2975 m_TargetCoord.dec().toDMSString()));
2976 setState(ALIGN_FAILED);
2977 emit newStatus(state);
2978 solveB->setEnabled(true);
2979 loadSlewB->setEnabled(true);
2980 }
2981}
2982
2984{
2985 if (canSync && !m_SolveFromFile)
2986 {
2987 // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided
2988#if 0
2989 if (Ekos::Manager::Instance()->getCurrentJobName().isEmpty())
2990 {
2991 KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"),
2992 i18n("Ekos job (%1) - Telescope synced",
2993 Ekos::Manager::Instance()->getCurrentJobName()));
2994 }
2995#endif
2996
2997 if (Options::astrometryDifferentialSlewing()) // Remember: Target coords will move the telescope
2998 {
2999 if (!RotatorGOTO) // Only for mount movements
3000 {
3001 m_TargetCoord.setRA(m_TargetCoord.ra() - m_AlignCoord.ra().deltaAngle(m_DestinationCoord.ra()));
3002 m_TargetCoord.setDec(m_TargetCoord.dec() - m_AlignCoord.dec().deltaAngle(m_DestinationCoord.dec()));
3003 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Target RA:" << m_TargetCoord.ra().toHMSString()
3004 << " DE:" << m_TargetCoord.dec().toDMSString();
3005 }
3006 Slew();
3007 }
3008 else
3009 Sync();
3010 return;
3011 }
3012 Slew();
3013}
3014
3015
3016void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str)
3017{
3018 dms ra_s, dec_s;
3019 ra_s.setH(ra);
3020 dec_s.setD(dec);
3021
3022 ra_str = QString("%1:%2:%3")
3023 .arg(ra_s.hour(), 2, 10, QChar('0'))
3024 .arg(ra_s.minute(), 2, 10, QChar('0'))
3025 .arg(ra_s.second(), 2, 10, QChar('0'));
3026 if (dec_s.Degrees() < 0)
3027 dec_str = QString("-%1:%2:%3")
3028 .arg(abs(dec_s.degree()), 2, 10, QChar('0'))
3029 .arg(abs(dec_s.arcmin()), 2, 10, QChar('0'))
3030 .arg(dec_s.arcsec(), 2, 10, QChar('0'));
3031 else
3032 dec_str = QString("%1:%2:%3")
3033 .arg(dec_s.degree(), 2, 10, QChar('0'))
3034 .arg(dec_s.arcmin(), 2, 10, QChar('0'))
3035 .arg(dec_s.arcsec(), 2, 10, QChar('0'));
3036}
3037
3039{
3040 if (fileURL.isEmpty())
3041 fileURL = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Load Image"), dirPath,
3042 "Images (*.fits *.fits.fz *.fit *.fts *.xisf "
3043 "*.jpg *.jpeg *.png *.gif *.bmp "
3044 "*.cr2 *.cr3 *.crw *.nef *.raf *.dng *.arw *.orf)");
3045
3046 if (fileURL.isEmpty())
3047 return false;
3048
3049 QFileInfo fileInfo(fileURL);
3050 if (fileInfo.exists() == false)
3051 return false;
3052
3053 dirPath = fileInfo.absolutePath();
3054
3055 RotatorGOTO = false;
3056
3057 m_SolveFromFile = true;
3058
3059 if (m_PolarAlignmentAssistant)
3060 m_PolarAlignmentAssistant->stopPAHProcess();
3061
3062 slewR->setChecked(true);
3063 m_CurrentGotoMode = GOTO_SLEW;
3064
3065 solveB->setEnabled(false);
3066 loadSlewB->setEnabled(false);
3067 stopB->setEnabled(true);
3068 pi->startAnimation();
3069
3070 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice == nullptr)
3071 {
3072 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
3073 setSolverMode(SOLVER_LOCAL);
3074 }
3075
3076 m_ImageData.clear();
3077
3078 m_AlignView->loadFile(fileURL);
3079 //m_FileToSolve = fileURL;
3080 connect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving);
3081
3082 return true;
3083}
3084
3085bool Align::loadAndSlew(const QByteArray &image, const QString &extension)
3086{
3087 RotatorGOTO = false;
3088 m_SolveFromFile = true;
3089 RUN_PAH(stopPAHProcess());
3090 slewR->setChecked(true);
3091 m_CurrentGotoMode = GOTO_SLEW;
3092 solveB->setEnabled(false);
3093 loadSlewB->setEnabled(false);
3094 stopB->setEnabled(true);
3095 pi->startAnimation();
3096
3097 // Must clear image data so we are forced to read the
3098 // image data again from align view when solving begins.
3099 m_ImageData.clear();
3101 data.reset(new FITSData(), &QObject::deleteLater);
3102 data->setExtension(extension);
3103 data->loadFromBuffer(image);
3104 m_AlignView->loadData(data);
3105 startSolving();
3106 return true;
3107}
3108
3109void Align::setExposure(double value)
3110{
3111 alignExposure->setValue(value);
3112}
3113
3114void Align::setBinningIndex(int binIndex)
3115{
3116 // If sender is not our combo box, then we need to update the combobox itself
3117 if (dynamic_cast<QComboBox *>(sender()) != alignBinning)
3118 {
3119 alignBinning->blockSignals(true);
3120 alignBinning->setCurrentIndex(binIndex);
3121 alignBinning->blockSignals(false);
3122 }
3123
3124 // Need to calculate FOV and args for APP
3125 if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
3126 calculateFOV();
3127}
3128
3129bool Align::setFilterWheel(ISD::FilterWheel * device)
3130{
3131 if (m_FilterWheel && m_FilterWheel == device)
3132 {
3133 checkFilter();
3134 return false;
3135 }
3136
3137 if (m_FilterWheel)
3138 m_FilterWheel->disconnect(this);
3139
3140 m_FilterWheel = device;
3141
3142 if (m_FilterWheel)
3143 {
3144 connect(m_FilterWheel, &ISD::ConcreteDevice::Connected, this, [this]()
3145 {
3146 FilterPosLabel->setEnabled(true);
3147 alignFilter->setEnabled(true);
3148 });
3149 connect(m_FilterWheel, &ISD::ConcreteDevice::Disconnected, this, [this]()
3150 {
3151 FilterPosLabel->setEnabled(false);
3152 alignFilter->setEnabled(false);
3153 });
3154 }
3155
3156 auto isConnected = m_FilterWheel && m_FilterWheel->isConnected();
3157 FilterPosLabel->setEnabled(isConnected);
3158 alignFilter->setEnabled(isConnected);
3159
3160 checkFilter();
3161 return true;
3162}
3163
3164QString Align::filterWheel()
3165{
3166 if (m_FilterWheel)
3167 return m_FilterWheel->getDeviceName();
3168
3169 return QString();
3170}
3171
3172bool Align::setFilter(const QString &filter)
3173{
3174 if (m_FilterWheel)
3175 {
3176 alignFilter->setCurrentText(filter);
3177 return true;
3178 }
3179
3180 return false;
3181}
3182
3183
3184QString Align::filter()
3185{
3186 return alignFilter->currentText();
3187}
3188
3190{
3191 alignFilter->clear();
3192
3193 if (!m_FilterWheel)
3194 {
3195 FilterPosLabel->setEnabled(false);
3196 alignFilter->setEnabled(false);
3197 return;
3198 }
3199
3200 auto isConnected = m_FilterWheel->isConnected();
3201 FilterPosLabel->setEnabled(isConnected);
3202 alignFilter->setEnabled(alignUseCurrentFilter->isChecked() == false);
3203
3204 setupFilterManager();
3205
3206 alignFilter->addItems(m_FilterManager->getFilterLabels());
3207 currentFilterPosition = m_FilterManager->getFilterPosition();
3208
3209 if (alignUseCurrentFilter->isChecked())
3210 {
3211 // use currently selected filter
3212 alignFilter->setCurrentIndex(currentFilterPosition - 1);
3213 }
3214 else
3215 {
3216 // use the fixed filter
3217 auto filter = m_Settings["alignFilter"];
3218 if (filter.isValid())
3219 alignFilter->setCurrentText(filter.toString());
3220 }
3221}
3222
3224{
3225 if ((Manager::Instance()->existRotatorController()) && (!m_Rotator || !(Device == m_Rotator)))
3226 {
3227 rotatorB->setEnabled(false);
3228 if (m_Rotator)
3229 {
3230 m_Rotator->disconnect(this);
3231 m_RotatorControlPanel->close();
3232 }
3233 m_Rotator = Device;
3234 if (m_Rotator)
3235 {
3236 if (Manager::Instance()->getRotatorController(m_Rotator->getDeviceName(), m_RotatorControlPanel))
3237 {
3238 connect(m_Rotator, &ISD::Rotator::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
3239 connect(rotatorB, &QPushButton::clicked, this, [this]()
3240 {
3241 m_RotatorControlPanel->show();
3242 m_RotatorControlPanel->raise();
3243 });
3244 rotatorB->setEnabled(true);
3245 }
3246 }
3247 }
3248}
3249
3250void Align::setWCSEnabled(bool enable)
3251{
3252 if (!m_Camera)
3253 return;
3254
3255 auto wcsControl = m_Camera->getSwitch("WCS_CONTROL");
3256
3257 if (!wcsControl)
3258 return;
3259
3260 auto wcs_enable = wcsControl->findWidgetByName("WCS_ENABLE");
3261 auto wcs_disable = wcsControl->findWidgetByName("WCS_DISABLE");
3262
3263 if (!wcs_enable || !wcs_disable)
3264 return;
3265
3266 if ((wcs_enable->getState() == ISS_ON && enable) || (wcs_disable->getState() == ISS_ON && !enable))
3267 return;
3268
3269 wcsControl->reset();
3270 if (enable)
3271 {
3272 appendLogText(i18n("World Coordinate System (WCS) is enabled."));
3273 wcs_enable->setState(ISS_ON);
3274 }
3275 else
3276 {
3277 appendLogText(i18n("World Coordinate System (WCS) is disabled."));
3278 wcs_disable->setState(ISS_ON);
3279 m_wcsSynced = false;
3280 }
3281
3282 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
3283 if (clientManager)
3284 clientManager->sendNewProperty(wcsControl);
3285}
3286
3287void Align::checkCameraExposureProgress(ISD::CameraChip * targetChip, double remaining, IPState state)
3288{
3289 INDI_UNUSED(targetChip);
3290 INDI_UNUSED(remaining);
3291
3292 if (state == IPS_ALERT)
3293 {
3294 if (++m_CaptureErrorCounter == 3 && !matchPAHStage(PolarAlignmentAssistant::PAH_REFRESH))
3295 {
3296 appendLogText(i18n("Capture error. Aborting..."));
3297 abort();
3298 return;
3299 }
3300
3301 appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter));
3302 setAlignTableResult(ALIGN_RESULT_FAILED);
3303 captureAndSolve(false);
3304 }
3305}
3306
3307void Align::setAlignTableResult(AlignResult result)
3308{
3309 // Do nothing if the progress indicator is not running.
3310 // This is necessary since it could happen that a problem occurs
3311 // before #captureAndSolve() has been started and there does not
3312 // exist a table entry for the current run.
3313 QProgressIndicator *progress_indicator = getProgressStatus();
3314 if (progress_indicator == nullptr || ! progress_indicator->isAnimated())
3315 return;
3316 stopProgressAnimation();
3317
3318 QIcon icon;
3319 switch (result)
3320 {
3321 case ALIGN_RESULT_SUCCESS:
3322 icon = QIcon(":/icons/AlignSuccess.svg");
3323 break;
3324
3325 case ALIGN_RESULT_WARNING:
3326 icon = QIcon(":/icons/AlignWarning.svg");
3327 break;
3328
3329 case ALIGN_RESULT_FAILED:
3330 default:
3331 icon = QIcon(":/icons/AlignFailure.svg");
3332 break;
3333 }
3334 int currentRow = solutionTable->rowCount() - 1;
3335 solutionTable->setCellWidget(currentRow, 3, new QWidget());
3336 QTableWidgetItem *statusReport = new QTableWidgetItem();
3337 statusReport->setIcon(icon);
3338 statusReport->setFlags(Qt::ItemIsSelectable);
3339 solutionTable->setItem(currentRow, 3, statusReport);
3340}
3341
3342void Align::setFocusStatus(Ekos::FocusState state)
3343{
3344 m_FocusState = state;
3345}
3346
3347uint8_t Align::getSolverDownsample(uint16_t binnedW)
3348{
3349 uint8_t downsample = Options::astrometryDownsample();
3350
3351 if (!Options::astrometryAutoDownsample())
3352 return downsample;
3353
3354 while (downsample < 8)
3355 {
3356 if (binnedW / downsample <= 1024)
3357 break;
3358
3359 downsample += 2;
3360 }
3361
3362 return downsample;
3363}
3364
3365void Align::setCaptureStatus(CaptureState newState)
3366{
3367 switch (newState)
3368 {
3369 case CAPTURE_ALIGNING:
3370 if (m_Mount && m_Mount->hasAlignmentModel() && Options::resetMountModelAfterMeridian())
3371 {
3372 qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (m_Mount->clearAlignmentModel() ?
3373 "successful." : "failed.");
3374 }
3375 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
3376 appendLogText(i18n("Settling..."));
3377 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
3378 m_CaptureTimer.start(alignSettlingTime->value());
3379 break;
3380 // Is this needed anymore with new flip policy? (sb 2023-10-20)
3381 // On meridian flip, reset Target Position Angle to fully rotated value
3382 // expected after MF so that we do not end up with reversed camera rotation
3384 if (std::isnan(m_TargetPositionAngle) == false)
3385 m_TargetPositionAngle = KSUtils::rangePA(m_TargetPositionAngle + 180.0);
3386 break;
3387 default:
3388 break;
3389 }
3390
3391 m_CaptureState = newState;
3392}
3393
3394void Align::showFITSViewer()
3395{
3396 static int lastFVTabID = -1;
3397 if (m_ImageData)
3398 {
3399 QUrl url = QUrl::fromLocalFile("align.fits");
3400 if (fv.isNull())
3401 {
3402 fv = KStars::Instance()->createFITSViewer();
3403 fv->loadData(m_ImageData, url, &lastFVTabID);
3404 connect(fv.get(), &FITSViewer::terminated, this, [this]()
3405 {
3406 fv.clear();
3407 });
3408 }
3409 else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
3410 fv->loadData(m_ImageData, url, &lastFVTabID);
3411
3412 fv->show();
3413 }
3414}
3415
3416void Align::toggleAlignWidgetFullScreen()
3417{
3418 if (alignWidget->parent() == nullptr)
3419 {
3420 alignWidget->setParent(this);
3421 rightLayout->insertWidget(0, alignWidget);
3422 alignWidget->showNormal();
3423 }
3424 else
3425 {
3426 alignWidget->setParent(nullptr);
3427 alignWidget->setWindowTitle(i18nc("@title:window", "Align Frame"));
3428 alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
3429 alignWidget->showMaximized();
3430 alignWidget->show();
3431 }
3432}
3433
3434void Align::setMountStatus(ISD::Mount::Status newState)
3435{
3436 switch (newState)
3437 {
3438 case ISD::Mount::MOUNT_PARKED:
3439 solveB->setEnabled(false);
3440 loadSlewB->setEnabled(false);
3441 break;
3442 case ISD::Mount::MOUNT_IDLE:
3443 solveB->setEnabled(true);
3444 loadSlewB->setEnabled(true);
3445 break;
3446 case ISD::Mount::MOUNT_PARKING:
3447 solveB->setEnabled(false);
3448 loadSlewB->setEnabled(false);
3449 break;
3450 case ISD::Mount::MOUNT_SLEWING:
3451 case ISD::Mount::MOUNT_MOVING:
3452 solveB->setEnabled(false);
3453 loadSlewB->setEnabled(false);
3454 break;
3455
3456 default:
3457 if (state != ALIGN_PROGRESS)
3458 {
3459 solveB->setEnabled(true);
3460 if (matchPAHStage(PAA::PAH_IDLE))
3461 {
3462 loadSlewB->setEnabled(true);
3463 }
3464 }
3465 break;
3466 }
3467
3468 RUN_PAH(setMountStatus(newState));
3469}
3470
3472{
3473 m_RemoteParserDevice = device;
3474
3475 remoteSolverR->setEnabled(true);
3476 if (remoteParser.get() != nullptr)
3477 {
3478 remoteParser->setAstrometryDevice(m_RemoteParserDevice);
3479 connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
3480 connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
3481 }
3482}
3483
3485{
3486 solverFOV->setImageDisplay(Options::astrometrySolverWCS());
3487 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
3488 if (m_Rotator)
3489 m_RotatorControlPanel->updateFlipPolicy(Options::astrometryFlipRotationAllowed());
3490}
3491
3492void Align::setupOptions()
3493{
3494 KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self());
3495
3496#ifdef Q_OS_MACOS
3497 dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
3498#endif
3499
3500 opsAlign = new OpsAlign(this);
3501 connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions);
3502 KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("StellarSolver Options"));
3503 page->setIcon(QIcon(":/icons/StellarSolverIcon.png"));
3504 // connect(rotatorB, &QPushButton::clicked, dialog, &KConfigDialog::show);
3505
3506 opsPrograms = new OpsPrograms(this);
3507 page = dialog->addPage(opsPrograms, i18n("External & Online Programs"));
3508 page->setIcon(QIcon(":/icons/astrometry.svg"));
3509
3510 opsAstrometry = new OpsAstrometry(this);
3511 page = dialog->addPage(opsAstrometry, i18n("Scale & Position"));
3512 page->setIcon(QIcon(":/icons/center_telescope_red.svg"));
3513
3514 optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::AlignProfiles, dialog);
3515 page = dialog->addPage(optionsProfileEditor, i18n("Align Options Profiles Editor"));
3516 connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, [this]()
3517 {
3518 if(QFile(savedOptionsProfiles).exists())
3519 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
3520 else
3521 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
3522 opsAlign->reloadOptionsProfiles();
3523 });
3524 page->setIcon(QIcon::fromTheme("configure"));
3525
3526 connect(opsAlign, &OpsAlign::needToLoadProfile, this, [this, dialog, page](const QString & profile)
3527 {
3528 optionsProfileEditor->loadProfile(profile);
3529 dialog->setCurrentPage(page);
3530 });
3531
3532 opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this);
3533 connect(opsAstrometryIndexFiles, &OpsAstrometryIndexFiles::newDownloadProgress, this, &Align::newDownloadProgress);
3534 m_IndexFilesPage = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files"));
3535 m_IndexFilesPage->setIcon(QIcon::fromTheme("map-flat"));
3536}
3537
3538void Align::setupSolutionTable()
3539{
3540 solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
3541
3542 clearAllSolutionsB->setIcon(
3543 QIcon::fromTheme("application-exit"));
3544 clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3545
3546 removeSolutionB->setIcon(QIcon::fromTheme("list-remove"));
3547 removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3548
3549 exportSolutionsCSV->setIcon(
3550 QIcon::fromTheme("document-save-as"));
3551 exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3552
3553 autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best"));
3554 autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3555
3556 connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints);
3557 connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint);
3558 connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints);
3559 connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph);
3560 connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel);
3561 connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow);
3562}
3563
3564void Align::setupPlot()
3565{
3566 double accuracyRadius = alignAccuracyThreshold->value();
3567
3568 alignPlot->setBackground(QBrush(Qt::black));
3569 alignPlot->setSelectionTolerance(10);
3570
3571 alignPlot->xAxis->setBasePen(QPen(Qt::white, 1));
3572 alignPlot->yAxis->setBasePen(QPen(Qt::white, 1));
3573
3574 alignPlot->xAxis->setTickPen(QPen(Qt::white, 1));
3575 alignPlot->yAxis->setTickPen(QPen(Qt::white, 1));
3576
3577 alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
3578 alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));
3579
3580 alignPlot->xAxis->setTickLabelColor(Qt::white);
3581 alignPlot->yAxis->setTickLabelColor(Qt::white);
3582
3583 alignPlot->xAxis->setLabelColor(Qt::white);
3584 alignPlot->yAxis->setLabelColor(Qt::white);
3585
3586 alignPlot->xAxis->setLabelFont(QFont(font().family(), 10));
3587 alignPlot->yAxis->setLabelFont(QFont(font().family(), 10));
3588
3589 alignPlot->xAxis->setLabelPadding(2);
3590 alignPlot->yAxis->setLabelPadding(2);
3591
3592 alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
3593 alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
3594 alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
3595 alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
3596 alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
3597 alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
3598
3599 alignPlot->xAxis->setLabel(i18n("dRA (arcsec)"));
3600 alignPlot->yAxis->setLabel(i18n("dDE (arcsec)"));
3601
3602 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
3603 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
3604
3605 alignPlot->setInteractions(QCP::iRangeZoom);
3606 alignPlot->setInteraction(QCP::iRangeDrag, true);
3607
3608 alignPlot->addGraph();
3609 alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone);
3610 alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15));
3611
3612 buildTarget();
3613
3614 connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip);
3615 connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange);
3616 connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange);
3617
3618 alignPlot->resize(190, 190);
3619 alignPlot->replot();
3620}
3621
3622void Align::setupFilterManager()
3623{
3624 // Do we have an existing filter manager?
3625 if (m_FilterManager)
3626 m_FilterManager->disconnect(this);
3627
3628 // Create new or refresh device
3629 Ekos::Manager::Instance()->createFilterManager(m_FilterWheel);
3630
3631 // Return global filter manager for this filter wheel.
3632 Ekos::Manager::Instance()->getFilterManager(m_FilterWheel->getDeviceName(), m_FilterManager);
3633
3634 connect(m_FilterManager.get(), &FilterManager::ready, this, [this]()
3635 {
3636 if (filterPositionPending)
3637 {
3638 m_FocusState = FOCUS_IDLE;
3639 filterPositionPending = false;
3640 captureAndSolve(false);
3641 }
3642 });
3643
3644 connect(m_FilterManager.get(), &FilterManager::failed, this, [this]()
3645 {
3646 if (filterPositionPending)
3647 {
3648 appendLogText(i18n("Filter operation failed."));
3649 abort();
3650 }
3651 });
3652
3653 connect(m_FilterManager.get(), &FilterManager::newStatus, this, [this](Ekos::FilterState filterState)
3654 {
3655 if (filterPositionPending)
3656 {
3657 switch (filterState)
3658 {
3659 case FILTER_OFFSET:
3660 appendLogText(i18n("Changing focus offset by %1 steps...", m_FilterManager->getTargetFilterOffset()));
3661 break;
3662
3663 case FILTER_CHANGE:
3664 {
3665 const int filterComboIndex = m_FilterManager->getTargetFilterPosition() - 1;
3666 if (filterComboIndex >= 0 && filterComboIndex < alignFilter->count())
3667 appendLogText(i18n("Changing filter to %1...", alignFilter->itemText(filterComboIndex)));
3668 }
3669 break;
3670
3671 case FILTER_AUTOFOCUS:
3672 appendLogText(i18n("Auto focus on filter change..."));
3673 break;
3674
3675 default:
3676 break;
3677 }
3678 }
3679 });
3680
3681 connect(m_FilterManager.get(), &FilterManager::labelsChanged, this, &Align::checkFilter);
3682 connect(m_FilterManager.get(), &FilterManager::positionChanged, this, &Align::checkFilter);
3683}
3684
3685QVariantMap Align::getEffectiveFOV()
3686{
3687 KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs);
3688
3689 m_FOVWidth = m_FOVHeight = 0;
3690
3691 for (auto &map : effectiveFOVs)
3692 {
3693 if (map["Profile"].toString() == m_ActiveProfile->name && map["Train"].toString() == opticalTrain())
3694 {
3695 if (isEqual(map["Width"].toInt(), m_CameraWidth) &&
3696 isEqual(map["Height"].toInt(), m_CameraHeight) &&
3697 isEqual(map["PixelW"].toDouble(), m_CameraPixelWidth) &&
3698 isEqual(map["PixelH"].toDouble(), m_CameraPixelHeight) &&
3699 isEqual(map["FocalLength"].toDouble(), m_FocalLength) &&
3700 isEqual(map["FocalRedcuer"].toDouble(), m_Reducer) &&
3701 isEqual(map["FocalRatio"].toDouble(), m_FocalRatio))
3702 {
3703 m_FOVWidth = map["FovW"].toDouble();
3704 m_FOVHeight = map["FovH"].toDouble();
3705 return map;
3706 }
3707 }
3708 }
3709
3710 return QVariantMap();
3711}
3712
3713void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH)
3714{
3715 if (newFOVW < 0 || newFOVH < 0 || (isEqual(newFOVW, m_FOVWidth) && isEqual(newFOVH, m_FOVHeight)))
3716 return;
3717
3718 QVariantMap effectiveMap = getEffectiveFOV();
3719
3720 // If ID exists, delete it first.
3721 if (effectiveMap.isEmpty() == false)
3722 KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString());
3723
3724 // If FOV is 0x0, then we just remove existing effective FOV
3725 if (newFOVW == 0.0 && newFOVH == 0.0)
3726 {
3727 calculateFOV();
3728 return;
3729 }
3730
3731 effectiveMap["Profile"] = m_ActiveProfile->name;
3732 effectiveMap["Train"] = opticalTrainCombo->currentText();
3733 effectiveMap["Width"] = m_CameraWidth;
3734 effectiveMap["Height"] = m_CameraHeight;
3735 effectiveMap["PixelW"] = m_CameraPixelWidth;
3736 effectiveMap["PixelH"] = m_CameraPixelHeight;
3737 effectiveMap["FocalLength"] = m_FocalLength;
3738 effectiveMap["FocalReducer"] = m_Reducer;
3739 effectiveMap["FocalRatio"] = m_FocalRatio;
3740 effectiveMap["FovW"] = newFOVW;
3741 effectiveMap["FovH"] = newFOVH;
3742
3743 KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap);
3744
3745 calculateFOV();
3746
3747}
3748
3749void Align::zoomAlignView()
3750{
3751 m_AlignView->ZoomDefault();
3752
3753 // Frame update is not immediate to reduce too many refreshes
3754 // So emit updated frame in 500ms
3755 QTimer::singleShot(500, this, [this]()
3756 {
3757 emit newFrame(m_AlignView);
3758 });
3759}
3760
3761void Align::setAlignZoom(double scale)
3762{
3763 if (scale > 1)
3764 m_AlignView->ZoomIn();
3765 else if (scale < 1)
3766 m_AlignView->ZoomOut();
3767
3768 // Frame update is not immediate to reduce too many refreshes
3769 // So emit updated frame in 500ms
3770 QTimer::singleShot(500, this, [this]()
3771 {
3772 emit newFrame(m_AlignView);
3773 });
3774}
3775
3776void Align::syncFOV()
3777{
3778 QString newFOV = FOVOut->text();
3779 QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)");
3780 QRegularExpressionMatch match = re.match(newFOV);
3781 if (match.hasMatch())
3782 {
3783 double newFOVW = match.captured(1).toDouble();
3784 double newFOVH = match.captured(2).toDouble();
3785
3786 //if (newFOVW > 0 && newFOVH > 0)
3787 saveNewEffectiveFOV(newFOVW, newFOVH);
3788
3789 FOVOut->setStyleSheet(QString());
3790 }
3791 else
3792 {
3793 KSNotification::error(i18n("Invalid FOV."));
3794 FOVOut->setStyleSheet("background-color:red");
3795 }
3796}
3797
3798// m_wasSlewStarted can't be false for more than 10s after a slew starts.
3799bool Align::didSlewStart()
3800{
3801 if (m_wasSlewStarted)
3802 return true;
3803 if (slewStartTimer.isValid() && slewStartTimer.elapsed() > MAX_WAIT_FOR_SLEW_START_MSEC)
3804 {
3805 qCDebug(KSTARS_EKOS_ALIGN) << "Slew timed out...waited > 10s, it must have started already.";
3806 return true;
3807 }
3808 return false;
3809}
3810
3811void Align::setTargetCoords(double ra0, double de0)
3812{
3813 SkyPoint target;
3814 target.setRA0(ra0);
3815 target.setDec0(de0);
3816 target.updateCoordsNow(KStarsData::Instance()->updateNum());
3817 setTarget(target);
3818}
3819
3820void Align::setTarget(const SkyPoint &targetCoord)
3821{
3822 m_TargetCoord = targetCoord;
3823 qCInfo(KSTARS_EKOS_ALIGN) << "Target coordinates updated to JNow RA:" << m_TargetCoord.ra().toHMSString()
3824 << "DE:" << m_TargetCoord.dec().toDMSString();
3825}
3826
3828{
3829 return QList<double>() << m_TargetCoord.ra0().Hours() << m_TargetCoord.dec0().Degrees();
3830}
3831
3832void Align::setTargetPositionAngle(double value)
3833{
3834 m_TargetPositionAngle = value;
3835 qCDebug(KSTARS_EKOS_ALIGN) << "Target PA updated to: " << m_TargetPositionAngle;
3836}
3837
3838void Align::calculateAlignTargetDiff()
3839{
3840 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
3841 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
3842 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
3843 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
3844 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
3845 matchPAHStage(PAA::PAH_THIRD_SOLVE) ||
3846 nothingR->isChecked() ||
3847 syncR->isChecked())
3848 return;
3849
3850 if (!Options::astrometryDifferentialSlewing()) // Normal align: Target coords are destinations coords
3851 {
3852 m_TargetDiffRA = (m_AlignCoord.ra().deltaAngle(m_TargetCoord.ra())).Degrees() * 3600; // arcsec
3853 m_TargetDiffDE = (m_AlignCoord.dec().deltaAngle(m_TargetCoord.dec())).Degrees() * 3600; // arcsec
3854 }
3855 else // Differential slewing: Target coords are new position coords
3856 {
3857 m_TargetDiffRA = (m_AlignCoord.ra().deltaAngle(m_DestinationCoord.ra())).Degrees() * 3600; // arcsec
3858 m_TargetDiffDE = (m_AlignCoord.dec().deltaAngle(m_DestinationCoord.dec())).Degrees() * 3600; // arcsec
3859 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Solution RA:" << m_AlignCoord.ra().toHMSString()
3860 << " DE:" << m_AlignCoord.dec().toDMSString();
3861 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Destination RA:" << m_DestinationCoord.ra().toHMSString()
3862 << " DE:" << m_DestinationCoord.dec().toDMSString();
3863 }
3864
3865 m_TargetDiffTotal = sqrt(m_TargetDiffRA * m_TargetDiffRA + m_TargetDiffDE * m_TargetDiffDE);
3866
3867 errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg(
3868 QString::number(m_TargetDiffTotal, 'f', 0),
3869 QString::number(m_TargetDiffRA, 'f', 0),
3870 QString::number(m_TargetDiffDE, 'f', 0)));
3871 if (m_TargetDiffTotal <= static_cast<double>(alignAccuracyThreshold->value()))
3872 errOut->setStyleSheet("color:green");
3873 else if (m_TargetDiffTotal < 1.5 * alignAccuracyThreshold->value())
3874 errOut->setStyleSheet("color:yellow");
3875 else
3876 errOut->setStyleSheet("color:red");
3877
3878 //This block of code will write the result into the solution table and plot it on the graph.
3879 int currentRow = solutionTable->rowCount() - 1;
3880 QTableWidgetItem *dRAReport = new QTableWidgetItem();
3881 if (dRAReport)
3882 {
3883 dRAReport->setText(QString::number(m_TargetDiffRA, 'f', 3) + "\"");
3885 dRAReport->setFlags(Qt::ItemIsSelectable);
3886 solutionTable->setItem(currentRow, 4, dRAReport);
3887 }
3888
3889 QTableWidgetItem *dDECReport = new QTableWidgetItem();
3890 if (dDECReport)
3891 {
3892 dDECReport->setText(QString::number(m_TargetDiffDE, 'f', 3) + "\"");
3894 dDECReport->setFlags(Qt::ItemIsSelectable);
3895 solutionTable->setItem(currentRow, 5, dDECReport);
3896 }
3897
3898 double raPlot = m_TargetDiffRA;
3899 double decPlot = m_TargetDiffDE;
3900 alignPlot->graph(0)->addData(raPlot, decPlot);
3901
3902 QCPItemText *textLabel = new QCPItemText(alignPlot);
3904
3905 textLabel->position->setType(QCPItemPosition::ptPlotCoords);
3906 textLabel->position->setCoords(raPlot, decPlot);
3907 textLabel->setColor(Qt::red);
3908 textLabel->setPadding(QMargins(0, 0, 0, 0));
3909 textLabel->setBrush(Qt::white);
3910 textLabel->setPen(Qt::NoPen);
3911 textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' ');
3912 textLabel->setFont(QFont(font().family(), 8));
3913
3914 if (!alignPlot->xAxis->range().contains(m_TargetDiffRA))
3915 {
3916 alignPlot->graph(0)->rescaleKeyAxis(true);
3917 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
3918 }
3919 if (!alignPlot->yAxis->range().contains(m_TargetDiffDE))
3920 {
3921 alignPlot->graph(0)->rescaleValueAxis(true);
3922 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
3923 }
3924 alignPlot->replot();
3925}
3926
3928{
3929 QStringList profiles;
3930 for (auto &param : m_StellarSolverProfiles)
3931 profiles << param.listName;
3932
3933 return profiles;
3934}
3935
3936void Align::exportSolutionPoints()
3937{
3938 if (solutionTable->rowCount() == 0)
3939 return;
3940
3941 QUrl exportFile = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Export Solution Points"),
3942 alignURLPath,
3943 "CSV File (*.csv)");
3944 if (exportFile.isEmpty()) // if user presses cancel
3945 return;
3946 if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false)
3947 exportFile.setPath(exportFile.toLocalFile() + ".csv");
3948
3949 QString path = exportFile.toLocalFile();
3950
3951 if (QFile::exists(path))
3952 {
3953 int r = KMessageBox::warningContinueCancel(nullptr,
3954 i18n("A file named \"%1\" already exists. "
3955 "Overwrite it?",
3956 exportFile.fileName()),
3957 i18n("Overwrite File?"), KStandardGuiItem::overwrite());
3958 if (r == KMessageBox::Cancel)
3959 return;
3960 }
3961
3962 if (!exportFile.isValid())
3963 {
3964 QString message = i18n("Invalid URL: %1", exportFile.url());
3965 KSNotification::sorry(message, i18n("Invalid URL"));
3966 return;
3967 }
3968
3969 QFile file;
3970 file.setFileName(path);
3971 if (!file.open(QIODevice::WriteOnly))
3972 {
3973 QString message = i18n("Unable to write to file %1", path);
3974 KSNotification::sorry(message, i18n("Could Not Open File"));
3975 return;
3976 }
3977
3978 QTextStream outstream(&file);
3979
3981
3982 outstream << "RA (J" << epoch << "),DE (J" << epoch
3983 << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << Qt::endl;
3984
3985 for (int i = 0; i < solutionTable->rowCount(); i++)
3986 {
3987 QTableWidgetItem *raCell = solutionTable->item(i, 0);
3988 QTableWidgetItem *deCell = solutionTable->item(i, 1);
3989 QTableWidgetItem *objNameCell = solutionTable->item(i, 2);
3990 QTableWidgetItem *raErrorCell = solutionTable->item(i, 4);
3991 QTableWidgetItem *deErrorCell = solutionTable->item(i, 5);
3992
3993 if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell)
3994 {
3995 KSNotification::sorry(i18n("Error in table structure."));
3996 return;
3997 }
3998 dms raDMS = dms::fromString(raCell->text(), false);
3999 dms deDMS = dms::fromString(deCell->text(), true);
4000 outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ','
4001 << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ','
4002 << deErrorCell->text().remove('\"') << Qt::endl;
4003 }
4004 emit newLog(i18n("Solution Points Saved as: %1", path));
4005 file.close();
4006}
4007
4008void Align::setupPolarAlignmentAssistant()
4009{
4010 // Create PAA instance
4011 m_PolarAlignmentAssistant = new PolarAlignmentAssistant(this, m_AlignView);
4012 connect(m_PolarAlignmentAssistant, &Ekos::PAA::captureAndSolve, this, [this]()
4013 {
4014 captureAndSolve(true);
4015 });
4016 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newAlignTableResult, this, &Ekos::Align::setAlignTableResult);
4017 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newFrame, this, &Ekos::Align::newFrame);
4018 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newPAHStage, this, &Ekos::Align::processPAHStage);
4019 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newLog, this, &Ekos::Align::appendLogText);
4020
4021 tabWidget->addTab(m_PolarAlignmentAssistant, i18n("Polar Alignment"));
4022}
4023
4024void Align::setupManualRotator()
4025{
4026 if (m_ManualRotator)
4027 return;
4028
4029 m_ManualRotator = new ManualRotator(this);
4030 connect(m_ManualRotator, &Ekos::ManualRotator::captureAndSolve, this, [this]()
4031 {
4032 captureAndSolve(false);
4033 });
4034 // If user cancel manual rotator, reset load slew target PA, otherwise it will keep popping up
4035 // for any subsequent solves.
4036 connect(m_ManualRotator, &Ekos::ManualRotator::rejected, this, [this]()
4037 {
4038 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
4039 // If in progress stop it
4040 if (state > ALIGN_COMPLETE)
4041 stop(ALIGN_IDLE);
4042 });
4043}
4044
4045void Align::setuptDarkProcessor()
4046{
4047 if (m_DarkProcessor)
4048 return;
4049
4050 m_DarkProcessor = new DarkProcessor(this);
4051 connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Align::appendLogText);
4052 connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
4053 {
4054 alignDarkFrame->setChecked(completed);
4055 m_AlignView->setProperty("suspended", false);
4056 if (completed)
4057 {
4058 m_AlignView->rescale(ZOOM_KEEP_LEVEL);
4059 m_AlignView->updateFrame();
4060 }
4061 setCaptureComplete();
4062 });
4063}
4064
4065void Align::processPAHStage(int stage)
4066{
4067 switch (stage)
4068 {
4069 case PAA::PAH_IDLE:
4070 // Abort any solver that might be running.
4071 // Assumes this state change won't happen randomly (e.g. in the middle of align).
4072 // Alternatively could just let the stellarsolver finish naturally.
4073 if (m_StellarSolver && m_StellarSolver->isRunning())
4074 m_StellarSolver->abort();
4075 break;
4076 case PAA::PAH_POST_REFRESH:
4077 {
4078 Options::setAstrometrySolverWCS(rememberSolverWCS);
4079 Options::setAutoWCS(rememberAutoWCS);
4080 stop(ALIGN_IDLE);
4081 }
4082 break;
4083
4084 case PAA::PAH_FIRST_CAPTURE:
4085 nothingR->setChecked(true);
4086 m_CurrentGotoMode = GOTO_NOTHING;
4087 loadSlewB->setEnabled(false);
4088
4089 rememberSolverWCS = Options::astrometrySolverWCS();
4090 rememberAutoWCS = Options::autoWCS();
4091
4092 Options::setAutoWCS(false);
4093 Options::setAstrometrySolverWCS(true);
4094 break;
4095 case PAA::PAH_SECOND_CAPTURE:
4096 case PAA::PAH_THIRD_CAPTURE:
4097 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
4098 emit newLog(i18n("Settling..."));
4099 m_CaptureTimer.start(alignSettlingTime->value());
4100 break;
4101
4102 default:
4103 break;
4104 }
4105
4106 emit newPAAStage(stage);
4107}
4108
4109bool Align::matchPAHStage(uint32_t stage)
4110{
4111 return m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->getPAHStage() == stage;
4112}
4113
4114void Align::toggleManualRotator(bool toggled)
4115{
4116 if (toggled)
4117 {
4118 m_ManualRotator->show();
4119 m_ManualRotator->raise();
4120 }
4121 else
4122 m_ManualRotator->close();
4123}
4124
4125void Align::setupOpticalTrainManager()
4126{
4127 connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &Align::refreshOpticalTrain);
4128 connect(trainB, &QPushButton::clicked, this, [this]()
4129 {
4130 OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
4131 });
4132 connect(opticalTrainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
4133 {
4134 ProfileSettings::Instance()->setOneSetting(ProfileSettings::AlignOpticalTrain,
4135 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
4136 refreshOpticalTrain();
4137 emit trainChanged();
4138 });
4139}
4140
4141void Align::refreshOpticalTrain()
4142{
4143 opticalTrainCombo->blockSignals(true);
4144 opticalTrainCombo->clear();
4145 opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
4146 trainB->setEnabled(true);
4147
4148 QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::AlignOpticalTrain);
4149
4150 if (trainID.isValid())
4151 {
4152 auto id = trainID.toUInt();
4153
4154 // If train not found, select the first one available.
4155 if (OpticalTrainManager::Instance()->exists(id) == false)
4156 {
4157 qCWarning(KSTARS_EKOS_ALIGN) << "Optical train doesn't exist for id" << id;
4158 id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
4159 }
4160
4161 auto name = OpticalTrainManager::Instance()->name(id);
4162
4163 opticalTrainCombo->setCurrentText(name);
4164
4165 auto scope = OpticalTrainManager::Instance()->getScope(name);
4166 m_FocalLength = scope["focal_length"].toDouble(-1);
4167 m_Aperture = scope["aperture"].toDouble(-1);
4168 m_FocalRatio = scope["focal_ratio"].toDouble(-1);
4169 m_Reducer = OpticalTrainManager::Instance()->getReducer(name);
4170
4171 // DSLR Lens Aperture
4172 if (m_Aperture < 0 && m_FocalRatio > 0)
4173 m_Aperture = m_FocalLength / m_FocalRatio;
4174
4175 auto mount = OpticalTrainManager::Instance()->getMount(name);
4176 setMount(mount);
4177
4178 auto camera = OpticalTrainManager::Instance()->getCamera(name);
4179 if (camera)
4180 {
4181 camera->setScopeInfo(m_FocalLength * m_Reducer, m_Aperture);
4182 opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
4183 }
4184 setCamera(camera);
4185
4186 syncTelescopeInfo();
4187
4188 auto filterWheel = OpticalTrainManager::Instance()->getFilterWheel(name);
4189 setFilterWheel(filterWheel);
4190
4191 auto rotator = OpticalTrainManager::Instance()->getRotator(name);
4192 setRotator(rotator);
4193
4194 // Load train settings
4195 OpticalTrainSettings::Instance()->setOpticalTrainID(id);
4196 auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::Align);
4197 if (settings.isValid())
4198 {
4199 auto map = settings.toJsonObject().toVariantMap();
4200 if (map != m_Settings)
4201 {
4202 m_Settings.clear();
4203 setAllSettings(map);
4204 }
4205 }
4206 else
4207 m_Settings = m_GlobalSettings;
4208
4209 // Need to save information used for Mosaic planner
4210 Options::setTelescopeFocalLength(m_FocalLength);
4211 Options::setCameraPixelWidth(m_CameraPixelWidth);
4212 Options::setCameraPixelHeight(m_CameraPixelHeight);
4213 Options::setCameraWidth(m_CameraWidth);
4214 Options::setCameraHeight(m_CameraHeight);
4215 }
4216
4217 opticalTrainCombo->blockSignals(false);
4218}
4219
4220void Align::syncSettings()
4221{
4222 QDoubleSpinBox *dsb = nullptr;
4223 QSpinBox *sb = nullptr;
4224 QCheckBox *cb = nullptr;
4225 QComboBox *cbox = nullptr;
4226 QRadioButton *cradio = nullptr;
4227
4228 QString key;
4229 QVariant value;
4230
4231 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
4232 {
4233 key = dsb->objectName();
4234 value = dsb->value();
4235
4236 }
4237 else if ( (sb = qobject_cast<QSpinBox*>(sender())))
4238 {
4239 key = sb->objectName();
4240 value = sb->value();
4241 }
4242 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
4243 {
4244 key = cb->objectName();
4245 value = cb->isChecked();
4246 }
4247 else if ( (cbox = qobject_cast<QComboBox*>(sender())))
4248 {
4249 key = cbox->objectName();
4250 value = cbox->currentText();
4251 }
4252 else if ( (cradio = qobject_cast<QRadioButton*>(sender())))
4253 {
4254 key = cradio->objectName();
4255 // Discard false requests
4256 if (cradio->isChecked() == false)
4257 {
4258 m_Settings.remove(key);
4259 return;
4260 }
4261 value = true;
4262 }
4263
4264 // Save immediately
4265 Options::self()->setProperty(key.toLatin1(), value);
4266
4267 m_Settings[key] = value;
4268 m_GlobalSettings[key] = value;
4269 m_DebounceTimer.start();
4270}
4271
4272///////////////////////////////////////////////////////////////////////////////////////////
4273///
4274///////////////////////////////////////////////////////////////////////////////////////////
4276{
4277 emit settingsUpdated(getAllSettings());
4278 // Save to optical train specific settings as well
4279 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4280 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4281}
4282
4283void Align::loadGlobalSettings()
4284{
4285 QString key;
4286 QVariant value;
4287
4288 QVariantMap settings;
4289 // All Combo Boxes
4290 for (auto &oneWidget : findChildren<QComboBox*>())
4291 {
4292 if (oneWidget->objectName() == "opticalTrainCombo")
4293 continue;
4294
4295 key = oneWidget->objectName();
4296 value = Options::self()->property(key.toLatin1());
4297 if (value.isValid() && oneWidget->count() > 0)
4298 {
4299 oneWidget->setCurrentText(value.toString());
4300 settings[key] = value;
4301 }
4302 }
4303
4304 // All Double Spin Boxes
4305 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4306 {
4307 key = oneWidget->objectName();
4308 value = Options::self()->property(key.toLatin1());
4309 if (value.isValid())
4310 {
4311 oneWidget->setValue(value.toDouble());
4312 settings[key] = value;
4313 }
4314 }
4315
4316 // All Spin Boxes
4317 for (auto &oneWidget : findChildren<QSpinBox*>())
4318 {
4319 key = oneWidget->objectName();
4320 value = Options::self()->property(key.toLatin1());
4321 if (value.isValid())
4322 {
4323 oneWidget->setValue(value.toInt());
4324 settings[key] = value;
4325 }
4326 }
4327
4328 // All Checkboxes
4329 for (auto &oneWidget : findChildren<QCheckBox*>())
4330 {
4331 key = oneWidget->objectName();
4332 value = Options::self()->property(key.toLatin1());
4333 if (value.isValid())
4334 {
4335 oneWidget->setChecked(value.toBool());
4336 settings[key] = value;
4337 }
4338 }
4339
4340 // All Radio buttons
4341 for (auto &oneWidget : findChildren<QRadioButton*>())
4342 {
4343 key = oneWidget->objectName();
4344 value = Options::self()->property(key.toLatin1());
4345 if (value.isValid())
4346 {
4347 oneWidget->setChecked(value.toBool());
4348 settings[key] = value;
4349 }
4350 }
4351
4352 m_GlobalSettings = m_Settings = settings;
4353}
4354
4355
4356void Align::connectSettings()
4357{
4358 // All Combo Boxes
4359 for (auto &oneWidget : findChildren<QComboBox*>())
4360 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4361
4362 // All Double Spin Boxes
4363 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4364 connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4365
4366 // All Spin Boxes
4367 for (auto &oneWidget : findChildren<QSpinBox*>())
4368 connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4369
4370 // All Checkboxes
4371 for (auto &oneWidget : findChildren<QCheckBox*>())
4372 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4373
4374 // All Radio buttons
4375 for (auto &oneWidget : findChildren<QRadioButton*>())
4376 connect(oneWidget, &QRadioButton::toggled, this, &Ekos::Align::syncSettings);
4377
4378 // Train combo box should NOT be synced.
4379 disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4380}
4381
4382void Align::disconnectSettings()
4383{
4384 // All Combo Boxes
4385 for (auto &oneWidget : findChildren<QComboBox*>())
4386 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4387
4388 // All Double Spin Boxes
4389 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4390 disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4391
4392 // All Spin Boxes
4393 for (auto &oneWidget : findChildren<QSpinBox*>())
4394 disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4395
4396 // All Checkboxes
4397 for (auto &oneWidget : findChildren<QCheckBox*>())
4398 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4399
4400 // All Radio buttons
4401 for (auto &oneWidget : findChildren<QRadioButton*>())
4402 disconnect(oneWidget, &QRadioButton::toggled, this, &Ekos::Align::syncSettings);
4403
4404}
4405
4406///////////////////////////////////////////////////////////////////////////////////////////
4407///
4408///////////////////////////////////////////////////////////////////////////////////////////
4409QVariantMap Align::getAllSettings() const
4410{
4411 QVariantMap settings;
4412
4413 // All Combo Boxes
4414 for (auto &oneWidget : findChildren<QComboBox*>())
4415 settings.insert(oneWidget->objectName(), oneWidget->currentText());
4416
4417 // All Double Spin Boxes
4418 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4419 settings.insert(oneWidget->objectName(), oneWidget->value());
4420
4421 // All Spin Boxes
4422 for (auto &oneWidget : findChildren<QSpinBox*>())
4423 settings.insert(oneWidget->objectName(), oneWidget->value());
4424
4425 // All Checkboxes
4426 for (auto &oneWidget : findChildren<QCheckBox*>())
4427 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
4428
4429 // All Radio Buttons
4430 for (auto &oneWidget : findChildren<QRadioButton*>())
4431 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
4432
4433 return settings;
4434}
4435
4436///////////////////////////////////////////////////////////////////////////////////////////
4437///
4438///////////////////////////////////////////////////////////////////////////////////////////
4439void Align::setAllSettings(const QVariantMap &settings)
4440{
4441 // Disconnect settings that we don't end up calling syncSettings while
4442 // performing the changes.
4443 disconnectSettings();
4444
4445 for (auto &name : settings.keys())
4446 {
4447 // Combo
4448 auto comboBox = findChild<QComboBox*>(name);
4449 if (comboBox)
4450 {
4451 syncControl(settings, name, comboBox);
4452 continue;
4453 }
4454
4455 // Double spinbox
4456 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
4457 if (doubleSpinBox)
4458 {
4459 syncControl(settings, name, doubleSpinBox);
4460 continue;
4461 }
4462
4463 // spinbox
4464 auto spinBox = findChild<QSpinBox*>(name);
4465 if (spinBox)
4466 {
4467 syncControl(settings, name, spinBox);
4468 continue;
4469 }
4470
4471 // checkbox
4472 auto checkbox = findChild<QCheckBox*>(name);
4473 if (checkbox)
4474 {
4475 syncControl(settings, name, checkbox);
4476 continue;
4477 }
4478
4479 // Radio button
4480 auto radioButton = findChild<QRadioButton*>(name);
4481 if (radioButton)
4482 {
4483 syncControl(settings, name, radioButton);
4484 continue;
4485 }
4486 }
4487
4488 // Sync to options
4489 for (auto &key : settings.keys())
4490 {
4491 auto value = settings[key];
4492 // Save immediately
4493 Options::self()->setProperty(key.toLatin1(), value);
4494 Options::self()->save();
4495
4496 m_Settings[key] = value;
4497 m_GlobalSettings[key] = value;
4498 }
4499
4500 emit settingsUpdated(getAllSettings());
4501
4502 // Save to optical train specific settings as well
4503 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4504 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4505
4506 // Restablish connections
4507 connectSettings();
4508}
4509
4510///////////////////////////////////////////////////////////////////////////////////////////
4511///
4512///////////////////////////////////////////////////////////////////////////////////////////
4513bool Align::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
4514{
4515 QSpinBox *pSB = nullptr;
4516 QDoubleSpinBox *pDSB = nullptr;
4517 QCheckBox *pCB = nullptr;
4518 QComboBox *pComboBox = nullptr;
4519 QRadioButton *pRadioButton = nullptr;
4520 bool ok = false;
4521
4522 if ((pSB = qobject_cast<QSpinBox *>(widget)))
4523 {
4524 const int value = settings[key].toInt(&ok);
4525 if (ok)
4526 {
4527 pSB->setValue(value);
4528 return true;
4529 }
4530 }
4531 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
4532 {
4533 const double value = settings[key].toDouble(&ok);
4534 if (ok)
4535 {
4536 pDSB->setValue(value);
4537 return true;
4538 }
4539 }
4540 else if ((pCB = qobject_cast<QCheckBox *>(widget)))
4541 {
4542 const bool value = settings[key].toBool();
4543 if (value != pCB->isChecked())
4544 pCB->setChecked(value);
4545 return true;
4546 }
4547 // ONLY FOR STRINGS, not INDEX
4548 else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
4549 {
4550 const QString value = settings[key].toString();
4551 pComboBox->setCurrentText(value);
4552 return true;
4553 }
4554 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
4555 {
4556 const bool value = settings[key].toBool();
4557 if (value)
4558 pRadioButton->setChecked(true);
4559 return true;
4560 }
4561
4562 return false;
4563}
4564
4565void Align::setState(AlignState value)
4566{
4567 qCDebug(KSTARS_EKOS_ALIGN) << "Align state changed to" << getAlignStatusString(value);
4568 state = value;
4569};
4570
4571void Align::processCaptureTimeout()
4572{
4573 if (m_CaptureTimeoutCounter++ > 3)
4574 {
4575 appendLogText(i18n("Capture timed out."));
4576 m_CaptureTimer.stop();
4577 abort();
4578 }
4579 else
4580 {
4581 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
4582 if (targetChip->isCapturing())
4583 {
4584 appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500));
4585 targetChip->abortExposure();
4586 m_CaptureTimer.start( m_CaptureTimer.interval() * 2);
4587 }
4588 else
4589 {
4590 setAlignTableResult(ALIGN_RESULT_FAILED);
4591 if (m_resetCaptureTimeoutCounter)
4592 {
4593 m_resetCaptureTimeoutCounter = false;
4594 m_CaptureTimeoutCounter = 0;
4595 }
4596 captureAndSolve(false);
4597 }
4598 }
4599}
4600}
void Slew()
Slew the telescope to the solved alignment coordinate.
Definition align.cpp:2954
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:3287
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:3172
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:2620
QStringList getStellarSolverProfiles()
getStellarSolverProfiles
Definition align.cpp:3927
Q_SCRIPTABLE Q_NOREPLY void startSolving()
DBUS interface function.
Definition align.cpp:1831
void refreshAlignOptions()
refreshAlignOptions is called when settings are updated in OpsAlign.
Definition align.cpp:3484
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:2914
bool loadAndSlew(const QByteArray &image, const QString &extension)
DBUS interface function.
Definition align.cpp:3085
void setAstrometryDevice(const QSharedPointer< ISD::GenericDevice > &device)
setAstrometryDevice
Definition align.cpp:3471
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:2094
bool checkIfRotationRequired()
checkIfRotationRequired Check whether we need to perform an ALIGN_ROTATING action,...
Definition align.cpp:2455
bool setFilterWheel(ISD::FilterWheel *device)
addFilterWheel Add new filter wheel filter device.
Definition align.cpp:3129
void Sync()
Sync the telescope to the solved alignment coordinate.
Definition align.cpp:2936
void checkFilter()
Check Filter and make sure information is updated accordingly.
Definition align.cpp:3189
void setRotator(ISD::Rotator *device)
Add new Rotator.
Definition align.cpp:3223
void processData(const QSharedPointer< FITSData > &data)
Process new FITS received from CCD.
Definition align.cpp:1710
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:3109
void settleSettings()
settleSettings Run this function after timeout from debounce timer to update database and emit settin...
Definition align.cpp:4275
Q_SCRIPTABLE Q_NOREPLY void setSolverAction(int mode)
DBUS interface function.
Definition align.cpp:1825
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:2645
void solverFailed()
Process solver failure.
Definition align.cpp:2374
void SlewToTarget()
Sync the telescope to the solved alignment coordinate, and then slew to the target coordinate.
Definition align.cpp:2983
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 setIcon(const QIcon &icon)
const KStarsDateTime & lt() const
Definition kstarsdata.h:153
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.
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:50
virtual QString longname(void) const
Definition skyobject.h:182
The sky coordinates of a point in the sky.
Definition skypoint.h:45
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:410
const CachingDms & ra() const
Definition skypoint.h:263
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition dms.cpp:429
virtual void setH(const double &x)
Sets floating-point value of angle, in hours.
Definition dms.h:210
int second() const
Definition dms.cpp:231
const QString toDMSString(const bool forceSign=false, const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:287
int minute() const
Definition dms.cpp:221
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:378
int degree() const
Definition dms.h:116
int arcmin() const
Definition dms.cpp:180
int arcsec() const
Definition dms.cpp:193
int hour() const
Definition dms.h:147
virtual void setD(const double &x)
Sets floating-point value of angle, in degrees.
Definition dms.h:179
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
void setTarget(const SkyPoint &targetCoord)
Set the alignment target where the mount is expected to point at.
Definition align.cpp:3820
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:2534
Q_SCRIPTABLE Q_NOREPLY void setTargetCoords(double ra0, double de0)
DBUS interface function.
Definition align.cpp:3811
Q_SCRIPTABLE Q_NOREPLY void setBinningIndex(int binIndex)
DBUS interface function.
Definition align.cpp:3114
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 QList< double > getTargetCoords()
getTargetCoords QList of target coordinates.
Definition align.cpp:3827
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
CaptureState
Capture states.
Definition ekos.h:92
@ 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)
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()
bool exists(const QString &fileName)
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()
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
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 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 repaint()
void show()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:53:00 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.