Kstars

schedulerjob.cpp
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 "schedulermodulestate.h"
16#include "schedulerutils.h"
17#include "ksmoon.h"
18
19#include <knotification.h>
20
21#include <ekos_scheduler_debug.h>
22
23#define BAD_SCORE -1000
24#define MIN_ALTITUDE 15.0
25
26namespace Ekos
27{
28GeoLocation *SchedulerJob::storedGeo = nullptr;
29KStarsDateTime *SchedulerJob::storedLocalTime = nullptr;
30ArtificialHorizon *SchedulerJob::storedHorizon = nullptr;
31
32QString SchedulerJob::jobStatusString(SchedulerJobStatus state)
33{
34 switch(state)
35 {
36 case SCHEDJOB_IDLE:
37 return "IDLE";
39 return "EVAL";
41 return "SCHEDULED";
42 case SCHEDJOB_BUSY:
43 return "BUSY";
44 case SCHEDJOB_ERROR:
45 return "ERROR";
47 return "ABORTED";
49 return "INVALID";
51 return "COMPLETE";
52 }
53 return QString("????");
54}
55
56QString SchedulerJob::jobStageString(SchedulerJobStage state)
57{
58 switch(state)
59 {
60 case SCHEDSTAGE_IDLE:
61 return "IDLE";
62 case SCHEDSTAGE_SLEWING:
63 return "SLEWING";
64 case SCHEDSTAGE_SLEW_COMPLETE:
65 return "SLEW_COMPLETE";
66 case SCHEDSTAGE_FOCUSING:
67 return "FOCUSING";
68 case SCHEDSTAGE_FOCUS_COMPLETE:
69 return "FOCUS_COMPLETE";
70 case SCHEDSTAGE_ALIGNING:
71 return "ALIGNING";
72 case SCHEDSTAGE_ALIGN_COMPLETE:
73 return "ALIGN_COMPLETE";
74 case SCHEDSTAGE_RESLEWING:
75 return "RESLEWING";
76 case SCHEDSTAGE_RESLEWING_COMPLETE:
77 return "RESLEWING_COMPLETE";
78 case SCHEDSTAGE_POSTALIGN_FOCUSING:
79 return "POSTALIGN_FOCUSING";
80 case SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE:
81 return "POSTALIGN_FOCUSING_COMPLETE";
82 case SCHEDSTAGE_GUIDING:
83 return "GUIDING";
84 case SCHEDSTAGE_GUIDING_COMPLETE:
85 return "GUIDING_COMPLETE";
86 case SCHEDSTAGE_CAPTURING:
87 return "CAPTURING";
88 case SCHEDSTAGE_COMPLETE:
89 return "COMPLETE";
90 }
91 return QString("????");
92}
93
94QString SchedulerJob::jobStartupConditionString(StartupCondition condition) const
95{
96 switch(condition)
97 {
98 case START_ASAP:
99 return "ASAP";
100 case START_AT:
101 return QString("AT %1").arg(getStartAtTime().toString("MM/dd hh:mm"));
102 }
103 return QString("????");
104}
105
106QString SchedulerJob::jobCompletionConditionString(CompletionCondition condition) const
107{
108 switch(condition)
109 {
110 case FINISH_SEQUENCE:
111 return "FINISH";
112 case FINISH_REPEAT:
113 return "REPEAT";
114 case FINISH_LOOP:
115 return "LOOP";
116 case FINISH_AT:
117 return QString("AT %1").arg(getFinishAtTime().toString("MM/dd hh:mm"));
118 }
119 return QString("????");
120}
121
122SchedulerJob::SchedulerJob()
123{
124 if (KStarsData::Instance() != nullptr)
125 moon = dynamic_cast<KSMoon *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Moon")));
126}
127
128// Private constructor for unit testing.
129SchedulerJob::SchedulerJob(KSMoon *moonPtr)
130{
131 moon = moonPtr;
132}
133
134void SchedulerJob::setName(const QString &value)
135{
136 name = value;
137}
138
139void SchedulerJob::setGroup(const QString &value)
140{
141 group = value;
142}
143
144
145
146void SchedulerJob::setCompletedIterations(int value)
147{
148 completedIterations = value;
149 if (completionCondition == FINISH_REPEAT)
150 setRepeatsRemaining(getRepeatsRequired() - completedIterations);
151}
152
153KStarsDateTime SchedulerJob::getLocalTime()
154{
155 return Ekos::SchedulerModuleState::getLocalTime();
156}
157
158ArtificialHorizon const *SchedulerJob::getHorizon()
159{
160 if (hasHorizon())
161 return storedHorizon;
162 if (KStarsData::Instance() == nullptr || KStarsData::Instance()->skyComposite() == nullptr
163 || KStarsData::Instance()->skyComposite()->artificialHorizon() == nullptr)
164 return nullptr;
165 return &KStarsData::Instance()->skyComposite()->artificialHorizon()->getHorizon();
166}
167
168void SchedulerJob::setStartupCondition(const StartupCondition &value)
169{
170 startupCondition = value;
171
172 /* Keep startup time and condition valid */
173 if (value == START_ASAP)
174 startupTime = QDateTime();
175
176 /* Refresh estimated time - which update job cells */
177 setEstimatedTime(estimatedTime);
178
179 /* Refresh dawn and dusk for startup date */
180 SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
181}
182
183void SchedulerJob::setStartupTime(const QDateTime &value, bool refreshDawnDusk)
184{
185 startupTime = value;
186
187 /* Keep startup time and condition valid */
188 if (value.isValid())
189 startupCondition = START_AT;
190 else
191 startupCondition = fileStartupCondition;
192
193 // Refresh altitude - invalid date/time is taken care of when rendering
194 altitudeAtStartup = SchedulerUtils::findAltitude(getTargetCoords(), startupTime, &settingAtStartup);
195
196 /* Refresh estimated time - which update job cells */
197 setEstimatedTime(estimatedTime);
198
199 /* propagate it to all follower jobs, but avoid unnecessary dawn/dusk refresh */
200 for (auto follower : followerJobs())
201 follower->setStartupTime(value, false);
202
203 /* Refresh dawn and dusk for startup date */
204 if (refreshDawnDusk)
205 SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
206}
207
208void SchedulerJob::setSequenceFile(const QUrl &value)
209{
210 sequenceFile = value;
211}
212
213void SchedulerJob::setFITSFile(const QUrl &value)
214{
215 fitsFile = value;
216}
217
218void SchedulerJob::setMinAltitude(const double &value)
219{
220 minAltitude = value;
221}
222
223bool SchedulerJob::hasAltitudeConstraint() const
224{
225 return hasMinAltitude() ||
226 (getEnforceArtificialHorizon() && (getHorizon() != nullptr) && getHorizon()->altitudeConstraintsExist()) ||
227 (Options::enableAltitudeLimits() &&
228 (Options::minimumAltLimit() > 0 ||
229 Options::maximumAltLimit() < 90));
230}
231
232void SchedulerJob::setMinMoonSeparation(const double &value)
233{
234 minMoonSeparation = value;
235}
236
237void SchedulerJob::setMaxMoonAltitude(const double &value)
238{
239 maxMoonAltitude = value;
240}
241
242void SchedulerJob::setEnforceWeather(bool value)
243{
244 enforceWeather = value;
245}
246
247void SchedulerJob::setStopTime(const QDateTime &value)
248{
249 stopTime = value;
250
251 // update altitude and setting at stop time
252 if (value.isValid())
253 {
254 altitudeAtStop = SchedulerUtils::findAltitude(getTargetCoords(), stopTime, &settingAtStop);
255
256 /* propagate it to all follower jobs, but avoid unnecessary dawn/dusk refresh */
257 for (auto follower : followerJobs())
258 {
259 // if the lead job completes earlier as the follower would do, set its completion time to the lead job's
260 if (follower->getStartupTime().isValid() && value.isValid()
261 && (follower->getEstimatedTime() < 0 || follower->getEstimatedTime() > getEstimatedTime()))
262 follower->setEstimatedTime(getEstimatedTime());
263 }
264 }
265}
266
267void SchedulerJob::setFinishAtTime(const QDateTime &value)
268{
269 setStopTime(QDateTime());
270
271 /* If completion time is valid, automatically switch condition to FINISH_AT */
272 if (value.isValid())
273 {
274 setCompletionCondition(FINISH_AT);
275 finishAtTime = value;
276 setEstimatedTime(-1);
277 }
278 /* If completion time is invalid, and job is looping, keep completion time undefined */
279 else if (FINISH_LOOP == completionCondition)
280 {
281 finishAtTime = QDateTime();
282 setEstimatedTime(-1);
283 }
284 /* If completion time is invalid, deduce completion from startup and duration */
285 else if (startupTime.isValid())
286 {
287 finishAtTime = startupTime.addSecs(estimatedTime);
288 }
289 /* Else just refresh estimated time - which update job cells */
290 else setEstimatedTime(estimatedTime);
291
292
293 /* Invariants */
294 Q_ASSERT_X(finishAtTime.isValid() ?
295 (FINISH_AT == completionCondition || FINISH_REPEAT == completionCondition || FINISH_SEQUENCE == completionCondition) :
296 FINISH_LOOP == completionCondition,
297 __FUNCTION__, "Valid completion time implies job is FINISH_AT/REPEAT/SEQUENCE, else job is FINISH_LOOP.");
298}
299
300void SchedulerJob::setCompletionCondition(const CompletionCondition &value)
301{
302 completionCondition = value;
303
304 // Update repeats requirement, looping jobs have none
305 switch (completionCondition)
306 {
307 case FINISH_LOOP:
308 setFinishAtTime(QDateTime());
309 /* Fall through */
310 case FINISH_AT:
311 if (0 < getRepeatsRequired())
312 setRepeatsRequired(0);
313 break;
314
315 case FINISH_SEQUENCE:
316 if (1 != getRepeatsRequired())
317 setRepeatsRequired(1);
318 break;
319
320 case FINISH_REPEAT:
321 if (0 == getRepeatsRequired())
322 setRepeatsRequired(1);
323 break;
324
325 default:
326 break;
327 }
328}
329
330void SchedulerJob::setStepPipeline(const StepPipeline &value)
331{
332 stepPipeline = value;
333}
334
335void SchedulerJob::setState(const SchedulerJobStatus &value, bool force)
336{
337 state = value;
338 stateTime = getLocalTime();
339
340 switch (state)
341 {
342 case SCHEDJOB_ERROR:
343 /* FIXME: move this to Scheduler, SchedulerJob is mostly a model */
344 lastErrorTime = getLocalTime();
345 KNotification::event(QLatin1String("EkosSchedulerJobFail"), i18n("Ekos job failed (%1)", getName()));
346 break;
347 case SCHEDJOB_INVALID:
348 case SCHEDJOB_IDLE:
349 /* If job becomes invalid or idle, automatically reset its startup characteristics, and force its duration to be reestimated */
350 setStartupCondition(fileStartupCondition);
351 setStartupTime(startAtTime);
352 setEstimatedTime(-1);
353 break;
354 case SCHEDJOB_ABORTED:
355 /* If job is aborted, automatically reset its startup characteristics */
356 lastAbortTime = getLocalTime();
357 setStartupCondition(fileStartupCondition);
358 /* setStartupTime(fileStartupTime); */
359 break;
360 default:
361 /* do nothing */
362 break;
363 }
364 // propagate it to the follower jobs
365 if (isLead())
366 setFollowerState(value, force);
367}
368
369void SchedulerJob::setFollowerState(const SchedulerJobStatus &value, bool force)
370{
371 for (auto follower : followerJobs())
372 {
373 // do not propagate the state if the job is running
374 if (follower->getState() == SCHEDJOB_BUSY && force == false)
375 continue;
376
377 switch (value)
378 {
380 case SCHEDJOB_IDLE:
381 // always forward evaluation and startup, since only the lead job is part of the scheduling
382 follower->setState(value);
383 break;
385 // if the lead job is scheduled, the follower job will be scheduled unless it is complete
386 follower->setState(follower->getCompletionCondition() == FINISH_LOOP
387 || follower->getEstimatedTime() > 0 ? value : SCHEDJOB_COMPLETE);
388 break;
389 default:
390 // do NOT forward the state, each follower job needs to be started individually
391 if (force)
392 follower->setState(value);
393 break;
394 }
395 }
396}
397
398void SchedulerJob::updateSharedFollowerAttributes()
399{
400 if (isLead())
401 for (auto follower : followerJobs())
402 {
403 follower->setStartupTime(getStartupTime(), false);
404 follower->setStartAtTime(getStartAtTime());
405 follower->setFollowerState(getState(), true);
406 }
407}
408
409
410void SchedulerJob::setSequenceCount(const int count)
411{
412 sequenceCount = count;
413}
414
415void SchedulerJob::setCompletedCount(const int count)
416{
417 completedCount = count;
418}
419
420
421void SchedulerJob::setStage(const SchedulerJobStage &value)
422{
423 stage = value;
424}
425
426void SchedulerJob::setFileStartupCondition(const StartupCondition &value)
427{
428 fileStartupCondition = value;
429}
430
431void SchedulerJob::setStartAtTime(const QDateTime &value)
432{
433 startAtTime = value;
434}
435
436void SchedulerJob::setEstimatedTime(const int64_t &value)
437{
438 /* Estimated time is generally the difference between startup and completion times:
439 * - It is fixed when startup and completion times are fixed, that is, we disregard the argument
440 * - Else mostly it pushes completion time from startup time
441 *
442 * However it cannot advance startup time when completion time is fixed because of the way jobs are scheduled.
443 * This situation requires a warning in the user interface when there is not enough time for the job to process.
444 */
445
446 /* If startup and completion times are fixed, estimated time cannot change - disregard the argument */
447 if (START_ASAP != fileStartupCondition && FINISH_AT == completionCondition)
448 {
449 estimatedTime = startupTime.secsTo(finishAtTime);
450 }
451 /* If completion time isn't fixed, estimated time adjusts completion time */
452 else if (FINISH_AT != completionCondition && FINISH_LOOP != completionCondition)
453 {
454 estimatedTime = value;
455 }
456 /* Else estimated time is simply stored as is - covers FINISH_LOOP from setCompletionTime */
457 else estimatedTime = value;
458}
459
460void SchedulerJob::setInSequenceFocus(bool value)
461{
462 inSequenceFocus = value;
463}
464
465void SchedulerJob::setEnforceTwilight(bool value)
466{
467 enforceTwilight = value;
468 SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
469}
470
471void SchedulerJob::setEnforceArtificialHorizon(bool value)
472{
473 enforceArtificialHorizon = value;
474}
475
476void SchedulerJob::setLightFramesRequired(bool value)
477{
478 lightFramesRequired = value;
479}
480
481void SchedulerJob::setCalibrationMountPark(bool value)
482{
483 m_CalibrationMountPark = value;
484}
485
486void SchedulerJob::setRepeatsRequired(const uint16_t &value)
487{
488 repeatsRequired = value;
489
490 // Update completion condition to be compatible
491 if (1 < repeatsRequired)
492 {
493 if (FINISH_REPEAT != completionCondition)
494 setCompletionCondition(FINISH_REPEAT);
495 }
496 else if (0 < repeatsRequired)
497 {
498 if (FINISH_SEQUENCE != completionCondition)
499 setCompletionCondition(FINISH_SEQUENCE);
500 }
501 else
502 {
503 if (FINISH_LOOP != completionCondition)
504 setCompletionCondition(FINISH_LOOP);
505 }
506}
507
508void SchedulerJob::setRepeatsRemaining(const uint16_t &value)
509{
510 repeatsRemaining = value;
511}
512
513void SchedulerJob::setCapturedFramesMap(const CapturedFramesMap &value)
514{
515 capturedFramesMap = value;
516}
517
518void SchedulerJob::setTargetCoords(const dms &ra, const dms &dec, double djd)
519{
520 targetCoords.setRA0(ra);
521 targetCoords.setDec0(dec);
522
523 targetCoords.apparentCoord(static_cast<long double>(J2000), djd);
524}
525
526void SchedulerJob::setPositionAngle(double value)
527{
528 m_PositionAngle = value;
529}
530
531void SchedulerJob::reset()
532{
533 state = SCHEDJOB_IDLE;
534 stage = SCHEDSTAGE_IDLE;
535 stateTime = getLocalTime();
536 lastAbortTime = QDateTime();
537 lastErrorTime = QDateTime();
538 estimatedTime = -1;
539 startupCondition = fileStartupCondition;
540 startupTime = fileStartupCondition == START_AT ? startAtTime : QDateTime();
541
542 /* Refresh dawn and dusk for startup date */
543 SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
544
545 stopTime = QDateTime();
546 stopReason.clear();
547
548 /* No change to culmination offset */
549 repeatsRemaining = repeatsRequired;
550 completedIterations = 0;
551 clearProgress();
552
553 clearCache();
554}
555
556bool SchedulerJob::decreasingAltitudeOrder(SchedulerJob const *job1, SchedulerJob const *job2, QDateTime const &when)
557{
558 bool A_is_setting = job1->settingAtStartup;
559 double const altA = when.isValid() ?
560 SchedulerUtils::findAltitude(job1->getTargetCoords(), when, &A_is_setting) :
561 job1->altitudeAtStartup;
562
563 bool B_is_setting = job2->settingAtStartup;
564 double const altB = when.isValid() ?
565 SchedulerUtils::findAltitude(job2->getTargetCoords(), when, &B_is_setting) :
566 job2->altitudeAtStartup;
567
568 // Sort with the setting target first
569 if (A_is_setting && !B_is_setting)
570 return true;
571 else if (!A_is_setting && B_is_setting)
572 return false;
573
574 // If both targets rise or set, sort by decreasing altitude, considering a setting target is prioritary
575 return (A_is_setting && B_is_setting) ? altA < altB : altB < altA;
576}
577
578bool SchedulerJob::satisfiesAltitudeConstraint(double azimuth, double altitude, QString *altitudeReason) const
579{
580 if (m_LeadJob != nullptr)
581 return m_LeadJob->satisfiesAltitudeConstraint(azimuth, altitude, altitudeReason);
582
583 // Check the mount's altitude constraints.
584 if (Options::enableAltitudeLimits() &&
585 (altitude < Options::minimumAltLimit() ||
586 altitude > Options::maximumAltLimit()))
587 {
588 if (altitudeReason != nullptr)
589 {
590 if (altitude < Options::minimumAltLimit())
591 *altitudeReason = QString("altitude %1 < mount altitude limit %2")
592 .arg(altitude, 0, 'f', 1).arg(Options::minimumAltLimit(), 0, 'f', 1);
593 else
594 *altitudeReason = QString("altitude %1 > mount altitude limit %2")
595 .arg(altitude, 0, 'f', 1).arg(Options::maximumAltLimit(), 0, 'f', 1);
596 }
597 return false;
598 }
599 // Check the global min-altitude constraint.
600 if (altitude < getMinAltitude())
601 {
602 if (altitudeReason != nullptr)
603 *altitudeReason = QString("altitude %1 < minAltitude %2").arg(altitude, 0, 'f', 1).arg(getMinAltitude(), 0, 'f', 1);
604 return false;
605 }
606 // Check the artificial horizon.
607 if (getHorizon() != nullptr && enforceArtificialHorizon)
608 return getHorizon()->isAltitudeOK(azimuth, altitude, altitudeReason);
609
610 return true;
611}
612
613bool SchedulerJob::moonConstraintsOK(QDateTime const &when, QString *reason) const
614{
615 if (moon == nullptr) return true;
616
617 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
618 KStarsDateTime ltWhen(when.isValid() ?
619 Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when :
620 getLocalTime());
621
622 // Create a sky object with the target catalog coordinates
623 SkyPoint const target = getTargetCoords();
624 SkyObject o;
625 o.setRA0(target.ra0());
626 o.setDec0(target.dec0());
627
628 // Update RA/DEC of the target for the current fraction of the day
629 KSNumbers numbers(ltWhen.djd());
630 o.updateCoordsNow(&numbers);
631
632 CachingDms LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltWhen).gst());
633 moon->updateCoords(&numbers, true, SchedulerModuleState::getGeo()->lat(), &LST, true);
634 moon->EquatorialToHorizontal(&LST, SchedulerModuleState::getGeo()->lat());
635
636 bool const separationOK = (getMinMoonSeparation() < 0 || (moon->angularDistanceTo(&o).Degrees() >= getMinMoonSeparation()));
637 bool const altitudeOK = (getMaxMoonAltitude() >= 90 || (moon->alt().Degrees() <= getMaxMoonAltitude()));
638 bool result = separationOK && altitudeOK;
639
640 // set the result string if at least one of the constraints is not met
641 if (reason != nullptr && !result)
642 {
643 if (!separationOK && !altitudeOK)
644 *reason = QString("moon separation and altitude");
645 else if (!separationOK)
646 *reason = QString("moon separation");
647 else if (!altitudeOK)
648 *reason = QString("moon altitude");
649 }
650
651 return result;
652}
653
654QDateTime SchedulerJob::calculateNextTime(QDateTime const &when, bool checkIfConstraintsAreMet, int increment,
655 QString *reason, bool runningJob, const QDateTime &until) const
656{
657 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
658
659 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
660 KStarsDateTime ltWhen(when.isValid() ?
661 Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when :
662 getLocalTime());
663
664 // Create a sky object with the target catalog coordinates
665 SkyPoint const target = getTargetCoords();
666 SkyObject o;
667 o.setRA0(target.ra0());
668 o.setDec0(target.dec0());
669
670 // Calculate the UT at the argument time
671 KStarsDateTime const ut = SchedulerModuleState::getGeo()->LTtoUT(ltWhen);
672
673 auto maxMinute = 1e8;
674 if (!runningJob && until.isValid())
675 maxMinute = when.secsTo(until) / 60;
676
677 if (maxMinute > 24 * 60)
678 maxMinute = 24 * 60;
679
680 // Within the next 24 hours, search when the job target matches the altitude and moon constraints
681 for (unsigned int minute = 0; minute < maxMinute; minute += increment)
682 {
683 KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60));
684
685 // Is this violating twilight?
686 QDateTime nextSuccess;
687 if (getEnforceTwilight() && !runsDuringAstronomicalNightTime(ltOffset, &nextSuccess))
688 {
689 if (checkIfConstraintsAreMet)
690 {
691 // Change the minute to increment-minutes before next success.
692 if (nextSuccess.isValid())
693 {
694 const int minutesToSuccess = ltOffset.secsTo(nextSuccess) / 60 - increment;
695 if (minutesToSuccess > 0)
696 minute += minutesToSuccess;
697 }
698 continue;
699 }
700 else
701 {
702 if (reason) *reason = "twilight";
703 return ltOffset;
704 }
705 }
706
707 // Update RA/DEC of the target for the current fraction of the day
708 KSNumbers numbers(ltOffset.djd());
709 o.updateCoordsNow(&numbers);
710
711 // Compute local sidereal time for the current fraction of the day, calculate altitude
712 CachingDms const LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltOffset).gst());
713 o.EquatorialToHorizontal(&LST, SchedulerModuleState::getGeo()->lat());
714 double const altitude = o.alt().Degrees();
715 double const azimuth = o.az().Degrees();
716
717 bool const altitudeOK = satisfiesAltitudeConstraint(azimuth, altitude, reason);
718 if (altitudeOK)
719 {
720 // Don't test proximity to dawn in this situation, we only cater for altitude here
721
722 // Check moon constraints (moon altitude and distance between target and moon)
723 if (!moonConstraintsOK(ltOffset, checkIfConstraintsAreMet ? nullptr : reason))
724 {
725 if (checkIfConstraintsAreMet)
726 continue;
727 else
728 return ltOffset;
729 }
730
731 if (checkIfConstraintsAreMet)
732 return ltOffset;
733 }
734 else if (!checkIfConstraintsAreMet)
735 return ltOffset;
736 }
737
738 return QDateTime();
739}
740
741bool SchedulerJob::runsDuringAstronomicalNightTime(const QDateTime &time,
742 QDateTime *nextPossibleSuccess) const
743{
744 if (m_LeadJob != nullptr)
745 return m_LeadJob->runsDuringAstronomicalNightTime(time, nextPossibleSuccess);
746
747 // We call this very frequently in the Greedy Algorithm, and the calls
748 // below are expensive. Almost all the calls are redundent (e.g. if it's not nighttime
749 // now, it's not nighttime in 10 minutes). So, cache the answer and return it if the next
750 // call is for a time between this time and the next dawn/dusk (whichever is sooner).
751
752 static QDateTime previousMinDawnDusk, previousTime;
753 static GeoLocation const *previousGeo = nullptr; // A dangling pointer, I suppose, but we never reference it.
754 static bool previousAnswer;
755 static double previousPreDawnTime = 0;
756 static QDateTime nextSuccess;
757
758 // Lock this method because of all the statics
759 static std::mutex nightTimeMutex;
760 const std::lock_guard<std::mutex> lock(nightTimeMutex);
761
762 // We likely can rely on the previous calculations.
763 if (previousTime.isValid() && previousMinDawnDusk.isValid() &&
764 time >= previousTime && time < previousMinDawnDusk &&
765 SchedulerModuleState::getGeo() == previousGeo &&
766 Options::preDawnTime() == previousPreDawnTime)
767 {
768 if (!previousAnswer && nextPossibleSuccess != nullptr)
769 *nextPossibleSuccess = nextSuccess;
770 return previousAnswer;
771 }
772 else
773 {
774 previousAnswer = runsDuringAstronomicalNightTimeInternal(time, &previousMinDawnDusk, &nextSuccess);
775 previousTime = time;
776 previousGeo = SchedulerModuleState::getGeo();
777 previousPreDawnTime = Options::preDawnTime();
778 if (!previousAnswer && nextPossibleSuccess != nullptr)
779 *nextPossibleSuccess = nextSuccess;
780 return previousAnswer;
781 }
782}
783
784
785bool SchedulerJob::runsDuringAstronomicalNightTimeInternal(const QDateTime &time, QDateTime *minDawnDusk,
786 QDateTime *nextPossibleSuccess) const
787{
788 if (m_LeadJob != nullptr)
789 return m_LeadJob->runsDuringAstronomicalNightTimeInternal(time, minDawnDusk, nextPossibleSuccess);
790
791 QDateTime t;
792 QDateTime nDawn = nextDawn, nDusk = nextDusk;
793 if (time.isValid())
794 {
795 // Can't rely on the pre-computed dawn/dusk if we're giving it an arbitary time.
796 SchedulerModuleState::calculateDawnDusk(time, nDawn, nDusk);
797 t = time;
798 }
799 else
800 {
801 t = startupTime;
802 }
803
804 // Calculate the next astronomical dawn time, adjusted with the Ekos pre-dawn offset
805 QDateTime const earlyDawn = nDawn.addSecs(-60.0 * abs(Options::preDawnTime()));
806
807 *minDawnDusk = earlyDawn < nDusk ? earlyDawn : nDusk;
808
809 // Dawn and dusk are ordered as the immediate next events following the observation time
810 // Thus if dawn comes first, the job startup time occurs during the dusk/dawn interval.
811 bool result = nDawn < nDusk && t <= earlyDawn;
812
813 // Return a hint about when it might succeed.
814 if (nextPossibleSuccess != nullptr)
815 {
816 if (result) *nextPossibleSuccess = QDateTime();
817 else *nextPossibleSuccess = nDusk;
818 }
819
820 return result;
821}
822
823void SchedulerJob::setInitialFilter(const QString &value)
824{
825 m_InitialFilter = value;
826}
827
828const QString &SchedulerJob::getInitialFilter() const
829{
830 return m_InitialFilter;
831}
832
833bool SchedulerJob::StartTimeCache::check(const QDateTime &from, const QDateTime &until,
834 QDateTime *result, QDateTime *newFrom) const
835{
836 // Look at the cached results from getNextPossibleStartTime.
837 // If the desired 'from' time is in one of them, that is, between computation.from and computation.until,
838 // then we can re-use that result (as long as the desired until time is < computation.until).
839 foreach (const StartTimeComputation &computation, startComputations)
840 {
841 if (from >= computation.from &&
842 (!computation.until.isValid() || from < computation.until) &&
843 (!computation.result.isValid() || from < computation.result))
844 {
845 if (computation.result.isValid() || until <= computation.until)
846 {
847 // We have a cached result.
848 *result = computation.result;
849 *newFrom = QDateTime();
850 return true;
851 }
852 else
853 {
854 // No cached result, but at least we can constrain the search.
855 *result = QDateTime();
856 *newFrom = computation.until;
857 return true;
858 }
859 }
860 }
861 return false;
862}
863
864void SchedulerJob::StartTimeCache::clear() const
865{
866 startComputations.clear();
867}
868
869void SchedulerJob::StartTimeCache::add(const QDateTime &from, const QDateTime &until, const QDateTime &result) const
870{
871 // Manage the cache size.
872 if (startComputations.size() > 10)
873 startComputations.clear();
874
875 // The getNextPossibleStartTime computation (which calls calculateNextTime) searches ahead at most 24 hours.
876 QDateTime endTime;
877 if (!until.isValid())
878 endTime = from.addSecs(24 * 3600);
879 else
880 {
881 QDateTime oneDay = from.addSecs(24 * 3600);
882 if (until > oneDay)
883 endTime = oneDay;
884 else
885 endTime = until;
886 }
887
888 StartTimeComputation c;
889 c.from = from;
890 c.until = endTime;
891 c.result = result;
892 startComputations.push_back(c);
893}
894
895// When can this job start? For now ignores culmination constraint.
896QDateTime SchedulerJob::getNextPossibleStartTime(const QDateTime &when, int increment, bool runningJob,
897 const QDateTime &until) const
898{
899 QDateTime ltWhen(
900 when.isValid() ? (Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when)
901 : getLocalTime());
902
903 // We do not consider job state here. It is the responsibility of the caller
904 // to filter for that, if desired.
905
906 if (!runningJob && START_AT == getFileStartupCondition())
907 {
908 int secondsFromNow = ltWhen.secsTo(getStartAtTime());
909 if (secondsFromNow < -500)
910 // We missed it.
911 return QDateTime();
912 ltWhen = secondsFromNow > 0 ? getStartAtTime() : ltWhen;
913 }
914
915 // Can't start if we're past the finish time.
916 if (getCompletionCondition() == FINISH_AT)
917 {
918 const QDateTime &t = getFinishAtTime();
919 if (t.isValid() && t < ltWhen)
920 return QDateTime(); // return an invalid time.
921 }
922
923 if (runningJob)
924 return calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
925 else
926 {
927 QDateTime result, newFrom;
928 if (startTimeCache.check(ltWhen, until, &result, &newFrom))
929 {
930 if (result.isValid() || !newFrom.isValid())
931 return result;
932 if (newFrom.isValid())
933 ltWhen = newFrom;
934 }
935 result = calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
936 result.setTimeZone(ltWhen.timeZone());
937 startTimeCache.add(ltWhen, until, result);
938 return result;
939 }
940}
941
942// When will this job end (not looking at capture plan)?
943QDateTime SchedulerJob::getNextEndTime(const QDateTime &start, int increment, QString *reason, const QDateTime &until) const
944{
945 QDateTime ltStart(
946 start.isValid() ? (Qt::UTC == start.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(start)) : start)
947 : getLocalTime());
948
949 // We do not consider job state here. It is the responsibility of the caller
950 // to filter for that, if desired.
951
952 if (START_AT == getFileStartupCondition())
953 {
954 if (getStartAtTime().secsTo(ltStart) < -120)
955 {
956 // if the file startup time is in the future, then end now.
957 // This case probably wouldn't happen in the running code.
958 if (reason) *reason = "before start-at time";
959 return QDateTime();
960 }
961 // otherwise, test from now.
962 }
963
964 // Can't start if we're past the finish time.
965 if (getCompletionCondition() == FINISH_AT)
966 {
967 const QDateTime &t = getFinishAtTime();
968 if (t.isValid() && t < ltStart)
969 {
970 if (reason) *reason = "end-at time";
971 return QDateTime(); // return an invalid time.
972 }
973 auto result = calculateNextTime(ltStart, false, increment, reason, false, until);
974 if (!result.isValid() || result.secsTo(getFinishAtTime()) < 0)
975 {
976 if (reason) *reason = "end-at time";
977 return getFinishAtTime();
978 }
979 else return result;
980 }
981
982 return calculateNextTime(ltStart, false, increment, reason, false, until);
983}
984
985namespace
986{
987
988QString progressLineLabel(CCDFrameType frameType, const QMap<SequenceJob::PropertyID, QVariant> &properties,
989 bool isDarkFlat)
990{
991 QString jobTargetName = properties[SequenceJob::SJ_TargetName].toString();
992 auto exposure = properties[SequenceJob::SJ_Exposure].toDouble();
993 QString label;
994
995 int precisionRequired = 0;
996 double fraction = exposure - fabs(exposure);
997 if (fraction > .0001)
998 {
999 precisionRequired = 1;
1000 fraction = fraction * 10;
1001 fraction = fraction - fabs(fraction);
1002 if (fraction > .0001)
1003 {
1004 precisionRequired = 2;
1005 fraction = fraction * 10;
1006 fraction = fraction - fabs(fraction);
1007 if (fraction > .0001)
1008 precisionRequired = 3;
1009 }
1010 }
1011 if (precisionRequired == 0)
1012 label += QString("%1s").arg(static_cast<int>(exposure));
1013 else
1014 label += QString("%1s").arg(exposure, 0, 'f', precisionRequired);
1015
1016 if (properties.contains(SequenceJob::SJ_Filter))
1017 {
1018 auto filterType = properties[SequenceJob::SJ_Filter].toString();
1019 if (label.size() > 0) label += " ";
1020 label += filterType;
1021 }
1022
1023 if (isDarkFlat)
1024 {
1025 if (label.size() > 0) label += " ";
1026 label += i18n("DarkFlat");
1027 }
1028 else if (frameType != FRAME_LIGHT)
1029 {
1030 if (label.size() > 0) label += " ";
1031 label += (char)frameType;
1032 }
1033
1034 return label;
1035}
1036
1037QString progressLine(const SchedulerJob::JobProgress &progress)
1038{
1039 QString label = progressLineLabel(progress.type, progress.properties, progress.isDarkFlat).append(":");
1040
1041 const double seconds = progress.numCompleted * progress.properties[SequenceJob::SJ_Exposure].toDouble();
1042 QString timeStr;
1043 if (seconds == 0)
1044 timeStr = "";
1045 else if (seconds < 60)
1046 timeStr = QString("%1 %2").arg(static_cast<int>(seconds)).arg(i18n("seconds"));
1047 else if (seconds < 60 * 60)
1048 timeStr = QString("%1 %2").arg(seconds / 60.0, 0, 'f', 1).arg(i18n("minutes"));
1049 else
1050 timeStr = QString("%1 %3").arg(seconds / 3600.0, 0, 'f', 1).arg(i18n("hours"));
1051
1052 // Hacky formatting. I tried html and html tables, but the tooltips got narrow boxes.
1053 // Would be nice to redo with proper formatting, or fixed-width font.
1054 return QString("%1\t%2 %3 %4")
1055 .arg(label, -12, ' ')
1056 .arg(progress.numCompleted, 4)
1057 .arg(i18n("images"))
1058 .arg(timeStr);
1059}
1060} // namespace
1061
1062const QString SchedulerJob::getProgressSummary() const
1063{
1064 QString summary;
1065 for (const auto &p : m_Progress)
1066 {
1067 summary.append(progressLine(p));
1068 summary.append("\n");
1069 }
1070 return summary;
1071}
1072
1073QJsonObject SchedulerJob::toJson() const
1074{
1075 bool is_setting = false;
1076 double const alt = SchedulerUtils::findAltitude(getTargetCoords(), QDateTime(), &is_setting);
1077
1078 return
1079 {
1080 {"name", name},
1081 {"pa", m_PositionAngle},
1082 {"targetRA", getTargetCoords().ra0().Hours()},
1083 {"targetDEC", getTargetCoords().dec0().Degrees()},
1084 {"state", state},
1085 {"stage", stage},
1086 {"sequenceCount", sequenceCount},
1087 {"completedCount", completedCount},
1088 {"minAltitude", minAltitude},
1089 {"minMoonSeparation", minMoonSeparation},
1090 {"maxMoonAltitude", maxMoonAltitude},
1091 {"repeatsRequired", repeatsRequired},
1092 {"repeatsRemaining", repeatsRemaining},
1093 {"inSequenceFocus", inSequenceFocus},
1094 {"startupTime", startupTime.isValid() ? startupTime.toString() : "--"},
1095 {"completionTime", finishAtTime.isValid() ? finishAtTime.toString() : "--"},
1096 {"altitude", alt},
1097 {"altitudeFormatted", m_AltitudeFormatted},
1098 {"startupFormatted", m_StartupFormatted},
1099 {"endFormatted", m_EndFormatted},
1100 {"sequence", sequenceFile.toString() },
1101 };
1102}
1103
1104} // Ekos namespace
static KNotification * event(const QString &eventId, const QString &text=QString(), const QPixmap &pixmap=QPixmap(), const NotificationFlags &flags=CloseOnTimeout, const QString &componentName=QString())
SkyMapComposite * skyComposite()
Definition kstarsdata.h:174
SkyObject * findByName(const QString &name, bool exact=true) override
Search the children of this SkyMapComposite for a SkyObject whose name matches the argument.
const CachingDms & ra0() const
Definition skypoint.h:251
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:410
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition skypoint.cpp:77
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
const dms & az() const
Definition skypoint.h:275
const dms & alt() const
Definition skypoint.h:281
const CachingDms & dec0() const
Definition skypoint.h:257
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
const double & Degrees() const
Definition dms.h:141
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
SchedulerJobStage
Running stages of a SchedulerJob.
StartupCondition
Conditions under which a SchedulerJob may start.
SchedulerJobStatus
States of a SchedulerJob.
@ 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.
QMap< QString, uint16_t > CapturedFramesMap
mapping signature --> frames count
CompletionCondition
Conditions under which a SchedulerJob may complete.
KGuiItem properties()
QString label(StandardShortcut id)
QDateTime addSecs(qint64 s) const const
bool isValid() const const
qint64 secsTo(const QDateTime &other) const const
void setTimeZone(const QTimeZone &toZone)
Qt::TimeSpec timeSpec() const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
void clear()
qsizetype size() const const
double toDouble(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:56:00 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.