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