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

KDE's Doxygen guidelines are available online.