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

KDE's Doxygen guidelines are available online.