Kstars

scheduler.cpp
1/*
2 SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 DBus calls from GSoC 2015 Ekos Scheduler project:
5 SPDX-FileCopyrightText: 2015 Daniel Leu <daniel_mihai.leu@cti.pub.ro>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include "scheduler.h"
11
12#include "ekos/scheduler/framingassistantui.h"
13#include "ksnotification.h"
14#include "ksmessagebox.h"
15#include "kstars.h"
16#include "kstarsdata.h"
17#include "skymap.h"
18#include "Options.h"
19#include "scheduleradaptor.h"
20#include "schedulerjob.h"
21#include "schedulerprocess.h"
22#include "schedulermodulestate.h"
23#include "schedulerutils.h"
24#include "skymapcomposite.h"
25#include "skycomponents/mosaiccomponent.h"
26#include "skyobjects/mosaictiles.h"
27#include "auxiliary/QProgressIndicator.h"
28#include "dialogs/finddialog.h"
29#include "ekos/manager.h"
30#include "ekos/capture/sequencejob.h"
31#include "ekos/capture/placeholderpath.h"
32#include "skyobjects/starobject.h"
33#include "greedyscheduler.h"
34#include "ekos/auxiliary/solverutils.h"
35#include "ekos/auxiliary/stellarsolverprofile.h"
36
37#include <KConfigDialog>
38#include <KActionCollection>
39
40#include <fitsio.h>
41#include <ekos_scheduler_debug.h>
42#include <indicom.h>
43#include "ekos/capture/sequenceeditor.h"
44
45// Qt version calming
46#include <qtendl.h>
47
48#define BAD_SCORE -1000
49#define RESTART_GUIDING_DELAY_MS 5000
50
51#define DEFAULT_MIN_ALTITUDE 15
52#define DEFAULT_MIN_MOON_SEPARATION 0
53
54// This is a temporary debugging printout introduced while gaining experience developing
55// the unit tests in test_ekos_scheduler_ops.cpp.
56// All these printouts should be eventually removed.
57#define TEST_PRINT if (false) fprintf
58
59namespace
60{
61
62// This needs to match the definition order for the QueueTable in scheduler.ui
63enum QueueTableColumns
64{
65 NAME_COLUMN = 0,
66 STATUS_COLUMN,
67 CAPTURES_COLUMN,
68 ALTITUDE_COLUMN,
69 START_TIME_COLUMN,
70 END_TIME_COLUMN,
71};
72
73}
74
75namespace Ekos
76{
77
79{
80 // Use the default path and interface when running the scheduler.
81 setupScheduler(ekosPathString, ekosInterfaceString);
82}
83
84Scheduler::Scheduler(const QString path, const QString interface,
85 const QString &ekosPathStr, const QString &ekosInterfaceStr)
86{
87 // During testing, when mocking ekos, use a special purpose path and interface.
88 schedulerPathString = path;
89 kstarsInterfaceString = interface;
90 setupScheduler(ekosPathStr, ekosInterfaceStr);
91}
92
93void Scheduler::setupScheduler(const QString &ekosPathStr, const QString &ekosInterfaceStr)
94{
95 setupUi(this);
96
97 qRegisterMetaType<Ekos::SchedulerState>("Ekos::SchedulerState");
98 qDBusRegisterMetaType<Ekos::SchedulerState>();
99
100 m_moduleState.reset(new SchedulerModuleState());
101 m_process.reset(new SchedulerProcess(moduleState(), ekosPathStr, ekosInterfaceStr));
102
104
105 // Get current KStars time and set seconds to zero
106 QDateTime currentDateTime = SchedulerModuleState::getLocalTime();
107 QTime currentTime = currentDateTime.time();
108 currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0);
109 currentDateTime.setTime(currentTime);
110
111 // Set initial time for startup and completion times
112 startupTimeEdit->setDateTime(currentDateTime);
113 schedulerUntilValue->setDateTime(currentDateTime);
114
115
116 sleepLabel->setPixmap(
117 QIcon::fromTheme("chronometer").pixmap(QSize(32, 32)));
118 changeSleepLabel("", false);
119
120 pi = new QProgressIndicator(this);
121 bottomLayout->addWidget(pi, 0);
122
123 geo = KStarsData::Instance()->geo();
124
125 //RA box should be HMS-style
126 raBox->setUnits(dmsBox::HOURS);
127
128 /* FIXME: Find a way to have multi-line tooltips in the .ui file, then move the widget configuration there - what about i18n? */
129
130 queueTable->setToolTip(
131 i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields."));
132 QTableWidgetItem *statusHeader = queueTable->horizontalHeaderItem(SCHEDCOL_STATUS);
133 QTableWidgetItem *altitudeHeader = queueTable->horizontalHeaderItem(SCHEDCOL_ALTITUDE);
134 QTableWidgetItem *startupHeader = queueTable->horizontalHeaderItem(SCHEDCOL_STARTTIME);
135 QTableWidgetItem *completionHeader = queueTable->horizontalHeaderItem(SCHEDCOL_ENDTIME);
136 QTableWidgetItem *captureCountHeader = queueTable->horizontalHeaderItem(SCHEDCOL_CAPTURES);
137
138 if (statusHeader != nullptr)
139 statusHeader->setToolTip(i18n("Current status of the job, managed by the Scheduler.\n"
140 "If invalid, the Scheduler was not able to find a proper observation time for the target.\n"
141 "If aborted, the Scheduler missed the scheduled time or encountered transitory issues and will reschedule the job.\n"
142 "If complete, the Scheduler verified that all sequence captures requested were stored, including repeats."));
143 if (altitudeHeader != nullptr)
144 altitudeHeader->setToolTip(i18n("Current altitude of the target of the job.\n"
145 "A rising target is indicated with an arrow going up.\n"
146 "A setting target is indicated with an arrow going down."));
147 if (startupHeader != nullptr)
148 startupHeader->setToolTip(i18n("Startup time of the job, as estimated by the Scheduler.\n"
149 "The altitude at startup, if available, is displayed too.\n"
150 "Fixed time from user or culmination time is marked with a chronometer symbol."));
151 if (completionHeader != nullptr)
152 completionHeader->setToolTip(i18n("Completion time for the job, as estimated by the Scheduler.\n"
153 "You may specify a fixed time to limit duration of looping jobs. "
154 "A warning symbol indicates the altitude at completion may cause the job to abort before completion.\n"));
155 if (captureCountHeader != nullptr)
156 captureCountHeader->setToolTip(i18n("Count of captures stored for the job, based on its sequence job.\n"
157 "This is a summary, additional specific frame types may be required to complete the job."));
158
159 /* Set first button mode to add observation job from left-hand fields */
160 setJobAddApply(true);
161
162 removeFromQueueB->setIcon(QIcon::fromTheme("list-remove"));
163 removeFromQueueB->setToolTip(
164 i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal."));
165 removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
166
167 queueUpB->setIcon(QIcon::fromTheme("go-up"));
168 queueUpB->setToolTip(i18n("Move selected job one line up in the list.\n"));
169 queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
170 queueDownB->setIcon(QIcon::fromTheme("go-down"));
171 queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n"));
172 queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
173
174 evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot"));
175 evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs."));
176 evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
177 sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical"));
178 sortJobsB->setToolTip(
179 i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n"
180 "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n"
181 "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs."));
182 sortJobsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
183 mosaicB->setIcon(QIcon::fromTheme("zoom-draw"));
184 mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
185
186 positionAngleSpin->setSpecialValueText("--");
187
188 queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as"));
189 queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
190 queueSaveB->setIcon(QIcon::fromTheme("document-save"));
191 queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
192 queueLoadB->setIcon(QIcon::fromTheme("document-open"));
193 queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
194 queueAppendB->setIcon(QIcon::fromTheme("document-import"));
195 queueAppendB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
196
197 loadSequenceB->setIcon(QIcon::fromTheme("document-open"));
198 loadSequenceB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
199 selectStartupScriptB->setIcon(QIcon::fromTheme("document-open"));
200 selectStartupScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
201 selectShutdownScriptB->setIcon(
202 QIcon::fromTheme("document-open"));
203 selectShutdownScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
204 selectFITSB->setIcon(QIcon::fromTheme("document-open"));
205 selectFITSB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
206
207 startupB->setIcon(
208 QIcon::fromTheme("media-playback-start"));
209 startupB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
210 shutdownB->setIcon(
211 QIcon::fromTheme("media-playback-start"));
212 shutdownB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
213
214 // 2023-06-27 sterne-jaeger: For simplicity reasons, the repeat option
215 // for all sequences is only active if we do consider the past
216 schedulerRepeatEverything->setEnabled(Options::rememberJobProgress() == false);
217 executionSequenceLimit->setEnabled(Options::rememberJobProgress() == false);
218 executionSequenceLimit->setValue(Options::schedulerExecutionSequencesLimit());
219
222
226 connect(selectStartupScriptB, &QPushButton::clicked, this, &Scheduler::selectStartupScript);
227 connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript);
228
229 connect(KStars::Instance()->actionCollection()->action("show_mosaic_panel"), &QAction::triggered, this, [this](bool checked)
230 {
231 mosaicB->setDown(checked);
232 });
233 connect(mosaicB, &QPushButton::clicked, this, []()
234 {
235 KStars::Instance()->actionCollection()->action("show_mosaic_panel")->trigger();
236 });
237 connect(addToQueueB, &QPushButton::clicked, [this]()
238 {
239 // add job from UI
240 addJob();
241 });
242 connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob);
245 connect(evaluateOnlyB, &QPushButton::clicked, process().data(), &SchedulerProcess::startJobEvaluation);
250
251
252 // These connections are looking for changes in the rows queueTable is displaying.
253 connect(queueTable->verticalScrollBar(), &QScrollBar::valueChanged, [this]()
254 {
255 updateJobTable();
256 });
257 connect(queueTable->verticalScrollBar(), &QAbstractSlider::rangeChanged, [this]()
258 {
259 updateJobTable();
260 });
261
262 startB->setIcon(QIcon::fromTheme("media-playback-start"));
263 startB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
264 pauseB->setIcon(QIcon::fromTheme("media-playback-pause"));
265 pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
266 pauseB->setCheckable(false);
267
268 connect(startB, &QPushButton::clicked, this, &Scheduler::toggleScheduler);
269 connect(pauseB, &QPushButton::clicked, this, &Scheduler::pause);
270
271 connect(queueSaveAsB, &QPushButton::clicked, this, &Scheduler::saveAs);
272 connect(queueSaveB, &QPushButton::clicked, this, &Scheduler::save);
273 connect(queueLoadB, &QPushButton::clicked, this, [&]()
274 {
275 load(true);
276 });
277 connect(queueAppendB, &QPushButton::clicked, this, [&]()
278 {
279 load(false);
280 });
281
283
284 // Connect to the state machine
285 connect(moduleState().data(), &SchedulerModuleState::ekosStateChanged, this, &Scheduler::ekosStateChanged);
286 connect(moduleState().data(), &SchedulerModuleState::indiStateChanged, this, &Scheduler::indiStateChanged);
287 connect(moduleState().data(), &SchedulerModuleState::schedulerStateChanged, this, &Scheduler::handleSchedulerStateChanged);
288 connect(moduleState().data(), &SchedulerModuleState::startupStateChanged, this, &Scheduler::startupStateChanged);
289 connect(moduleState().data(), &SchedulerModuleState::shutdownStateChanged, this, &Scheduler::shutdownStateChanged);
290 connect(moduleState().data(), &SchedulerModuleState::parkWaitStateChanged, this, &Scheduler::parkWaitStateChanged);
291 connect(moduleState().data(), &SchedulerModuleState::profilesChanged, this, &Scheduler::updateProfiles);
292 connect(moduleState().data(), &SchedulerModuleState::currentPositionChanged, queueTable, &QTableWidget::selectRow);
293 connect(moduleState().data(), &SchedulerModuleState::jobStageChanged, this, &Scheduler::updateJobStageUI);
294 connect(moduleState().data(), &SchedulerModuleState::updateNightTime, this, &Scheduler::updateNightTime);
295 connect(moduleState().data(), &SchedulerModuleState::currentProfileChanged, this, [&]()
296 {
297 schedulerProfileCombo->setCurrentText(moduleState()->currentProfile());
298 });
299 // Connect to process engine
300 connect(process().data(), &SchedulerProcess::schedulerStopped, this, &Scheduler::schedulerStopped);
301 connect(process().data(), &SchedulerProcess::schedulerPaused, this, &Scheduler::handleSetPaused);
302 connect(process().data(), &SchedulerProcess::shutdownStarted, this, &Scheduler::handleShutdownStarted);
303 connect(process().data(), &SchedulerProcess::schedulerSleeping, this, &Scheduler::handleSchedulerSleeping);
304 connect(process().data(), &SchedulerProcess::jobsUpdated, this, &Scheduler::handleJobsUpdated);
305 connect(process().data(), &SchedulerProcess::targetDistance, this, &Scheduler::targetDistance);
306 connect(process().data(), &SchedulerProcess::updateJobTable, this, &Scheduler::updateJobTable);
307 connect(process().data(), &SchedulerProcess::clearJobTable, this, &Scheduler::clearJobTable);
308 connect(process().data(), &SchedulerProcess::addJob, this, &Scheduler::addJob);
309 connect(process().data(), &SchedulerProcess::changeCurrentSequence, this, &Scheduler::setSequence);
310 connect(process().data(), &SchedulerProcess::jobStarted, this, &Scheduler::jobStarted);
311 connect(process().data(), &SchedulerProcess::jobEnded, this, &Scheduler::jobEnded);
312 connect(process().data(), &SchedulerProcess::syncGreedyParams, this, &Scheduler::syncGreedyParams);
313 connect(process().data(), &SchedulerProcess::syncGUIToGeneralSettings, this, &Scheduler::syncGUIToGeneralSettings);
314 connect(process().data(), &SchedulerProcess::changeSleepLabel, this, &Scheduler::changeSleepLabel);
315 connect(process().data(), &SchedulerProcess::updateSchedulerURL, this, &Scheduler::updateSchedulerURL);
316 connect(process().data(), &SchedulerProcess::interfaceReady, this, &Scheduler::interfaceReady);
317 connect(process().data(), &SchedulerProcess::newWeatherStatus, this, &Scheduler::setWeatherStatus);
318 // Connect geographical location - when it is available
319 //connect(KStarsData::Instance()..., &LocationDialog::locationChanged..., this, &Scheduler::simClockTimeChanged);
320
321 // Restore values for general settings.
323
324
325 connect(errorHandlingButtonGroup, static_cast<void (QButtonGroup::*)(QAbstractButton *)>
327 {
328 Q_UNUSED(button)
330 Options::setErrorHandlingStrategy(strategy);
331 errorHandlingStrategyDelay->setEnabled(strategy != ERROR_DONT_RESTART);
332 });
333 connect(errorHandlingStrategyDelay, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [](int value)
334 {
335 Options::setErrorHandlingStrategyDelay(value);
336 });
337
338 // Retiring the Classic algorithm.
339 if (Options::schedulerAlgorithm() != ALGORITHM_GREEDY)
340 {
341 process()->appendLogText(
342 i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm."));
343 Options::setSchedulerAlgorithm(ALGORITHM_GREEDY);
344 }
345
346 // restore default values for scheduler algorithm
347 setAlgorithm(Options::schedulerAlgorithm());
348
349 connect(copySkyCenterB, &QPushButton::clicked, this, [this]()
350 {
351 SkyPoint center = SkyMap::Instance()->getCenterPoint();
352 //center.deprecess(KStarsData::Instance()->updateNum());
353 center.catalogueCoord(KStarsData::Instance()->updateNum()->julianDay());
354 raBox->show(center.ra0());
355 decBox->show(center.dec0());
356 });
357
359
360 connect(editSequenceB, &QPushButton::clicked, this, [this]()
361 {
362 if (!m_SequenceEditor)
363 m_SequenceEditor.reset(new SequenceEditor(this));
364
365 m_SequenceEditor->show();
366 m_SequenceEditor->raise();
367 });
368
369 m_JobUpdateDebounce.setSingleShot(true);
370 m_JobUpdateDebounce.setInterval(1000);
371 connect(&m_JobUpdateDebounce, &QTimer::timeout, this, [this]()
372 {
373 emit jobsUpdated(moduleState()->getJSONJobs());
374 });
375
376 moduleState()->calculateDawnDusk();
377 process()->loadProfiles();
378
379 watchJobChanges(true);
380
381 loadGlobalSettings();
382 connectSettings();
383}
384
385QString Scheduler::getCurrentJobName()
386{
387 return (activeJob() != nullptr ? activeJob()->getName() : "");
388}
389
391{
392 /* Don't double watch, this will cause multiple signals to be connected */
393 if (enable == jobChangesAreWatched)
394 return;
395
396 /* These are the widgets we want to connect, per signal function, to listen for modifications */
397 QLineEdit * const lineEdits[] =
398 {
399 nameEdit,
400 groupEdit,
401 raBox,
402 decBox,
403 fitsEdit,
404 sequenceEdit,
405 schedulerStartupScript,
406 schedulerShutdownScript
407 };
408
409 QDateTimeEdit * const dateEdits[] =
410 {
411 startupTimeEdit,
412 schedulerUntilValue
413 };
414
415 QComboBox * const comboBoxes[] =
416 {
417 schedulerProfileCombo,
418 };
419
420 QButtonGroup * const buttonGroups[] =
421 {
422 stepsButtonGroup,
423 errorHandlingButtonGroup,
424 startupButtonGroup,
425 constraintButtonGroup,
426 completionButtonGroup,
427 startupProcedureButtonGroup,
428 shutdownProcedureGroup
429 };
430
431 QAbstractButton * const buttons[] =
432 {
433 errorHandlingRescheduleErrorsCB
434 };
435
436 QSpinBox * const spinBoxes[] =
437 {
438 schedulerExecutionSequencesLimit,
439 errorHandlingStrategyDelay
440 };
441
442 QDoubleSpinBox * const dspinBoxes[] =
443 {
444 schedulerMoonSeparationValue,
445 schedulerAltitudeValue,
446 positionAngleSpin,
447 };
448
449 if (enable)
450 {
451 /* Connect the relevant signal to setDirty. Note that we are not keeping the connection object: we will
452 * only use that signal once, and there will be no leaks. If we were connecting multiple receiver functions
453 * to the same signal, we would have to be selective when disconnecting. We also use a lambda to absorb the
454 * excess arguments which cannot be passed to setDirty, and limit captured arguments to 'this'.
455 * The main problem with this implementation compared to the macro method is that it is now possible to
456 * stack signal connections. That is, multiple calls to WatchJobChanges will cause multiple signal-to-slot
457 * instances to be registered. As a result, one click will produce N signals, with N*=2 for each call to
458 * WatchJobChanges(true) missing its WatchJobChanges(false) counterpart.
459 */
460 for (auto * const control : lineEdits)
461 connect(control, &QLineEdit::editingFinished, this, [this]()
462 {
463 setDirty();
464 });
465 for (auto * const control : dateEdits)
466 connect(control, &QDateTimeEdit::editingFinished, this, [this]()
467 {
468 setDirty();
469 });
470 for (auto * const control : comboBoxes)
471 connect(control, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this]()
472 {
473 setDirty();
474 });
475 for (auto * const control : buttonGroups)
476#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
477 connect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, [this](int, bool)
478#else
479 connect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this, [this](int, bool)
480#endif
481 {
482 setDirty();
483 });
484 for (auto * const control : buttons)
485 connect(control, static_cast<void (QAbstractButton::*)(bool)>(&QAbstractButton::clicked), this, [this](bool)
486 {
487 setDirty();
488 });
489 for (auto * const control : spinBoxes)
490 connect(control, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]()
491 {
492 setDirty();
493 });
494 for (auto * const control : dspinBoxes)
495 connect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [this](double)
496 {
497 setDirty();
498 });
499 }
500 else
501 {
502 /* Disconnect the relevant signal from each widget. Actually, this method removes all signals from the widgets,
503 * because we did not take care to keep the connection object when connecting. No problem in our case, we do not
504 * expect other signals to be connected. Because we used a lambda, we cannot use the same function object to
505 * disconnect selectively.
506 */
507 for (auto * const control : lineEdits)
508 disconnect(control, &QLineEdit::editingFinished, this, nullptr);
509 for (auto * const control : dateEdits)
510 disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr);
511 for (auto * const control : comboBoxes)
512 disconnect(control, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, nullptr);
513 for (auto * const control : buttons)
514 disconnect(control, static_cast<void (QAbstractButton::*)(bool)>(&QAbstractButton::clicked), this, nullptr);
515 for (auto * const control : buttonGroups)
516#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
517 disconnect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, nullptr);
518#else
519 disconnect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this, nullptr);
520#endif
521 for (auto * const control : spinBoxes)
522 disconnect(control, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, nullptr);
523 for (auto * const control : dspinBoxes)
524 disconnect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, nullptr);
525 }
526
527 jobChangesAreWatched = enable;
528}
529
531{
532 schedulerRepeatEverything->setEnabled(Options::rememberJobProgress() == false);
533 executionSequenceLimit->setEnabled(Options::rememberJobProgress() == false);
534}
535
537{
538 if (FindDialog::Instance()->execWithParent(Ekos::Manager::Instance()) == QDialog::Accepted)
539 {
540 SkyObject *object = FindDialog::Instance()->targetObject();
541 addObject(object);
542 }
543}
544
545void Scheduler::addObject(SkyObject *object)
546{
547 if (object != nullptr)
548 {
549 QString finalObjectName(object->name());
550
551 if (object->name() == "star")
552 {
553 StarObject *s = dynamic_cast<StarObject *>(object);
554
555 if (s->getHDIndex() != 0)
556 finalObjectName = QString("HD %1").arg(s->getHDIndex());
557 }
558
559 nameEdit->setText(finalObjectName);
560 raBox->show(object->ra0());
561 decBox->show(object->dec0());
562
563 setDirty();
564 }
565}
566
568{
569 auto url = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Select FITS/XISF Image"), dirPath,
570 "FITS (*.fits *.fit);;XISF (*.xisf)");
571 if (url.isEmpty())
572 return;
573
574 processFITSSelection(url);
575}
576
577void Scheduler::processFITSSelection(const QUrl &url)
578{
579 if (url.isEmpty())
580 return;
581
582 fitsURL = url;
583 dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename));
584 fitsEdit->setText(fitsURL.toLocalFile());
585 setDirty();
586
587 const QString filename = fitsEdit->text();
588 int status = 0;
589 double ra = 0, dec = 0;
590 dms raDMS, deDMS;
591 char comment[128], error_status[512];
592 fitsfile *fptr = nullptr;
593
594 if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status))
595 {
596 fits_report_error(stderr, status);
597 fits_get_errstatus(status, error_status);
598 qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status);
599 return;
600 }
601
602 status = 0;
603 if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status))
604 {
605 fits_report_error(stderr, status);
606 fits_get_errstatus(status, error_status);
607 qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status);
608 return;
609 }
610
611 status = 0;
612 char objectra_str[32] = {0};
613 if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status))
614 {
615 if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status))
616 {
617 fits_report_error(stderr, status);
618 fits_get_errstatus(status, error_status);
619 process()->appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status)));
620 return;
621 }
622
623 raDMS.setD(ra);
624 }
625 else
626 {
627 raDMS = dms::fromString(objectra_str, false);
628 }
629
630 status = 0;
631 char objectde_str[32] = {0};
632 if (fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status))
633 {
634 if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status))
635 {
636 fits_report_error(stderr, status);
637 fits_get_errstatus(status, error_status);
638 process()->appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status)));
639 return;
640 }
641
642 deDMS.setD(dec);
643 }
644 else
645 {
646 deDMS = dms::fromString(objectde_str, true);
647 }
648
649 raBox->show(raDMS);
650 decBox->show(deDMS);
651
652 char object_str[256] = {0};
653 if (fits_read_key(fptr, TSTRING, "OBJECT", object_str, comment, &status))
654 {
655 QFileInfo info(filename);
656 nameEdit->setText(info.completeBaseName());
657 }
658 else
659 {
660 nameEdit->setText(object_str);
661 }
662}
663
664void Scheduler::setSequence(const QString &sequenceFileURL)
665{
666 sequenceURL = QUrl::fromLocalFile(sequenceFileURL);
667
668 if (sequenceFileURL.isEmpty())
669 return;
670 dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename));
671
672 sequenceEdit->setText(sequenceURL.toLocalFile());
673
674 setDirty();
675}
676
678{
679 QString file = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Select Sequence Queue"),
680 dirPath.toLocalFile(),
681 i18n("Ekos Sequence Queue (*.esq)"));
682
683 setSequence(file);
684}
685
687{
688 moduleState()->setStartupScriptURL(QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window",
689 "Select Startup Script"),
690 dirPath,
691 i18n("Script (*)")));
692 if (moduleState()->startupScriptURL().isEmpty())
693 return;
694
695 dirPath = QUrl(moduleState()->startupScriptURL().url(QUrl::RemoveFilename));
696
697 moduleState()->setDirty(true);
698 schedulerStartupScript->setText(moduleState()->startupScriptURL().toLocalFile());
699}
700
702{
703 moduleState()->setShutdownScriptURL(QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window",
704 "Select Shutdown Script"),
705 dirPath,
706 i18n("Script (*)")));
707 if (moduleState()->shutdownScriptURL().isEmpty())
708 return;
709
710 dirPath = QUrl(moduleState()->shutdownScriptURL().url(QUrl::RemoveFilename));
711
712 moduleState()->setDirty(true);
713 schedulerShutdownScript->setText(moduleState()->shutdownScriptURL().toLocalFile());
714}
715
716void Scheduler::addJob(SchedulerJob *job)
717{
718 if (0 <= jobUnderEdit)
719 {
720 // select the job currently being edited
721 job = moduleState()->jobs().at(jobUnderEdit);
722 // if existing, save it
723 if (job != nullptr)
724 saveJob(job);
725 // in any case, reset editing
726 resetJobEdit();
727 }
728 else
729 {
730 // remember the number of rows to select the first one appended
731 int currentRow = moduleState()->currentPosition();
732
733 //If no row is selected, the job will be appended at the end of the list, otherwise below the current selection
734 if (currentRow < 0)
735 currentRow = queueTable->rowCount();
736 else
737 currentRow++;
738
739 /* If a job is being added, save fields into a new job */
740 saveJob(job);
741
742 // select the first appended row (if any was added)
743 if (moduleState()->jobs().count() > currentRow)
744 moduleState()->setCurrentPosition(currentRow);
745 }
746
747 emit jobsUpdated(moduleState()->getJSONJobs());
748}
749
750bool Scheduler::fillJobFromUI(SchedulerJob *job)
751{
752 if (nameEdit->text().isEmpty())
753 {
754 process()->appendLogText(i18n("Warning: Target name is required."));
755 return false;
756 }
757
758 if (sequenceEdit->text().isEmpty())
759 {
760 process()->appendLogText(i18n("Warning: Sequence file is required."));
761 return false;
762 }
763
764 // Coordinates are required unless it is a FITS file
765 if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty())
766 {
767 process()->appendLogText(i18n("Warning: Target coordinates are required."));
768 return false;
769 }
770
771 bool raOk = false, decOk = false;
772 dms /*const*/ ra(raBox->createDms(&raOk));
773 dms /*const*/ dec(decBox->createDms(&decOk));
774
775 if (raOk == false)
776 {
777 process()->appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text()));
778 return false;
779 }
780
781 if (decOk == false)
782 {
783 process()->appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text()));
784 return false;
785 }
786
787 /* Configure or reconfigure the observation job */
788 fitsURL = QUrl::fromLocalFile(fitsEdit->text());
789
790 // Get several job values depending on the state of the UI.
791
792 StartupCondition startCondition = START_AT;
793 if (asapConditionR->isChecked())
794 startCondition = START_ASAP;
795
796 CompletionCondition stopCondition = FINISH_AT;
797 if (schedulerCompleteSequences->isChecked())
798 stopCondition = FINISH_SEQUENCE;
799 else if (schedulerRepeatSequences->isChecked())
800 stopCondition = FINISH_REPEAT;
801 else if (schedulerUntilTerminated->isChecked())
802 stopCondition = FINISH_LOOP;
803
804 double altConstraint = SchedulerJob::UNDEFINED_ALTITUDE;
805 if (schedulerAltitude->isChecked())
806 altConstraint = schedulerAltitudeValue->value();
807
808 double moonConstraint = -1;
809 if (schedulerMoonSeparation->isChecked())
810 moonConstraint = schedulerMoonSeparationValue->value();
811
812 // The reason for this kitchen-sink function is to separate the UI from the
813 // job setup, to allow for testing.
814 SchedulerUtils::setupJob(*job, nameEdit->text(), groupEdit->text(), ra, dec,
815 KStarsData::Instance()->ut().djd(),
816 positionAngleSpin->value(), sequenceURL, fitsURL,
817
818 startCondition, startupTimeEdit->dateTime(),
819 stopCondition, schedulerUntilValue->dateTime(), schedulerExecutionSequencesLimit->value(),
820
821 altConstraint,
822 moonConstraint,
823 schedulerWeather->isChecked(),
824 schedulerTwilight->isChecked(),
825 schedulerHorizon->isChecked(),
826
827 schedulerTrackStep->isChecked(),
828 schedulerFocusStep->isChecked(),
829 schedulerAlignStep->isChecked(),
830 schedulerGuideStep->isChecked());
831
832 // success
833 updateJobTable(job);
834 return true;
835}
836
837void Scheduler::saveJob(SchedulerJob *job)
838{
839 watchJobChanges(false);
840
841 /* Create or Update a scheduler job, append below current selection */
842 int currentRow = moduleState()->currentPosition() + 1;
843
844 /* Add job to queue only if it is new, else reuse current row.
845 * Make sure job is added at the right index, now that queueTable may have a line selected without being edited.
846 */
847 if (0 <= jobUnderEdit)
848 {
849 /* FIXME: jobUnderEdit is a parallel variable that may cause issues if it desyncs from moduleState()->currentPosition(). */
850 if (jobUnderEdit != currentRow - 1)
851 {
852 qCWarning(KSTARS_EKOS_SCHEDULER) << "BUG: the observation job under edit does not match the selected row in the job table.";
853 }
854
855 /* Use the job in the row currently edited */
856 job = moduleState()->jobs().at(jobUnderEdit);
857 // try to fill the job from the UI and exit if it fails
858 if (fillJobFromUI(job) == false)
859 {
860 watchJobChanges(true);
861 return;
862 }
863 }
864 else
865 {
866 if (job == nullptr)
867 {
868 /* Instantiate a new job, insert it in the job list and add a row in the table for it just after the row currently selected. */
869 job = new SchedulerJob();
870 // try to fill the job from the UI and exit if it fails
871 if (fillJobFromUI(job) == false)
872 {
873 delete(job);
874 watchJobChanges(true);
875 return;
876 }
877 }
878 /* Insert the job in the job list and add a row in the table for it just after the row currently selected. */
879 moduleState()->mutlableJobs().insert(currentRow, job);
880 insertJobTableRow(currentRow);
881 }
882
883 /* Verifications */
884 // Warn user if a duplicated job is in the list - same target, same sequence
885 // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list!
886 int numWarnings = 0;
887 foreach (SchedulerJob *a_job, moduleState()->jobs())
888 {
889 if (a_job == job)
890 {
891 break;
892 }
893 else if (a_job->getName() == job->getName())
894 {
895 int const a_job_row = moduleState()->jobs().indexOf(a_job);
896
897 /* FIXME: Warning about duplicate jobs only checks the target name, doing it properly would require checking storage for each sequence job of each scheduler job. */
898 process()->appendLogText(i18n("Warning: job '%1' at row %2 has a duplicate target at row %3, "
899 "the scheduler may consider the same storage for captures.",
900 job->getName(), currentRow, a_job_row));
901
902 /* Warn the user in case the two jobs are really identical */
903 if (a_job->getSequenceFile() == job->getSequenceFile())
904 {
905 if (a_job->getRepeatsRequired() == job->getRepeatsRequired() && Options::rememberJobProgress())
906 process()->appendLogText(i18n("Warning: jobs '%1' at row %2 and %3 probably require a different repeat count "
907 "as currently they will complete simultaneously after %4 batches (or disable option 'Remember job progress')",
908 job->getName(), currentRow, a_job_row, job->getRepeatsRequired()));
909 }
910
911 // Don't need to warn over and over.
912 if (++numWarnings >= 1)
913 {
914 process()->appendLogText(i18n("Skipped checking for duplicates."));
915 break;
916 }
917 }
918 }
919
920 updateJobTable(job);
921
922 /* We just added or saved a job, so we have a job in the list - enable relevant buttons */
923 queueSaveAsB->setEnabled(true);
924 queueSaveB->setEnabled(true);
925 startB->setEnabled(true);
926 evaluateOnlyB->setEnabled(true);
927 setJobManipulation(true, true);
928 checkJobInputComplete();
929
930 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 was saved.").arg(job->getName()).arg(currentRow + 1);
931
932 watchJobChanges(true);
933
934 if (SCHEDULER_LOADING != moduleState()->schedulerState())
935 {
936 process()->evaluateJobs(true);
937 }
938}
939
940void Scheduler::syncGUIToJob(SchedulerJob *job)
941{
942 nameEdit->setText(job->getName());
943 groupEdit->setText(job->getGroup());
944
945 raBox->show(job->getTargetCoords().ra0());
946 decBox->show(job->getTargetCoords().dec0());
947
948 // fitsURL/sequenceURL are not part of UI, but the UI serves as model, so keep them here for now
949 fitsURL = job->getFITSFile().isEmpty() ? QUrl() : job->getFITSFile();
950 sequenceURL = job->getSequenceFile();
951 fitsEdit->setText(fitsURL.toLocalFile());
952 sequenceEdit->setText(sequenceURL.toLocalFile());
953
954 positionAngleSpin->setValue(job->getPositionAngle());
955
956 schedulerTrackStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK);
957 schedulerFocusStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS);
958 schedulerAlignStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN);
959 schedulerGuideStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE);
960
961 switch (job->getFileStartupCondition())
962 {
963 case START_ASAP:
964 asapConditionR->setChecked(true);
965 break;
966
967 case START_AT:
968 startupTimeConditionR->setChecked(true);
969 startupTimeEdit->setDateTime(job->getStartupTime());
970 break;
971 }
972
973 if (job->getMinAltitude())
974 {
975 schedulerAltitude->setChecked(true);
976 schedulerAltitudeValue->setValue(job->getMinAltitude());
977 }
978 else
979 {
980 schedulerAltitude->setChecked(false);
981 schedulerAltitudeValue->setValue(DEFAULT_MIN_ALTITUDE);
982 }
983
984 if (job->getMinMoonSeparation() >= 0)
985 {
986 schedulerMoonSeparation->setChecked(true);
987 schedulerMoonSeparationValue->setValue(job->getMinMoonSeparation());
988 }
989 else
990 {
991 schedulerMoonSeparation->setChecked(false);
992 schedulerMoonSeparationValue->setValue(DEFAULT_MIN_MOON_SEPARATION);
993 }
994
995 schedulerWeather->setChecked(job->getEnforceWeather());
996
997 schedulerTwilight->blockSignals(true);
998 schedulerTwilight->setChecked(job->getEnforceTwilight());
999 schedulerTwilight->blockSignals(false);
1000
1001 schedulerHorizon->blockSignals(true);
1002 schedulerHorizon->setChecked(job->getEnforceArtificialHorizon());
1003 schedulerHorizon->blockSignals(false);
1004
1005 switch (job->getCompletionCondition())
1006 {
1007 case FINISH_SEQUENCE:
1008 schedulerCompleteSequences->setChecked(true);
1009 break;
1010
1011 case FINISH_REPEAT:
1012 schedulerRepeatSequences->setChecked(true);
1013 schedulerExecutionSequencesLimit->setValue(job->getRepeatsRequired());
1014 break;
1015
1016 case FINISH_LOOP:
1017 schedulerUntilTerminated->setChecked(true);
1018 break;
1019
1020 case FINISH_AT:
1021 schedulerUntil->setChecked(true);
1022 schedulerUntilValue->setDateTime(job->getCompletionTime());
1023 break;
1024 }
1025
1026 updateNightTime(job);
1027
1028 setJobManipulation(true, true);
1029}
1030
1032{
1033 schedulerParkDome->setChecked(Options::schedulerParkDome());
1034 schedulerParkMount->setChecked(Options::schedulerParkMount());
1035 schedulerCloseDustCover->setChecked(Options::schedulerCloseDustCover());
1036 schedulerWarmCCD->setChecked(Options::schedulerWarmCCD());
1037 schedulerUnparkDome->setChecked(Options::schedulerUnparkDome());
1038 schedulerUnparkMount->setChecked(Options::schedulerUnparkMount());
1039 schedulerOpenDustCover->setChecked(Options::schedulerOpenDustCover());
1040 setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy()));
1041 errorHandlingStrategyDelay->setValue(Options::errorHandlingStrategyDelay());
1042 errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors());
1043 schedulerStartupScript->setText(moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile));
1044 schedulerShutdownScript->setText(moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile));
1045
1046 if (process()->captureInterface() != nullptr)
1047 {
1048 QVariant hasCoolerControl = process()->captureInterface()->property("coolerControl");
1049 if (hasCoolerControl.isValid())
1050 {
1051 schedulerWarmCCD->setEnabled(hasCoolerControl.toBool());
1052 moduleState()->setCaptureReady(true);
1053 }
1054 }
1055}
1056
1057void Scheduler::updateNightTime(SchedulerJob const *job)
1058{
1059 // select job from current position
1060 if (job == nullptr && moduleState()->jobs().size() > 0)
1061 {
1062 int const currentRow = moduleState()->currentPosition();
1063 if (0 <= currentRow && currentRow < moduleState()->jobs().size())
1064 job = moduleState()->jobs().at(currentRow);
1065
1066 if (job == nullptr)
1067 {
1068 qCWarning(KSTARS_EKOS_SCHEDULER()) << "Cannot update night time, no matching job found at line" << currentRow;
1069 return;
1070 }
1071 }
1072
1073 QDateTime const dawn = job ? job->getDawnAstronomicalTwilight() : moduleState()->Dawn();
1074 QDateTime const dusk = job ? job->getDuskAstronomicalTwilight() : moduleState()->Dusk();
1075
1076 QChar const warning(dawn == dusk ? 0x26A0 : '-');
1077 nightTime->setText(i18n("%1 %2 %3", dusk.toString("hh:mm"), warning, dawn.toString("hh:mm")));
1078}
1079
1081{
1082 if (jobUnderEdit == i.row())
1083 return;
1084
1085 SchedulerJob * const job = moduleState()->jobs().at(i.row());
1086
1087 if (job == nullptr)
1088 return;
1089
1090 watchJobChanges(false);
1091
1092 //job->setState(SCHEDJOB_IDLE);
1093 //job->setStage(SCHEDSTAGE_IDLE);
1094 syncGUIToJob(job);
1095
1096 /* Turn the add button into an apply button */
1097 setJobAddApply(false);
1098
1099 /* Disable scheduler start/evaluate buttons */
1100 startB->setEnabled(false);
1101 evaluateOnlyB->setEnabled(false);
1102
1103 /* Don't let the end-user remove a job being edited */
1104 setJobManipulation(false, false);
1105
1106 jobUnderEdit = i.row();
1107 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg(
1108 jobUnderEdit + 1);
1109
1110 watchJobChanges(true);
1111}
1112
1114{
1115 schedulerURL = QUrl::fromLocalFile(fileURL);
1116 // update save button tool tip
1117 queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName());
1118}
1119
1121{
1122 Q_UNUSED(deselected)
1123
1124
1125 if (jobChangesAreWatched == false || selected.empty())
1126 // || (current.row() + 1) > moduleState()->jobs().size())
1127 return;
1128
1129 const QModelIndex current = selected.indexes().first();
1130 // this should not happen, but avoids crashes
1131 if ((current.row() + 1) > moduleState()->jobs().size())
1132 {
1133 qCWarning(KSTARS_EKOS_SCHEDULER()) << "Unexpected row number" << current.row() << "- ignoring.";
1134 return;
1135 }
1136 moduleState()->setCurrentPosition(current.row());
1137 SchedulerJob * const job = moduleState()->jobs().at(current.row());
1138
1139 if (job != nullptr)
1140 {
1141 if (jobUnderEdit < 0)
1142 syncGUIToJob(job);
1143 else if (jobUnderEdit != current.row())
1144 {
1145 // avoid changing the UI values for the currently edited job
1146 process()->appendLogText(i18n("Stop editing of job #%1, resetting to original value.", jobUnderEdit + 1));
1147 resetJobEdit();
1148 syncGUIToJob(job);
1149 }
1150 }
1151 else nightTime->setText("-");
1152}
1153
1155{
1156 setJobManipulation(index.isValid(), index.isValid());
1157}
1158
1159void Scheduler::setJobAddApply(bool add_mode)
1160{
1161 if (add_mode)
1162 {
1163 addToQueueB->setIcon(QIcon::fromTheme("list-add"));
1164 addToQueueB->setToolTip(i18n("Use edition fields to create a new job in the observation list."));
1165 addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
1166 }
1167 else
1168 {
1169 addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply"));
1170 addToQueueB->setToolTip(i18n("Apply job changes."));
1171 }
1172 // check if the button should be enabled
1173 checkJobInputComplete();
1174}
1175
1176void Scheduler::setJobManipulation(bool can_reorder, bool can_delete)
1177{
1178 if (can_reorder)
1179 {
1180 int const currentRow = moduleState()->currentPosition();
1181 queueUpB->setEnabled(0 < currentRow);
1182 queueDownB->setEnabled(currentRow < queueTable->rowCount() - 1);
1183 }
1184 else
1185 {
1186 queueUpB->setEnabled(false);
1187 queueDownB->setEnabled(false);
1188 }
1189 sortJobsB->setEnabled(can_reorder);
1190 removeFromQueueB->setEnabled(can_delete);
1191}
1192
1194{
1195 /* Add jobs not reordered at the end of the list, in initial order */
1196 foreach (SchedulerJob* job, moduleState()->jobs())
1197 if (!reordered_sublist.contains(job))
1198 reordered_sublist.append(job);
1199
1200 if (moduleState()->jobs() != reordered_sublist)
1201 {
1202 /* Remember job currently selected */
1203 int const selectedRow = moduleState()->currentPosition();
1204 SchedulerJob * const selectedJob = 0 <= selectedRow ? moduleState()->jobs().at(selectedRow) : nullptr;
1205
1206 /* Reassign list */
1207 moduleState()->setJobs(reordered_sublist);
1208
1209 /* Refresh the table */
1210 for (SchedulerJob *job : moduleState()->jobs())
1211 updateJobTable(job);
1212
1213 /* Reselect previously selected job */
1214 if (nullptr != selectedJob)
1215 moduleState()->setCurrentPosition(moduleState()->jobs().indexOf(selectedJob));
1216
1217 return true;
1218 }
1219 else return false;
1220}
1221
1223{
1224 int const rowCount = queueTable->rowCount();
1225 int const currentRow = queueTable->currentRow();
1226 int const destinationRow = currentRow - 1;
1227
1228 /* No move if no job selected, if table has one line or less or if destination is out of table */
1229 if (currentRow < 0 || rowCount <= 1 || destinationRow < 0)
1230 return;
1231
1232 /* Swap jobs in the list */
1233#if QT_VERSION >= QT_VERSION_CHECK(5,13,0)
1234 moduleState()->mutlableJobs().swapItemsAt(currentRow, destinationRow);
1235#else
1236 moduleState()->jobs().swap(currentRow, destinationRow);
1237#endif
1238
1239 //Update the two table rows
1240 updateJobTable(moduleState()->jobs().at(currentRow));
1241 updateJobTable(moduleState()->jobs().at(destinationRow));
1242
1243 /* Move selection to destination row */
1244 moduleState()->setCurrentPosition(destinationRow);
1245 setJobManipulation(true, true);
1246
1247 /* Make list modified and evaluate jobs */
1248 moduleState()->setDirty(true);
1249 process()->evaluateJobs(true);
1250}
1251
1253{
1254 int const rowCount = queueTable->rowCount();
1255 int const currentRow = queueTable->currentRow();
1256 int const destinationRow = currentRow + 1;
1257
1258 /* No move if no job selected, if table has one line or less or if destination is out of table */
1259 if (currentRow < 0 || rowCount <= 1 || destinationRow >= rowCount)
1260 return;
1261
1262 /* Swap jobs in the list */
1263#if QT_VERSION >= QT_VERSION_CHECK(5,13,0)
1264 moduleState()->mutlableJobs().swapItemsAt(currentRow, destinationRow);
1265#else
1266 moduleState()->mutlableJobs().swap(currentRow, destinationRow);
1267#endif
1268
1269 //Update the two table rows
1270 updateJobTable(moduleState()->jobs().at(currentRow));
1271 updateJobTable(moduleState()->jobs().at(destinationRow));
1272
1273 /* Move selection to destination row */
1274 moduleState()->setCurrentPosition(destinationRow);
1275 setJobManipulation(true, true);
1276
1277 /* Make list modified and evaluate jobs */
1278 moduleState()->setDirty(true);
1279 process()->evaluateJobs(true);
1280}
1281
1282void Scheduler::updateJobTable(SchedulerJob *job)
1283{
1284 // handle full table update
1285 if (job == nullptr)
1286 {
1287 for (auto onejob : moduleState()->jobs())
1288 updateJobTable(onejob);
1289
1290 return;
1291 }
1292
1293 const int row = moduleState()->jobs().indexOf(job);
1294 // Ignore unknown jobs
1295 if (row < 0)
1296 return;
1297 // ensure that the row in the table exists
1298 if (row >= queueTable->rowCount())
1299 insertJobTableRow(row - 1, false);
1300
1301 QTableWidgetItem *nameCell = queueTable->item(row, static_cast<int>(SCHEDCOL_NAME));
1302 QTableWidgetItem *statusCell = queueTable->item(row, static_cast<int>(SCHEDCOL_STATUS));
1303 QTableWidgetItem *altitudeCell = queueTable->item(row, static_cast<int>(SCHEDCOL_ALTITUDE));
1304 QTableWidgetItem *startupCell = queueTable->item(row, static_cast<int>(SCHEDCOL_STARTTIME));
1305 QTableWidgetItem *completionCell = queueTable->item(row, static_cast<int>(SCHEDCOL_ENDTIME));
1306 QTableWidgetItem *captureCountCell = queueTable->item(row, static_cast<int>(SCHEDCOL_CAPTURES));
1307
1308 // Only in testing.
1309 if (!nameCell) return;
1310
1311 if (nullptr != nameCell)
1312 {
1313 nameCell->setText(job->getName());
1314 updateCellStyle(job, nameCell);
1315 if (nullptr != nameCell->tableWidget())
1316 nameCell->tableWidget()->resizeColumnToContents(nameCell->column());
1317 }
1318
1319 if (nullptr != statusCell)
1320 {
1321 static QMap<SchedulerJobStatus, QString> stateStrings;
1322 static QString stateStringUnknown;
1323 if (stateStrings.isEmpty())
1324 {
1325 stateStrings[SCHEDJOB_IDLE] = i18n("Idle");
1326 stateStrings[SCHEDJOB_EVALUATION] = i18n("Evaluating");
1327 stateStrings[SCHEDJOB_SCHEDULED] = i18n("Scheduled");
1328 stateStrings[SCHEDJOB_BUSY] = i18n("Running");
1329 stateStrings[SCHEDJOB_INVALID] = i18n("Invalid");
1330 stateStrings[SCHEDJOB_COMPLETE] = i18n("Complete");
1331 stateStrings[SCHEDJOB_ABORTED] = i18n("Aborted");
1332 stateStrings[SCHEDJOB_ERROR] = i18n("Error");
1333 stateStringUnknown = i18n("Unknown");
1334 }
1335 statusCell->setText(stateStrings.value(job->getState(), stateStringUnknown));
1336 updateCellStyle(job, statusCell);
1337
1338 if (nullptr != statusCell->tableWidget())
1339 statusCell->tableWidget()->resizeColumnToContents(statusCell->column());
1340 }
1341
1342 if (nullptr != startupCell)
1343 {
1344 auto time = (job->getState() == SCHEDJOB_BUSY) ? job->getStateTime() : job->getStartupTime();
1345 /* Display startup time if it is valid */
1346 if (time.isValid())
1347 {
1348 startupCell->setText(QString("%1%2%L3° %4")
1349 .arg(job->getAltitudeAtStartup() < job->getMinAltitude() ? QString(QChar(0x26A0)) : "")
1350 .arg(QChar(job->isSettingAtStartup() ? 0x2193 : 0x2191))
1351 .arg(job->getAltitudeAtStartup(), 0, 'f', 1)
1352 .arg(time.toString(startupTimeEdit->displayFormat())));
1353
1354 switch (job->getFileStartupCondition())
1355 {
1356 /* If the original condition is START_AT/START_CULMINATION, startup time is fixed */
1357 case START_AT:
1358 startupCell->setIcon(QIcon::fromTheme("chronometer"));
1359 break;
1360
1361 /* If the original condition is START_ASAP, startup time is informational */
1362 case START_ASAP:
1363 startupCell->setIcon(QIcon());
1364 break;
1365
1366 default:
1367 break;
1368 }
1369 }
1370 /* Else do not display any startup time */
1371 else
1372 {
1373 startupCell->setText("-");
1374 startupCell->setIcon(QIcon());
1375 }
1376
1377 updateCellStyle(job, startupCell);
1378
1379 if (nullptr != startupCell->tableWidget())
1380 startupCell->tableWidget()->resizeColumnToContents(startupCell->column());
1381 }
1382
1383 if (nullptr != altitudeCell)
1384 {
1385 // FIXME: Cache altitude calculations
1386 bool is_setting = false;
1387 double const alt = SchedulerUtils::findAltitude(job->getTargetCoords(), QDateTime(), &is_setting);
1388
1389 altitudeCell->setText(QString("%1%L2°")
1390 .arg(QChar(is_setting ? 0x2193 : 0x2191))
1391 .arg(alt, 0, 'f', 1));
1392 updateCellStyle(job, altitudeCell);
1393
1394 if (nullptr != altitudeCell->tableWidget())
1395 altitudeCell->tableWidget()->resizeColumnToContents(altitudeCell->column());
1396 }
1397
1398 if (nullptr != completionCell)
1399 {
1400 if (job->getGreedyCompletionTime().isValid())
1401 {
1402 completionCell->setText(QString("%1")
1403 .arg(job->getGreedyCompletionTime().toString("hh:mm")));
1404 }
1405 else
1406 /* Display completion time if it is valid and job is not looping */
1407 if (FINISH_LOOP != job->getCompletionCondition() && job->getCompletionTime().isValid())
1408 {
1409 completionCell->setText(QString("%1%2%L3° %4")
1410 .arg(job->getAltitudeAtCompletion() < job->getMinAltitude() ? QString(QChar(0x26A0)) : "")
1411 .arg(QChar(job->isSettingAtCompletion() ? 0x2193 : 0x2191))
1412 .arg(job->getAltitudeAtCompletion(), 0, 'f', 1)
1413 .arg(job->getCompletionTime().toString(startupTimeEdit->displayFormat())));
1414
1415 switch (job->getCompletionCondition())
1416 {
1417 case FINISH_AT:
1418 completionCell->setIcon(QIcon::fromTheme("chronometer"));
1419 break;
1420
1421 case FINISH_SEQUENCE:
1422 case FINISH_REPEAT:
1423 default:
1424 completionCell->setIcon(QIcon());
1425 break;
1426 }
1427 }
1428 /* Else do not display any completion time */
1429 else
1430 {
1431 completionCell->setText("-");
1432 completionCell->setIcon(QIcon());
1433 }
1434
1435 updateCellStyle(job, completionCell);
1436 if (nullptr != completionCell->tableWidget())
1437 completionCell->tableWidget()->resizeColumnToContents(completionCell->column());
1438 }
1439
1440 if (nullptr != captureCountCell)
1441 {
1442 switch (job->getCompletionCondition())
1443 {
1444 case FINISH_AT:
1445 // FIXME: Attempt to calculate the number of frames until end - requires detailed imaging time
1446
1447 case FINISH_LOOP:
1448 // If looping, display the count of completed frames
1449 captureCountCell->setText(QString("%L1/-").arg(job->getCompletedCount()));
1450 break;
1451
1452 case FINISH_SEQUENCE:
1453 case FINISH_REPEAT:
1454 default:
1455 // If repeating, display the count of completed frames to the count of requested frames
1456 captureCountCell->setText(QString("%L1/%L2").arg(job->getCompletedCount()).arg(job->getSequenceCount()));
1457 break;
1458 }
1459
1460 QString tooltip = job->getProgressSummary();
1461 if (tooltip.size() == 0)
1462 tooltip = i18n("Count of captures stored for the job, based on its sequence job.\n"
1463 "This is a summary, additional specific frame types may be required to complete the job.");
1464 captureCountCell->setToolTip(tooltip);
1465
1466 updateCellStyle(job, captureCountCell);
1467 if (nullptr != captureCountCell->tableWidget())
1468 captureCountCell->tableWidget()->resizeColumnToContents(captureCountCell->column());
1469 }
1470
1471 m_JobUpdateDebounce.start();
1472}
1473
1474void Scheduler::insertJobTableRow(int row, bool above)
1475{
1476 const int pos = above ? row : row + 1;
1477
1478 // ensure that there are no gaps
1479 if (row > queueTable->rowCount())
1480 insertJobTableRow(row - 1, above);
1481
1482 queueTable->insertRow(pos);
1483
1484 QTableWidgetItem *nameCell = new QTableWidgetItem();
1485 queueTable->setItem(row, static_cast<int>(SCHEDCOL_NAME), nameCell);
1488
1489 QTableWidgetItem *statusCell = new QTableWidgetItem();
1490 queueTable->setItem(row, static_cast<int>(SCHEDCOL_STATUS), statusCell);
1493
1494 QTableWidgetItem *captureCount = new QTableWidgetItem();
1495 queueTable->setItem(row, static_cast<int>(SCHEDCOL_CAPTURES), captureCount);
1498
1499 QTableWidgetItem *startupCell = new QTableWidgetItem();
1500 queueTable->setItem(row, static_cast<int>(SCHEDCOL_STARTTIME), startupCell);
1503
1504 QTableWidgetItem *altitudeCell = new QTableWidgetItem();
1505 queueTable->setItem(row, static_cast<int>(SCHEDCOL_ALTITUDE), altitudeCell);
1508
1509 QTableWidgetItem *completionCell = new QTableWidgetItem();
1510 queueTable->setItem(row, static_cast<int>(SCHEDCOL_ENDTIME), completionCell);
1513}
1514
1515void Scheduler::updateCellStyle(SchedulerJob *job, QTableWidgetItem *cell)
1516{
1517 QFont font(cell->font());
1518 font.setBold(job->getState() == SCHEDJOB_BUSY);
1519 font.setItalic(job->getState() == SCHEDJOB_BUSY);
1520 cell->setFont(font);
1521}
1522
1523void Scheduler::resetJobEdit()
1524{
1525 if (jobUnderEdit < 0)
1526 return;
1527
1528 SchedulerJob * const job = moduleState()->jobs().at(jobUnderEdit);
1529 Q_ASSERT_X(job != nullptr, __FUNCTION__, "Edited job must be valid");
1530
1531 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg(
1532 jobUnderEdit + 1);
1533
1534 jobUnderEdit = -1;
1535
1536 watchJobChanges(false);
1537
1538 /* Revert apply button to add */
1539 setJobAddApply(true);
1540
1541 /* Refresh state of job manipulation buttons */
1542 setJobManipulation(true, true);
1543
1544 /* Restore scheduler operation buttons */
1545 evaluateOnlyB->setEnabled(true);
1546 startB->setEnabled(true);
1547
1548 watchJobChanges(true);
1549 Q_ASSERT_X(jobUnderEdit == -1, __FUNCTION__, "No more edited/selected job after exiting edit mode");
1550}
1551
1553{
1554 int currentRow = moduleState()->currentPosition();
1555
1556 watchJobChanges(false);
1557 if (moduleState()->removeJob(currentRow) == false)
1558 return;
1559
1560 /* removing the job succeeded, update UI */
1561 /* Remove the job from the table */
1562 queueTable->removeRow(currentRow);
1563
1564 /* If there are no job rows left, update UI buttons */
1565 if (queueTable->rowCount() == 0)
1566 {
1567 setJobManipulation(false, false);
1568 evaluateOnlyB->setEnabled(false);
1569 queueSaveAsB->setEnabled(false);
1570 queueSaveB->setEnabled(false);
1571 startB->setEnabled(false);
1572 pauseB->setEnabled(false);
1573 }
1574
1575 // Otherwise, clear the selection, leave the UI values holding the values of the removed job.
1576 // The position in the job list, where the job has been removed from, is still held in the module state.
1577 // This leaves the option directly adding the old values reverting the deletion.
1578 else
1579 queueTable->clearSelection();
1580
1581 /* If needed, reset edit mode to clean up UI */
1582 if (jobUnderEdit >= 0)
1583 resetJobEdit();
1584
1585 watchJobChanges(true);
1586 process()->evaluateJobs(true);
1588 // disable moving and deleting, since selection is cleared
1589 setJobManipulation(false, false);
1590}
1591
1593{
1594 moduleState()->setCurrentPosition(index);
1595 removeJob();
1596}
1597void Scheduler::toggleScheduler()
1598{
1599 if (moduleState()->schedulerState() == SCHEDULER_RUNNING)
1600 {
1601 moduleState()->disablePreemptiveShutdown();
1602 process()->stop();
1603 }
1604 else
1605 process()->start();
1606}
1607
1608void Scheduler::pause()
1609{
1610 moduleState()->setSchedulerState(SCHEDULER_PAUSED);
1611 process()->appendLogText(i18n("Scheduler pause planned..."));
1612 pauseB->setEnabled(false);
1613
1614 startB->setIcon(QIcon::fromTheme("media-playback-start"));
1615 startB->setToolTip(i18n("Resume Scheduler"));
1616}
1617
1618void Scheduler::syncGreedyParams()
1619{
1620 process()->getGreedyScheduler()->setParams(
1621 errorHandlingRestartImmediatelyButton->isChecked(),
1622 errorHandlingRestartQueueButton->isChecked(),
1623 errorHandlingRescheduleErrorsCB->isChecked(),
1624 errorHandlingStrategyDelay->value(),
1625 errorHandlingStrategyDelay->value());
1626}
1627
1628void Scheduler::handleShutdownStarted()
1629{
1630 KSNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process"),
1631 KSNotification::Scheduler);
1632 weatherLabel->hide();
1633}
1634
1635void Ekos::Scheduler::changeSleepLabel(QString text, bool show)
1636{
1637 sleepLabel->setToolTip(text);
1638 if (show)
1639 sleepLabel->show();
1640 else
1641 sleepLabel->hide();
1642}
1643
1645{
1646 TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_NOTHING).toLatin1().data());
1647
1648 // Update job table rows for aborted ones (the others remain unchanged in their state)
1649 bool wasAborted = false;
1650 for (auto &oneJob : moduleState()->jobs())
1651 {
1652 if (oneJob->getState() == SCHEDJOB_ABORTED)
1653 {
1654 updateJobTable(oneJob);
1655 wasAborted = true;
1656 }
1657 }
1658
1659 if (wasAborted)
1660 KSNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted."), KSNotification::Scheduler,
1661 KSNotification::Alert);
1662
1663 startupB->setEnabled(true);
1664 shutdownB->setEnabled(true);
1665
1666 // If soft shutdown, we return for now
1667 if (moduleState()->preemptiveShutdown())
1668 {
1669 changeSleepLabel(i18n("Scheduler is in shutdown until next job is ready"));
1670 pi->stopAnimation();
1671 return;
1672 }
1673
1674 changeSleepLabel("", false);
1675
1676 startB->setIcon(QIcon::fromTheme("media-playback-start"));
1677 startB->setToolTip(i18n("Start Scheduler"));
1678 pauseB->setEnabled(false);
1679 //startB->setText("Start Scheduler");
1680
1681 queueLoadB->setEnabled(true);
1682 queueAppendB->setEnabled(true);
1683 addToQueueB->setEnabled(true);
1684 setJobManipulation(false, false);
1685 //mosaicB->setEnabled(true);
1686 evaluateOnlyB->setEnabled(true);
1687}
1688
1689
1690bool Scheduler::loadFile(const QUrl &path)
1691{
1692 return load(true, path.toLocalFile());
1693}
1694
1695bool Scheduler::load(bool clearQueue, const QString &filename)
1696{
1697 QUrl fileURL;
1698
1699 if (filename.isEmpty())
1700 fileURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Open Ekos Scheduler List"),
1701 dirPath,
1702 "Ekos Scheduler List (*.esl)");
1703 else
1704 fileURL = QUrl::fromLocalFile(filename);
1705
1706 if (fileURL.isEmpty())
1707 return false;
1708
1709 if (fileURL.isValid() == false)
1710 {
1711 QString message = i18n("Invalid URL: %1", fileURL.toLocalFile());
1712 KSNotification::sorry(message, i18n("Invalid URL"));
1713 return false;
1714 }
1715
1716 dirPath = QUrl(fileURL.url(QUrl::RemoveFilename));
1717
1718 if (clearQueue)
1719 process()->removeAllJobs();
1720 // remember toe number of rows to select the first one appended
1721 const int row = moduleState()->jobs().count();
1722
1723 // do not update while appending
1724 watchJobChanges(false);
1725 // try appending the jobs from the file to the job list
1726 const bool success = process()->appendEkosScheduleList(fileURL.toLocalFile());
1727 // turn on whatching
1728 watchJobChanges(true);
1729
1730 if (success)
1731 {
1732 // select the first appended row (if any was added)
1733 if (moduleState()->jobs().count() > row)
1734 moduleState()->setCurrentPosition(row);
1735
1736 /* Run a job idle evaluation after a successful load */
1737 process()->startJobEvaluation();
1738
1739 return true;
1740 }
1741
1742 return false;
1743}
1744
1746{
1747 if (jobUnderEdit >= 0)
1748 resetJobEdit();
1749
1750 while (queueTable->rowCount() > 0)
1751 queueTable->removeRow(0);
1752}
1753
1755{
1756 process()->clearLog();
1757}
1758
1759void Scheduler::saveAs()
1760{
1761 schedulerURL.clear();
1762 save();
1763}
1764
1765bool Scheduler::saveFile(const QUrl &path)
1766{
1767 QUrl backupCurrent = schedulerURL;
1768 schedulerURL = path;
1769
1770 if (save())
1771 return true;
1772 else
1773 {
1774 schedulerURL = backupCurrent;
1775 return false;
1776 }
1777}
1778
1779bool Scheduler::save()
1780{
1781 QUrl backupCurrent = schedulerURL;
1782
1783 if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp"))
1784 schedulerURL.clear();
1785
1786 // If no changes made, return.
1787 if (moduleState()->dirty() == false && !schedulerURL.isEmpty())
1788 return true;
1789
1790 if (schedulerURL.isEmpty())
1791 {
1792 schedulerURL =
1793 QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Save Ekos Scheduler List"), dirPath,
1794 "Ekos Scheduler List (*.esl)");
1795 // if user presses cancel
1796 if (schedulerURL.isEmpty())
1797 {
1798 schedulerURL = backupCurrent;
1799 return false;
1800 }
1801
1802 dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename));
1803
1804 if (schedulerURL.toLocalFile().contains('.') == 0)
1805 schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl");
1806 }
1807
1808 if (schedulerURL.isValid())
1809 {
1810 if ((process()->saveScheduler(schedulerURL)) == false)
1811 {
1812 KSNotification::error(i18n("Failed to save scheduler list"), i18n("Save"));
1813 return false;
1814 }
1815
1816 // update save button tool tip
1817 queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName());
1818 }
1819 else
1820 {
1821 QString message = i18n("Invalid URL: %1", schedulerURL.url());
1822 KSNotification::sorry(message, i18n("Invalid URL"));
1823 return false;
1824 }
1825
1826 return true;
1827}
1828
1829void Scheduler::checkJobInputComplete()
1830{
1831 // For object selection, all fields must be filled
1832 bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty();
1833
1834 // For FITS selection, only the name and fits URL should be filled.
1835 bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty();
1836
1837 // Sequence selection is required
1838 bool const seqSelectionOK = !sequenceEdit->text().isEmpty();
1839
1840 // Finally, adding is allowed upon object/FITS and sequence selection
1841 bool const addingOK = (nameSelectionOK || fitsSelectionOK) && seqSelectionOK;
1842
1843 addToQueueB->setEnabled(addingOK);
1844}
1845
1847{
1848 // check if all fields are filled to allow adding a job
1849 checkJobInputComplete();
1850
1851 // ignore changes that are a result of syncGUIToJob() or syncGUIToGeneralSettings()
1852 if (jobUnderEdit < 0)
1853 return;
1854
1855 moduleState()->setDirty(true);
1856
1857 if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup)
1858 return;
1859
1860 // update state
1861 if (sender() == schedulerStartupScript)
1862 moduleState()->setStartupScriptURL(QUrl::fromUserInput(schedulerStartupScript->text()));
1863 else if (sender() == schedulerShutdownScript)
1864 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(schedulerShutdownScript->text()));
1865}
1866
1868{
1869 // We require a first job to sort, so bail out if list is empty
1870 if (moduleState()->jobs().isEmpty())
1871 return;
1872
1873 // Don't reset current job
1874 // setCurrentJob(nullptr);
1875
1876 // Don't reset scheduler jobs startup times before sorting - we need the first job startup time
1877
1878 // Sort by startup time, using the first job time as reference for altitude calculations
1879 using namespace std::placeholders;
1880 QList<SchedulerJob*> sortedJobs = moduleState()->jobs();
1881 std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(),
1882 std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, moduleState()->jobs().first()->getStartupTime()));
1883
1884 // If order changed, reset and re-evaluate
1885 if (reorderJobs(sortedJobs))
1886 {
1887 for (SchedulerJob * job : moduleState()->jobs())
1888 job->reset();
1889
1890 process()->evaluateJobs(true);
1891 }
1892}
1893
1895{
1896 disconnect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus);
1897 TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
1898 moduleState()->setupNextIteration(RUN_SCHEDULER);
1899}
1900
1902{
1903 // The UI holds the state
1904 if (errorHandlingRestartQueueButton->isChecked())
1905 return ERROR_RESTART_AFTER_TERMINATION;
1906 else if (errorHandlingRestartImmediatelyButton->isChecked())
1907 return ERROR_RESTART_IMMEDIATELY;
1908 else
1909 return ERROR_DONT_RESTART;
1910}
1911
1913{
1914 errorHandlingStrategyDelay->setEnabled(strategy != ERROR_DONT_RESTART);
1915
1916 switch (strategy)
1917 {
1918 case ERROR_RESTART_AFTER_TERMINATION:
1919 errorHandlingRestartQueueButton->setChecked(true);
1920 break;
1921 case ERROR_RESTART_IMMEDIATELY:
1922 errorHandlingRestartImmediatelyButton->setChecked(true);
1923 break;
1924 default:
1925 errorHandlingDontRestartButton->setChecked(true);
1926 break;
1927 }
1928}
1929
1930// Can't use a SchedulerAlgorithm type for the arg here
1931// as the compiler is unhappy connecting the signals currentIndexChanged(int)
1932// or activated(int) to an enum.
1933void Scheduler::setAlgorithm(int algIndex)
1934{
1935 if (algIndex != ALGORITHM_GREEDY)
1936 {
1937 process()->appendLogText(
1938 i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm."));
1939 algIndex = ALGORITHM_GREEDY;
1940 }
1941 Options::setSchedulerAlgorithm(algIndex);
1942
1943 groupLabel->setDisabled(false);
1944 groupEdit->setDisabled(false);
1945 queueTable->model()->setHeaderData(START_TIME_COLUMN, Qt::Horizontal, tr("Next Start"));
1946 queueTable->model()->setHeaderData(END_TIME_COLUMN, Qt::Horizontal, tr("Next End"));
1947 queueTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
1948}
1949
1951{
1952 if (enabled)
1953 return;
1954 else
1955 process()->appendLogText(i18n("Turning off astronomical twilight check may cause the observatory to run during daylight. This can cause irreversible damage to your equipment!"));;
1956}
1957
1958void Scheduler::updateProfiles()
1959{
1960 schedulerProfileCombo->blockSignals(true);
1961 schedulerProfileCombo->clear();
1962 schedulerProfileCombo->addItems(moduleState()->profiles());
1963 schedulerProfileCombo->setCurrentText(moduleState()->currentProfile());
1964 schedulerProfileCombo->blockSignals(false);
1965}
1966
1967void Scheduler::updateJobStageUI(SchedulerJobStage stage)
1968{
1969 /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case should ; also, not thread-safe */
1970 /* FIXME: this should work with a static initializer in C++11, but QT versions are touchy on this, and perhaps i18n can't be used? */
1971 static QMap<SchedulerJobStage, QString> stageStrings;
1972 static QString stageStringUnknown;
1973 if (stageStrings.isEmpty())
1974 {
1975 stageStrings[SCHEDSTAGE_IDLE] = i18n("Idle");
1976 stageStrings[SCHEDSTAGE_SLEWING] = i18n("Slewing");
1977 stageStrings[SCHEDSTAGE_SLEW_COMPLETE] = i18n("Slew complete");
1978 stageStrings[SCHEDSTAGE_FOCUSING] =
1979 stageStrings[SCHEDSTAGE_POSTALIGN_FOCUSING] = i18n("Focusing");
1980 stageStrings[SCHEDSTAGE_FOCUS_COMPLETE] =
1981 stageStrings[SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE ] = i18n("Focus complete");
1982 stageStrings[SCHEDSTAGE_ALIGNING] = i18n("Aligning");
1983 stageStrings[SCHEDSTAGE_ALIGN_COMPLETE] = i18n("Align complete");
1984 stageStrings[SCHEDSTAGE_RESLEWING] = i18n("Repositioning");
1985 stageStrings[SCHEDSTAGE_RESLEWING_COMPLETE] = i18n("Repositioning complete");
1986 /*stageStrings[SCHEDSTAGE_CALIBRATING] = i18n("Calibrating");*/
1987 stageStrings[SCHEDSTAGE_GUIDING] = i18n("Guiding");
1988 stageStrings[SCHEDSTAGE_GUIDING_COMPLETE] = i18n("Guiding complete");
1989 stageStrings[SCHEDSTAGE_CAPTURING] = i18n("Capturing");
1990 stageStringUnknown = i18n("Unknown");
1991 }
1992
1993 if (activeJob() == nullptr)
1994 jobStatus->setText(stageStrings[SCHEDSTAGE_IDLE]);
1995 else
1996 jobStatus->setText(QString("%1: %2").arg(activeJob()->getName(),
1997 stageStrings.value(stage, stageStringUnknown)));
1998
1999}
2000
2002{
2003 if (iface == process()->mountInterface())
2004 {
2005 QVariant canMountPark = process()->mountInterface()->property("canPark");
2006 if (canMountPark.isValid())
2007 {
2008 schedulerUnparkMount->setEnabled(canMountPark.toBool());
2009 schedulerParkMount->setEnabled(canMountPark.toBool());
2010 }
2011 }
2012 else if (iface == process()->capInterface())
2013 {
2014 QVariant canCapPark = process()->capInterface()->property("canPark");
2015 if (canCapPark.isValid())
2016 {
2017 schedulerCloseDustCover->setEnabled(canCapPark.toBool());
2018 schedulerOpenDustCover->setEnabled(canCapPark.toBool());
2019 }
2020 else
2021 {
2022 schedulerCloseDustCover->setEnabled(false);
2023 schedulerOpenDustCover->setEnabled(false);
2024 }
2025 }
2026 else if (iface == process()->weatherInterface())
2027 {
2028 QVariant status = process()->weatherInterface()->property("status");
2029 if (status.isValid())
2030 {
2031 // auto newStatus = static_cast<ISD::Weather::Status>(status.toInt());
2032 // if (newStatus != m_moduleState->weatherStatus())
2033 // setWeatherStatus(newStatus);
2034 schedulerWeather->setEnabled(true);
2035 }
2036 else
2037 schedulerWeather->setEnabled(false);
2038 }
2039 else if (iface == process()->domeInterface())
2040 {
2041 QVariant canDomePark = process()->domeInterface()->property("canPark");
2042 if (canDomePark.isValid())
2043 {
2044 schedulerUnparkDome->setEnabled(canDomePark.toBool());
2045 schedulerParkDome->setEnabled(canDomePark.toBool());
2046 }
2047 }
2048 else if (iface == process()->captureInterface())
2049 {
2050 QVariant hasCoolerControl = process()->captureInterface()->property("coolerControl");
2051 if (hasCoolerControl.isValid())
2052 {
2053 schedulerWarmCCD->setEnabled(hasCoolerControl.toBool());
2054 }
2055 }
2056}
2057
2058void Scheduler::setWeatherStatus(ISD::Weather::Status status)
2059{
2060 TEST_PRINT(stderr, "sch%d @@@setWeatherStatus(%d)\n", __LINE__, static_cast<int>(status));
2061 ISD::Weather::Status newStatus = status;
2062 QString statusString;
2063
2064 switch (newStatus)
2065 {
2066 case ISD::Weather::WEATHER_OK:
2067 statusString = i18n("Weather conditions are OK.");
2068 break;
2069
2070 case ISD::Weather::WEATHER_WARNING:
2071 statusString = i18n("Warning: weather conditions are in the WARNING zone.");
2072 break;
2073
2074 case ISD::Weather::WEATHER_ALERT:
2075 statusString = i18n("Caution: weather conditions are in the DANGER zone!");
2076 break;
2077
2078 default:
2079 break;
2080 }
2081
2082 qCDebug(KSTARS_EKOS_SCHEDULER) << statusString;
2083
2084 if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_OK)
2085 weatherLabel->setPixmap(
2086 QIcon::fromTheme("security-high")
2087 .pixmap(QSize(32, 32)));
2088 else if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_WARNING)
2089 {
2090 weatherLabel->setPixmap(
2091 QIcon::fromTheme("security-medium")
2092 .pixmap(QSize(32, 32)));
2093 KSNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone"),
2094 KSNotification::Scheduler, KSNotification::Warn);
2095 }
2096 else if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_ALERT)
2097 {
2098 weatherLabel->setPixmap(
2099 QIcon::fromTheme("security-low")
2100 .pixmap(QSize(32, 32)));
2101 KSNotification::event(QLatin1String("WeatherAlert"),
2102 i18n("Weather conditions are critical. Observatory shutdown is imminent"), KSNotification::Scheduler,
2103 KSNotification::Alert);
2104 }
2105 else
2106 weatherLabel->setPixmap(QIcon::fromTheme("chronometer")
2107 .pixmap(QSize(32, 32)));
2108
2109 weatherLabel->show();
2110 weatherLabel->setToolTip(statusString);
2111
2112 process()->appendLogText(statusString);
2113
2114 emit weatherChanged(moduleState()->weatherStatus());
2115}
2116
2117void Scheduler::handleSchedulerSleeping(bool shutdown, bool sleep)
2118{
2119 if (shutdown)
2120 {
2121 schedulerWeather->setEnabled(false);
2122 weatherLabel->hide();
2123 }
2124 if (sleep)
2125 changeSleepLabel(i18n("Scheduler is in sleep mode"));
2126}
2127
2129{
2130 switch (newState)
2131 {
2132 case SCHEDULER_RUNNING:
2133 /* Update UI to reflect startup */
2134 pi->startAnimation();
2135 sleepLabel->hide();
2136 startB->setIcon(QIcon::fromTheme("media-playback-stop"));
2137 startB->setToolTip(i18n("Stop Scheduler"));
2138 pauseB->setEnabled(true);
2139 pauseB->setChecked(false);
2140
2141 /* Disable edit-related buttons */
2142 queueLoadB->setEnabled(false);
2143 setJobManipulation(true, false);
2144 //mosaicB->setEnabled(false);
2145 evaluateOnlyB->setEnabled(false);
2146 startupB->setEnabled(false);
2147 shutdownB->setEnabled(false);
2148 break;
2149
2150 default:
2151 break;
2152 }
2153 // forward the state chqnge
2154 emit newStatus(newState);
2155}
2156
2158{
2159 pauseB->setCheckable(true);
2160 pauseB->setChecked(true);
2161}
2162
2163void Scheduler::handleJobsUpdated(QJsonArray jobsList)
2164{
2165 syncGreedyParams();
2167
2168 emit jobsUpdated(jobsList);
2169}
2170
2172{
2173 QScopedPointer<FramingAssistantUI> assistant(new FramingAssistantUI());
2174 return assistant->importMosaic(payload);
2175}
2176
2177void Scheduler::startupStateChanged(StartupState state)
2178{
2179 jobStatus->setText(startupStateString(state));
2180
2181 switch (moduleState()->startupState())
2182 {
2183 case STARTUP_IDLE:
2184 startupB->setIcon(QIcon::fromTheme("media-playback-start"));
2185 break;
2186 case STARTUP_COMPLETE:
2187 startupB->setIcon(QIcon::fromTheme("media-playback-start"));
2188 process()->appendLogText(i18n("Manual startup procedure completed successfully."));
2189 break;
2190 case STARTUP_ERROR:
2191 startupB->setIcon(QIcon::fromTheme("media-playback-start"));
2192 process()->appendLogText(i18n("Manual startup procedure terminated due to errors."));
2193 break;
2194 default:
2195 // in all other cases startup is running
2196 startupB->setIcon(QIcon::fromTheme("media-playback-stop"));
2197 break;
2198 }
2199}
2200void Scheduler::shutdownStateChanged(ShutdownState state)
2201{
2202 if (state == SHUTDOWN_COMPLETE || state == SHUTDOWN_IDLE
2203 || state == SHUTDOWN_ERROR)
2204 {
2205 shutdownB->setIcon(QIcon::fromTheme("media-playback-start"));
2206 pi->stopAnimation();
2207 }
2208 else
2209 shutdownB->setIcon(QIcon::fromTheme("media-playback-stop"));
2210
2211 if (state == SHUTDOWN_IDLE)
2212 jobStatus->setText(i18n("Idle"));
2213 else
2214 jobStatus->setText(shutdownStateString(state));
2215}
2216void Scheduler::ekosStateChanged(EkosState state)
2217{
2218 if (state == EKOS_IDLE)
2219 {
2220 jobStatus->setText(i18n("Idle"));
2221 pi->stopAnimation();
2222 }
2223 else
2224 jobStatus->setText(ekosStateString(state));
2225}
2226void Scheduler::indiStateChanged(INDIState state)
2227{
2228 if (state == INDI_IDLE)
2229 {
2230 jobStatus->setText(i18n("Idle"));
2231 pi->stopAnimation();
2232 }
2233 else
2234 jobStatus->setText(indiStateString(state));
2235}
2236void Scheduler::parkWaitStateChanged(ParkWaitState state)
2237{
2238 jobStatus->setText(parkWaitStateString(state));
2239}
2240
2241SchedulerJob *Scheduler::activeJob()
2242{
2243 return moduleState()->activeJob();
2244}
2245
2246void Scheduler::loadGlobalSettings()
2247{
2248 QString key;
2249 QVariant value;
2250
2251 QVariantMap settings;
2252 // All Combo Boxes
2253 for (auto &oneWidget : findChildren<QComboBox*>())
2254 {
2255 key = oneWidget->objectName();
2256 value = Options::self()->property(key.toLatin1());
2257 if (value.isValid() && oneWidget->count() > 0)
2258 {
2259 oneWidget->setCurrentText(value.toString());
2260 settings[key] = value;
2261 }
2262 else
2263 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2264 }
2265
2266 // All Double Spin Boxes
2267 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2268 {
2269 key = oneWidget->objectName();
2270 value = Options::self()->property(key.toLatin1());
2271 if (value.isValid())
2272 {
2273 oneWidget->setValue(value.toDouble());
2274 settings[key] = value;
2275 }
2276 else
2277 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2278 }
2279
2280 // All Spin Boxes
2281 for (auto &oneWidget : findChildren<QSpinBox*>())
2282 {
2283 key = oneWidget->objectName();
2284 value = Options::self()->property(key.toLatin1());
2285 if (value.isValid())
2286 {
2287 oneWidget->setValue(value.toInt());
2288 settings[key] = value;
2289 }
2290 else
2291 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2292 }
2293
2294 // All Checkboxes
2295 for (auto &oneWidget : findChildren<QCheckBox*>())
2296 {
2297 key = oneWidget->objectName();
2298 value = Options::self()->property(key.toLatin1());
2299 if (value.isValid())
2300 {
2301 oneWidget->setChecked(value.toBool());
2302 settings[key] = value;
2303 }
2304 else
2305 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2306 }
2307
2308 // All Line Edits
2309 for (auto &oneWidget : findChildren<QLineEdit*>())
2310 {
2311 key = oneWidget->objectName();
2312 value = Options::self()->property(key.toLatin1());
2313 if (value.isValid())
2314 {
2315 oneWidget->setText(value.toString());
2316 settings[key] = value;
2317
2318 if (key == "sequenceEdit")
2319 setSequence(value.toString());
2320 else if (key == "schedulerStartupScript")
2321 moduleState()->setStartupScriptURL(QUrl::fromUserInput(value.toString()));
2322 else if (key == "schedulerShutdownScript")
2323 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(value.toString()));
2324 }
2325 else
2326 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2327 }
2328
2329 // All Radio buttons
2330 for (auto &oneWidget : findChildren<QRadioButton*>())
2331 {
2332 key = oneWidget->objectName();
2333 value = Options::self()->property(key.toLatin1());
2334 if (value.isValid())
2335 {
2336 oneWidget->setChecked(value.toBool());
2337 settings[key] = value;
2338 }
2339 }
2340
2341 // All QDateTime edits
2342 for (auto &oneWidget : findChildren<QDateTimeEdit*>())
2343 {
2344 key = oneWidget->objectName();
2345 value = Options::self()->property(key.toLatin1());
2346 if (value.isValid())
2347 {
2348 oneWidget->setDateTime(QDateTime::fromString(value.toString(), Qt::ISODate));
2349 settings[key] = value;
2350 }
2351 }
2352
2353 setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy()));
2354
2355 m_GlobalSettings = m_Settings = settings;
2356}
2357
2358void Scheduler::syncSettings()
2359{
2360 QDoubleSpinBox *dsb = nullptr;
2361 QSpinBox *sb = nullptr;
2362 QCheckBox *cb = nullptr;
2363 QRadioButton *rb = nullptr;
2364 QComboBox *cbox = nullptr;
2365 QLineEdit *lineedit = nullptr;
2366 QDateTimeEdit *datetimeedit = nullptr;
2367
2368 QString key;
2369 QVariant value;
2370
2371 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
2372 {
2373 key = dsb->objectName();
2374 value = dsb->value();
2375
2376 }
2377 else if ( (sb = qobject_cast<QSpinBox*>(sender())))
2378 {
2379 key = sb->objectName();
2380 value = sb->value();
2381 }
2382 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
2383 {
2384 key = cb->objectName();
2385 value = cb->isChecked();
2386 }
2387 else if ( (rb = qobject_cast<QRadioButton*>(sender())))
2388 {
2389 key = rb->objectName();
2390 if (rb->isChecked() == false)
2391 {
2392 m_Settings.remove(key);
2393 return;
2394 }
2395 value = true;
2396 }
2397 else if ( (cbox = qobject_cast<QComboBox*>(sender())))
2398 {
2399 key = cbox->objectName();
2400 value = cbox->currentText();
2401 }
2402 else if ( (lineedit = qobject_cast<QLineEdit*>(sender())))
2403 {
2404 key = lineedit->objectName();
2405 value = lineedit->text();
2406 }
2407 else if ( (datetimeedit = qobject_cast<QDateTimeEdit*>(sender())))
2408 {
2409 key = datetimeedit->objectName();
2410 value = datetimeedit->dateTime().toString(Qt::ISODate);
2411 }
2412
2413 // Save immediately
2414 Options::self()->setProperty(key.toLatin1(), value);
2415
2416 m_Settings[key] = value;
2417 m_GlobalSettings[key] = value;
2418
2419 emit settingsUpdated(getAllSettings());
2420}
2421
2422///////////////////////////////////////////////////////////////////////////////////////////
2423///
2424///////////////////////////////////////////////////////////////////////////////////////////
2425QVariantMap Scheduler::getAllSettings() const
2426{
2427 QVariantMap settings;
2428
2429 // All Combo Boxes
2430 for (auto &oneWidget : findChildren<QComboBox*>())
2431 settings.insert(oneWidget->objectName(), oneWidget->currentText());
2432
2433 // All Double Spin Boxes
2434 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2435 settings.insert(oneWidget->objectName(), oneWidget->value());
2436
2437 // All Spin Boxes
2438 for (auto &oneWidget : findChildren<QSpinBox*>())
2439 settings.insert(oneWidget->objectName(), oneWidget->value());
2440
2441 // All Checkboxes
2442 for (auto &oneWidget : findChildren<QCheckBox*>())
2443 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
2444
2445 // All Line Edits
2446 for (auto &oneWidget : findChildren<QLineEdit*>())
2447 {
2448 // Many other widget types (e.g. spinboxes) apparently have QLineEdit inside them so we want to skip those
2449 if (!oneWidget->objectName().startsWith("qt_"))
2450 settings.insert(oneWidget->objectName(), oneWidget->text());
2451 }
2452
2453 // All Radio Buttons
2454 for (auto &oneWidget : findChildren<QRadioButton*>())
2455 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
2456
2457 // All QDateTime
2458 for (auto &oneWidget : findChildren<QDateTimeEdit*>())
2459 {
2460 settings.insert(oneWidget->objectName(), oneWidget->dateTime().toString(Qt::ISODate));
2461 }
2462
2463 return settings;
2464}
2465
2466///////////////////////////////////////////////////////////////////////////////////////////
2467///
2468///////////////////////////////////////////////////////////////////////////////////////////
2469void Scheduler::setAllSettings(const QVariantMap &settings)
2470{
2471 // Disconnect settings that we don't end up calling syncSettings while
2472 // performing the changes.
2473 disconnectSettings();
2474
2475 for (auto &name : settings.keys())
2476 {
2477 // Combo
2478 auto comboBox = findChild<QComboBox*>(name);
2479 if (comboBox)
2480 {
2481 syncControl(settings, name, comboBox);
2482 continue;
2483 }
2484
2485 // Double spinbox
2486 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
2487 if (doubleSpinBox)
2488 {
2489 syncControl(settings, name, doubleSpinBox);
2490 continue;
2491 }
2492
2493 // spinbox
2494 auto spinBox = findChild<QSpinBox*>(name);
2495 if (spinBox)
2496 {
2497 syncControl(settings, name, spinBox);
2498 continue;
2499 }
2500
2501 // checkbox
2502 auto checkbox = findChild<QCheckBox*>(name);
2503 if (checkbox)
2504 {
2505 syncControl(settings, name, checkbox);
2506 continue;
2507 }
2508
2509 // Line Edits
2510 auto lineedit = findChild<QLineEdit*>(name);
2511 if (lineedit)
2512 {
2513 syncControl(settings, name, lineedit);
2514
2515 if (name == "sequenceEdit")
2516 setSequence(lineedit->text());
2517 else if (name == "fitsEdit")
2518 processFITSSelection(QUrl::fromLocalFile(lineedit->text()));
2519 else if (name == "schedulerStartupScript")
2520 moduleState()->setStartupScriptURL(QUrl::fromUserInput(lineedit->text()));
2521 else if (name == "schedulerShutdownScript")
2522 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(lineedit->text()));
2523
2524 continue;
2525 }
2526
2527 // Radio button
2528 auto radioButton = findChild<QRadioButton*>(name);
2529 if (radioButton)
2530 {
2531 syncControl(settings, name, radioButton);
2532 continue;
2533 }
2534
2535 auto datetimeedit = findChild<QDateTimeEdit*>(name);
2536 if (datetimeedit)
2537 {
2538 syncControl(settings, name, datetimeedit);
2539 continue;
2540 }
2541 }
2542
2543 m_Settings = settings;
2544
2545 // Restablish connections
2546 connectSettings();
2547}
2548
2549///////////////////////////////////////////////////////////////////////////////////////////
2550///
2551///////////////////////////////////////////////////////////////////////////////////////////
2552bool Scheduler::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
2553{
2554 QSpinBox *pSB = nullptr;
2555 QDoubleSpinBox *pDSB = nullptr;
2556 QCheckBox *pCB = nullptr;
2557 QComboBox *pComboBox = nullptr;
2558 QLineEdit *pLineEdit = nullptr;
2559 QRadioButton *pRadioButton = nullptr;
2560 QDateTimeEdit *pDateTimeEdit = nullptr;
2561 bool ok = true;
2562
2563 if ((pSB = qobject_cast<QSpinBox *>(widget)))
2564 {
2565 const int value = settings[key].toInt(&ok);
2566 if (ok)
2567 {
2568 pSB->setValue(value);
2569 return true;
2570 }
2571 }
2572 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
2573 {
2574 const double value = settings[key].toDouble(&ok);
2575 if (ok)
2576 {
2577 pDSB->setValue(value);
2578 return true;
2579 }
2580 }
2581 else if ((pCB = qobject_cast<QCheckBox *>(widget)))
2582 {
2583 const bool value = settings[key].toBool();
2584 if (value != pCB->isChecked())
2585 pCB->click();
2586 return true;
2587 }
2588 // ONLY FOR STRINGS, not INDEX
2589 else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
2590 {
2591 const QString value = settings[key].toString();
2592 pComboBox->setCurrentText(value);
2593 return true;
2594 }
2595 else if ((pLineEdit = qobject_cast<QLineEdit *>(widget)))
2596 {
2597 const auto value = settings[key].toString();
2598 pLineEdit->setText(value);
2599 return true;
2600 }
2601 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
2602 {
2603 const bool value = settings[key].toBool();
2604 if (value)
2605 pRadioButton->click();
2606 return true;
2607 }
2608 else if ((pDateTimeEdit = qobject_cast<QDateTimeEdit *>(widget)))
2609 {
2610 const auto value = QDateTime::fromString(settings[key].toString(), Qt::ISODate);
2611 pDateTimeEdit->setDateTime(value);
2612 return true;
2613 }
2614
2615 return false;
2616};
2617
2618void Scheduler::connectSettings()
2619{
2620 // All Combo Boxes
2621 for (auto &oneWidget : findChildren<QComboBox*>())
2622 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Scheduler::syncSettings);
2623
2624 // All Double Spin Boxes
2625 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2626 connect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings);
2627
2628 // All Spin Boxes
2629 for (auto &oneWidget : findChildren<QSpinBox*>())
2630 connect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings);
2631
2632 // All Checkboxes
2633 for (auto &oneWidget : findChildren<QCheckBox*>())
2634 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Scheduler::syncSettings);
2635
2636 // All Radio Butgtons
2637 for (auto &oneWidget : findChildren<QRadioButton*>())
2638 connect(oneWidget, &QRadioButton::toggled, this, &Ekos::Scheduler::syncSettings);
2639
2640 // All QLineEdits
2641 for (auto &oneWidget : findChildren<QLineEdit*>())
2642 {
2643 // Many other widget types (e.g. spinboxes) apparently have QLineEdit inside them so we want to skip those
2644 if (!oneWidget->objectName().startsWith("qt_"))
2645 connect(oneWidget, &QLineEdit::textChanged, this, &Ekos::Scheduler::syncSettings);
2646 }
2647
2648 // All QDateTimeEdit
2649 for (auto &oneWidget : findChildren<QDateTimeEdit*>())
2650 connect(oneWidget, &QDateTimeEdit::dateTimeChanged, this, &Ekos::Scheduler::syncSettings);
2651}
2652
2653void Scheduler::disconnectSettings()
2654{
2655 // All Combo Boxes
2656 for (auto &oneWidget : findChildren<QComboBox*>())
2657 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Scheduler::syncSettings);
2658
2659 // All Double Spin Boxes
2660 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2661 disconnect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings);
2662
2663 // All Spin Boxes
2664 for (auto &oneWidget : findChildren<QSpinBox*>())
2665 disconnect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings);
2666
2667 // All Checkboxes
2668 for (auto &oneWidget : findChildren<QCheckBox*>())
2669 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Scheduler::syncSettings);
2670
2671 // All Radio Butgtons
2672 for (auto &oneWidget : findChildren<QRadioButton*>())
2673 disconnect(oneWidget, &QRadioButton::toggled, this, &Ekos::Scheduler::syncSettings);
2674
2675 // All QLineEdits
2676 for (auto &oneWidget : findChildren<QLineEdit*>())
2677 disconnect(oneWidget, &QLineEdit::editingFinished, this, &Ekos::Scheduler::syncSettings);
2678
2679 // All QDateTimeEdit
2680 for (auto &oneWidget : findChildren<QDateTimeEdit*>())
2681 disconnect(oneWidget, &QDateTimeEdit::editingFinished, this, &Ekos::Scheduler::syncSettings);
2682}
2683
2684}
The SchedulerProcess class holds the entire business logic for controlling the execution of the EKOS ...
Q_SCRIPTABLE Q_NOREPLY void runStartupProcedure()
runStartupProcedure Execute the startup of the scheduler itself to be prepared for running scheduler ...
Q_SCRIPTABLE Q_NOREPLY void startJobEvaluation()
startJobEvaluation Start job evaluation only without starting the scheduler process itself.
Q_SCRIPTABLE Q_NOREPLY void runShutdownProcedure()
runShutdownProcedure Shutdown the scheduler itself and EKOS (if configured to do so).
ErrorHandlingStrategy getErrorHandlingStrategy()
retrieve the error handling strategy from the UI
void moveJobUp()
moveJobUp Move the selected job up in the job list.
void watchJobChanges(bool enable)
Q_INVOKABLE void clearLog()
clearLog Clears log entry
void checkTwilightWarning(bool enabled)
checkWeather Check weather status and act accordingly depending on the current status of the schedule...
void saveJob(SchedulerJob *job=nullptr)
addToQueue Construct a SchedulerJob and add it to the queue or save job settings from current form va...
void updateSchedulerURL(const QString &fileURL)
updateSchedulerURL Update scheduler URL after succesful loading a new file.
void addJob(SchedulerJob *job=nullptr)
addJob Add a new job from form values
void selectSequence()
Selects sequence queue.
void insertJobTableRow(int row, bool above=true)
insertJobTableRow Insert a new row (empty) into the job table
bool load(bool clearQueue, const QString &filename=QString())
load Open a file dialog to select an ESL file, and load its contents.
void resumeCheckStatus()
resumeCheckStatus If the scheduler primary loop was suspended due to weather or sleep event,...
void handleSchedulerSleeping(bool shutdown, bool sleep)
handleSchedulerSleeping Update UI if scheduler is set to sleep
void setJobManipulation(bool can_reorder, bool can_delete)
setJobManipulation Enable or disable job manipulation buttons.
void moveJobDown()
moveJobDown Move the selected job down in the list.
bool importMosaic(const QJsonObject &payload)
importMosaic Import mosaic into planner and generate jobs for the scheduler.
void handleSetPaused()
handleSetPaused Update the UI when {
bool reorderJobs(QList< SchedulerJob * > reordered_sublist)
reorderJobs Change the order of jobs in the UI based on a subset of its jobs.
void syncGUIToGeneralSettings()
syncGUIToGeneralSettings set all UI fields that are not job specific
void updateNightTime(SchedulerJob const *job=nullptr)
updateNightTime update the Twilight restriction with the argument job properties.
bool loadFile(const QUrl &path)
loadFile Load scheduler jobs from disk
void handleSchedulerStateChanged(SchedulerState newState)
handleSchedulerStateChanged Update UI when the scheduler state changes
bool fillJobFromUI(SchedulerJob *job)
createJob Create a new job from form values.
void loadJob(QModelIndex i)
editJob Edit an observation job
void setSequence(const QString &sequenceFileURL)
Set the file URL pointing to the capture sequence file.
void selectStartupScript()
Selects sequence queue.
void syncGUIToJob(SchedulerJob *job)
set all GUI fields to the values of the given scheduler job
void schedulerStopped()
schedulerStopped React when the process engine has stopped the scheduler
void selectObject()
select object from KStars's find dialog.
void updateCellStyle(SchedulerJob *job, QTableWidgetItem *cell)
Update the style of a cell, depending on the job's state.
void clearJobTable()
clearJobTable delete all rows in the job table
void setJobAddApply(bool add_mode)
setJobAddApply Set first button state to add new job or apply changes.
void handleConfigChanged()
handleConfigChanged Update UI after changes to the global configuration
bool saveFile(const QUrl &path)
saveFile Save scheduler jobs to disk
Q_SCRIPTABLE void sortJobsPerAltitude()
DBUS interface function.
void setErrorHandlingStrategy(ErrorHandlingStrategy strategy)
select the error handling strategy (no restart, restart after all terminated, restart immediately)
void clickQueueTable(QModelIndex index)
jobSelectionChanged Update UI state when the job list is clicked once.
void updateJobTable(SchedulerJob *job=nullptr)
updateJobTable Update the job's row in the job table.
void removeJob()
Remove a job from current table row.
void removeOneJob(int index)
Remove a job by selecting a table row.
void selectFITS()
Selects FITS file for solving.
Scheduler()
Constructor, the starndard scheduler constructor.
Definition scheduler.cpp:78
void interfaceReady(QDBusInterface *iface)
checkInterfaceReady Sometimes syncProperties() is not sufficient since the ready signal could have fi...
void queueTableSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
Update scheduler parameters to the currently selected scheduler job.
void selectShutdownScript()
Selects sequence queue.
SkyObject * targetObject()
Definition finddialog.h:53
Q_INVOKABLE QAction * action(const QString &name) const
static KConfigDialog * exists(const QString &name)
void settingsChanged(const QString &dialogName)
const KStarsDateTime & ut() const
Definition kstarsdata.h:157
GeoLocation * geo()
Definition kstarsdata.h:230
long double djd() const
static KStars * Instance()
Definition kstars.h:123
virtual KActionCollection * actionCollection() const
The QProgressIndicator class lets an application display a progress indicator to show that a long tas...
void stopAnimation()
Stops the spin animation.
void startAnimation()
Starts the spin animation.
The SchedulerState class holds all attributes defining the scheduler's state.
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
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & ra0() const
Definition skypoint.h:251
const CachingDms & dec0() const
Definition skypoint.h:257
This is a subclass of SkyObject.
Definition starobject.h:33
int getHDIndex() const
Definition starobject.h:248
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition dms.cpp:429
virtual void setD(const double &x)
Sets floating-point value of angle, in degrees.
Definition dms.h:179
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
StartupCondition
Conditions under which a SchedulerJob may start.
@ SCHEDJOB_ABORTED
Job encountered a transitory issue while processing, and will be rescheduled.
@ SCHEDJOB_INVALID
Job has an incorrect configuration, and cannot proceed.
@ SCHEDJOB_ERROR
Job encountered a fatal issue while processing, and must be reset manually.
@ SCHEDJOB_COMPLETE
Job finished all required captures.
@ SCHEDJOB_EVALUATION
Job is being evaluated.
@ SCHEDJOB_SCHEDULED
Job was evaluated, and has a schedule.
@ SCHEDJOB_BUSY
Job is being processed.
@ SCHEDJOB_IDLE
Job was just created, and is not evaluated yet.
ErrorHandlingStrategy
options what should happen if an error or abort occurs
CompletionCondition
Conditions under which a SchedulerJob may complete.
KGuiItem insert()
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
bool isChecked() const const
void clicked(bool checked)
void toggled(bool checked)
void clicked(const QModelIndex &index)
void doubleClicked(const QModelIndex &index)
void rangeChanged(int min, int max)
void valueChanged(int value)
void editingFinished()
void trigger()
void triggered(bool checked)
void buttonClicked(QAbstractButton *button)
void buttonToggled(QAbstractButton *button, bool checked)
void idToggled(int id, bool checked)
void currentIndexChanged(int index)
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
void setTime(QTime time)
QTime time() const const
QString toString(QStringView format, QCalendar cal) const const
QString homePath()
void valueChanged(double d)
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QUrl getOpenFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
QIcon fromTheme(const QString &name)
QModelIndexList indexes() const const
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
void editingFinished()
void textChanged(const QString &text)
void append(QList< T > &&value)
iterator begin()
bool contains(const AT &value) const const
bool empty() const const
iterator end()
bool isEmpty() const const
T value(const Key &key, const T &defaultValue) const const
bool isValid() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
T findChild(const QString &name, Qt::FindChildOptions options) const const
QList< T > findChildren(Qt::FindChildOptions options) const const
T qobject_cast(QObject *object)
QObject * sender() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
void valueChanged(int i)
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
AlignHCenter
ItemIsSelectable
Horizontal
WA_LayoutUsesWidgetRect
QTextStream & center(QTextStream &stream)
void resizeColumnToContents(int column)
void selectRow(int row)
int column() const const
QFont font() const const
void setFlags(Qt::ItemFlags flags)
void setFont(const QFont &font)
void setIcon(const QIcon &icon)
void setText(const QString &text)
void setTextAlignment(Qt::Alignment alignment)
void setToolTip(const QString &toolTip)
QTableWidget * tableWidget() const const
int hour() const const
int minute() const const
bool setHMS(int h, int m, int s, int ms)
void setInterval(int msec)
void setSingleShot(bool singleShot)
void start()
void timeout()
RemoveFilename
void clear()
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
bool isEmpty() const const
bool isValid() const const
void setPath(const QString &path, ParsingMode mode)
QString toLocalFile() const const
QString url(FormattingOptions options) const const
bool isValid() const const
void setValue(QVariant &&value)
bool toBool() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
QString toString() const const
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.