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

KDE's Doxygen guidelines are available online.