Kstars

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

KDE's Doxygen guidelines are available online.