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

KDE's Doxygen guidelines are available online.