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
503 if (A_is_setting && !B_is_setting)
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());
561 o.updateCoordsNow(&numbers);
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 auto maxMinute = 1e8;
591 if (!runningJob && until.isValid())
592 maxMinute = when.secsTo(until) / 60;
593
594 if (maxMinute > 24 * 60)
595 maxMinute = 24 * 60;
596
597 // Within the next 24 hours, search when the job target matches the altitude and moon constraints
598 for (unsigned int minute = 0; minute < maxMinute; minute += increment)
599 {
600 KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60));
601
602 // Is this violating twilight?
603 QDateTime nextSuccess;
604 if (getEnforceTwilight() && !runsDuringAstronomicalNightTime(ltOffset, &nextSuccess))
605 {
606 if (checkIfConstraintsAreMet)
607 {
608 // Change the minute to increment-minutes before next success.
609 if (nextSuccess.isValid())
610 {
611 const int minutesToSuccess = ltOffset.secsTo(nextSuccess) / 60 - increment;
612 if (minutesToSuccess > 0)
613 minute += minutesToSuccess;
614 }
615 continue;
616 }
617 else
618 {
619 if (reason) *reason = "twilight";
620 return ltOffset;
621 }
622 }
623
624 // Update RA/DEC of the target for the current fraction of the day
625 KSNumbers numbers(ltOffset.djd());
626 o.updateCoordsNow(&numbers);
627
628 // Compute local sidereal time for the current fraction of the day, calculate altitude
629 CachingDms const LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltOffset).gst());
630 o.EquatorialToHorizontal(&LST, SchedulerModuleState::getGeo()->lat());
631 double const altitude = o.alt().Degrees();
632 double const azimuth = o.az().Degrees();
633
634 bool const altitudeOK = satisfiesAltitudeConstraint(azimuth, altitude, reason);
635 if (altitudeOK)
636 {
637 // Don't test proximity to dawn in this situation, we only cater for altitude here
638
639 // Continue searching if Moon separation is not good enough
640 if (0 < getMinMoonSeparation() && !moonSeparationOK(ltOffset))
641 {
642 if (checkIfConstraintsAreMet)
643 continue;
644 else
645 {
646 if (reason) *reason = QString("moon separation");
647 return ltOffset;
648 }
649 }
650
651 if (checkIfConstraintsAreMet)
652 return ltOffset;
653 }
654 else if (!checkIfConstraintsAreMet)
655 return ltOffset;
656 }
657
658 return QDateTime();
659}
660
661bool SchedulerJob::runsDuringAstronomicalNightTime(const QDateTime &time,
662 QDateTime *nextPossibleSuccess) const
663{
664 // We call this very frequently in the Greedy Algorithm, and the calls
665 // below are expensive. Almost all the calls are redundent (e.g. if it's not nighttime
666 // now, it's not nighttime in 10 minutes). So, cache the answer and return it if the next
667 // call is for a time between this time and the next dawn/dusk (whichever is sooner).
668
669 static QDateTime previousMinDawnDusk, previousTime;
670 static GeoLocation const *previousGeo = nullptr; // A dangling pointer, I suppose, but we never reference it.
671 static bool previousAnswer;
672 static double previousPreDawnTime = 0;
673 static QDateTime nextSuccess;
674
675 // Lock this method because of all the statics
676 static std::mutex nightTimeMutex;
677 const std::lock_guard<std::mutex> lock(nightTimeMutex);
678
679 // We likely can rely on the previous calculations.
680 if (previousTime.isValid() && previousMinDawnDusk.isValid() &&
681 time >= previousTime && time < previousMinDawnDusk &&
682 SchedulerModuleState::getGeo() == previousGeo &&
683 Options::preDawnTime() == previousPreDawnTime)
684 {
685 if (!previousAnswer && nextPossibleSuccess != nullptr)
686 *nextPossibleSuccess = nextSuccess;
687 return previousAnswer;
688 }
689 else
690 {
691 previousAnswer = runsDuringAstronomicalNightTimeInternal(time, &previousMinDawnDusk, &nextSuccess);
692 previousTime = time;
693 previousGeo = SchedulerModuleState::getGeo();
694 previousPreDawnTime = Options::preDawnTime();
695 if (!previousAnswer && nextPossibleSuccess != nullptr)
696 *nextPossibleSuccess = nextSuccess;
697 return previousAnswer;
698 }
699}
700
701
702bool SchedulerJob::runsDuringAstronomicalNightTimeInternal(const QDateTime &time, QDateTime *minDawnDusk,
703 QDateTime *nextPossibleSuccess) const
704{
705 QDateTime t;
706 QDateTime nDawn = nextDawn, nDusk = nextDusk;
707 if (time.isValid())
708 {
709 // Can't rely on the pre-computed dawn/dusk if we're giving it an arbitary time.
710 SchedulerModuleState::calculateDawnDusk(time, nDawn, nDusk);
711 t = time;
712 }
713 else
714 {
715 t = startupTime;
716 }
717
718 // Calculate the next astronomical dawn time, adjusted with the Ekos pre-dawn offset
719 QDateTime const earlyDawn = nDawn.addSecs(-60.0 * abs(Options::preDawnTime()));
720
721 *minDawnDusk = earlyDawn < nDusk ? earlyDawn : nDusk;
722
723 // Dawn and dusk are ordered as the immediate next events following the observation time
724 // Thus if dawn comes first, the job startup time occurs during the dusk/dawn interval.
725 bool result = nDawn < nDusk && t <= earlyDawn;
726
727 // Return a hint about when it might succeed.
728 if (nextPossibleSuccess != nullptr)
729 {
730 if (result) *nextPossibleSuccess = QDateTime();
731 else *nextPossibleSuccess = nDusk;
732 }
733
734 return result;
735}
736
737void SchedulerJob::setInitialFilter(const QString &value)
738{
739 m_InitialFilter = value;
740}
741
742const QString &SchedulerJob::getInitialFilter() const
743{
744 return m_InitialFilter;
745}
746
747bool SchedulerJob::StartTimeCache::check(const QDateTime &from, const QDateTime &until,
748 QDateTime *result, QDateTime *newFrom) const
749{
750 // Look at the cached results from getNextPossibleStartTime.
751 // If the desired 'from' time is in one of them, that is, between computation.from and computation.until,
752 // then we can re-use that result (as long as the desired until time is < computation.until).
753 foreach (const StartTimeComputation &computation, startComputations)
754 {
755 if (from >= computation.from &&
756 (!computation.until.isValid() || from < computation.until) &&
757 (!computation.result.isValid() || from < computation.result))
758 {
759 if (computation.result.isValid() || until <= computation.until)
760 {
761 // We have a cached result.
762 *result = computation.result;
763 *newFrom = QDateTime();
764 return true;
765 }
766 else
767 {
768 // No cached result, but at least we can constrain the search.
769 *result = QDateTime();
770 *newFrom = computation.until;
771 return true;
772 }
773 }
774 }
775 return false;
776}
777
778void SchedulerJob::StartTimeCache::clear() const
779{
780 startComputations.clear();
781}
782
783void SchedulerJob::StartTimeCache::add(const QDateTime &from, const QDateTime &until, const QDateTime &result) const
784{
785 // Manage the cache size.
786 if (startComputations.size() > 10)
787 startComputations.clear();
788
789 // The getNextPossibleStartTime computation (which calls calculateNextTime) searches ahead at most 24 hours.
790 QDateTime endTime;
791 if (!until.isValid())
792 endTime = from.addSecs(24 * 3600);
793 else
794 {
795 QDateTime oneDay = from.addSecs(24 * 3600);
796 if (until > oneDay)
797 endTime = oneDay;
798 else
799 endTime = until;
800 }
801
802 StartTimeComputation c;
803 c.from = from;
804 c.until = endTime;
805 c.result = result;
806 startComputations.push_back(c);
807}
808
809// When can this job start? For now ignores culmination constraint.
810QDateTime SchedulerJob::getNextPossibleStartTime(const QDateTime &when, int increment, bool runningJob,
811 const QDateTime &until) const
812{
813 QDateTime ltWhen(
814 when.isValid() ? (Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when)
815 : getLocalTime());
816
817 // We do not consider job state here. It is the responsibility of the caller
818 // to filter for that, if desired.
819
820 if (!runningJob && START_AT == getFileStartupCondition())
821 {
822 int secondsFromNow = ltWhen.secsTo(getFileStartupTime());
823 if (secondsFromNow < -500)
824 // We missed it.
825 return QDateTime();
826 ltWhen = secondsFromNow > 0 ? getFileStartupTime() : ltWhen;
827 }
828
829 // Can't start if we're past the finish time.
830 if (getCompletionCondition() == FINISH_AT)
831 {
832 const QDateTime &t = getCompletionTime();
833 if (t.isValid() && t < ltWhen)
834 return QDateTime(); // return an invalid time.
835 }
836
837 if (runningJob)
838 return calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
839 else
840 {
841 QDateTime result, newFrom;
842 if (startTimeCache.check(ltWhen, until, &result, &newFrom))
843 {
844 if (result.isValid() || !newFrom.isValid())
845 return result;
846 if (newFrom.isValid())
847 ltWhen = newFrom;
848 }
849 result = calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
850 startTimeCache.add(ltWhen, until, result);
851 return result;
852 }
853}
854
855// When will this job end (not looking at capture plan)?
856QDateTime SchedulerJob::getNextEndTime(const QDateTime &start, int increment, QString *reason, const QDateTime &until) const
857{
858 QDateTime ltStart(
859 start.isValid() ? (Qt::UTC == start.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(start)) : start)
860 : getLocalTime());
861
862 // We do not consider job state here. It is the responsibility of the caller
863 // to filter for that, if desired.
864
865 if (START_AT == getFileStartupCondition())
866 {
867 if (getFileStartupTime().secsTo(ltStart) < -120)
868 {
869 // if the file startup time is in the future, then end now.
870 // This case probably wouldn't happen in the running code.
871 if (reason) *reason = "before start-at time";
872 return QDateTime();
873 }
874 // otherwise, test from now.
875 }
876
877 // Can't start if we're past the finish time.
878 if (getCompletionCondition() == FINISH_AT)
879 {
880 const QDateTime &t = getCompletionTime();
881 if (t.isValid() && t < ltStart)
882 {
883 if (reason) *reason = "end-at time";
884 return QDateTime(); // return an invalid time.
885 }
886 auto result = calculateNextTime(ltStart, false, increment, reason, false, until);
887 if (!result.isValid() || result.secsTo(getCompletionTime()) < 0)
888 {
889 if (reason) *reason = "end-at time";
890 return getCompletionTime();
891 }
892 else return result;
893 }
894
895 return calculateNextTime(ltStart, false, increment, reason, false, until);
896}
897
898namespace
899{
900
901QString progressLineLabel(CCDFrameType frameType, const QMap<SequenceJob::PropertyID, QVariant> &properties,
902 bool isDarkFlat)
903{
904 QString jobTargetName = properties[SequenceJob::SJ_TargetName].toString();
905 auto exposure = properties[SequenceJob::SJ_Exposure].toDouble();
907
908 int precisionRequired = 0;
909 double fraction = exposure - fabs(exposure);
910 if (fraction > .0001)
911 {
912 precisionRequired = 1;
913 fraction = fraction * 10;
914 fraction = fraction - fabs(fraction);
915 if (fraction > .0001)
916 {
917 precisionRequired = 2;
918 fraction = fraction * 10;
919 fraction = fraction - fabs(fraction);
920 if (fraction > .0001)
921 precisionRequired = 3;
922 }
923 }
924 if (precisionRequired == 0)
925 label += QString("%1s").arg(static_cast<int>(exposure));
926 else
927 label += QString("%1s").arg(exposure, 0, 'f', precisionRequired);
928
929 if (properties.contains(SequenceJob::SJ_Filter))
930 {
931 auto filterType = properties[SequenceJob::SJ_Filter].toString();
932 if (label.size() > 0) label += " ";
933 label += filterType;
934 }
935
936 if (isDarkFlat)
937 {
938 if (label.size() > 0) label += " ";
939 label += i18n("DarkFlat");
940 }
941 else if (frameType != FRAME_LIGHT)
942 {
943 if (label.size() > 0) label += " ";
944 label += frameType;
945 }
946
947 return label;
948}
949
950QString progressLine(const SchedulerJob::JobProgress &progress)
951{
952 QString label = progressLineLabel(progress.type, progress.properties, progress.isDarkFlat).append(":");
953
954 const double seconds = progress.numCompleted * progress.properties[SequenceJob::SJ_Exposure].toDouble();
955 QString timeStr;
956 if (seconds == 0)
957 timeStr = "";
958 else if (seconds < 60)
959 timeStr = QString("%1 %2").arg(static_cast<int>(seconds)).arg(i18n("seconds"));
960 else if (seconds < 60 * 60)
961 timeStr = QString("%1 %2").arg(seconds / 60.0, 0, 'f', 1).arg(i18n("minutes"));
962 else
963 timeStr = QString("%1 %3").arg(seconds / 3600.0, 0, 'f', 1).arg(i18n("hours"));
964
965 // Hacky formatting. I tried html and html tables, but the tooltips got narrow boxes.
966 // Would be nice to redo with proper formatting, or fixed-width font.
967 return QString("%1\t%2 %3 %4")
968 .arg(label, -12, ' ')
969 .arg(progress.numCompleted, 4)
970 .arg(i18n("images"))
971 .arg(timeStr);
972}
973} // namespace
974
975const QString SchedulerJob::getProgressSummary() const
976{
977 QString summary;
978 for (const auto &p : m_Progress)
979 {
980 summary.append(progressLine(p));
981 summary.append("\n");
982 }
983 return summary;
984}
985
986QJsonObject SchedulerJob::toJson() const
987{
988 bool is_setting = false;
989 double const alt = SchedulerUtils::findAltitude(getTargetCoords(), QDateTime(), &is_setting);
990
991 return
992 {
993 {"name", name},
994 {"pa", m_PositionAngle},
995 {"targetRA", targetCoords.ra0().Hours()},
996 {"targetDEC", targetCoords.dec0().Degrees()},
997 {"state", state},
998 {"stage", stage},
999 {"sequenceCount", sequenceCount},
1000 {"completedCount", completedCount},
1001 {"minAltitude", minAltitude},
1002 {"minMoonSeparation", minMoonSeparation},
1003 {"repeatsRequired", repeatsRequired},
1004 {"repeatsRemaining", repeatsRemaining},
1005 {"inSequenceFocus", inSequenceFocus},
1006 {"startupTime", startupTime.isValid() ? startupTime.toString() : "--"},
1007 {"completionTime", completionTime.isValid() ? completionTime.toString() : "--"},
1008 {"altitude", alt},
1009 {"sequence", sequenceFile.toString() },
1010 };
1011}
1012} // 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:382
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:79
@ 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 Jul 26 2024 11:59:51 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.