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

KDE's Doxygen guidelines are available online.