Kstars

framingassistantui.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include <indicom.h>
8
9#include "framingassistantui.h"
10#include "ui_framingassistant.h"
11#include "mosaiccomponent.h"
12#include "mosaictiles.h"
13#include "kstars.h"
14#include "Options.h"
15#include "scheduler.h"
16#include "skymap.h"
17#include "ekos/manager.h"
18#include "ekos/mount/mount.h"
19#include "schedulerprocess.h"
20#include "skymapcomposite.h"
21#include "ksparser.h"
22
23#include <KLocalizedString>
24#include <QFileDialog>
25#include <QtDBus/QDBusReply>
26
27namespace Ekos
28{
29
30FramingAssistantUI::FramingAssistantUI(): QDialog(KStars::Instance()), ui(new Ui::FramingAssistant())
31{
32 ui->setupUi(this);
33
34 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
35
36 ui->raBox->setUnits(dmsBox::HOURS);
37
38 // Initial optics information is taken from Ekos options
39 ui->focalLenSpin->setValue(Options::telescopeFocalLength());
40 ui->focalReducerSpin->setValue(Options::telescopeFocalReducer());
41 ui->pixelWSizeSpin->setValue(Options::cameraPixelWidth());
42 ui->pixelHSizeSpin->setValue(Options::cameraPixelHeight());
43 ui->cameraWSpin->setValue(Options::cameraWidth());
44 ui->cameraHSpin->setValue(Options::cameraHeight());
45
46 ui->positionAngleSpin->setValue(tiles->positionAngle());
47 ui->sequenceEdit->setText(tiles->sequenceFile());
48 ui->directoryEdit->setText(tiles->outputDirectory());
49 ui->targetEdit->setText(tiles->targetName());
50 ui->focusEvery->setValue(tiles->focusEveryN());
51 ui->alignEvery->setValue(tiles->alignEveryN());
52 ui->trackStepCheck->setChecked(tiles->isTrackChecked());
53 ui->focusStepCheck->setChecked(tiles->isFocusChecked());
54 ui->alignStepCheck->setChecked(tiles->isAlignChecked());
55 ui->guideStepCheck->setChecked(tiles->isGuideChecked());
56 ui->mosaicWSpin->setValue(tiles->gridSize().width());
57 ui->mosaicHSpin->setValue(tiles->gridSize().height());
58 ui->overlapSpin->setValue(tiles->overlap());
59
60 ui->groupEdit->setText(tiles->group());
61 QString completionVal, completionArg;
62 completionVal = tiles->completionCondition(&completionArg);
63 if (completionVal == "FinishSequence")
64 ui->sequenceCompletionR->setChecked(true);
65 else if (completionVal == "FinishRepeat")
66 {
67 ui->repeatCompletionR->setChecked(true);
68 ui->repeatsSpin->setValue(completionArg.toInt());
69 }
70 else if (completionVal == "FinishLoop")
71 ui->loopCompletionR->setChecked(true);
72
73 if (tiles->operationMode() == MosaicTiles::MODE_OPERATION)
74 {
75 m_CenterPoint = *tiles.data();
76 }
77 else
78 {
79 // Focus only has JNow coords (in both ra0 and ra)
80 // so we need to get catalog coords so it can have valid coordinates.
81 m_CenterPoint = *SkyMap::Instance()->focus();
82 auto J2000Coords = m_CenterPoint.catalogueCoord(KStars::Instance()->data()->ut().djd());
83 m_CenterPoint.setRA0(J2000Coords.ra0());
84 m_CenterPoint.setDec0(J2000Coords.dec0());
85 }
86
87 m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
88 ui->raBox->show(m_CenterPoint.ra0());
89 ui->decBox->show(m_CenterPoint.dec0());
90
91 // Page Navigation
92 connect(ui->backToEquipmentB, &QPushButton::clicked, this, [this]()
93 {
94 ui->stackedWidget->setCurrentIndex(PAGE_EQUIPMENT);
95 });
96
97 // Go and Solve
98 if (Ekos::Manager::Instance()->ekosStatus() == Ekos::Success)
99 {
100 ui->goSolveB->setEnabled(true);
101 ui->goRotateB->setEnabled(true);
102 connect(Ekos::Manager::Instance()->mountModule(), &Ekos::Mount::newStatus, this, &Ekos::FramingAssistantUI::setMountState,
104 connect(Ekos::Manager::Instance()->alignModule(), &Ekos::Align::newStatus, this, &Ekos::FramingAssistantUI::setAlignState,
106 }
107 connect(Ekos::Manager::Instance(), &Ekos::Manager::ekosStatusChanged, this, [this](Ekos::CommunicationStatus status)
108 {
109 ui->goSolveB->setEnabled(status == Ekos::Success);
110 ui->goRotateB->setEnabled(status == Ekos::Success);
111
112 // GO AND SOLVE
113 if (status == Ekos::Success)
114 {
115 connect(Ekos::Manager::Instance()->mountModule(), &Ekos::Mount::newStatus, this, &Ekos::FramingAssistantUI::setMountState,
117 connect(Ekos::Manager::Instance()->alignModule(), &Ekos::Align::newStatus, this, &Ekos::FramingAssistantUI::setAlignState,
119 }
120 });
121 connect(ui->goSolveB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::goAndSolve);
122 connect(ui->goRotateB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::goAndRotate);
123
124 // Import
125 connect(ui->importB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::selectImport);
126
127 // Page Navigation Controls
128 connect(ui->nextToAdjustGrid, &QPushButton::clicked, this, [this]()
129 {
130 ui->stackedWidget->setCurrentIndex(PAGE_ADJUST_GRID);
131 });
132 connect(ui->backToAdjustGridB, &QPushButton::clicked, this, [this]()
133 {
134 ui->stackedWidget->setCurrentIndex(PAGE_ADJUST_GRID);
135 });
136 connect(ui->nextToSelectGridB, &QPushButton::clicked, this, [this]()
137 {
138 ui->stackedWidget->setCurrentIndex(PAGE_SELECT_GRID);
139 });
140 connect(ui->backToSelectGrid, &QPushButton::clicked, this, [this]()
141 {
142 ui->stackedWidget->setCurrentIndex(PAGE_SELECT_GRID);
143 });
144 connect(ui->nextToJobsB, &QPushButton::clicked, this, [this]()
145 {
146 ui->stackedWidget->setCurrentIndex(PAGE_CREATE_JOBS);
147 ui->createJobsB->setEnabled(!ui->targetEdit->text().isEmpty() && !ui->sequenceEdit->text().isEmpty() &&
148 !ui->directoryEdit->text().isEmpty());
149 });
150
151 // Respond to sky map drag event that causes a shift in the ra and de coords of the center
152 connect(SkyMap::Instance(), &SkyMap::mosaicCenterChanged, this, [this](dms dRA, dms dDE)
153 {
154 m_CenterPoint.setRA0(range24(m_CenterPoint.ra0().Hours() + dRA.Hours()));
155 m_CenterPoint.setDec0(rangeDec(m_CenterPoint.dec0().Degrees() + dDE.Degrees()));
156 m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
157 ui->raBox->show(m_CenterPoint.ra0());
158 ui->decBox->show(m_CenterPoint.dec0());
159 //m_CenterPoint.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
160 m_DebounceTimer->start();
161 });
162
163 // Update target name after edit
164 connect(ui->targetEdit, &QLineEdit::editingFinished, this, &FramingAssistantUI::sanitizeTarget);
165
166 // Recenter
167 connect(ui->recenterB, &QPushButton::clicked, this, [this]()
168 {
169 // Focus only has JNow coords (in both ra0 and ra)
170 // so we need to get catalog coords so it can have valid coordinates.
171 m_CenterPoint = *SkyMap::Instance()->focus();
172 auto J2000Coords = m_CenterPoint.catalogueCoord(KStars::Instance()->data()->ut().djd());
173 m_CenterPoint.setRA0(J2000Coords.ra0());
174 m_CenterPoint.setDec0(J2000Coords.dec0());
175
176 m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
177 ui->raBox->show(m_CenterPoint.ra0());
178 ui->decBox->show(m_CenterPoint.dec0());
179 m_DebounceTimer->start();
180 });
181
182 // Set initial target on startup
183 if (tiles->operationMode() == MosaicTiles::MODE_PLANNING && SkyMap::IsFocused())
184 {
185 auto sanitized = KSUtils::sanitize(SkyMap::Instance()->focusObject()->name());
186 if (sanitized != i18n("unnamed"))
187 {
188 ui->targetEdit->setText(sanitized);
189
190 if (m_JobsDirectory.isEmpty())
191 ui->directoryEdit->setText(QDir::cleanPath(QDir::homePath() + QDir::separator() + sanitized));
192 else
193 ui->directoryEdit->setText(m_JobsDirectory + QDir::separator() + sanitized);
194 }
195 }
196
197 // Update object name
198 connect(SkyMap::Instance(), &SkyMap::objectChanged, this, [this](SkyObject * o)
199 {
200 QString sanitized = o->name();
201 if (sanitized != i18n("unnamed"))
202 {
203 // Remove illegal characters that can be problematic
204 sanitized = KSUtils::sanitize(sanitized);
205 ui->targetEdit->setText(sanitized);
206
207 if (m_JobsDirectory.isEmpty())
208 ui->directoryEdit->setText(QDir::cleanPath(QDir::homePath() + QDir::separator() + sanitized));
209 else
210 ui->directoryEdit->setText(m_JobsDirectory + QDir::separator() + sanitized);
211 }
212 });
213
214 // Watch for manual changes in ra box
215 connect(ui->raBox, &dmsBox::editingFinished, this, [this]
216 {
217 m_CenterPoint.setRA0(ui->raBox->createDms());
218 m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
219 m_DebounceTimer->start();
220 });
221
222 // Watch for manual hanges in de box
223 connect(ui->decBox, &dmsBox::editingFinished, this, [this]
224 {
225 m_CenterPoint.setDec0(ui->decBox->createDms());
226 m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
227 m_DebounceTimer->start();
228 });
229
230 connect(ui->loadSequenceB, &QPushButton::clicked, this, &FramingAssistantUI::selectSequence);
231 connect(ui->selectJobsDirB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::selectDirectory);
232 // Rendering options
233 ui->transparencySlider->setValue(Options::mosaicTransparencyLevel());
234 ui->transparencySlider->setEnabled(!Options::mosaicTransparencyAuto());
235 tiles->setPainterAlpha(Options::mosaicTransparencyLevel());
236 connect(ui->transparencySlider, QOverload<int>::of(&QSlider::valueChanged), this, [&](int v)
237 {
238 ui->transparencySlider->setToolTip(i18nc("%1 is the value, % is the percent sign", "%1%", v));
239 Options::setMosaicTransparencyLevel(v);
240 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
241 tiles->setPainterAlpha(v);
242 m_DebounceTimer->start();
243 });
244 ui->transparencyAuto->setChecked(Options::mosaicTransparencyAuto());
245 tiles->setPainterAlphaAuto(Options::mosaicTransparencyAuto());
246 connect(ui->transparencyAuto, &QCheckBox::toggled, this, [&](bool v)
247 {
248 ui->transparencySlider->setEnabled(!v);
249 Options::setMosaicTransparencyAuto(v);
250 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
251 tiles->setPainterAlphaAuto(v);
252 if (v)
253 m_DebounceTimer->start();
254 });
255
256 // The update timer avoids stacking updates which crash the sky map renderer
257 m_DebounceTimer = new QTimer(this);
258 m_DebounceTimer->setSingleShot(true);
259 m_DebounceTimer->setInterval(500);
260 connect(m_DebounceTimer, &QTimer::timeout, this, &Ekos::FramingAssistantUI::constructMosaic);
261
262 // Scope optics information
263 // - Changing the optics configuration changes the FOV, which changes the target field dimensions
264 connect(ui->focalLenSpin, &QDoubleSpinBox::editingFinished, this, &Ekos::FramingAssistantUI::calculateFOV);
265 connect(ui->focalReducerSpin, &QDoubleSpinBox::editingFinished, this, &Ekos::FramingAssistantUI::calculateFOV);
266 connect(ui->cameraWSpin, &QSpinBox::editingFinished, this, &Ekos::FramingAssistantUI::calculateFOV);
267 connect(ui->cameraHSpin, &QSpinBox::editingFinished, this, &Ekos::FramingAssistantUI::calculateFOV);
268 connect(ui->pixelWSizeSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
269 &Ekos::FramingAssistantUI::calculateFOV);
270 connect(ui->pixelHSizeSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
271 &Ekos::FramingAssistantUI::calculateFOV);
272 connect(ui->positionAngleSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
273 &Ekos::FramingAssistantUI::calculateFOV);
274
275 // Mosaic configuration
276 // - Changing the target field dimensions changes the grid dimensions
277 // - Changing the overlap field changes the grid dimensions (more intuitive than changing the field)
278 // - Changing the grid dimensions changes the target field dimensions
279 connect(ui->targetHFOVSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
280 &Ekos::FramingAssistantUI::updateGridFromTargetFOV);
281 connect(ui->targetWFOVSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
282 &Ekos::FramingAssistantUI::updateGridFromTargetFOV);
283 connect(ui->overlapSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
284 &Ekos::FramingAssistantUI::updateGridFromTargetFOV);
285 connect(ui->mosaicWSpin, QOverload<int>::of(&QSpinBox::valueChanged), this,
286 &Ekos::FramingAssistantUI::updateTargetFOVFromGrid);
287 connect(ui->mosaicHSpin, QOverload<int>::of(&QSpinBox::valueChanged), this,
288 &Ekos::FramingAssistantUI::updateTargetFOVFromGrid);
289
290 // Lazy update for s-shape
291 connect(ui->reverseOddRows, &QCheckBox::toggled, this, [&]()
292 {
293 renderedHFOV = 0;
294 m_DebounceTimer->start();
295 });
296
297 // Buttons
298 connect(ui->resetB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::updateTargetFOVFromGrid);
299 connect(ui->fetchB, &QPushButton::clicked, this, &FramingAssistantUI::fetchINDIInformation);
300 connect(ui->createJobsB, &QPushButton::clicked, this, &FramingAssistantUI::createJobs);
301
302 // Job options
303 connect(ui->alignEvery, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::FramingAssistantUI::rewordStepEvery);
304 connect(ui->focusEvery, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::FramingAssistantUI::rewordStepEvery);
305
306 // Get INDI Information, if avaialble.
307 if (tiles->operationMode() == MosaicTiles::MODE_PLANNING)
308 fetchINDIInformation();
309
310 if (isEquipmentValid())
311 ui->stackedWidget->setCurrentIndex(PAGE_SELECT_GRID);
312
313 tiles->setOperationMode(MosaicTiles::MODE_PLANNING);
314}
315
316FramingAssistantUI::~FramingAssistantUI()
317{
318 delete m_DebounceTimer;
319}
320
321bool FramingAssistantUI::isEquipmentValid() const
322{
323 return (ui->focalLenSpin->value() > 0 && ui->cameraWSpin->value() > 0 && ui->cameraHSpin->value() > 0 &&
324 ui->pixelWSizeSpin->value() > 0 && ui->pixelHSizeSpin->value() > 0);
325}
326
327double FramingAssistantUI::getTargetWFOV() const
328{
329 double const xFOV = ui->cameraWFOVSpin->value() * (1 - ui->overlapSpin->value() / 100.0);
330 return ui->cameraWFOVSpin->value() + xFOV * (ui->mosaicWSpin->value() - 1);
331}
332
333double FramingAssistantUI::getTargetHFOV() const
334{
335 double const yFOV = ui->cameraHFOVSpin->value() * (1 - ui->overlapSpin->value() / 100.0);
336 return ui->cameraHFOVSpin->value() + yFOV * (ui->mosaicHSpin->value() - 1);
337}
338
339double FramingAssistantUI::getTargetMosaicW() const
340{
341 // If FOV is invalid, or target FOV is null, or target FOV is smaller than camera FOV, we get one tile
342 if (!isEquipmentValid() || !ui->targetWFOVSpin->value() || ui->targetWFOVSpin->value() <= ui->cameraWFOVSpin->value())
343 return 1;
344
345 // Else we get one tile, plus as many overlapping camera FOVs in the remnant of the target FOV
346 double const xFOV = ui->cameraWFOVSpin->value() * (1 - ui->overlapSpin->value() / 100.0);
347 int const tiles = 1 + ceil((ui->targetWFOVSpin->value() - ui->cameraWFOVSpin->value()) / xFOV);
348 //Ekos::Manager::Instance()->schedulerModule()->appendLogText(QString("[W] Target FOV %1, camera FOV %2 after overlap %3, %4 tiles.").arg(ui->targetWFOVSpin->value()).arg(ui->cameraWFOVSpin->value()).arg(xFOV).arg(tiles));
349 return tiles;
350}
351
352double FramingAssistantUI::getTargetMosaicH() const
353{
354 // If FOV is invalid, or target FOV is null, or target FOV is smaller than camera FOV, we get one tile
355 if (!isEquipmentValid() || !ui->targetHFOVSpin->value() || ui->targetHFOVSpin->value() <= ui->cameraHFOVSpin->value())
356 return 1;
357
358 // Else we get one tile, plus as many overlapping camera FOVs in the remnant of the target FOV
359 double const yFOV = ui->cameraHFOVSpin->value() * (1 - ui->overlapSpin->value() / 100.0);
360 int const tiles = 1 + ceil((ui->targetHFOVSpin->value() - ui->cameraHFOVSpin->value()) / yFOV);
361 //Ekos::Manager::Instance()->schedulerModule()->appendLogText(QString("[H] Target FOV %1, camera FOV %2 after overlap %3, %4 tiles.").arg(ui->targetHFOVSpin->value()).arg(ui->cameraHFOVSpin->value()).arg(yFOV).arg(tiles));
362 return tiles;
363}
364
365void FramingAssistantUI::calculateFOV()
366{
367 if (!isEquipmentValid())
368 return;
369
370 ui->nextToSelectGridB->setEnabled(true);
371
372 ui->targetWFOVSpin->setMinimum(ui->cameraWFOVSpin->value());
373 ui->targetHFOVSpin->setMinimum(ui->cameraHFOVSpin->value());
374
375 Options::setTelescopeFocalLength(ui->focalLenSpin->value());
376 Options::setTelescopeFocalReducer(ui->focalReducerSpin->value());
377 Options::setCameraPixelWidth(ui->pixelWSizeSpin->value());
378 Options::setCameraPixelHeight(ui->pixelHSizeSpin->value());
379 Options::setCameraWidth(ui->cameraWSpin->value());
380 Options::setCameraHeight(ui->cameraHSpin->value());
381 Options::setCameraRotation(ui->positionAngleSpin->value());
382
383 auto reducedFocalLength = ui->focalLenSpin->value() * ui->focalReducerSpin->value();
384 // Calculate FOV in arcmins
385 const auto fov_x = 206264.8062470963552 * ui->cameraWSpin->value() * ui->pixelWSizeSpin->value() / 60000.0 /
386 reducedFocalLength;
387 const auto fov_y = 206264.8062470963552 * ui->cameraHSpin->value() * ui->pixelHSizeSpin->value() / 60000.0 /
388 reducedFocalLength;
389
390 ui->cameraWFOVSpin->setValue(fov_x);
391 ui->cameraHFOVSpin->setValue(fov_y);
392
393 double const target_fov_w = getTargetWFOV();
394 double const target_fov_h = getTargetHFOV();
395
396 if (ui->targetWFOVSpin->value() < target_fov_w)
397 {
398 bool const sig = ui->targetWFOVSpin->blockSignals(true);
399 ui->targetWFOVSpin->setValue(target_fov_w);
400 ui->targetWFOVSpin->blockSignals(sig);
401 }
402
403 if (ui->targetHFOVSpin->value() < target_fov_h)
404 {
405 bool const sig = ui->targetHFOVSpin->blockSignals(true);
406 ui->targetHFOVSpin->setValue(target_fov_h);
407 ui->targetHFOVSpin->blockSignals(sig);
408 }
409
410 m_DebounceTimer->start();
411}
412
413void FramingAssistantUI::resetFOV()
414{
415 if (!isEquipmentValid())
416 return;
417
418 ui->targetWFOVSpin->setValue(getTargetWFOV());
419 ui->targetHFOVSpin->setValue(getTargetHFOV());
420}
421
422void FramingAssistantUI::updateTargetFOVFromGrid()
423{
424 if (!isEquipmentValid())
425 return;
426
427 double const targetWFOV = getTargetWFOV();
428 double const targetHFOV = getTargetHFOV();
429
430 if (ui->targetWFOVSpin->value() != targetWFOV)
431 {
432 bool const sig = ui->targetWFOVSpin->blockSignals(true);
433 ui->targetWFOVSpin->setValue(targetWFOV);
434 ui->targetWFOVSpin->blockSignals(sig);
435 m_DebounceTimer->start();
436 }
437
438 if (ui->targetHFOVSpin->value() != targetHFOV)
439 {
440 bool const sig = ui->targetHFOVSpin->blockSignals(true);
441 ui->targetHFOVSpin->setValue(targetHFOV);
442 ui->targetHFOVSpin->blockSignals(sig);
443 m_DebounceTimer->start();
444 }
445}
446
447void FramingAssistantUI::updateGridFromTargetFOV()
448{
449 if (!isEquipmentValid())
450 return;
451
452 double const expectedW = getTargetMosaicW();
453 double const expectedH = getTargetMosaicH();
454
455 if (expectedW != ui->mosaicWSpin->value())
456 {
457 bool const sig = ui->mosaicWSpin->blockSignals(true);
458 ui->mosaicWSpin->setValue(expectedW);
459 ui->mosaicWSpin->blockSignals(sig);
460 }
461
462 if (expectedH != ui->mosaicHSpin->value())
463 {
464 bool const sig = ui->mosaicHSpin->blockSignals(true);
465 ui->mosaicHSpin->setValue(expectedH);
466 ui->mosaicHSpin->blockSignals(sig);
467 }
468
469 // Update unconditionally, as we may be updating the overlap or the target FOV covered by the mosaic
470 m_DebounceTimer->start();
471}
472
473void FramingAssistantUI::constructMosaic()
474{
475 m_DebounceTimer->stop();
476
477 if (!isEquipmentValid())
478 return;
479
480 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
481 // Set Basic Metadata
482
483 // Center
484 tiles->setRA0(m_CenterPoint.ra0());
485 tiles->setDec0(m_CenterPoint.dec0());
486 tiles->updateCoordsNow(KStarsData::Instance()->updateNum());
487
488 // Grid Size
489 tiles->setGridSize(QSize(ui->mosaicWSpin->value(), ui->mosaicHSpin->value()));
490 // Position Angle
491 tiles->setPositionAngle(ui->positionAngleSpin->value());
492 // Camera FOV in arcmins
493 tiles->setCameraFOV(QSizeF(ui->cameraWFOVSpin->value(), ui->cameraHFOVSpin->value()));
494 // Mosaic overall FOV in arcsmins
495 tiles->setMosaicFOV(QSizeF(ui->targetWFOVSpin->value(), ui->targetHFOVSpin->value()));
496 // Overlap in %
497 tiles->setOverlap(ui->overlapSpin->value());
498 // Generate Tiles
499 tiles->createTiles(ui->reverseOddRows->checkState() == Qt::CheckState::Checked);
500}
501
502void FramingAssistantUI::fetchINDIInformation()
503{
504 // Block all signals so we can set the values directly.
505 for (auto oneWidget : ui->equipment->children())
506 oneWidget->blockSignals(true);
507 for (auto oneWidget : ui->createGrid->children())
508 oneWidget->blockSignals(true);
509
510 QDBusInterface alignInterface("org.kde.kstars",
511 "/KStars/Ekos/Align",
512 "org.kde.kstars.Ekos.Align",
514
515 QDBusReply<QList<double>> cameraReply = alignInterface.call("cameraInfo");
516 if (cameraReply.isValid())
517 {
518 QList<double> const values = cameraReply.value();
519
520 m_CameraSize = QSize(values[0], values[1]);
521 ui->cameraWSpin->setValue(m_CameraSize.width());
522 ui->cameraHSpin->setValue(m_CameraSize.height());
523 m_PixelSize = QSizeF(values[2], values[3]);
524 ui->pixelWSizeSpin->setValue(m_PixelSize.width());
525 ui->pixelHSizeSpin->setValue(m_PixelSize.height());
526 }
527
528 QDBusReply<QList<double>> telescopeReply = alignInterface.call("telescopeInfo");
529 if (telescopeReply.isValid())
530 {
531 QList<double> const values = telescopeReply.value();
532 m_FocalLength = values[0];
533 m_FocalReducer = values[2];
534 ui->focalLenSpin->setValue(m_FocalLength);
535 ui->focalReducerSpin->setValue(m_FocalReducer);
536 }
537
538 QDBusReply<QList<double>> solutionReply = alignInterface.call("getSolutionResult");
539 if (solutionReply.isValid())
540 {
541 QList<double> const values = solutionReply.value();
542 if (values[0] > INVALID_VALUE)
543 {
544 m_PA = KSUtils::rotationToPositionAngle(values[0]);
545 ui->positionAngleSpin->setValue(m_PA);
546 }
547 }
548
549 calculateFOV();
550
551 // Restore all signals
552 for (auto oneWidget : ui->equipment->children())
553 oneWidget->blockSignals(false);
554 for (auto oneWidget : ui->createGrid->children())
555 oneWidget->blockSignals(false);
556}
557
558void FramingAssistantUI::rewordStepEvery(int v)
559{
560 QSpinBox * sp = dynamic_cast<QSpinBox *>(sender());
561 if (0 < v)
562 sp->setSuffix(i18np(" Scheduler job", " Scheduler jobs", v));
563 else
564 sp->setSuffix(i18n(" (first only)"));
565}
566
567void FramingAssistantUI::goAndSolve()
568{
569 // If user click again before solver did not start while GOTO is pending
570 // let's start solver immediately if the mount is already tracking.
571 if (m_GOTOSolvePending && m_MountState == ISD::Mount::MOUNT_TRACKING)
572 {
573 m_GOTOSolvePending = false;
574 ui->goSolveB->setStyleSheet("border: 1px outset yellow");
575 ui->goRotateB->setStyleSheet("border: 1px outset yellow");
576 Ekos::Manager::Instance()->alignModule()->captureAndSolve();
577 }
578 // Otherwise, initiate GOTO
579 else
580 {
581 Ekos::Manager::Instance()->alignModule()->setSolverAction(Ekos::Align::GOTO_SLEW);
582 Ekos::Manager::Instance()->mountModule()->gotoTarget(m_CenterPoint);
583 ui->goSolveB->setStyleSheet("border: 1px outset magenta");
584 ui->goRotateB->setStyleSheet("border: 1px outset magenta");
585 m_GOTOSolvePending = true;
586 }
587}
588
589void FramingAssistantUI::goAndRotate()
590{
591 Ekos::Manager::Instance()->alignModule()->setProperty("targetPositionAngle", ui->positionAngleSpin->value());
592 goAndSolve();
593}
594
595void FramingAssistantUI::createJobs()
596{
597 auto scheduler = Ekos::Manager::Instance()->schedulerModule();
598 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
599 auto sequence = ui->sequenceEdit->text();
600 auto outputDirectory = ui->directoryEdit->text();
601 auto target = ui->targetEdit->text();
602 auto group = ui->groupEdit->text();
603
604 tiles->setTargetName(target);
605 tiles->setGroup(group);
606 tiles->setOutputDirectory(outputDirectory);
607 tiles->setSequenceFile(sequence);
608 tiles->setFocusEveryN(ui->focusEvery->value());
609 tiles->setAlignEveryN(ui->alignEvery->value());
610 tiles->setStepChecks(ui->trackStepCheck->isChecked(), ui->focusStepCheck->isChecked(),
611 ui->alignStepCheck->isChecked(), ui->guideStepCheck->isChecked());
612
613 if (ui->sequenceCompletionR->isChecked())
614 tiles->setCompletionCondition("FinishSequence", "");
615 else if (ui->loopCompletionR->isChecked())
616 tiles->setCompletionCondition("FinishLoop", "");
617 else if (ui->repeatCompletionR->isChecked())
618 tiles->setCompletionCondition("FinishRepeat", QString("%1").arg(ui->repeatsSpin->value()));
619
620 tiles->setPositionAngle(ui->positionAngleSpin->value());
621 // Start by removing any jobs.
622 scheduler->process()->removeAllJobs();
623
624 QString completionVal, completionArg;
625
626 // Completion values are for all tiles.
627 completionVal = tiles->completionCondition(&completionArg);
628 QJsonObject completionSettings;
629 if (completionVal == "FinishSequence")
630 completionSettings = {{"sequenceCheck", true}};
631 else if (completionVal == "FinishRepeat")
632 completionSettings = {{"repeatCheck", true}, {"repeatRuns", completionArg.toInt()}};
633 else if (completionVal == "FinishLoop")
634 completionSettings = {{"loopCheck", true}};
635
636 int batchCount = 0;
637 for (auto oneTile : tiles->tiles())
638 {
639 batchCount++;
640 XMLEle *root = scheduler->process()->getSequenceJobRoot(sequence);
641 if (root == nullptr)
642 return;
643
644 const auto oneTarget = QString("%1-Part_%2").arg(target).arg(batchCount);
645 if (scheduler->process()->createJobSequence(root, oneTarget, outputDirectory) == false)
646 {
647 delXMLEle(root);
648 return;
649 }
650
651 delXMLEle(root);
652 auto oneSequence = QString("%1/%2.esq").arg(outputDirectory, oneTarget);
653
654 // First job should Always focus if possible
655 bool shouldFocus = ui->focusStepCheck->isChecked() && (batchCount == 1 || (batchCount % ui->focusEvery->value()) == 0);
656 bool shouldAlign = ui->alignStepCheck->isChecked() && (batchCount == 1 || (batchCount % ui->alignEvery->value()) == 0);
657 QVariantMap settings =
658 {
659 {"nameEdit", oneTarget},
660 {"groupEdit", tiles->group()},
661 {"raBox", oneTile->skyCenter.ra0().toHMSString()},
662 {"decBox", oneTile->skyCenter.dec0().toDMSString()},
663 // Take care of standard range for position angle
664 {"positionAngleSpin", KSUtils::rangePA(tiles->positionAngle())},
665 {"sequenceEdit", oneSequence},
666 {"schedulerTrackStep", ui->trackStepCheck->isChecked()},
667 {"schedulerFocusStep", shouldFocus},
668 {"schedulerAlignStep", shouldAlign},
669 {"schedulerGuideStep", ui->guideStepCheck->isChecked()}
670 };
671
672 scheduler->setAllSettings(settings);
673 scheduler->saveJob();
674 }
675
676 auto schedulerListFile = QString("%1/%2.esl").arg(outputDirectory, target);
677 scheduler->process()->saveScheduler(QUrl::fromLocalFile(schedulerListFile));
678 accept();
679 Ekos::Manager::Instance()->activateModule(i18n("Scheduler"), true);
680 scheduler->updateJobTable();
681}
682
683void FramingAssistantUI::setMountState(ISD::Mount::Status value)
684{
685 m_MountState = value;
686 if (m_GOTOSolvePending && m_MountState == ISD::Mount::MOUNT_TRACKING)
687 {
688 m_GOTOSolvePending = false;
689 ui->goSolveB->setStyleSheet("border: 1px outset yellow");
690 ui->goRotateB->setStyleSheet("border: 1px outset yellow");
691 Ekos::Manager::Instance()->alignModule()->captureAndSolve();
692 }
693}
694
695void FramingAssistantUI::setAlignState(AlignState value)
696{
697 m_AlignState = value;
698
699 if (m_AlignState == Ekos::ALIGN_COMPLETE)
700 {
701 ui->goSolveB->setStyleSheet("border: 1px outset green");
702 ui->goRotateB->setStyleSheet("border: 1px outset green");
703 }
704 else if (m_AlignState == Ekos::ALIGN_ABORTED || m_AlignState == Ekos::ALIGN_FAILED)
705 {
706 ui->goSolveB->setStyleSheet("border: 1px outset red");
707 ui->goRotateB->setStyleSheet("border: 1px outset red");
708 }
709}
710
711void FramingAssistantUI::selectSequence()
712{
713 QString file = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Select Sequence Queue"),
715 i18n("Ekos Sequence Queue (*.esq)"));
716
717 if (!file.isEmpty())
718 {
719 ui->sequenceEdit->setText(file);
720 ui->createJobsB->setEnabled(!ui->targetEdit->text().isEmpty() && !ui->sequenceEdit->text().isEmpty() &&
721 !ui->directoryEdit->text().isEmpty());
722 }
723}
724
725void FramingAssistantUI::selectImport()
726{
727 QString file = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Select Mosaic Import"),
729 i18n("Telescopius CSV (*.csv)"));
730
731 if (!file.isEmpty())
732 parseMosaicCSV(file);
733}
734
735bool FramingAssistantUI::parseMosaicCSV(const QString &filename)
736{
738 csv_sequence.append(qMakePair(QString("Pane"), KSParser::D_QSTRING));
739 csv_sequence.append(qMakePair(QString("RA"), KSParser::D_QSTRING));
740 csv_sequence.append(qMakePair(QString("DEC"), KSParser::D_QSTRING));
741 csv_sequence.append(qMakePair(QString("Position Angle (East)"), KSParser::D_DOUBLE));
742 csv_sequence.append(qMakePair(QString("Pane width (arcmins)"), KSParser::D_DOUBLE));
743 csv_sequence.append(qMakePair(QString("Pane height (arcmins)"), KSParser::D_DOUBLE));
744 csv_sequence.append(qMakePair(QString("Overlap"), KSParser::D_QSTRING));
745 csv_sequence.append(qMakePair(QString("Row"), KSParser::D_INT));
746 csv_sequence.append(qMakePair(QString("Column"), KSParser::D_INT));
747 KSParser csvParser(filename, ',', csv_sequence);
748
749 QHash<QString, QVariant> row_content;
750 int maxRow = 1, maxCol = 1;
751 auto haveCenter = false;
752 while (csvParser.HasNextRow())
753 {
754 row_content = csvParser.ReadNextRow();
755 auto pane = row_content["Pane"].toString();
756
757 // Skip first line
758 if (pane == "Pane")
759 continue;
760
761 if (pane != "Center")
762 {
763 auto row = row_content["Row"].toInt();
764 maxRow = qMax(row, maxRow);
765 auto col = row_content["Column"].toInt();
766 maxCol = qMax(col, maxCol);
767 continue;
768 }
769
770 haveCenter = true;
771
772 auto ra = row_content["RA"].toString().trimmed();
773 auto dec = row_content["DEC"].toString().trimmed();
774
775 ui->raBox->setText(ra.replace("hr", "h"));
776 ui->decBox->setText(dec.remove("ยบ"));
777
778 auto pa = row_content["Position Angle (East)"].toDouble();
779 ui->positionAngleSpin->setValue(pa);
780
781 // eg. 10% --> 10
782 auto overlap = row_content["Overlap"].toString().trimmed().mid(0, 2).toDouble();
783 ui->overlapSpin->setValue(overlap);
784 }
785
786 if (haveCenter == false)
787 {
788 KSNotification::sorry(i18n("Import must contain center coordinates."), i18n("Sorry"), 15);
789 return false;
790 }
791
792 // Set WxH
793 ui->mosaicWSpin->setValue(maxRow);
794 ui->mosaicHSpin->setValue(maxCol);
795 // Set J2000 Center
796 m_CenterPoint.setRA0(ui->raBox->createDms());
797 m_CenterPoint.setDec0(ui->decBox->createDms());
798 m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
799 // Slew to center
800 SkyMap::Instance()->setDestination(m_CenterPoint);
801 SkyMap::Instance()->slewFocus();
802 // Now go to position adjustments
803 ui->nextToAdjustGrid->click();
804
805 return true;
806}
807
808bool FramingAssistantUI::importMosaic(const QJsonObject &payload)
809{
810 // CSV should contain postion angle, ra/de of each panel, and center coordinates.
811 auto csv = payload["csv"].toString();
812 // Full path to sequence file to be used for imaging.
813 auto sequence = payload["sequence"].toString();
814 // Name of target (needs sanitization)
815 auto target = payload["target"].toString();
816 // Jobs directory
817 auto directory = payload["directory"].toString();
818
819 // Scheduler steps
820 auto track = payload["track"].toBool();
821 auto focus = payload["focus"].toBool();
822 auto align = payload["align"].toBool();
823 auto guide = payload["guide"].toBool();
824
825 // Create temporary file to save the CSV info
826 QTemporaryFile csvFile;
827 if (!csvFile.open())
828 return false;
829 csvFile.write(csv.toUtf8());
830 csvFile.close();
831
832 // Delete debounce timer since we update all parameters programatically at once
833 m_DebounceTimer->disconnect();
834
835 if (parseMosaicCSV(csvFile.fileName()) == false)
836 return false;
837
838 constructMosaic();
839
840 m_JobsDirectory = directory;
841
842 // Set scheduler options.
843 ui->trackStepCheck->setChecked(track);
844 ui->focusStepCheck->setChecked(focus);
845 ui->alignStepCheck->setChecked(align);
846 ui->guideStepCheck->setChecked(guide);
847
848 ui->sequenceEdit->setText(sequence);
849 ui->targetEdit->setText(target);
850
851 sanitizeTarget();
852
853 // If create job is still disabled, then some configuation is missing or wrong.
854 if (ui->createJobsB->isEnabled() == false)
855 return false;
856
857 // Need to wait a bit since parseMosaicCSV needs to trigger UI
858 // But button clicks need to be executed first in the event loop
859 ui->createJobsB->click();
860
861 return true;
862}
863
864void FramingAssistantUI::selectDirectory()
865{
866 m_JobsDirectory = QFileDialog::getExistingDirectory(Ekos::Manager::Instance(), i18nc("@title:window",
867 "Select Jobs Directory"),
869
870 if (!m_JobsDirectory.isEmpty())
871 {
872 // If we already have a target specified, then append it to directory path.
873 QString sanitized = ui->targetEdit->text();
874 if (sanitized.isEmpty() == false && sanitized != i18n("unnamed"))
875 {
876 // Remove illegal characters that can be problematic
877 sanitized = KSUtils::sanitize(sanitized);
878 ui->directoryEdit->setText(m_JobsDirectory + QDir::separator() + sanitized);
879
880 }
881 else
882 ui->directoryEdit->setText(m_JobsDirectory);
883
884
885 ui->createJobsB->setEnabled(!ui->targetEdit->text().isEmpty() && !ui->sequenceEdit->text().isEmpty() &&
886 !ui->directoryEdit->text().isEmpty());
887 }
888}
889
890void FramingAssistantUI::sanitizeTarget()
891{
892 QString sanitized = ui->targetEdit->text();
893 if (sanitized != i18n("unnamed"))
894 {
895 // Remove illegal characters that can be problematic
896 sanitized = KSUtils::sanitize(sanitized);
897 ui->targetEdit->blockSignals(true);
898 ui->targetEdit->setText(sanitized);
899 ui->targetEdit->blockSignals(false);
900
901 if (m_JobsDirectory.isEmpty())
902 ui->directoryEdit->setText(QDir::cleanPath(QDir::homePath() + QDir::separator() + sanitized));
903 else
904 ui->directoryEdit->setText(m_JobsDirectory + QDir::separator() + sanitized);
905
906 ui->createJobsB->setEnabled(!ui->targetEdit->text().isEmpty() && !ui->sequenceEdit->text().isEmpty() &&
907 !ui->directoryEdit->text().isEmpty());
908 }
909}
910}
void newStatus(ISD::Mount::Status status)
Change in the mount status.
static KStars * Instance()
Definition kstars.h:122
void objectChanged(SkyObject *)
Emitted when current object changed.
void mosaicCenterChanged(dms dRA, dms dDE)
Emitter when mosaic center is dragged in the sky map.
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:50
virtual QString name(void) const
Definition skyobject.h:154
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
double Hours() const
Definition dms.h:168
const double & Degrees() const
Definition dms.h:141
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
@ ALIGN_FAILED
Alignment failed.
Definition ekos.h:148
@ ALIGN_ABORTED
Alignment aborted by user or agent.
Definition ekos.h:149
@ ALIGN_COMPLETE
Alignment successfully completed.
Definition ekos.h:147
QString name(const QVariant &location)
void clicked(bool checked)
void toggled(bool checked)
void valueChanged(int value)
void editingFinished()
QDBusConnection sessionBus()
bool isValid() const const
QString cleanPath(const QString &path)
QString homePath()
QChar separator()
void valueChanged(double d)
virtual void close() override
QString getExistingDirectory(QWidget *parent, const QString &caption, const QString &dir, Options options)
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
qint64 write(const QByteArray &data)
void editingFinished()
void append(QList< T > &&value)
void setSuffix(const QString &suffix)
void valueChanged(int i)
QString arg(Args &&... args) const const
bool isEmpty() const const
int toInt(bool *ok, int base) const const
UniqueConnection
QTextStream & dec(QTextStream &stream)
virtual QString fileName() const const override
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
QUrl fromLocalFile(const QString &localFile)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 28 2025 11:57:24 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.