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

KDE's Doxygen guidelines are available online.