Kstars

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

KDE's Doxygen guidelines are available online.