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

KDE's Doxygen guidelines are available online.