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

KDE's Doxygen guidelines are available online.