1 /* Ekos Scheduler Job
2 SPDX-FileCopyrightText: Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "schedulerjob.h"
8
9 #include "dms.h"
10 #include "artificialhorizoncomponent.h"
11 #include "kstarsdata.h"
12 #include "skymapcomposite.h"
13 #include "Options.h"
14 #include "scheduler.h"
15 #include "ksalmanac.h"
16
17 #include <knotification.h>
18
19 #include <QTableWidgetItem>
20
21 #include <ekos_scheduler_debug.h>
22
23 #define BAD_SCORE -1000
24 #define MIN_ALTITUDE 15.0
25
26
27 GeoLocation *SchedulerJob::storedGeo = nullptr;
28 KStarsDateTime *SchedulerJob::storedLocalTime = nullptr;
29 ArtificialHorizon *SchedulerJob::storedHorizon = nullptr;
30
jobStatusString(JOBStatus state)31 QString SchedulerJob::jobStatusString(JOBStatus state)
32 {
33 switch(state)
34 {
35 case SchedulerJob::JOB_IDLE:
36 return "JOB_IDLE";
37 case SchedulerJob::JOB_EVALUATION:
38 return "JOB_EVALUATION";
39 case SchedulerJob::JOB_SCHEDULED:
40 return "JOB_SCHEDULED";
41 case SchedulerJob::JOB_BUSY:
42 return "JOB_BUSY";
43 case SchedulerJob::JOB_ERROR:
44 return "JOB_ERROR";
45 case SchedulerJob::JOB_ABORTED:
46 return "JOB_ABORTED";
47 case SchedulerJob::JOB_INVALID:
48 return "JOB_INVALID";
49 case SchedulerJob::JOB_COMPLETE:
50 return "JOB_COMPLETE";
51 }
52 return QString("????");
53 }
54
jobStageString(JOBStage state)55 QString SchedulerJob::jobStageString(JOBStage state)
56 {
57 switch(state)
58 {
59 case SchedulerJob::STAGE_IDLE:
60 return "STAGE_IDLE";
61 case SchedulerJob::STAGE_SLEWING:
62 return "STAGE_SLEWING";
63 case SchedulerJob::STAGE_SLEW_COMPLETE:
64 return "STAGE_SLEW_COMPLETE";
65 case SchedulerJob::STAGE_FOCUSING:
66 return "STAGE_FOCUSING";
67 case SchedulerJob::STAGE_FOCUS_COMPLETE:
68 return "STAGE_FOCUS_COMPLETE";
69 case SchedulerJob::STAGE_ALIGNING:
70 return "STAGE_ALIGNING";
71 case SchedulerJob::STAGE_ALIGN_COMPLETE:
72 return "STAGE_ALIGN_COMPLETE";
73 case SchedulerJob::STAGE_RESLEWING:
74 return "STAGE_RESLEWING";
75 case SchedulerJob::STAGE_RESLEWING_COMPLETE:
76 return "STAGE_RESLEWING_COMPLETE";
77 case SchedulerJob::STAGE_POSTALIGN_FOCUSING:
78 return "STAGE_POSTALIGN_FOCUSING";
79 case SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE:
80 return "STAGE_POSTALIGN_FOCUSING_COMPLETE";
81 case SchedulerJob::STAGE_GUIDING:
82 return "STAGE_GUIDING";
83 case SchedulerJob::STAGE_GUIDING_COMPLETE:
84 return "STAGE_GUIDING_COMPLETE";
85 case SchedulerJob::STAGE_CAPTURING:
86 return "STAGE_CAPTURING";
87 case SchedulerJob::STAGE_COMPLETE:
88 return "STAGE_COMPLETE";
89 }
90 return QString("????");
91 }
92
SchedulerJob()93 SchedulerJob::SchedulerJob()
94 {
95 moon = dynamic_cast<KSMoon *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Moon")));
96 }
97
98 // Private constructor for unit testing.
SchedulerJob(KSMoon * moonPtr)99 SchedulerJob::SchedulerJob(KSMoon *moonPtr)
100 {
101 moon = moonPtr;
102 }
103
setName(const QString & value)104 void SchedulerJob::setName(const QString &value)
105 {
106 name = value;
107 updateJobCells();
108 }
109
getLocalTime()110 KStarsDateTime SchedulerJob::getLocalTime()
111 {
112 if (hasLocalTime())
113 return *storedLocalTime;
114 return getGeo()->UTtoLT(KStarsData::Instance()->clock()->utc());
115 }
116
getGeo()117 GeoLocation const *SchedulerJob::getGeo()
118 {
119 if (hasGeo())
120 return storedGeo;
121 return KStarsData::Instance()->geo();
122 }
123
getHorizon()124 ArtificialHorizon const *SchedulerJob::getHorizon()
125 {
126 if (hasHorizon())
127 return storedHorizon;
128 if (KStarsData::Instance() == nullptr || KStarsData::Instance()->skyComposite() == nullptr
129 || KStarsData::Instance()->skyComposite()->artificialHorizon() == nullptr)
130 return nullptr;
131 return &KStarsData::Instance()->skyComposite()->artificialHorizon()->getHorizon();
132 }
133
setStartupCondition(const StartupCondition & value)134 void SchedulerJob::setStartupCondition(const StartupCondition &value)
135 {
136 startupCondition = value;
137
138 /* Keep startup time and condition valid */
139 if (value == START_ASAP)
140 startupTime = QDateTime();
141
142 /* Refresh estimated time - which update job cells */
143 setEstimatedTime(estimatedTime);
144
145 /* Refresh dawn and dusk for startup date */
146 calculateDawnDusk(startupTime, nextDawn, nextDusk);
147 }
148
setStartupTime(const QDateTime & value)149 void SchedulerJob::setStartupTime(const QDateTime &value)
150 {
151 startupTime = value;
152
153 /* Keep startup time and condition valid */
154 if (value.isValid())
155 startupCondition = START_AT;
156 else
157 startupCondition = fileStartupCondition;
158
159 // Refresh altitude - invalid date/time is taken care of when rendering
160 altitudeAtStartup = findAltitude(targetCoords, startupTime, &isSettingAtStartup);
161
162 /* Refresh estimated time - which update job cells */
163 setEstimatedTime(estimatedTime);
164
165 /* Refresh dawn and dusk for startup date */
166 calculateDawnDusk(startupTime, nextDawn, nextDusk);
167 }
168
setSequenceFile(const QUrl & value)169 void SchedulerJob::setSequenceFile(const QUrl &value)
170 {
171 sequenceFile = value;
172 }
173
setFITSFile(const QUrl & value)174 void SchedulerJob::setFITSFile(const QUrl &value)
175 {
176 fitsFile = value;
177 }
178
setMinAltitude(const double & value)179 void SchedulerJob::setMinAltitude(const double &value)
180 {
181 minAltitude = value;
182 }
183
hasAltitudeConstraint() const184 bool SchedulerJob::hasAltitudeConstraint() const
185 {
186 return hasMinAltitude() ||
187 (enforceArtificialHorizon && (getHorizon() != nullptr) && getHorizon()->altitudeConstraintsExist());
188 }
189
setMinMoonSeparation(const double & value)190 void SchedulerJob::setMinMoonSeparation(const double &value)
191 {
192 minMoonSeparation = value;
193 }
194
setEnforceWeather(bool value)195 void SchedulerJob::setEnforceWeather(bool value)
196 {
197 enforceWeather = value;
198 }
199
setCompletionTime(const QDateTime & value)200 void SchedulerJob::setCompletionTime(const QDateTime &value)
201 {
202 /* If completion time is valid, automatically switch condition to FINISH_AT */
203 if (value.isValid())
204 {
205 setCompletionCondition(FINISH_AT);
206 completionTime = value;
207 altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion);
208 setEstimatedTime(-1);
209 }
210 /* If completion time is invalid, and job is looping, keep completion time undefined */
211 else if (FINISH_LOOP == completionCondition)
212 {
213 completionTime = QDateTime();
214 altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion);
215 setEstimatedTime(-1);
216 }
217 /* If completion time is invalid, deduce completion from startup and duration */
218 else if (startupTime.isValid())
219 {
220 completionTime = startupTime.addSecs(estimatedTime);
221 altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion);
222 updateJobCells();
223 }
224 /* Else just refresh estimated time - which update job cells */
225 else setEstimatedTime(estimatedTime);
226
227
228 /* Invariants */
229 Q_ASSERT_X(completionTime.isValid() ?
230 (FINISH_AT == completionCondition || FINISH_REPEAT == completionCondition || FINISH_SEQUENCE == completionCondition) :
231 FINISH_LOOP == completionCondition,
232 __FUNCTION__, "Valid completion time implies job is FINISH_AT/REPEAT/SEQUENCE, else job is FINISH_LOOP.");
233 }
234
setCompletionCondition(const CompletionCondition & value)235 void SchedulerJob::setCompletionCondition(const CompletionCondition &value)
236 {
237 completionCondition = value;
238
239 // Update repeats requirement, looping jobs have none
240 switch (completionCondition)
241 {
242 case FINISH_LOOP:
243 setCompletionTime(QDateTime());
244 /* Fall through */
245 case FINISH_AT:
246 if (0 < getRepeatsRequired())
247 setRepeatsRequired(0);
248 break;
249
250 case FINISH_SEQUENCE:
251 if (1 != getRepeatsRequired())
252 setRepeatsRequired(1);
253 break;
254
255 case FINISH_REPEAT:
256 if (0 == getRepeatsRequired())
257 setRepeatsRequired(1);
258 break;
259
260 default:
261 break;
262 }
263
264 updateJobCells();
265 }
266
setStepPipeline(const StepPipeline & value)267 void SchedulerJob::setStepPipeline(const StepPipeline &value)
268 {
269 stepPipeline = value;
270 }
271
setState(const JOBStatus & value)272 void SchedulerJob::setState(const JOBStatus &value)
273 {
274 state = value;
275
276 /* FIXME: move this to Scheduler, SchedulerJob is mostly a model */
277 if (JOB_ERROR == state)
278 KNotification::event(QLatin1String("EkosSchedulerJobFail"), i18n("Ekos job failed (%1)", getName()));
279
280 /* If job becomes invalid or idle, automatically reset its startup characteristics, and force its duration to be reestimated */
281 if (JOB_INVALID == value || JOB_IDLE == value)
282 {
283 setStartupCondition(fileStartupCondition);
284 setStartupTime(fileStartupTime);
285 setEstimatedTime(-1);
286 }
287
288 /* If job is aborted, automatically reset its startup characteristics */
289 if (JOB_ABORTED == value)
290 {
291 setStartupCondition(fileStartupCondition);
292 /* setStartupTime(fileStartupTime); */
293 }
294
295 updateJobCells();
296 }
297
setLeadTime(const int64_t & value)298 void SchedulerJob::setLeadTime(const int64_t &value)
299 {
300 leadTime = value;
301 updateJobCells();
302 }
303
setScore(int value)304 void SchedulerJob::setScore(int value)
305 {
306 score = value;
307 updateJobCells();
308 }
309
setCulminationOffset(const int16_t & value)310 void SchedulerJob::setCulminationOffset(const int16_t &value)
311 {
312 culminationOffset = value;
313 }
314
setSequenceCount(const int count)315 void SchedulerJob::setSequenceCount(const int count)
316 {
317 sequenceCount = count;
318 updateJobCells();
319 }
320
setNameCell(QTableWidgetItem * value)321 void SchedulerJob::setNameCell(QTableWidgetItem *value)
322 {
323 nameCell = value;
324 }
325
setCompletedCount(const int count)326 void SchedulerJob::setCompletedCount(const int count)
327 {
328 completedCount = count;
329 updateJobCells();
330 }
331
setStatusCell(QTableWidgetItem * value)332 void SchedulerJob::setStatusCell(QTableWidgetItem *value)
333 {
334 statusCell = value;
335 if (nullptr != statusCell)
336 statusCell->setToolTip(i18n("Current status of job '%1', managed by the Scheduler.\n"
337 "If invalid, the Scheduler was not able to find a proper observation time for the target.\n"
338 "If aborted, the Scheduler missed the scheduled time or encountered transitory issues and will reschedule the job.\n"
339 "If complete, the Scheduler verified that all sequence captures requested were stored, including repeats.",
340 name));
341 }
342
setAltitudeCell(QTableWidgetItem * value)343 void SchedulerJob::setAltitudeCell(QTableWidgetItem *value)
344 {
345 altitudeCell = value;
346 if (nullptr != altitudeCell)
347 altitudeCell->setToolTip(i18n("Current altitude of the target of job '%1'.\n"
348 "A rising target is indicated with an arrow going up.\n"
349 "A setting target is indicated with an arrow going down.",
350 name));
351 }
352
setStartupCell(QTableWidgetItem * value)353 void SchedulerJob::setStartupCell(QTableWidgetItem *value)
354 {
355 startupCell = value;
356 if (nullptr != startupCell)
357 startupCell->setToolTip(i18n("Startup time for job '%1', as estimated by the Scheduler.\n"
358 "The altitude at startup, if available, is displayed too.\n"
359 "Fixed time from user or culmination time is marked with a chronometer symbol. ",
360 name));
361 }
362
setCompletionCell(QTableWidgetItem * value)363 void SchedulerJob::setCompletionCell(QTableWidgetItem *value)
364 {
365 completionCell = value;
366 if (nullptr != completionCell)
367 completionCell->setToolTip(i18n("Completion time for job '%1', as estimated by the Scheduler.\n"
368 "You may specify a fixed time to limit duration of looping jobs. "
369 "A warning symbol indicates the altitude at completion may cause the job to abort before completion.\n",
370 name));
371 }
372
setCaptureCountCell(QTableWidgetItem * value)373 void SchedulerJob::setCaptureCountCell(QTableWidgetItem *value)
374 {
375 captureCountCell = value;
376 if (nullptr != captureCountCell)
377 captureCountCell->setToolTip(i18n("Count of captures stored for job '%1', based on its sequence job.\n"
378 "This is a summary, additional specific frame types may be required to complete the job.",
379 name));
380 }
381
setScoreCell(QTableWidgetItem * value)382 void SchedulerJob::setScoreCell(QTableWidgetItem *value)
383 {
384 scoreCell = value;
385 if (nullptr != scoreCell)
386 scoreCell->setToolTip(i18n("Current score for job '%1', from its altitude, moon separation and sky darkness.\n"
387 "Negative if adequate altitude is not achieved yet or if there is no proper observation time today.\n"
388 "The Scheduler will refresh scores when picking a new candidate job.",
389 name));
390 }
391
setLeadTimeCell(QTableWidgetItem * value)392 void SchedulerJob::setLeadTimeCell(QTableWidgetItem *value)
393 {
394 leadTimeCell = value;
395 if (nullptr != leadTimeCell)
396 leadTimeCell->setToolTip(i18n("Time interval from the job which precedes job '%1'.\n"
397 "Adjust the Lead Time in Ekos options to increase that duration and leave time for jobs to complete.\n"
398 "Rearrange jobs to minimize that duration and optimize your imaging time.",
399 name));
400 }
401
setDateTimeDisplayFormat(const QString & value)402 void SchedulerJob::setDateTimeDisplayFormat(const QString &value)
403 {
404 dateTimeDisplayFormat = value;
405 updateJobCells();
406 }
407
setStage(const JOBStage & value)408 void SchedulerJob::setStage(const JOBStage &value)
409 {
410 stage = value;
411 updateJobCells();
412 }
413
setStageCell(QTableWidgetItem * cell)414 void SchedulerJob::setStageCell(QTableWidgetItem *cell)
415 {
416 stageCell = cell;
417 // FIXME: Add a tool tip if cell is used
418 }
419
setStageLabel(QLabel * label)420 void SchedulerJob::setStageLabel(QLabel *label)
421 {
422 stageLabel = label;
423 }
424
setFileStartupCondition(const StartupCondition & value)425 void SchedulerJob::setFileStartupCondition(const StartupCondition &value)
426 {
427 fileStartupCondition = value;
428 }
429
setFileStartupTime(const QDateTime & value)430 void SchedulerJob::setFileStartupTime(const QDateTime &value)
431 {
432 fileStartupTime = value;
433 }
434
setEstimatedTime(const int64_t & value)435 void SchedulerJob::setEstimatedTime(const int64_t &value)
436 {
437 /* Estimated time is generally the difference between startup and completion times:
438 * - It is fixed when startup and completion times are fixed, that is, we disregard the argument
439 * - Else mostly it pushes completion time from startup time
440 *
441 * However it cannot advance startup time when completion time is fixed because of the way jobs are scheduled.
442 * This situation requires a warning in the user interface when there is not enough time for the job to process.
443 */
444
445 /* If startup and completion times are fixed, estimated time cannot change - disregard the argument */
446 if (START_ASAP != fileStartupCondition && FINISH_AT == completionCondition)
447 {
448 estimatedTime = startupTime.secsTo(completionTime);
449 }
450 /* If completion time isn't fixed, estimated time adjusts completion time */
451 else if (FINISH_AT != completionCondition && FINISH_LOOP != completionCondition)
452 {
453 estimatedTime = value;
454 completionTime = startupTime.addSecs(value);
455 altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion);
456 }
457 /* Else estimated time is simply stored as is - covers FINISH_LOOP from setCompletionTime */
458 else estimatedTime = value;
459
460 updateJobCells();
461 }
462
setInSequenceFocus(bool value)463 void SchedulerJob::setInSequenceFocus(bool value)
464 {
465 inSequenceFocus = value;
466 }
467
setPriority(const uint8_t & value)468 void SchedulerJob::setPriority(const uint8_t &value)
469 {
470 priority = value;
471 }
472
setEnforceTwilight(bool value)473 void SchedulerJob::setEnforceTwilight(bool value)
474 {
475 enforceTwilight = value;
476 calculateDawnDusk(startupTime, nextDawn, nextDusk);
477 }
478
setEnforceArtificialHorizon(bool value)479 void SchedulerJob::setEnforceArtificialHorizon(bool value)
480 {
481 enforceArtificialHorizon = value;
482 }
483
setEstimatedTimeCell(QTableWidgetItem * value)484 void SchedulerJob::setEstimatedTimeCell(QTableWidgetItem *value)
485 {
486 estimatedTimeCell = value;
487 if (estimatedTimeCell)
488 estimatedTimeCell->setToolTip(i18n("Duration job '%1' will take to complete when started, as estimated by the Scheduler.\n"
489 "Depends on the actions to be run, and the sequence job to be processed.",
490 name));
491 }
492
setLightFramesRequired(bool value)493 void SchedulerJob::setLightFramesRequired(bool value)
494 {
495 lightFramesRequired = value;
496 }
497
setRepeatsRequired(const uint16_t & value)498 void SchedulerJob::setRepeatsRequired(const uint16_t &value)
499 {
500 repeatsRequired = value;
501
502 // Update completion condition to be compatible
503 if (1 < repeatsRequired)
504 {
505 if (FINISH_REPEAT != completionCondition)
506 setCompletionCondition(FINISH_REPEAT);
507 }
508 else if (0 < repeatsRequired)
509 {
510 if (FINISH_SEQUENCE != completionCondition)
511 setCompletionCondition(FINISH_SEQUENCE);
512 }
513 else
514 {
515 if (FINISH_LOOP != completionCondition)
516 setCompletionCondition(FINISH_LOOP);
517 }
518
519 updateJobCells();
520 }
521
setRepeatsRemaining(const uint16_t & value)522 void SchedulerJob::setRepeatsRemaining(const uint16_t &value)
523 {
524 repeatsRemaining = value;
525 updateJobCells();
526 }
527
setCapturedFramesMap(const CapturedFramesMap & value)528 void SchedulerJob::setCapturedFramesMap(const CapturedFramesMap &value)
529 {
530 capturedFramesMap = value;
531 }
532
setTargetCoords(const dms & ra,const dms & dec,double djd)533 void SchedulerJob::setTargetCoords(const dms &ra, const dms &dec, double djd)
534 {
535 targetCoords.setRA0(ra);
536 targetCoords.setDec0(dec);
537
538 targetCoords.apparentCoord(static_cast<long double>(J2000), djd);
539 }
540
setRotation(double value)541 void SchedulerJob::setRotation(double value)
542 {
543 rotation = value;
544 }
545
updateJobCells()546 void SchedulerJob::updateJobCells()
547 {
548 if (nullptr != nameCell)
549 {
550 nameCell->setText(name);
551 if (nullptr != nameCell)
552 nameCell->tableWidget()->resizeColumnToContents(nameCell->column());
553 }
554
555 if (nullptr != nameLabel)
556 {
557 nameLabel->setText(name + QString(":"));
558 }
559
560 if (nullptr != statusCell)
561 {
562 static QMap<JOBStatus, QString> stateStrings;
563 static QString stateStringUnknown;
564 if (stateStrings.isEmpty())
565 {
566 stateStrings[JOB_IDLE] = i18n("Idle");
567 stateStrings[JOB_EVALUATION] = i18n("Evaluating");
568 stateStrings[JOB_SCHEDULED] = i18n("Scheduled");
569 stateStrings[JOB_BUSY] = i18n("Running");
570 stateStrings[JOB_INVALID] = i18n("Invalid");
571 stateStrings[JOB_COMPLETE] = i18n("Complete");
572 stateStrings[JOB_ABORTED] = i18n("Aborted");
573 stateStrings[JOB_ERROR] = i18n("Error");
574 stateStringUnknown = i18n("Unknown");
575 }
576 statusCell->setText(stateStrings.value(state, stateStringUnknown));
577
578 if (nullptr != statusCell->tableWidget())
579 statusCell->tableWidget()->resizeColumnToContents(statusCell->column());
580 }
581
582 if (nullptr != stageCell || nullptr != stageLabel)
583 {
584 /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case should ; also, not thread-safe */
585 /* 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? */
586 static QMap<JOBStage, QString> stageStrings;
587 static QString stageStringUnknown;
588 if (stageStrings.isEmpty())
589 {
590 stageStrings[STAGE_IDLE] = i18n("Idle");
591 stageStrings[STAGE_SLEWING] = i18n("Slewing");
592 stageStrings[STAGE_SLEW_COMPLETE] = i18n("Slew complete");
593 stageStrings[STAGE_FOCUSING] =
594 stageStrings[STAGE_POSTALIGN_FOCUSING] = i18n("Focusing");
595 stageStrings[STAGE_FOCUS_COMPLETE] =
596 stageStrings[STAGE_POSTALIGN_FOCUSING_COMPLETE ] = i18n("Focus complete");
597 stageStrings[STAGE_ALIGNING] = i18n("Aligning");
598 stageStrings[STAGE_ALIGN_COMPLETE] = i18n("Align complete");
599 stageStrings[STAGE_RESLEWING] = i18n("Repositioning");
600 stageStrings[STAGE_RESLEWING_COMPLETE] = i18n("Repositioning complete");
601 /*stageStrings[STAGE_CALIBRATING] = i18n("Calibrating");*/
602 stageStrings[STAGE_GUIDING] = i18n("Guiding");
603 stageStrings[STAGE_GUIDING_COMPLETE] = i18n("Guiding complete");
604 stageStrings[STAGE_CAPTURING] = i18n("Capturing");
605 stageStringUnknown = i18n("Unknown");
606 }
607 if (nullptr != stageCell)
608 {
609 stageCell->setText(stageStrings.value(stage, stageStringUnknown));
610 if (nullptr != stageCell->tableWidget())
611 stageCell->tableWidget()->resizeColumnToContents(stageCell->column());
612 }
613 if (nullptr != stageLabel)
614 {
615 stageLabel->setText(QString("%1: %2").arg(name, stageStrings.value(stage, stageStringUnknown)));
616 }
617 }
618
619 if (nullptr != startupCell)
620 {
621 /* Display startup time if it is valid */
622 if (startupTime.isValid())
623 {
624 startupCell->setText(QString("%1%2%L3° %4")
625 .arg(altitudeAtStartup < minAltitude ? QString(QChar(0x26A0)) : "")
626 .arg(QChar(isSettingAtStartup ? 0x2193 : 0x2191))
627 .arg(altitudeAtStartup, 0, 'f', 1)
628 .arg(startupTime.toString(dateTimeDisplayFormat)));
629
630 switch (fileStartupCondition)
631 {
632 /* If the original condition is START_AT/START_CULMINATION, startup time is fixed */
633 case START_AT:
634 case START_CULMINATION:
635 startupCell->setIcon(QIcon::fromTheme("chronometer"));
636 break;
637
638 /* If the original condition is START_ASAP, startup time is informational */
639 case START_ASAP:
640 startupCell->setIcon(QIcon());
641 break;
642
643 default:
644 break;
645 }
646 }
647 /* Else do not display any startup time */
648 else
649 {
650 startupCell->setText("-");
651 startupCell->setIcon(QIcon());
652 }
653
654 if (nullptr != startupCell->tableWidget())
655 startupCell->tableWidget()->resizeColumnToContents(startupCell->column());
656 }
657
658 if (nullptr != altitudeCell)
659 {
660 // FIXME: Cache altitude calculations
661 bool is_setting = false;
662 double const alt = findAltitude(targetCoords, QDateTime(), &is_setting);
663
664 altitudeCell->setText(QString("%1%L2°")
665 .arg(QChar(is_setting ? 0x2193 : 0x2191))
666 .arg(alt, 0, 'f', 1));
667
668 if (nullptr != altitudeCell->tableWidget())
669 altitudeCell->tableWidget()->resizeColumnToContents(altitudeCell->column());
670 }
671
672 if (nullptr != completionCell)
673 {
674 /* Display completion time if it is valid and job is not looping */
675 if (FINISH_LOOP != completionCondition && completionTime.isValid())
676 {
677 completionCell->setText(QString("%1%2%L3° %4")
678 .arg(altitudeAtCompletion < minAltitude ? QString(QChar(0x26A0)) : "")
679 .arg(QChar(isSettingAtCompletion ? 0x2193 : 0x2191))
680 .arg(altitudeAtCompletion, 0, 'f', 1)
681 .arg(completionTime.toString(dateTimeDisplayFormat)));
682
683 switch (completionCondition)
684 {
685 case FINISH_AT:
686 completionCell->setIcon(QIcon::fromTheme("chronometer"));
687 break;
688
689 case FINISH_SEQUENCE:
690 case FINISH_REPEAT:
691 default:
692 completionCell->setIcon(QIcon());
693 break;
694 }
695 }
696 /* Else do not display any completion time */
697 else
698 {
699 completionCell->setText("-");
700 completionCell->setIcon(QIcon());
701 }
702
703 if (nullptr != completionCell->tableWidget())
704 completionCell->tableWidget()->resizeColumnToContents(completionCell->column());
705 }
706
707 if (nullptr != estimatedTimeCell)
708 {
709 if (0 < estimatedTime)
710 /* Seconds to ms - this doesn't follow dateTimeDisplayFormat, which renders YMD too */
711 estimatedTimeCell->setText(QTime::fromMSecsSinceStartOfDay(estimatedTime * 1000).toString("HH:mm:ss"));
712 #if 0
713 else if(0 == estimatedTime)
714 /* FIXME: this special case could be merged with the previous, kept for future to indicate actual duration */
715 estimatedTimeCell->setText("00:00:00");
716 #endif
717 else
718 /* Invalid marker */
719 estimatedTimeCell->setText("-");
720
721 /* Warn the end-user if estimated time doesn't fit in the startup/completion interval */
722 if (estimatedTime < startupTime.secsTo(completionTime))
723 estimatedTimeCell->setIcon(QIcon::fromTheme("document-find"));
724 else
725 estimatedTimeCell->setIcon(QIcon());
726
727 if (nullptr != estimatedTimeCell->tableWidget())
728 estimatedTimeCell->tableWidget()->resizeColumnToContents(estimatedTimeCell->column());
729 }
730
731 if (nullptr != captureCountCell)
732 {
733 switch (completionCondition)
734 {
735 case FINISH_AT:
736 // FIXME: Attempt to calculate the number of frames until end - requires detailed imaging time
737
738 case FINISH_LOOP:
739 // If looping, display the count of completed frames
740 captureCountCell->setText(QString("%L1/-").arg(completedCount));
741 break;
742
743 case FINISH_SEQUENCE:
744 case FINISH_REPEAT:
745 default:
746 // If repeating, display the count of completed frames to the count of requested frames
747 captureCountCell->setText(QString("%L1/%L2").arg(completedCount).arg(sequenceCount));
748 break;
749 }
750
751 if (nullptr != captureCountCell->tableWidget())
752 captureCountCell->tableWidget()->resizeColumnToContents(captureCountCell->column());
753 }
754
755 if (nullptr != scoreCell)
756 {
757 if (0 <= score)
758 scoreCell->setText(QString("%L1").arg(score));
759 else
760 /* FIXME: negative scores are just weird for the end-user */
761 scoreCell->setText("<0");
762
763 if (nullptr != scoreCell->tableWidget())
764 scoreCell->tableWidget()->resizeColumnToContents(scoreCell->column());
765 }
766
767 if (nullptr != leadTimeCell)
768 {
769 // Display lead time, plus a warning if lead time is more than twice the lead time of the Ekos options
770 switch (state)
771 {
772 case JOB_INVALID:
773 case JOB_ERROR:
774 case JOB_COMPLETE:
775 leadTimeCell->setText("-");
776 break;
777
778 default:
779 leadTimeCell->setText(QString("%1%2")
780 .arg(Options::leadTime() * 60 * 2 < leadTime ? QString(QChar(0x26A0)) : "")
781 .arg(QTime::fromMSecsSinceStartOfDay(leadTime * 1000).toString("HH:mm:ss")));
782 break;
783 }
784
785 if (nullptr != leadTimeCell->tableWidget())
786 leadTimeCell->tableWidget()->resizeColumnToContents(leadTimeCell->column());
787 }
788 }
789
reset()790 void SchedulerJob::reset()
791 {
792 state = JOB_IDLE;
793 stage = STAGE_IDLE;
794 estimatedTime = -1;
795 leadTime = 0;
796 startupCondition = fileStartupCondition;
797 startupTime = fileStartupCondition == START_AT ? fileStartupTime : QDateTime();
798
799 /* Refresh dawn and dusk for startup date */
800 calculateDawnDusk(startupTime, nextDawn, nextDusk);
801
802 /* No change to culmination offset */
803 repeatsRemaining = repeatsRequired;
804 updateJobCells();
805 }
806
decreasingScoreOrder(SchedulerJob const * job1,SchedulerJob const * job2)807 bool SchedulerJob::decreasingScoreOrder(SchedulerJob const *job1, SchedulerJob const *job2)
808 {
809 return job1->getScore() > job2->getScore();
810 }
811
increasingPriorityOrder(SchedulerJob const * job1,SchedulerJob const * job2)812 bool SchedulerJob::increasingPriorityOrder(SchedulerJob const *job1, SchedulerJob const *job2)
813 {
814 return job1->getPriority() < job2->getPriority();
815 }
816
decreasingAltitudeOrder(SchedulerJob const * job1,SchedulerJob const * job2,QDateTime const & when)817 bool SchedulerJob::decreasingAltitudeOrder(SchedulerJob const *job1, SchedulerJob const *job2, QDateTime const &when)
818 {
819 bool A_is_setting = job1->isSettingAtStartup;
820 double const altA = when.isValid() ?
821 findAltitude(job1->getTargetCoords(), when, &A_is_setting) :
822 job1->altitudeAtStartup;
823
824 bool B_is_setting = job2->isSettingAtStartup;
825 double const altB = when.isValid() ?
826 findAltitude(job2->getTargetCoords(), when, &B_is_setting) :
827 job2->altitudeAtStartup;
828
829 // Sort with the setting target first
830 if (A_is_setting && !B_is_setting)
831 return true;
832 else if (!A_is_setting && B_is_setting)
833 return false;
834
835 // If both targets rise or set, sort by decreasing altitude, considering a setting target is prioritary
836 return (A_is_setting && B_is_setting) ? altA < altB : altB < altA;
837 }
838
increasingStartupTimeOrder(SchedulerJob const * job1,SchedulerJob const * job2)839 bool SchedulerJob::increasingStartupTimeOrder(SchedulerJob const *job1, SchedulerJob const *job2)
840 {
841 return job1->getStartupTime() < job2->getStartupTime();
842 }
843
844 // This uses both the user-setting minAltitude, as well as any artificial horizon
845 // constraints the user might have setup.
getMinAltitudeConstraint(double azimuth) const846 double SchedulerJob::getMinAltitudeConstraint(double azimuth) const
847 {
848 double constraint = getMinAltitude();
849 if (getHorizon() != nullptr && enforceArtificialHorizon)
850 constraint = std::max(constraint, getHorizon()->altitudeConstraint(azimuth));
851 return constraint;
852 }
853
getAltitudeScore(QDateTime const & when,double * altPtr) const854 int16_t SchedulerJob::getAltitudeScore(QDateTime const &when, double *altPtr) const
855 {
856 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
857
858 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
859 KStarsDateTime ltWhen(when.isValid() ?
860 Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
861 getLocalTime());
862
863 // Create a sky object with the target catalog coordinates
864 SkyPoint const target = getTargetCoords();
865 SkyObject o;
866 o.setRA0(target.ra0());
867 o.setDec0(target.dec0());
868
869 // Update RA/DEC of the target for the current fraction of the day
870 KSNumbers numbers(ltWhen.djd());
871 o.updateCoordsNow(&numbers);
872
873 // Compute local sidereal time for the current fraction of the day, calculate altitude
874 CachingDms const LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltWhen).gst());
875 o.EquatorialToHorizontal(&LST, getGeo()->lat());
876 double const altitude = o.alt().Degrees();
877 double const azimuth = o.az().Degrees();
878 if (altPtr != nullptr)
879 *altPtr = altitude;
880
881 double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff();
882 int16_t score = BAD_SCORE - 1;
883
884 const double minAlt = getMinAltitudeConstraint(azimuth);
885
886 // If altitude is negative, bad score
887 // FIXME: some locations may allow negative altitudes
888 if (altitude < 0)
889 {
890 score = BAD_SCORE;
891 }
892 else if (hasAltitudeConstraint())
893 {
894 // If under altitude constraint, bad score
895 if (altitude < minAlt)
896 score = BAD_SCORE;
897 // Else if setting and under altitude cutoff, job would end soon after starting, bad score
898 // FIXME: half bad score when under altitude cutoff risk getting positive again
899 else
900 {
901 double offset = LST.Hours() - o.ra().Hours();
902 if (24.0 <= offset)
903 offset -= 24.0;
904 else if (offset < 0.0)
905 offset += 24.0;
906 if (0.0 <= offset && offset < 12.0)
907 if (altitude - SETTING_ALTITUDE_CUTOFF < minAlt)
908 score = BAD_SCORE / 2;
909 }
910 }
911 // If not constrained but below minimum hard altitude, set score to 10% of altitude value
912 else if (altitude < MIN_ALTITUDE)
913 {
914 score = static_cast <int16_t> (altitude / 10.0);
915 }
916
917 // Else default score calculation without altitude constraint
918 if (score < BAD_SCORE)
919 score = static_cast <int16_t> ((1.5 * pow(1.06, altitude)) - (MIN_ALTITUDE / 10.0));
920
921 //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target altitude is %3 degrees at %2 (score %4).")
922 // .arg(getName())
923 // .arg(when.toString(getDateTimeDisplayFormat()))
924 // .arg(currentAlt, 0, 'f', minAltitude->decimals())
925 // .arg(QString::asprintf("%+d", score));
926
927 return score;
928 }
929
getMoonSeparationScore(QDateTime const & when) const930 int16_t SchedulerJob::getMoonSeparationScore(QDateTime const &when) const
931 {
932 if (moon == nullptr) return 100;
933
934 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
935
936 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
937 KStarsDateTime ltWhen(when.isValid() ?
938 Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
939 getLocalTime());
940
941 // Create a sky object with the target catalog coordinates
942 SkyPoint const target = getTargetCoords();
943 SkyObject o;
944 o.setRA0(target.ra0());
945 o.setDec0(target.dec0());
946
947 // Update RA/DEC of the target for the current fraction of the day
948 KSNumbers numbers(ltWhen.djd());
949 o.updateCoordsNow(&numbers);
950
951 // Update moon
952 //ut = getGeo()->LTtoUT(ltWhen);
953 //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation
954 //LST = getGeo()->GSTtoLST(ut.gst());
955 CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltWhen).gst());
956 moon->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
957
958 double const moonAltitude = moon->alt().Degrees();
959
960 // Lunar illumination %
961 double const illum = moon->illum() * 100.0;
962
963 // Moon/Sky separation p
964 double const separation = moon->angularDistanceTo(&o).Degrees();
965
966 // Zenith distance of the moon
967 double const zMoon = (90 - moonAltitude);
968 // Zenith distance of target
969 double const zTarget = (90 - o.alt().Degrees());
970
971 int16_t score = 0;
972
973 // If target = Moon, or no illuminiation, or moon below horizon, return static score.
974 if (zMoon == zTarget || illum == 0 || zMoon >= 90)
975 score = 100;
976 else
977 {
978 // JM: Some magic voodoo formula I came up with!
979 double moonEffect = (pow(separation, 1.7) * pow(zMoon, 0.5)) / (pow(zTarget, 1.1) * pow(illum, 0.5));
980
981 // Limit to 0 to 100 range.
982 moonEffect = KSUtils::clamp(moonEffect, 0.0, 100.0);
983
984 if (getMinMoonSeparation() > 0)
985 {
986 if (separation < getMinMoonSeparation())
987 score = BAD_SCORE * 5;
988 else
989 score = moonEffect;
990 }
991 else
992 score = moonEffect;
993 }
994
995 // Limit to 0 to 20
996 score /= 5.0;
997
998 //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target is %L3 degrees from Moon (score %2).")
999 // .arg(getName())
1000 // .arg(separation, 0, 'f', 3)
1001 // .arg(QString::asprintf("%+d", score));
1002
1003 return score;
1004 }
1005
1006
getCurrentMoonSeparation() const1007 double SchedulerJob::getCurrentMoonSeparation() const
1008 {
1009 if (moon == nullptr) return 180.0;
1010
1011 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
1012
1013 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1014 KStarsDateTime ltWhen(getLocalTime());
1015
1016 // Create a sky object with the target catalog coordinates
1017 SkyPoint const target = getTargetCoords();
1018 SkyObject o;
1019 o.setRA0(target.ra0());
1020 o.setDec0(target.dec0());
1021
1022 // Update RA/DEC of the target for the current fraction of the day
1023 KSNumbers numbers(ltWhen.djd());
1024 o.updateCoordsNow(&numbers);
1025
1026 // Update moon
1027 //ut = getGeo()->LTtoUT(ltWhen);
1028 //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation
1029 //LST = getGeo()->GSTtoLST(ut.gst());
1030 CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltWhen).gst());
1031 moon->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1032
1033 // Moon/Sky separation p
1034 return moon->angularDistanceTo(&o).Degrees();
1035 }
1036
calculateAltitudeTime(QDateTime const & when) const1037 QDateTime SchedulerJob::calculateAltitudeTime(QDateTime const &when) const
1038 {
1039 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
1040
1041 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1042 KStarsDateTime ltWhen(when.isValid() ?
1043 Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
1044 getLocalTime());
1045
1046 // Create a sky object with the target catalog coordinates
1047 SkyPoint const target = getTargetCoords();
1048 SkyObject o;
1049 o.setRA0(target.ra0());
1050 o.setDec0(target.dec0());
1051
1052 // Calculate the UT at the argument time
1053 KStarsDateTime const ut = getGeo()->LTtoUT(ltWhen);
1054
1055 double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff();
1056
1057 // Within the next 24 hours, search when the job target matches the altitude and moon constraints
1058 for (unsigned int minute = 0; minute < 24 * 60; minute++)
1059 {
1060 KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60));
1061
1062 // Update RA/DEC of the target for the current fraction of the day
1063 KSNumbers numbers(ltOffset.djd());
1064 o.updateCoordsNow(&numbers);
1065
1066 // Compute local sidereal time for the current fraction of the day, calculate altitude
1067 CachingDms const LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltOffset).gst());
1068 o.EquatorialToHorizontal(&LST, getGeo()->lat());
1069 double const altitude = o.alt().Degrees();
1070 double const azimuth = o.az().Degrees();
1071 double const minAlt = getMinAltitudeConstraint(azimuth);
1072
1073 if (minAlt <= altitude)
1074 {
1075 // Don't test proximity to dawn in this situation, we only cater for altitude here
1076
1077 // Continue searching if Moon separation is not good enough
1078 if (0 < getMinMoonSeparation() && getMoonSeparationScore(ltOffset) < 0)
1079 continue;
1080
1081 // Continue searching if target is setting and under the cutoff
1082 double offset = LST.Hours() - o.ra().Hours();
1083 if (24.0 <= offset)
1084 offset -= 24.0;
1085 else if (offset < 0.0)
1086 offset += 24.0;
1087 if (0.0 <= offset && offset < 12.0)
1088 if (altitude - SETTING_ALTITUDE_CUTOFF < minAlt)
1089 continue;
1090
1091 return ltOffset;
1092 }
1093 }
1094
1095 return QDateTime();
1096 }
1097
calculateCulmination(QDateTime const & when) const1098 QDateTime SchedulerJob::calculateCulmination(QDateTime const &when) const
1099 {
1100 // FIXME: culmination calculation is a min altitude requirement, should be an interval altitude requirement
1101
1102 // FIXME: block calculating target coordinates at a particular time is duplicated in calculateCulmination
1103
1104 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1105 KStarsDateTime ltWhen(when.isValid() ?
1106 Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
1107 getLocalTime());
1108
1109 // Create a sky object with the target catalog coordinates
1110 SkyPoint const target = getTargetCoords();
1111 SkyObject o;
1112 o.setRA0(target.ra0());
1113 o.setDec0(target.dec0());
1114
1115 // Update RA/DEC for the argument date/time
1116 KSNumbers numbers(ltWhen.djd());
1117 o.updateCoordsNow(&numbers);
1118
1119 // Calculate transit date/time at the argument date - transitTime requires UT and returns LocalTime
1120 KStarsDateTime transitDateTime(ltWhen.date(), o.transitTime(getGeo()->LTtoUT(ltWhen), getGeo()), Qt::LocalTime);
1121
1122 // Shift transit date/time by the argument offset
1123 KStarsDateTime observationDateTime = transitDateTime.addSecs(getCulminationOffset() * 60);
1124
1125 // Relax observation time, culmination calculation is stable at minute only
1126 KStarsDateTime relaxedDateTime = observationDateTime.addSecs(Options::leadTime() * 60);
1127
1128 // Verify resulting observation time is under lead time vs. argument time
1129 // If sooner, delay by 8 hours to get to the next transit - perhaps in a third call
1130 if (relaxedDateTime < ltWhen)
1131 {
1132 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' startup %2 is posterior to transit %3, shifting by 8 hours.")
1133 .arg(getName())
1134 .arg(ltWhen.toString(getDateTimeDisplayFormat()))
1135 .arg(relaxedDateTime.toString(getDateTimeDisplayFormat()));
1136
1137 return calculateCulmination(when.addSecs(8 * 60 * 60));
1138 }
1139
1140 // Guarantees - culmination calculation is stable at minute level, so relax by lead time
1141 Q_ASSERT_X(observationDateTime.isValid(), __FUNCTION__, "Observation time for target culmination is valid.");
1142 Q_ASSERT_X(ltWhen <= relaxedDateTime, __FUNCTION__,
1143 "Observation time for target culmination is at or after than argument time");
1144
1145 // Return consolidated culmination time
1146 return Qt::UTC == observationDateTime.timeSpec() ? getGeo()->UTtoLT(observationDateTime) : observationDateTime;
1147 }
1148
findAltitude(const SkyPoint & target,const QDateTime & when,bool * is_setting,bool debug)1149 double SchedulerJob::findAltitude(const SkyPoint &target, const QDateTime &when, bool * is_setting, bool debug)
1150 {
1151 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
1152
1153 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1154 KStarsDateTime ltWhen(when.isValid() ?
1155 Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
1156 getLocalTime());
1157
1158 // Create a sky object with the target catalog coordinates
1159 SkyObject o;
1160 o.setRA0(target.ra0());
1161 o.setDec0(target.dec0());
1162
1163 // Update RA/DEC of the target for the current fraction of the day
1164 KSNumbers numbers(ltWhen.djd());
1165 o.updateCoordsNow(&numbers);
1166
1167 // Calculate alt/az coordinates using KStars instance's geolocation
1168 CachingDms const LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltWhen).gst());
1169 o.EquatorialToHorizontal(&LST, getGeo()->lat());
1170
1171 // Hours are reduced to [0,24[, meridian being at 0
1172 double offset = LST.Hours() - o.ra().Hours();
1173 if (24.0 <= offset)
1174 offset -= 24.0;
1175 else if (offset < 0.0)
1176 offset += 24.0;
1177 bool const passed_meridian = 0.0 <= offset && offset < 12.0;
1178
1179 if (debug)
1180 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7")
1181 .arg(o.ra().toHMSString())
1182 .arg(o.ra0().toHMSString())
1183 .arg(o.dec().toHMSString())
1184 .arg(o.dec0().toHMSString())
1185 .arg(o.alt().Degrees())
1186 .arg(passed_meridian ? "yes" : "no")
1187 .arg(o.ra().Hours())
1188 .arg(LST.toHMSString())
1189 .arg(ltWhen.toString("HH:mm:ss"));
1190
1191 if (is_setting)
1192 *is_setting = passed_meridian;
1193
1194 return o.alt().Degrees();
1195 }
1196
calculateDawnDusk(QDateTime const & when,QDateTime & nextDawn,QDateTime & nextDusk)1197 void SchedulerJob::calculateDawnDusk(QDateTime const &when, QDateTime &nextDawn, QDateTime &nextDusk)
1198 {
1199 QDateTime startup = when;
1200
1201 if (!startup.isValid())
1202 startup = getLocalTime();
1203
1204 // Our local midnight - the KStarsDateTime date+time constructor is safe for local times
1205 KStarsDateTime midnight(startup.date(), QTime(0, 0), Qt::LocalTime);
1206
1207 QDateTime dawn = startup, dusk = startup;
1208
1209 // Loop dawn and dusk calculation until the events found are the next events
1210 for ( ; dawn <= startup || dusk <= startup ; midnight = midnight.addDays(1))
1211 {
1212 // KSAlmanac computes the closest dawn and dusk events from the local sidereal time corresponding to the midnight argument
1213 KSAlmanac const ksal(midnight, getGeo());
1214
1215 // If dawn is in the past compared to this observation, fetch the next dawn
1216 if (dawn <= startup)
1217 dawn = getGeo()->UTtoLT(ksal.getDate().addSecs((ksal.getDawnAstronomicalTwilight() * 24.0 + Options::dawnOffset()) *
1218 3600.0));
1219
1220 // If dusk is in the past compared to this observation, fetch the next dusk
1221 if (dusk <= startup)
1222 dusk = getGeo()->UTtoLT(ksal.getDate().addSecs((ksal.getDuskAstronomicalTwilight() * 24.0 + Options::duskOffset()) *
1223 3600.0));
1224 }
1225
1226 // Now we have the next events:
1227 // - if dawn comes first, observation runs during the night
1228 // - if dusk comes first, observation runs during the day
1229 nextDawn = dawn;
1230 nextDusk = dusk;
1231 }
1232
runsDuringAstronomicalNightTime() const1233 bool SchedulerJob::runsDuringAstronomicalNightTime() const
1234 {
1235 // Calculate the next astronomical dawn time, adjusted with the Ekos pre-dawn offset
1236 QDateTime const earlyDawn = nextDawn.addSecs(-60.0 * abs(Options::preDawnTime()));
1237
1238 // Dawn and dusk are ordered as the immediate next events following the observation time
1239 // Thus if dawn comes first, the job startup time occurs during the dusk/dawn interval.
1240 return nextDawn < nextDusk && startupTime <= earlyDawn;
1241 }
1242