Kstars

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

KDE's Doxygen guidelines are available online.