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::setEnforceWeather(bool value)
238{
239 enforceWeather = value;
240}
241
242void SchedulerJob::setStopTime(const QDateTime &value)
243{
244 stopTime = value;
245
246 // update altitude and setting at stop time
247 if (value.isValid())
248 {
249 altitudeAtStop = SchedulerUtils::findAltitude(getTargetCoords(), stopTime, &settingAtStop);
250
251 /* propagate it to all follower jobs, but avoid unnecessary dawn/dusk refresh */
252 for (auto follower : followerJobs())
253 {
254 // if the lead job completes earlier as the follower would do, set its completion time to the lead job's
255 if (follower->getStartupTime().isValid() && value.isValid()
256 && (follower->getEstimatedTime() < 0 || follower->getEstimatedTime() > getEstimatedTime()))
257 follower->setEstimatedTime(getEstimatedTime());
258 }
259 }
260}
261
262void SchedulerJob::setFinishAtTime(const QDateTime &value)
263{
264 setStopTime(QDateTime());
265
266 /* If completion time is valid, automatically switch condition to FINISH_AT */
267 if (value.isValid())
268 {
269 setCompletionCondition(FINISH_AT);
270 finishAtTime = value;
271 setEstimatedTime(-1);
272 }
273 /* If completion time is invalid, and job is looping, keep completion time undefined */
274 else if (FINISH_LOOP == completionCondition)
275 {
276 finishAtTime = QDateTime();
277 setEstimatedTime(-1);
278 }
279 /* If completion time is invalid, deduce completion from startup and duration */
280 else if (startupTime.isValid())
281 {
282 finishAtTime = startupTime.addSecs(estimatedTime);
283 }
284 /* Else just refresh estimated time - which update job cells */
285 else setEstimatedTime(estimatedTime);
286
287
288 /* Invariants */
289 Q_ASSERT_X(finishAtTime.isValid() ?
290 (FINISH_AT == completionCondition || FINISH_REPEAT == completionCondition || FINISH_SEQUENCE == completionCondition) :
291 FINISH_LOOP == completionCondition,
292 __FUNCTION__, "Valid completion time implies job is FINISH_AT/REPEAT/SEQUENCE, else job is FINISH_LOOP.");
293}
294
295void SchedulerJob::setCompletionCondition(const CompletionCondition &value)
296{
297 completionCondition = value;
298
299 // Update repeats requirement, looping jobs have none
300 switch (completionCondition)
301 {
302 case FINISH_LOOP:
303 setFinishAtTime(QDateTime());
304 /* Fall through */
305 case FINISH_AT:
306 if (0 < getRepeatsRequired())
307 setRepeatsRequired(0);
308 break;
309
310 case FINISH_SEQUENCE:
311 if (1 != getRepeatsRequired())
312 setRepeatsRequired(1);
313 break;
314
315 case FINISH_REPEAT:
316 if (0 == getRepeatsRequired())
317 setRepeatsRequired(1);
318 break;
319
320 default:
321 break;
322 }
323}
324
325void SchedulerJob::setStepPipeline(const StepPipeline &value)
326{
327 stepPipeline = value;
328}
329
330void SchedulerJob::setState(const SchedulerJobStatus &value, bool force)
331{
332 state = value;
333 stateTime = getLocalTime();
334
335 switch (state)
336 {
337 case SCHEDJOB_ERROR:
338 /* FIXME: move this to Scheduler, SchedulerJob is mostly a model */
339 lastErrorTime = getLocalTime();
340 KNotification::event(QLatin1String("EkosSchedulerJobFail"), i18n("Ekos job failed (%1)", getName()));
341 break;
342 case SCHEDJOB_INVALID:
343 case SCHEDJOB_IDLE:
344 /* If job becomes invalid or idle, automatically reset its startup characteristics, and force its duration to be reestimated */
345 setStartupCondition(fileStartupCondition);
346 setStartupTime(startAtTime);
347 setEstimatedTime(-1);
348 break;
349 case SCHEDJOB_ABORTED:
350 /* If job is aborted, automatically reset its startup characteristics */
351 lastAbortTime = getLocalTime();
352 setStartupCondition(fileStartupCondition);
353 /* setStartupTime(fileStartupTime); */
354 break;
355 default:
356 /* do nothing */
357 break;
358 }
359 // propagate it to the follower jobs
360 if (isLead())
361 setFollowerState(value, force);
362}
363
364void SchedulerJob::setFollowerState(const SchedulerJobStatus &value, bool force)
365{
366 for (auto follower : followerJobs())
367 {
368 // do not propagate the state if the job is running
369 if (follower->getState() == SCHEDJOB_BUSY && force == false)
370 continue;
371
372 switch (value)
373 {
375 case SCHEDJOB_IDLE:
376 // always forward evaluation and startup, since only the lead job is part of the scheduling
377 follower->setState(value);
378 break;
380 // if the lead job is scheduled, the follower job will be scheduled unless it is complete
381 follower->setState(follower->getCompletionCondition() == FINISH_LOOP
382 || follower->getEstimatedTime() > 0 ? value : SCHEDJOB_COMPLETE);
383 break;
384 default:
385 // do NOT forward the state, each follower job needs to be started individually
386 if (force)
387 follower->setState(value);
388 break;
389 }
390 }
391}
392
393void SchedulerJob::updateSharedFollowerAttributes()
394{
395 if (isLead())
396 for (auto follower : followerJobs())
397 {
398 follower->setStartupTime(getStartupTime(), false);
399 follower->setStartAtTime(getStartAtTime());
400 follower->setFollowerState(getState(), true);
401 }
402}
403
404
405void SchedulerJob::setSequenceCount(const int count)
406{
407 sequenceCount = count;
408}
409
410void SchedulerJob::setCompletedCount(const int count)
411{
412 completedCount = count;
413}
414
415
416void SchedulerJob::setStage(const SchedulerJobStage &value)
417{
418 stage = value;
419}
420
421void SchedulerJob::setFileStartupCondition(const StartupCondition &value)
422{
423 fileStartupCondition = value;
424}
425
426void SchedulerJob::setStartAtTime(const QDateTime &value)
427{
428 startAtTime = value;
429}
430
431void SchedulerJob::setEstimatedTime(const int64_t &value)
432{
433 /* Estimated time is generally the difference between startup and completion times:
434 * - It is fixed when startup and completion times are fixed, that is, we disregard the argument
435 * - Else mostly it pushes completion time from startup time
436 *
437 * However it cannot advance startup time when completion time is fixed because of the way jobs are scheduled.
438 * This situation requires a warning in the user interface when there is not enough time for the job to process.
439 */
440
441 /* If startup and completion times are fixed, estimated time cannot change - disregard the argument */
442 if (START_ASAP != fileStartupCondition && FINISH_AT == completionCondition)
443 {
444 estimatedTime = startupTime.secsTo(finishAtTime);
445 }
446 /* If completion time isn't fixed, estimated time adjusts completion time */
447 else if (FINISH_AT != completionCondition && FINISH_LOOP != completionCondition)
448 {
449 estimatedTime = value;
450 }
451 /* Else estimated time is simply stored as is - covers FINISH_LOOP from setCompletionTime */
452 else estimatedTime = value;
453}
454
455void SchedulerJob::setInSequenceFocus(bool value)
456{
457 inSequenceFocus = value;
458}
459
460void SchedulerJob::setEnforceTwilight(bool value)
461{
462 enforceTwilight = value;
463 SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
464}
465
466void SchedulerJob::setEnforceArtificialHorizon(bool value)
467{
468 enforceArtificialHorizon = value;
469}
470
471void SchedulerJob::setLightFramesRequired(bool value)
472{
473 lightFramesRequired = value;
474}
475
476void SchedulerJob::setCalibrationMountPark(bool value)
477{
478 m_CalibrationMountPark = value;
479}
480
481void SchedulerJob::setRepeatsRequired(const uint16_t &value)
482{
483 repeatsRequired = value;
484
485 // Update completion condition to be compatible
486 if (1 < repeatsRequired)
487 {
488 if (FINISH_REPEAT != completionCondition)
489 setCompletionCondition(FINISH_REPEAT);
490 }
491 else if (0 < repeatsRequired)
492 {
493 if (FINISH_SEQUENCE != completionCondition)
494 setCompletionCondition(FINISH_SEQUENCE);
495 }
496 else
497 {
498 if (FINISH_LOOP != completionCondition)
499 setCompletionCondition(FINISH_LOOP);
500 }
501}
502
503void SchedulerJob::setRepeatsRemaining(const uint16_t &value)
504{
505 repeatsRemaining = value;
506}
507
508void SchedulerJob::setCapturedFramesMap(const CapturedFramesMap &value)
509{
510 capturedFramesMap = value;
511}
512
513void SchedulerJob::setTargetCoords(const dms &ra, const dms &dec, double djd)
514{
515 targetCoords.setRA0(ra);
516 targetCoords.setDec0(dec);
517
518 targetCoords.apparentCoord(static_cast<long double>(J2000), djd);
519}
520
521void SchedulerJob::setPositionAngle(double value)
522{
523 m_PositionAngle = value;
524}
525
526void SchedulerJob::reset()
527{
528 state = SCHEDJOB_IDLE;
529 stage = SCHEDSTAGE_IDLE;
530 stateTime = getLocalTime();
531 lastAbortTime = QDateTime();
532 lastErrorTime = QDateTime();
533 estimatedTime = -1;
534 startupCondition = fileStartupCondition;
535 startupTime = fileStartupCondition == START_AT ? startAtTime : QDateTime();
536
537 /* Refresh dawn and dusk for startup date */
538 SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
539
540 stopTime = QDateTime();
541 stopReason.clear();
542
543 /* No change to culmination offset */
544 repeatsRemaining = repeatsRequired;
545 completedIterations = 0;
546 clearProgress();
547
548 clearCache();
549}
550
551bool SchedulerJob::decreasingAltitudeOrder(SchedulerJob const *job1, SchedulerJob const *job2, QDateTime const &when)
552{
553 bool A_is_setting = job1->settingAtStartup;
554 double const altA = when.isValid() ?
555 SchedulerUtils::findAltitude(job1->getTargetCoords(), when, &A_is_setting) :
556 job1->altitudeAtStartup;
557
558 bool B_is_setting = job2->settingAtStartup;
559 double const altB = when.isValid() ?
560 SchedulerUtils::findAltitude(job2->getTargetCoords(), when, &B_is_setting) :
561 job2->altitudeAtStartup;
562
563 // Sort with the setting target first
564 if (A_is_setting && !B_is_setting)
565 return true;
566 else if (!A_is_setting && B_is_setting)
567 return false;
568
569 // If both targets rise or set, sort by decreasing altitude, considering a setting target is prioritary
570 return (A_is_setting && B_is_setting) ? altA < altB : altB < altA;
571}
572
573bool SchedulerJob::satisfiesAltitudeConstraint(double azimuth, double altitude, QString *altitudeReason) const
574{
575 if (m_LeadJob != nullptr)
576 return m_LeadJob->satisfiesAltitudeConstraint(azimuth, altitude, altitudeReason);
577
578 // Check the mount's altitude constraints.
579 if (Options::enableAltitudeLimits() &&
580 (altitude < Options::minimumAltLimit() ||
581 altitude > Options::maximumAltLimit()))
582 {
583 if (altitudeReason != nullptr)
584 {
585 if (altitude < Options::minimumAltLimit())
586 *altitudeReason = QString("altitude %1 < mount altitude limit %2")
587 .arg(altitude, 0, 'f', 1).arg(Options::minimumAltLimit(), 0, 'f', 1);
588 else
589 *altitudeReason = QString("altitude %1 > mount altitude limit %2")
590 .arg(altitude, 0, 'f', 1).arg(Options::maximumAltLimit(), 0, 'f', 1);
591 }
592 return false;
593 }
594 // Check the global min-altitude constraint.
595 if (altitude < getMinAltitude())
596 {
597 if (altitudeReason != nullptr)
598 *altitudeReason = QString("altitude %1 < minAltitude %2").arg(altitude, 0, 'f', 1).arg(getMinAltitude(), 0, 'f', 1);
599 return false;
600 }
601 // Check the artificial horizon.
602 if (getHorizon() != nullptr && enforceArtificialHorizon)
603 return getHorizon()->isAltitudeOK(azimuth, altitude, altitudeReason);
604
605 return true;
606}
607
608bool SchedulerJob::moonSeparationOK(QDateTime const &when) const
609{
610 if (moon == nullptr) return true;
611
612 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
613 KStarsDateTime ltWhen(when.isValid() ?
614 Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when :
615 getLocalTime());
616
617 // Create a sky object with the target catalog coordinates
618 SkyPoint const target = getTargetCoords();
619 SkyObject o;
620 o.setRA0(target.ra0());
621 o.setDec0(target.dec0());
622
623 // Update RA/DEC of the target for the current fraction of the day
624 KSNumbers numbers(ltWhen.djd());
625 o.updateCoordsNow(&numbers);
626
627 CachingDms LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltWhen).gst());
628 moon->updateCoords(&numbers, true, SchedulerModuleState::getGeo()->lat(), &LST, true);
629
630 double const separation = moon->angularDistanceTo(&o).Degrees();
631
632 return (separation >= getMinMoonSeparation());
633}
634
635QDateTime SchedulerJob::calculateNextTime(QDateTime const &when, bool checkIfConstraintsAreMet, int increment,
636 QString *reason, bool runningJob, const QDateTime &until) const
637{
638 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
639
640 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
641 KStarsDateTime ltWhen(when.isValid() ?
642 Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when :
643 getLocalTime());
644
645 // Create a sky object with the target catalog coordinates
646 SkyPoint const target = getTargetCoords();
647 SkyObject o;
648 o.setRA0(target.ra0());
649 o.setDec0(target.dec0());
650
651 // Calculate the UT at the argument time
652 KStarsDateTime const ut = SchedulerModuleState::getGeo()->LTtoUT(ltWhen);
653
654 auto maxMinute = 1e8;
655 if (!runningJob && until.isValid())
656 maxMinute = when.secsTo(until) / 60;
657
658 if (maxMinute > 24 * 60)
659 maxMinute = 24 * 60;
660
661 // Within the next 24 hours, search when the job target matches the altitude and moon constraints
662 for (unsigned int minute = 0; minute < maxMinute; minute += increment)
663 {
664 KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60));
665
666 // Is this violating twilight?
667 QDateTime nextSuccess;
668 if (getEnforceTwilight() && !runsDuringAstronomicalNightTime(ltOffset, &nextSuccess))
669 {
670 if (checkIfConstraintsAreMet)
671 {
672 // Change the minute to increment-minutes before next success.
673 if (nextSuccess.isValid())
674 {
675 const int minutesToSuccess = ltOffset.secsTo(nextSuccess) / 60 - increment;
676 if (minutesToSuccess > 0)
677 minute += minutesToSuccess;
678 }
679 continue;
680 }
681 else
682 {
683 if (reason) *reason = "twilight";
684 return ltOffset;
685 }
686 }
687
688 // Update RA/DEC of the target for the current fraction of the day
689 KSNumbers numbers(ltOffset.djd());
690 o.updateCoordsNow(&numbers);
691
692 // Compute local sidereal time for the current fraction of the day, calculate altitude
693 CachingDms const LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltOffset).gst());
694 o.EquatorialToHorizontal(&LST, SchedulerModuleState::getGeo()->lat());
695 double const altitude = o.alt().Degrees();
696 double const azimuth = o.az().Degrees();
697
698 bool const altitudeOK = satisfiesAltitudeConstraint(azimuth, altitude, reason);
699 if (altitudeOK)
700 {
701 // Don't test proximity to dawn in this situation, we only cater for altitude here
702
703 // Continue searching if Moon separation is not good enough
704 if (0 < getMinMoonSeparation() && !moonSeparationOK(ltOffset))
705 {
706 if (checkIfConstraintsAreMet)
707 continue;
708 else
709 {
710 if (reason) *reason = QString("moon separation");
711 return ltOffset;
712 }
713 }
714
715 if (checkIfConstraintsAreMet)
716 return ltOffset;
717 }
718 else if (!checkIfConstraintsAreMet)
719 return ltOffset;
720 }
721
722 return QDateTime();
723}
724
725bool SchedulerJob::runsDuringAstronomicalNightTime(const QDateTime &time,
726 QDateTime *nextPossibleSuccess) const
727{
728 if (m_LeadJob != nullptr)
729 return m_LeadJob->runsDuringAstronomicalNightTime(time, nextPossibleSuccess);
730
731 // We call this very frequently in the Greedy Algorithm, and the calls
732 // below are expensive. Almost all the calls are redundent (e.g. if it's not nighttime
733 // now, it's not nighttime in 10 minutes). So, cache the answer and return it if the next
734 // call is for a time between this time and the next dawn/dusk (whichever is sooner).
735
736 static QDateTime previousMinDawnDusk, previousTime;
737 static GeoLocation const *previousGeo = nullptr; // A dangling pointer, I suppose, but we never reference it.
738 static bool previousAnswer;
739 static double previousPreDawnTime = 0;
740 static QDateTime nextSuccess;
741
742 // Lock this method because of all the statics
743 static std::mutex nightTimeMutex;
744 const std::lock_guard<std::mutex> lock(nightTimeMutex);
745
746 // We likely can rely on the previous calculations.
747 if (previousTime.isValid() && previousMinDawnDusk.isValid() &&
748 time >= previousTime && time < previousMinDawnDusk &&
749 SchedulerModuleState::getGeo() == previousGeo &&
750 Options::preDawnTime() == previousPreDawnTime)
751 {
752 if (!previousAnswer && nextPossibleSuccess != nullptr)
753 *nextPossibleSuccess = nextSuccess;
754 return previousAnswer;
755 }
756 else
757 {
758 previousAnswer = runsDuringAstronomicalNightTimeInternal(time, &previousMinDawnDusk, &nextSuccess);
759 previousTime = time;
760 previousGeo = SchedulerModuleState::getGeo();
761 previousPreDawnTime = Options::preDawnTime();
762 if (!previousAnswer && nextPossibleSuccess != nullptr)
763 *nextPossibleSuccess = nextSuccess;
764 return previousAnswer;
765 }
766}
767
768
769bool SchedulerJob::runsDuringAstronomicalNightTimeInternal(const QDateTime &time, QDateTime *minDawnDusk,
770 QDateTime *nextPossibleSuccess) const
771{
772 if (m_LeadJob != nullptr)
773 return m_LeadJob->runsDuringAstronomicalNightTimeInternal(time, minDawnDusk, nextPossibleSuccess);
774
775 QDateTime t;
776 QDateTime nDawn = nextDawn, nDusk = nextDusk;
777 if (time.isValid())
778 {
779 // Can't rely on the pre-computed dawn/dusk if we're giving it an arbitary time.
780 SchedulerModuleState::calculateDawnDusk(time, nDawn, nDusk);
781 t = time;
782 }
783 else
784 {
785 t = startupTime;
786 }
787
788 // Calculate the next astronomical dawn time, adjusted with the Ekos pre-dawn offset
789 QDateTime const earlyDawn = nDawn.addSecs(-60.0 * abs(Options::preDawnTime()));
790
791 *minDawnDusk = earlyDawn < nDusk ? earlyDawn : nDusk;
792
793 // Dawn and dusk are ordered as the immediate next events following the observation time
794 // Thus if dawn comes first, the job startup time occurs during the dusk/dawn interval.
795 bool result = nDawn < nDusk && t <= earlyDawn;
796
797 // Return a hint about when it might succeed.
798 if (nextPossibleSuccess != nullptr)
799 {
800 if (result) *nextPossibleSuccess = QDateTime();
801 else *nextPossibleSuccess = nDusk;
802 }
803
804 return result;
805}
806
807void SchedulerJob::setInitialFilter(const QString &value)
808{
809 m_InitialFilter = value;
810}
811
812const QString &SchedulerJob::getInitialFilter() const
813{
814 return m_InitialFilter;
815}
816
817bool SchedulerJob::StartTimeCache::check(const QDateTime &from, const QDateTime &until,
818 QDateTime *result, QDateTime *newFrom) const
819{
820 // Look at the cached results from getNextPossibleStartTime.
821 // If the desired 'from' time is in one of them, that is, between computation.from and computation.until,
822 // then we can re-use that result (as long as the desired until time is < computation.until).
823 foreach (const StartTimeComputation &computation, startComputations)
824 {
825 if (from >= computation.from &&
826 (!computation.until.isValid() || from < computation.until) &&
827 (!computation.result.isValid() || from < computation.result))
828 {
829 if (computation.result.isValid() || until <= computation.until)
830 {
831 // We have a cached result.
832 *result = computation.result;
833 *newFrom = QDateTime();
834 return true;
835 }
836 else
837 {
838 // No cached result, but at least we can constrain the search.
839 *result = QDateTime();
840 *newFrom = computation.until;
841 return true;
842 }
843 }
844 }
845 return false;
846}
847
848void SchedulerJob::StartTimeCache::clear() const
849{
850 startComputations.clear();
851}
852
853void SchedulerJob::StartTimeCache::add(const QDateTime &from, const QDateTime &until, const QDateTime &result) const
854{
855 // Manage the cache size.
856 if (startComputations.size() > 10)
857 startComputations.clear();
858
859 // The getNextPossibleStartTime computation (which calls calculateNextTime) searches ahead at most 24 hours.
860 QDateTime endTime;
861 if (!until.isValid())
862 endTime = from.addSecs(24 * 3600);
863 else
864 {
865 QDateTime oneDay = from.addSecs(24 * 3600);
866 if (until > oneDay)
867 endTime = oneDay;
868 else
869 endTime = until;
870 }
871
872 StartTimeComputation c;
873 c.from = from;
874 c.until = endTime;
875 c.result = result;
876 startComputations.push_back(c);
877}
878
879// When can this job start? For now ignores culmination constraint.
880QDateTime SchedulerJob::getNextPossibleStartTime(const QDateTime &when, int increment, bool runningJob,
881 const QDateTime &until) const
882{
883 QDateTime ltWhen(
884 when.isValid() ? (Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when)
885 : getLocalTime());
886
887 // We do not consider job state here. It is the responsibility of the caller
888 // to filter for that, if desired.
889
890 if (!runningJob && START_AT == getFileStartupCondition())
891 {
892 int secondsFromNow = ltWhen.secsTo(getStartAtTime());
893 if (secondsFromNow < -500)
894 // We missed it.
895 return QDateTime();
896 ltWhen = secondsFromNow > 0 ? getStartAtTime() : ltWhen;
897 }
898
899 // Can't start if we're past the finish time.
900 if (getCompletionCondition() == FINISH_AT)
901 {
902 const QDateTime &t = getFinishAtTime();
903 if (t.isValid() && t < ltWhen)
904 return QDateTime(); // return an invalid time.
905 }
906
907 if (runningJob)
908 return calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
909 else
910 {
911 QDateTime result, newFrom;
912 if (startTimeCache.check(ltWhen, until, &result, &newFrom))
913 {
914 if (result.isValid() || !newFrom.isValid())
915 return result;
916 if (newFrom.isValid())
917 ltWhen = newFrom;
918 }
919 result = calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
920 startTimeCache.add(ltWhen, until, result);
921 return result;
922 }
923}
924
925// When will this job end (not looking at capture plan)?
926QDateTime SchedulerJob::getNextEndTime(const QDateTime &start, int increment, QString *reason, const QDateTime &until) const
927{
928 QDateTime ltStart(
929 start.isValid() ? (Qt::UTC == start.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(start)) : start)
930 : getLocalTime());
931
932 // We do not consider job state here. It is the responsibility of the caller
933 // to filter for that, if desired.
934
935 if (START_AT == getFileStartupCondition())
936 {
937 if (getStartAtTime().secsTo(ltStart) < -120)
938 {
939 // if the file startup time is in the future, then end now.
940 // This case probably wouldn't happen in the running code.
941 if (reason) *reason = "before start-at time";
942 return QDateTime();
943 }
944 // otherwise, test from now.
945 }
946
947 // Can't start if we're past the finish time.
948 if (getCompletionCondition() == FINISH_AT)
949 {
950 const QDateTime &t = getFinishAtTime();
951 if (t.isValid() && t < ltStart)
952 {
953 if (reason) *reason = "end-at time";
954 return QDateTime(); // return an invalid time.
955 }
956 auto result = calculateNextTime(ltStart, false, increment, reason, false, until);
957 if (!result.isValid() || result.secsTo(getFinishAtTime()) < 0)
958 {
959 if (reason) *reason = "end-at time";
960 return getFinishAtTime();
961 }
962 else return result;
963 }
964
965 return calculateNextTime(ltStart, false, increment, reason, false, until);
966}
967
968namespace
969{
970
971QString progressLineLabel(CCDFrameType frameType, const QMap<SequenceJob::PropertyID, QVariant> &properties,
972 bool isDarkFlat)
973{
974 QString jobTargetName = properties[SequenceJob::SJ_TargetName].toString();
975 auto exposure = properties[SequenceJob::SJ_Exposure].toDouble();
977
978 int precisionRequired = 0;
979 double fraction = exposure - fabs(exposure);
980 if (fraction > .0001)
981 {
982 precisionRequired = 1;
983 fraction = fraction * 10;
984 fraction = fraction - fabs(fraction);
985 if (fraction > .0001)
986 {
987 precisionRequired = 2;
988 fraction = fraction * 10;
989 fraction = fraction - fabs(fraction);
990 if (fraction > .0001)
991 precisionRequired = 3;
992 }
993 }
994 if (precisionRequired == 0)
995 label += QString("%1s").arg(static_cast<int>(exposure));
996 else
997 label += QString("%1s").arg(exposure, 0, 'f', precisionRequired);
998
999 if (properties.contains(SequenceJob::SJ_Filter))
1000 {
1001 auto filterType = properties[SequenceJob::SJ_Filter].toString();
1002 if (label.size() > 0) label += " ";
1003 label += filterType;
1004 }
1005
1006 if (isDarkFlat)
1007 {
1008 if (label.size() > 0) label += " ";
1009 label += i18n("DarkFlat");
1010 }
1011 else if (frameType != FRAME_LIGHT)
1012 {
1013 if (label.size() > 0) label += " ";
1014 label += (char)frameType;
1015 }
1016
1017 return label;
1018}
1019
1020QString progressLine(const SchedulerJob::JobProgress &progress)
1021{
1022 QString label = progressLineLabel(progress.type, progress.properties, progress.isDarkFlat).append(":");
1023
1024 const double seconds = progress.numCompleted * progress.properties[SequenceJob::SJ_Exposure].toDouble();
1025 QString timeStr;
1026 if (seconds == 0)
1027 timeStr = "";
1028 else if (seconds < 60)
1029 timeStr = QString("%1 %2").arg(static_cast<int>(seconds)).arg(i18n("seconds"));
1030 else if (seconds < 60 * 60)
1031 timeStr = QString("%1 %2").arg(seconds / 60.0, 0, 'f', 1).arg(i18n("minutes"));
1032 else
1033 timeStr = QString("%1 %3").arg(seconds / 3600.0, 0, 'f', 1).arg(i18n("hours"));
1034
1035 // Hacky formatting. I tried html and html tables, but the tooltips got narrow boxes.
1036 // Would be nice to redo with proper formatting, or fixed-width font.
1037 return QString("%1\t%2 %3 %4")
1038 .arg(label, -12, ' ')
1039 .arg(progress.numCompleted, 4)
1040 .arg(i18n("images"))
1041 .arg(timeStr);
1042}
1043} // namespace
1044
1045const QString SchedulerJob::getProgressSummary() const
1046{
1047 QString summary;
1048 for (const auto &p : m_Progress)
1049 {
1050 summary.append(progressLine(p));
1051 summary.append("\n");
1052 }
1053 return summary;
1054}
1055
1056QJsonObject SchedulerJob::toJson() const
1057{
1058 bool is_setting = false;
1059 double const alt = SchedulerUtils::findAltitude(getTargetCoords(), QDateTime(), &is_setting);
1060
1061 return
1062 {
1063 {"name", name},
1064 {"pa", m_PositionAngle},
1065 {"targetRA", getTargetCoords().ra0().Hours()},
1066 {"targetDEC", getTargetCoords().dec0().Degrees()},
1067 {"state", state},
1068 {"stage", stage},
1069 {"sequenceCount", sequenceCount},
1070 {"completedCount", completedCount},
1071 {"minAltitude", minAltitude},
1072 {"minMoonSeparation", minMoonSeparation},
1073 {"repeatsRequired", repeatsRequired},
1074 {"repeatsRemaining", repeatsRemaining},
1075 {"inSequenceFocus", inSequenceFocus},
1076 {"startupTime", startupTime.isValid() ? startupTime.toString() : "--"},
1077 {"completionTime", finishAtTime.isValid() ? finishAtTime.toString() : "--"},
1078 {"altitude", alt},
1079 {"altitudeFormatted", m_AltitudeFormatted},
1080 {"startupFormatted", m_StartupFormatted},
1081 {"endFormatted", m_EndFormatted},
1082 {"sequence", sequenceFile.toString() },
1083 };
1084}
1085
1086} // Ekos namespace
Represents custom area from the horizon upwards which represent blocked views from the vantage point ...
a dms subclass that caches its sine and cosine values every time the angle is changed.
Definition cachingdms.h:19
Contains all relevant information for specifying a location on Earth: City Name, State/Province name,...
Definition geolocation.h:28
static KNotification * event(const QString &eventId, const QString &text=QString(), const QPixmap &pixmap=QPixmap(), const NotificationFlags &flags=CloseOnTimeout, const QString &componentName=QString())
Provides necessary information about the Moon.
Definition ksmoon.h:26
There are several time-dependent values used in position calculations, that are not specific to an ob...
Definition ksnumbers.h:43
void updateCoords(const KSNumbers *num, bool includePlanets=true, const CachingDms *lat=nullptr, const CachingDms *LST=nullptr, bool forceRecompute=false) override
Update position of the planet (reimplemented from SkyPoint)
SkyMapComposite * skyComposite()
Definition kstarsdata.h:166
Extension of QDateTime for KStars KStarsDateTime can represent the date/time as a Julian Day,...
SkyObject * findByName(const QString &name, bool exact=true) override
Search the children of this SkyMapComposite for a SkyObject whose name matches the argument.
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:42
The sky coordinates of a point in the sky.
Definition skypoint.h:45
void apparentCoord(long double jd0, long double jdf)
Computes the apparent coordinates for this SkyPoint for any epoch, accounting for the effects of prec...
Definition skypoint.cpp:700
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:391
dms angularDistanceTo(const SkyPoint *sp, double *const positionAngle=nullptr) const
Computes the angular distance between two SkyObjects.
Definition skypoint.cpp:899
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
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
double Hours() const
Definition dms.h:168
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
@ 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
KGuiItem properties()
QString label(StandardShortcut id)
QDateTime addSecs(qint64 s) const const
bool isValid() const const
qint64 secsTo(const QDateTime &other) const const
Qt::TimeSpec timeSpec() const const
QString toString(QStringView format, QCalendar cal) const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
void clear()
qsizetype size() const const
double toDouble(bool *ok) const const
QString toString(FormattingOptions options) const const
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.