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

KDE's Doxygen guidelines are available online.