File indexing completed on 2024-04-28 03:43:45

0001 /*  Ekos Scheduler Job
0002     SPDX-FileCopyrightText: Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "schedulerjob.h"
0008 
0009 #include "dms.h"
0010 #include "artificialhorizoncomponent.h"
0011 #include "kstarsdata.h"
0012 #include "skymapcomposite.h"
0013 #include "Options.h"
0014 #include "scheduler.h"
0015 #include "schedulermodulestate.h"
0016 #include "schedulerutils.h"
0017 #include "ksalmanac.h"
0018 #include "ksmoon.h"
0019 
0020 #include <knotification.h>
0021 
0022 #include <ekos_scheduler_debug.h>
0023 
0024 #define BAD_SCORE -1000
0025 #define MIN_ALTITUDE 15.0
0026 
0027 namespace Ekos
0028 {
0029 GeoLocation *SchedulerJob::storedGeo = nullptr;
0030 KStarsDateTime *SchedulerJob::storedLocalTime = nullptr;
0031 ArtificialHorizon *SchedulerJob::storedHorizon = nullptr;
0032 
0033 QString SchedulerJob::jobStatusString(SchedulerJobStatus state)
0034 {
0035     switch(state)
0036     {
0037         case SCHEDJOB_IDLE:
0038             return "IDLE";
0039         case SCHEDJOB_EVALUATION:
0040             return "EVAL";
0041         case SCHEDJOB_SCHEDULED:
0042             return "SCHEDULED";
0043         case SCHEDJOB_BUSY:
0044             return "BUSY";
0045         case SCHEDJOB_ERROR:
0046             return "ERROR";
0047         case SCHEDJOB_ABORTED:
0048             return "ABORTED";
0049         case SCHEDJOB_INVALID:
0050             return "INVALID";
0051         case SCHEDJOB_COMPLETE:
0052             return "COMPLETE";
0053     }
0054     return QString("????");
0055 }
0056 
0057 QString SchedulerJob::jobStageString(SchedulerJobStage state)
0058 {
0059     switch(state)
0060     {
0061         case SCHEDSTAGE_IDLE:
0062             return "IDLE";
0063         case SCHEDSTAGE_SLEWING:
0064             return "SLEWING";
0065         case SCHEDSTAGE_SLEW_COMPLETE:
0066             return "SLEW_COMPLETE";
0067         case SCHEDSTAGE_FOCUSING:
0068             return "FOCUSING";
0069         case SCHEDSTAGE_FOCUS_COMPLETE:
0070             return "FOCUS_COMPLETE";
0071         case SCHEDSTAGE_ALIGNING:
0072             return "ALIGNING";
0073         case SCHEDSTAGE_ALIGN_COMPLETE:
0074             return "ALIGN_COMPLETE";
0075         case SCHEDSTAGE_RESLEWING:
0076             return "RESLEWING";
0077         case SCHEDSTAGE_RESLEWING_COMPLETE:
0078             return "RESLEWING_COMPLETE";
0079         case SCHEDSTAGE_POSTALIGN_FOCUSING:
0080             return "POSTALIGN_FOCUSING";
0081         case SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE:
0082             return "POSTALIGN_FOCUSING_COMPLETE";
0083         case SCHEDSTAGE_GUIDING:
0084             return "GUIDING";
0085         case SCHEDSTAGE_GUIDING_COMPLETE:
0086             return "GUIDING_COMPLETE";
0087         case SCHEDSTAGE_CAPTURING:
0088             return "CAPTURING";
0089         case SCHEDSTAGE_COMPLETE:
0090             return "COMPLETE";
0091     }
0092     return QString("????");
0093 }
0094 
0095 QString SchedulerJob::jobStartupConditionString(StartupCondition condition) const
0096 {
0097     switch(condition)
0098     {
0099         case START_ASAP:
0100             return "ASAP";
0101         case START_AT:
0102             return QString("AT %1").arg(getFileStartupTime().toString("MM/dd hh:mm"));
0103     }
0104     return QString("????");
0105 }
0106 
0107 QString SchedulerJob::jobCompletionConditionString(CompletionCondition condition) const
0108 {
0109     switch(condition)
0110     {
0111         case FINISH_SEQUENCE:
0112             return "FINISH";
0113         case FINISH_REPEAT:
0114             return "REPEAT";
0115         case FINISH_LOOP:
0116             return "LOOP";
0117         case FINISH_AT:
0118             return QString("AT %1").arg(getCompletionTime().toString("MM/dd hh:mm"));
0119     }
0120     return QString("????");
0121 }
0122 
0123 SchedulerJob::SchedulerJob()
0124 {
0125     if (KStarsData::Instance() != nullptr)
0126         moon = dynamic_cast<KSMoon *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Moon")));
0127 }
0128 
0129 // Private constructor for unit testing.
0130 SchedulerJob::SchedulerJob(KSMoon *moonPtr)
0131 {
0132     moon = moonPtr;
0133 }
0134 
0135 void SchedulerJob::setName(const QString &value)
0136 {
0137     name = value;
0138 }
0139 
0140 void SchedulerJob::setGroup(const QString &value)
0141 {
0142     group = value;
0143 }
0144 
0145 void SchedulerJob::setCompletedIterations(int value)
0146 {
0147     completedIterations = value;
0148     if (completionCondition == FINISH_REPEAT)
0149         setRepeatsRemaining(getRepeatsRequired() - completedIterations);
0150 }
0151 
0152 KStarsDateTime SchedulerJob::getLocalTime()
0153 {
0154     return Ekos::SchedulerModuleState::getLocalTime();
0155 }
0156 
0157 ArtificialHorizon const *SchedulerJob::getHorizon()
0158 {
0159     if (hasHorizon())
0160         return storedHorizon;
0161     if (KStarsData::Instance() == nullptr || KStarsData::Instance()->skyComposite() == nullptr
0162             || KStarsData::Instance()->skyComposite()->artificialHorizon() == nullptr)
0163         return nullptr;
0164     return &KStarsData::Instance()->skyComposite()->artificialHorizon()->getHorizon();
0165 }
0166 
0167 void SchedulerJob::setStartupCondition(const StartupCondition &value)
0168 {
0169     startupCondition = value;
0170 
0171     /* Keep startup time and condition valid */
0172     if (value == START_ASAP)
0173         startupTime = QDateTime();
0174 
0175     /* Refresh estimated time - which update job cells */
0176     setEstimatedTime(estimatedTime);
0177 
0178     /* Refresh dawn and dusk for startup date */
0179     SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
0180 }
0181 
0182 void SchedulerJob::setStartupTime(const QDateTime &value)
0183 {
0184     startupTime = value;
0185 
0186     /* Keep startup time and condition valid */
0187     if (value.isValid())
0188         startupCondition = START_AT;
0189     else
0190         startupCondition = fileStartupCondition;
0191 
0192     // Refresh altitude - invalid date/time is taken care of when rendering
0193     altitudeAtStartup = SchedulerUtils::findAltitude(targetCoords, startupTime, &settingAtStartup);
0194 
0195     /* Refresh estimated time - which update job cells */
0196     setEstimatedTime(estimatedTime);
0197 
0198     /* Refresh dawn and dusk for startup date */
0199     SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
0200 }
0201 
0202 void SchedulerJob::setSequenceFile(const QUrl &value)
0203 {
0204     sequenceFile = value;
0205 }
0206 
0207 void SchedulerJob::setFITSFile(const QUrl &value)
0208 {
0209     fitsFile = value;
0210 }
0211 
0212 void SchedulerJob::setMinAltitude(const double &value)
0213 {
0214     minAltitude = value;
0215 }
0216 
0217 bool SchedulerJob::hasAltitudeConstraint() const
0218 {
0219     return hasMinAltitude() ||
0220            (enforceArtificialHorizon && (getHorizon() != nullptr) && getHorizon()->altitudeConstraintsExist()) ||
0221            (Options::enableAltitudeLimits() &&
0222             (Options::minimumAltLimit() > 0 ||
0223              Options::maximumAltLimit() < 90));
0224 }
0225 
0226 void SchedulerJob::setMinMoonSeparation(const double &value)
0227 {
0228     minMoonSeparation = value;
0229 }
0230 
0231 void SchedulerJob::setEnforceWeather(bool value)
0232 {
0233     enforceWeather = value;
0234 }
0235 
0236 void SchedulerJob::setGreedyCompletionTime(const QDateTime &value)
0237 {
0238     greedyCompletionTime = value;
0239 }
0240 
0241 void SchedulerJob::setCompletionTime(const QDateTime &value)
0242 {
0243     setGreedyCompletionTime(QDateTime());
0244 
0245     /* If completion time is valid, automatically switch condition to FINISH_AT */
0246     if (value.isValid())
0247     {
0248         setCompletionCondition(FINISH_AT);
0249         completionTime = value;
0250         altitudeAtCompletion = SchedulerUtils::findAltitude(targetCoords, completionTime, &settingAtCompletion);
0251         setEstimatedTime(-1);
0252     }
0253     /* If completion time is invalid, and job is looping, keep completion time undefined */
0254     else if (FINISH_LOOP == completionCondition)
0255     {
0256         completionTime = QDateTime();
0257         altitudeAtCompletion = SchedulerUtils::findAltitude(targetCoords, completionTime, &settingAtCompletion);
0258         setEstimatedTime(-1);
0259     }
0260     /* If completion time is invalid, deduce completion from startup and duration */
0261     else if (startupTime.isValid())
0262     {
0263         completionTime = startupTime.addSecs(estimatedTime);
0264         altitudeAtCompletion = SchedulerUtils::findAltitude(targetCoords, completionTime, &settingAtCompletion);
0265     }
0266     /* Else just refresh estimated time - which update job cells */
0267     else setEstimatedTime(estimatedTime);
0268 
0269 
0270     /* Invariants */
0271     Q_ASSERT_X(completionTime.isValid() ?
0272                (FINISH_AT == completionCondition || FINISH_REPEAT == completionCondition || FINISH_SEQUENCE == completionCondition) :
0273                FINISH_LOOP == completionCondition,
0274                __FUNCTION__, "Valid completion time implies job is FINISH_AT/REPEAT/SEQUENCE, else job is FINISH_LOOP.");
0275 }
0276 
0277 void SchedulerJob::setCompletionCondition(const CompletionCondition &value)
0278 {
0279     completionCondition = value;
0280 
0281     // Update repeats requirement, looping jobs have none
0282     switch (completionCondition)
0283     {
0284         case FINISH_LOOP:
0285             setCompletionTime(QDateTime());
0286         /* Fall through */
0287         case FINISH_AT:
0288             if (0 < getRepeatsRequired())
0289                 setRepeatsRequired(0);
0290             break;
0291 
0292         case FINISH_SEQUENCE:
0293             if (1 != getRepeatsRequired())
0294                 setRepeatsRequired(1);
0295             break;
0296 
0297         case FINISH_REPEAT:
0298             if (0 == getRepeatsRequired())
0299                 setRepeatsRequired(1);
0300             break;
0301 
0302         default:
0303             break;
0304     }
0305 }
0306 
0307 void SchedulerJob::setStepPipeline(const StepPipeline &value)
0308 {
0309     stepPipeline = value;
0310 }
0311 
0312 void SchedulerJob::setState(const SchedulerJobStatus &value)
0313 {
0314     state = value;
0315     stateTime = getLocalTime();
0316 
0317     /* FIXME: move this to Scheduler, SchedulerJob is mostly a model */
0318     if (SCHEDJOB_ERROR == state)
0319     {
0320         lastErrorTime = getLocalTime();
0321         KNotification::event(QLatin1String("EkosSchedulerJobFail"), i18n("Ekos job failed (%1)", getName()));
0322     }
0323 
0324     /* If job becomes invalid or idle, automatically reset its startup characteristics, and force its duration to be reestimated */
0325     if (SCHEDJOB_INVALID == value || SCHEDJOB_IDLE == value)
0326     {
0327         setStartupCondition(fileStartupCondition);
0328         setStartupTime(fileStartupTime);
0329         setEstimatedTime(-1);
0330     }
0331 
0332     /* If job is aborted, automatically reset its startup characteristics */
0333     if (SCHEDJOB_ABORTED == value)
0334     {
0335         lastAbortTime = getLocalTime();
0336         setStartupCondition(fileStartupCondition);
0337         /* setStartupTime(fileStartupTime); */
0338     }
0339 }
0340 
0341 
0342 void SchedulerJob::setSequenceCount(const int count)
0343 {
0344     sequenceCount = count;
0345 }
0346 
0347 void SchedulerJob::setCompletedCount(const int count)
0348 {
0349     completedCount = count;
0350 }
0351 
0352 
0353 void SchedulerJob::setStage(const SchedulerJobStage &value)
0354 {
0355     stage = value;
0356 }
0357 
0358 void SchedulerJob::setFileStartupCondition(const StartupCondition &value)
0359 {
0360     fileStartupCondition = value;
0361 }
0362 
0363 void SchedulerJob::setFileStartupTime(const QDateTime &value)
0364 {
0365     fileStartupTime = value;
0366 }
0367 
0368 void SchedulerJob::setEstimatedTime(const int64_t &value)
0369 {
0370     /* Estimated time is generally the difference between startup and completion times:
0371      * - It is fixed when startup and completion times are fixed, that is, we disregard the argument
0372      * - Else mostly it pushes completion time from startup time
0373      *
0374      * However it cannot advance startup time when completion time is fixed because of the way jobs are scheduled.
0375      * This situation requires a warning in the user interface when there is not enough time for the job to process.
0376      */
0377 
0378     /* If startup and completion times are fixed, estimated time cannot change - disregard the argument */
0379     if (START_ASAP != fileStartupCondition && FINISH_AT == completionCondition)
0380     {
0381         estimatedTime = startupTime.secsTo(completionTime);
0382     }
0383     /* If completion time isn't fixed, estimated time adjusts completion time */
0384     else if (FINISH_AT != completionCondition && FINISH_LOOP != completionCondition)
0385     {
0386         estimatedTime = value;
0387         completionTime = startupTime.addSecs(value);
0388         altitudeAtCompletion = SchedulerUtils::findAltitude(targetCoords, completionTime, &settingAtCompletion);
0389     }
0390     /* Else estimated time is simply stored as is - covers FINISH_LOOP from setCompletionTime */
0391     else estimatedTime = value;
0392 }
0393 
0394 void SchedulerJob::setInSequenceFocus(bool value)
0395 {
0396     inSequenceFocus = value;
0397 }
0398 
0399 void SchedulerJob::setEnforceTwilight(bool value)
0400 {
0401     enforceTwilight = value;
0402     SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
0403 }
0404 
0405 void SchedulerJob::setEnforceArtificialHorizon(bool value)
0406 {
0407     enforceArtificialHorizon = value;
0408 }
0409 
0410 void SchedulerJob::setLightFramesRequired(bool value)
0411 {
0412     lightFramesRequired = value;
0413 }
0414 
0415 void SchedulerJob::setRepeatsRequired(const uint16_t &value)
0416 {
0417     repeatsRequired = value;
0418 
0419     // Update completion condition to be compatible
0420     if (1 < repeatsRequired)
0421     {
0422         if (FINISH_REPEAT != completionCondition)
0423             setCompletionCondition(FINISH_REPEAT);
0424     }
0425     else if (0 < repeatsRequired)
0426     {
0427         if (FINISH_SEQUENCE != completionCondition)
0428             setCompletionCondition(FINISH_SEQUENCE);
0429     }
0430     else
0431     {
0432         if (FINISH_LOOP != completionCondition)
0433             setCompletionCondition(FINISH_LOOP);
0434     }
0435 }
0436 
0437 void SchedulerJob::setRepeatsRemaining(const uint16_t &value)
0438 {
0439     repeatsRemaining = value;
0440 }
0441 
0442 void SchedulerJob::setCapturedFramesMap(const CapturedFramesMap &value)
0443 {
0444     capturedFramesMap = value;
0445 }
0446 
0447 void SchedulerJob::setTargetCoords(const dms &ra, const dms &dec, double djd)
0448 {
0449     targetCoords.setRA0(ra);
0450     targetCoords.setDec0(dec);
0451 
0452     targetCoords.apparentCoord(static_cast<long double>(J2000), djd);
0453 }
0454 
0455 void SchedulerJob::setPositionAngle(double value)
0456 {
0457     m_PositionAngle = value;
0458 }
0459 
0460 void SchedulerJob::reset()
0461 {
0462     state = SCHEDJOB_IDLE;
0463     stage = SCHEDSTAGE_IDLE;
0464     stateTime = getLocalTime();
0465     lastAbortTime = QDateTime();
0466     lastErrorTime = QDateTime();
0467     estimatedTime = -1;
0468     startupCondition = fileStartupCondition;
0469     startupTime = fileStartupCondition == START_AT ? fileStartupTime : QDateTime();
0470 
0471     /* Refresh dawn and dusk for startup date */
0472     SchedulerModuleState::calculateDawnDusk(startupTime, nextDawn, nextDusk);
0473 
0474     greedyCompletionTime = QDateTime();
0475     stopReason.clear();
0476 
0477     /* No change to culmination offset */
0478     repeatsRemaining = repeatsRequired;
0479     completedIterations = 0;
0480     clearCache();
0481 }
0482 
0483 bool SchedulerJob::decreasingAltitudeOrder(SchedulerJob const *job1, SchedulerJob const *job2, QDateTime const &when)
0484 {
0485     bool A_is_setting = job1->settingAtStartup;
0486     double const altA = when.isValid() ?
0487                         SchedulerUtils::findAltitude(job1->getTargetCoords(), when, &A_is_setting) :
0488                         job1->altitudeAtStartup;
0489 
0490     bool B_is_setting = job2->settingAtStartup;
0491     double const altB = when.isValid() ?
0492                         SchedulerUtils::findAltitude(job2->getTargetCoords(), when, &B_is_setting) :
0493                         job2->altitudeAtStartup;
0494 
0495     // Sort with the setting target first
0496     if (A_is_setting && !B_is_setting)
0497         return true;
0498     else if (!A_is_setting && B_is_setting)
0499         return false;
0500 
0501     // If both targets rise or set, sort by decreasing altitude, considering a setting target is prioritary
0502     return (A_is_setting && B_is_setting) ? altA < altB : altB < altA;
0503 }
0504 
0505 bool SchedulerJob::satisfiesAltitudeConstraint(double azimuth, double altitude, QString *altitudeReason) const
0506 {
0507     // Check the mount's altitude constraints.
0508     if (Options::enableAltitudeLimits() &&
0509             (altitude < Options::minimumAltLimit() ||
0510              altitude > Options::maximumAltLimit()))
0511     {
0512         if (altitudeReason != nullptr)
0513         {
0514             if (altitude < Options::minimumAltLimit())
0515                 *altitudeReason = QString("altitude %1 < mount altitude limit %2")
0516                                   .arg(altitude, 0, 'f', 1).arg(Options::minimumAltLimit(), 0, 'f', 1);
0517             else
0518                 *altitudeReason = QString("altitude %1 > mount altitude limit %2")
0519                                   .arg(altitude, 0, 'f', 1).arg(Options::maximumAltLimit(), 0, 'f', 1);
0520         }
0521         return false;
0522     }
0523     // Check the global min-altitude constraint.
0524     if (altitude < getMinAltitude())
0525     {
0526         if (altitudeReason != nullptr)
0527             *altitudeReason = QString("altitude %1 < minAltitude %2").arg(altitude, 0, 'f', 1).arg(getMinAltitude(), 0, 'f', 1);
0528         return false;
0529     }
0530     // Check the artificial horizon.
0531     if (getHorizon() != nullptr && enforceArtificialHorizon)
0532         return getHorizon()->isAltitudeOK(azimuth, altitude, altitudeReason);
0533 
0534     return true;
0535 }
0536 
0537 bool SchedulerJob::moonSeparationOK(QDateTime const &when) const
0538 {
0539     if (moon == nullptr) return true;
0540 
0541     // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
0542     KStarsDateTime ltWhen(when.isValid() ?
0543                           Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when :
0544                           getLocalTime());
0545 
0546     // Create a sky object with the target catalog coordinates
0547     SkyPoint const target = getTargetCoords();
0548     SkyObject o;
0549     o.setRA0(target.ra0());
0550     o.setDec0(target.dec0());
0551 
0552     // Update RA/DEC of the target for the current fraction of the day
0553     KSNumbers numbers(ltWhen.djd());
0554     o.updateCoordsNow(&numbers);
0555 
0556     CachingDms LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltWhen).gst());
0557     moon->updateCoords(&numbers, true, SchedulerModuleState::getGeo()->lat(), &LST, true);
0558 
0559     double const separation = moon->angularDistanceTo(&o).Degrees();
0560 
0561     return (separation >= getMinMoonSeparation());
0562 }
0563 
0564 QDateTime SchedulerJob::calculateNextTime(QDateTime const &when, bool checkIfConstraintsAreMet, int increment,
0565         QString *reason, bool runningJob, const QDateTime &until) const
0566 {
0567     // FIXME: block calculating target coordinates at a particular time is duplicated in several places
0568 
0569     // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
0570     KStarsDateTime ltWhen(when.isValid() ?
0571                           Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when :
0572                           getLocalTime());
0573 
0574     // Create a sky object with the target catalog coordinates
0575     SkyPoint const target = getTargetCoords();
0576     SkyObject o;
0577     o.setRA0(target.ra0());
0578     o.setDec0(target.dec0());
0579 
0580     // Calculate the UT at the argument time
0581     KStarsDateTime const ut = SchedulerModuleState::getGeo()->LTtoUT(ltWhen);
0582 
0583     double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff();
0584 
0585     auto maxMinute = 1e8;
0586     if (!runningJob && until.isValid())
0587         maxMinute = when.secsTo(until) / 60;
0588 
0589     if (maxMinute > 24 * 60)
0590         maxMinute = 24 * 60;
0591 
0592     // Within the next 24 hours, search when the job target matches the altitude and moon constraints
0593     for (unsigned int minute = 0; minute < maxMinute; minute += increment)
0594     {
0595         KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60));
0596 
0597         // Is this violating twilight?
0598         QDateTime nextSuccess;
0599         if (getEnforceTwilight() && !runsDuringAstronomicalNightTime(ltOffset, &nextSuccess))
0600         {
0601             if (checkIfConstraintsAreMet)
0602             {
0603                 // Change the minute to increment-minutes before next success.
0604                 if (nextSuccess.isValid())
0605                 {
0606                     const int minutesToSuccess = ltOffset.secsTo(nextSuccess) / 60 - increment;
0607                     if (minutesToSuccess > 0)
0608                         minute += minutesToSuccess;
0609                 }
0610                 continue;
0611             }
0612             else
0613             {
0614                 if (reason) *reason = "twilight";
0615                 return ltOffset;
0616             }
0617         }
0618 
0619         // Update RA/DEC of the target for the current fraction of the day
0620         KSNumbers numbers(ltOffset.djd());
0621         o.updateCoordsNow(&numbers);
0622 
0623         // Compute local sidereal time for the current fraction of the day, calculate altitude
0624         CachingDms const LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltOffset).gst());
0625         o.EquatorialToHorizontal(&LST, SchedulerModuleState::getGeo()->lat());
0626         double const altitude = o.alt().Degrees();
0627         double const azimuth = o.az().Degrees();
0628 
0629         bool const altitudeOK = satisfiesAltitudeConstraint(azimuth, altitude, reason);
0630         if (altitudeOK)
0631         {
0632             // Don't test proximity to dawn in this situation, we only cater for altitude here
0633 
0634             // Continue searching if Moon separation is not good enough
0635             if (0 < getMinMoonSeparation() && !moonSeparationOK(ltOffset))
0636             {
0637                 if (checkIfConstraintsAreMet)
0638                     continue;
0639                 else
0640                 {
0641                     if (reason) *reason = QString("moon separation");
0642                     return ltOffset;
0643                 }
0644             }
0645 
0646             // Continue searching if target is setting and under the cutoff
0647             if (checkIfConstraintsAreMet)
0648             {
0649                 if (!runningJob)
0650                 {
0651                     double offset = LST.Hours() - o.ra().Hours();
0652                     if (24.0 <= offset)
0653                         offset -= 24.0;
0654                     else if (offset < 0.0)
0655                         offset += 24.0;
0656                     if (0.0 <= offset && offset < 12.0)
0657                     {
0658                         bool const settingAltitudeOK = satisfiesAltitudeConstraint(azimuth, altitude - SETTING_ALTITUDE_CUTOFF);
0659                         if (!settingAltitudeOK)
0660                             continue;
0661                     }
0662                 }
0663                 return ltOffset;
0664             }
0665         }
0666         else if (!checkIfConstraintsAreMet)
0667             return ltOffset;
0668     }
0669 
0670     return QDateTime();
0671 }
0672 
0673 bool SchedulerJob::runsDuringAstronomicalNightTime(const QDateTime &time,
0674         QDateTime *nextPossibleSuccess) const
0675 {
0676     // We call this very frequently in the Greedy Algorithm, and the calls
0677     // below are expensive. Almost all the calls are redundent (e.g. if it's not nighttime
0678     // now, it's not nighttime in 10 minutes). So, cache the answer and return it if the next
0679     // call is for a time between this time and the next dawn/dusk (whichever is sooner).
0680 
0681     static QDateTime previousMinDawnDusk, previousTime;
0682     static GeoLocation const *previousGeo = nullptr;  // A dangling pointer, I suppose, but we never reference it.
0683     static bool previousAnswer;
0684     static double previousPreDawnTime = 0;
0685     static QDateTime nextSuccess;
0686 
0687     // Lock this method because of all the statics
0688     static std::mutex nightTimeMutex;
0689     const std::lock_guard<std::mutex> lock(nightTimeMutex);
0690 
0691     // We likely can rely on the previous calculations.
0692     if (previousTime.isValid() && previousMinDawnDusk.isValid() &&
0693             time >= previousTime && time < previousMinDawnDusk &&
0694             SchedulerModuleState::getGeo() == previousGeo &&
0695             Options::preDawnTime() == previousPreDawnTime)
0696     {
0697         if (!previousAnswer && nextPossibleSuccess != nullptr)
0698             *nextPossibleSuccess = nextSuccess;
0699         return previousAnswer;
0700     }
0701     else
0702     {
0703         previousAnswer = runsDuringAstronomicalNightTimeInternal(time, &previousMinDawnDusk, &nextSuccess);
0704         previousTime = time;
0705         previousGeo = SchedulerModuleState::getGeo();
0706         previousPreDawnTime = Options::preDawnTime();
0707         if (!previousAnswer && nextPossibleSuccess != nullptr)
0708             *nextPossibleSuccess = nextSuccess;
0709         return previousAnswer;
0710     }
0711 }
0712 
0713 
0714 bool SchedulerJob::runsDuringAstronomicalNightTimeInternal(const QDateTime &time, QDateTime *minDawnDusk,
0715         QDateTime *nextPossibleSuccess) const
0716 {
0717     QDateTime t;
0718     QDateTime nDawn = nextDawn, nDusk = nextDusk;
0719     if (time.isValid())
0720     {
0721         // Can't rely on the pre-computed dawn/dusk if we're giving it an arbitary time.
0722         SchedulerModuleState::calculateDawnDusk(time, nDawn, nDusk);
0723         t = time;
0724     }
0725     else
0726     {
0727         t = startupTime;
0728     }
0729 
0730     // Calculate the next astronomical dawn time, adjusted with the Ekos pre-dawn offset
0731     QDateTime const earlyDawn = nDawn.addSecs(-60.0 * abs(Options::preDawnTime()));
0732 
0733     *minDawnDusk = earlyDawn < nDusk ? earlyDawn : nDusk;
0734 
0735     // Dawn and dusk are ordered as the immediate next events following the observation time
0736     // Thus if dawn comes first, the job startup time occurs during the dusk/dawn interval.
0737     bool result = nDawn < nDusk && t <= earlyDawn;
0738 
0739     // Return a hint about when it might succeed.
0740     if (nextPossibleSuccess != nullptr)
0741     {
0742         if (result) *nextPossibleSuccess = QDateTime();
0743         else *nextPossibleSuccess = nDusk;
0744     }
0745 
0746     return result;
0747 }
0748 
0749 void SchedulerJob::setInitialFilter(const QString &value)
0750 {
0751     m_InitialFilter = value;
0752 }
0753 
0754 const QString &SchedulerJob::getInitialFilter() const
0755 {
0756     return m_InitialFilter;
0757 }
0758 
0759 bool SchedulerJob::StartTimeCache::check(const QDateTime &from, const QDateTime &until,
0760         QDateTime *result, QDateTime *newFrom) const
0761 {
0762     // Look at the cached results from getNextPossibleStartTime.
0763     // If the desired 'from' time is in one of them, that is, between computation.from and computation.until,
0764     // then we can re-use that result (as long as the desired until time is < computation.until).
0765     foreach (const StartTimeComputation &computation, startComputations)
0766     {
0767         if (from >= computation.from &&
0768                 (!computation.until.isValid() || from < computation.until) &&
0769                 (!computation.result.isValid() || from < computation.result))
0770         {
0771             if (computation.result.isValid() || until <= computation.until)
0772             {
0773                 // We have a cached result.
0774                 *result = computation.result;
0775                 *newFrom = QDateTime();
0776                 return true;
0777             }
0778             else
0779             {
0780                 // No cached result, but at least we can constrain the search.
0781                 *result = QDateTime();
0782                 *newFrom = computation.until;
0783                 return true;
0784             }
0785         }
0786     }
0787     return false;
0788 }
0789 
0790 void SchedulerJob::StartTimeCache::clear() const
0791 {
0792     startComputations.clear();
0793 }
0794 
0795 void SchedulerJob::StartTimeCache::add(const QDateTime &from, const QDateTime &until, const QDateTime &result) const
0796 {
0797     // Manage the cache size.
0798     if (startComputations.size() > 10)
0799         startComputations.clear();
0800 
0801     // The getNextPossibleStartTime computation (which calls calculateNextTime) searches ahead at most 24 hours.
0802     QDateTime endTime;
0803     if (!until.isValid())
0804         endTime = from.addSecs(24 * 3600);
0805     else
0806     {
0807         QDateTime oneDay = from.addSecs(24 * 3600);
0808         if (until > oneDay)
0809             endTime = oneDay;
0810         else
0811             endTime = until;
0812     }
0813 
0814     StartTimeComputation c;
0815     c.from = from;
0816     c.until = endTime;
0817     c.result = result;
0818     startComputations.push_back(c);
0819 }
0820 
0821 // When can this job start? For now ignores culmination constraint.
0822 QDateTime SchedulerJob::getNextPossibleStartTime(const QDateTime &when, int increment, bool runningJob,
0823         const QDateTime &until) const
0824 {
0825     QDateTime ltWhen(
0826         when.isValid() ? (Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when)
0827         : getLocalTime());
0828 
0829     // We do not consider job state here. It is the responsibility of the caller
0830     // to filter for that, if desired.
0831 
0832     if (!runningJob && START_AT == getFileStartupCondition())
0833     {
0834         int secondsFromNow = ltWhen.secsTo(getFileStartupTime());
0835         if (secondsFromNow < -500)
0836             // We missed it.
0837             return QDateTime();
0838         ltWhen = secondsFromNow > 0 ? getFileStartupTime() : ltWhen;
0839     }
0840 
0841     // Can't start if we're past the finish time.
0842     if (getCompletionCondition() == FINISH_AT)
0843     {
0844         const QDateTime &t = getCompletionTime();
0845         if (t.isValid() && t < ltWhen)
0846             return QDateTime(); // return an invalid time.
0847     }
0848 
0849     if (runningJob)
0850         return calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
0851     else
0852     {
0853         QDateTime result, newFrom;
0854         if (startTimeCache.check(ltWhen, until, &result, &newFrom))
0855         {
0856             if (result.isValid() || !newFrom.isValid())
0857                 return result;
0858             if (newFrom.isValid())
0859                 ltWhen = newFrom;
0860         }
0861         result = calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
0862         startTimeCache.add(ltWhen, until, result);
0863         return result;
0864     }
0865 }
0866 
0867 // When will this job end (not looking at capture plan)?
0868 QDateTime SchedulerJob::getNextEndTime(const QDateTime &start, int increment, QString *reason, const QDateTime &until) const
0869 {
0870     QDateTime ltStart(
0871         start.isValid() ? (Qt::UTC == start.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(start)) : start)
0872         : getLocalTime());
0873 
0874     // We do not consider job state here. It is the responsibility of the caller
0875     // to filter for that, if desired.
0876 
0877     if (START_AT == getFileStartupCondition())
0878     {
0879         if (getFileStartupTime().secsTo(ltStart) < -120)
0880         {
0881             // if the file startup time is in the future, then end now.
0882             // This case probably wouldn't happen in the running code.
0883             if (reason) *reason = "before start-at time";
0884             return QDateTime();
0885         }
0886         // otherwise, test from now.
0887     }
0888 
0889     // Can't start if we're past the finish time.
0890     if (getCompletionCondition() == FINISH_AT)
0891     {
0892         const QDateTime &t = getCompletionTime();
0893         if (t.isValid() && t < ltStart)
0894         {
0895             if (reason) *reason = "end-at time";
0896             return QDateTime(); // return an invalid time.
0897         }
0898         auto result = calculateNextTime(ltStart, false, increment, reason, false, until);
0899         if (!result.isValid() || result.secsTo(getCompletionTime()) < 0)
0900         {
0901             if (reason) *reason = "end-at time";
0902             return getCompletionTime();
0903         }
0904         else return result;
0905     }
0906 
0907     return calculateNextTime(ltStart, false, increment, reason, false, until);
0908 }
0909 
0910 QJsonObject SchedulerJob::toJson() const
0911 {
0912     bool is_setting = false;
0913     double const alt = SchedulerUtils::findAltitude(getTargetCoords(), QDateTime(), &is_setting);
0914 
0915     return
0916     {
0917         {"name", name},
0918         {"pa", m_PositionAngle},
0919         {"targetRA", targetCoords.ra0().Hours()},
0920         {"targetDEC", targetCoords.dec0().Degrees()},
0921         {"state", state},
0922         {"stage", stage},
0923         {"sequenceCount", sequenceCount},
0924         {"completedCount", completedCount},
0925         {"minAltitude", minAltitude},
0926         {"minMoonSeparation", minMoonSeparation},
0927         {"repeatsRequired", repeatsRequired},
0928         {"repeatsRemaining", repeatsRemaining},
0929         {"inSequenceFocus", inSequenceFocus},
0930         {"startupTime", startupTime.isValid() ? startupTime.toString() : "--"},
0931         {"completionTime", completionTime.isValid() ? completionTime.toString() : "--"},
0932         {"altitude", alt},
0933     };
0934 }
0935 } // Ekos namespace