File indexing completed on 2024-04-21 03:43:39

0001 /*
0002     SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "schedulerutils.h"
0008 #include "schedulerjob.h"
0009 #include "schedulermodulestate.h"
0010 #include "ekos/capture/sequencejob.h"
0011 #include "Options.h"
0012 #include "skypoint.h"
0013 #include "kstarsdata.h"
0014 #include <ekos_scheduler_debug.h>
0015 
0016 namespace Ekos {
0017 
0018 SchedulerUtils::SchedulerUtils()
0019 {
0020 
0021 }
0022 
0023 SchedulerJob *SchedulerUtils::createJob(XMLEle *root)
0024 {
0025     SchedulerJob *job = new SchedulerJob();
0026     QString name, group;
0027     dms ra, dec;
0028     double rotation = 0.0, minimumAltitude = 0.0, minimumMoonSeparation = 0.0;
0029     QUrl sequenceURL, fitsURL;
0030     StartupCondition startup = START_ASAP;
0031     CompletionCondition completion = FINISH_SEQUENCE;
0032     QDateTime startupTime, completionTime;
0033     int completionRepeats = 0;
0034     bool enforceWeather = false, enforceTwilight = false, enforceArtificialHorizon = false,
0035             track = false, focus = false, align = false, guide = false;
0036 
0037 
0038     XMLEle *ep, *subEP;
0039     // We expect all data read from the XML to be in the C locale - QLocale::c()
0040     QLocale cLocale = QLocale::c();
0041 
0042     for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
0043     {
0044         if (!strcmp(tagXMLEle(ep), "Name"))
0045             name = pcdataXMLEle(ep);
0046         else if (!strcmp(tagXMLEle(ep), "Group"))
0047             group = pcdataXMLEle(ep);
0048         else if (!strcmp(tagXMLEle(ep), "Coordinates"))
0049         {
0050             subEP = findXMLEle(ep, "J2000RA");
0051             if (subEP)
0052                 ra.setH(cLocale.toDouble(pcdataXMLEle(subEP)));
0053 
0054             subEP = findXMLEle(ep, "J2000DE");
0055             if (subEP)
0056                 dec.setD(cLocale.toDouble(pcdataXMLEle(subEP)));
0057         }
0058         else if (!strcmp(tagXMLEle(ep), "Sequence"))
0059         {
0060             sequenceURL = QUrl::fromUserInput(pcdataXMLEle(ep));
0061         }
0062         else if (!strcmp(tagXMLEle(ep), "FITS"))
0063         {
0064             fitsURL.setPath(pcdataXMLEle(ep));
0065         }
0066         else if (!strcmp(tagXMLEle(ep), "PositionAngle"))
0067         {
0068             rotation = cLocale.toDouble(pcdataXMLEle(ep));
0069         }
0070         else if (!strcmp(tagXMLEle(ep), "StartupCondition"))
0071         {
0072             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
0073             {
0074                 if (!strcmp("ASAP", pcdataXMLEle(subEP)))
0075                     startup = START_ASAP;
0076                 else if (!strcmp("At", pcdataXMLEle(subEP)))
0077                 {
0078                     startup = START_AT;
0079                     startupTime = QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate);
0080                     // Todo sterne-jaeger 2024-01-01: setting time spec from KStars necessary?
0081                 }
0082             }
0083         }
0084         else if (!strcmp(tagXMLEle(ep), "Constraints"))
0085         {
0086             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
0087             {
0088                 if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP)))
0089                 {
0090                     Options::setEnableAltitudeLimits(true);
0091                     minimumAltitude = cLocale.toDouble(findXMLAttValu(subEP, "value"));
0092                 }
0093                 else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP)))
0094                 {
0095                     Options::setSchedulerMoonSeparation(true);
0096                     minimumMoonSeparation = cLocale.toDouble(findXMLAttValu(subEP, "value"));
0097                 }
0098                 else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP)))
0099                     enforceWeather = true;
0100                 else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP)))
0101                     enforceTwilight = true;
0102                 else if (!strcmp("EnforceArtificialHorizon", pcdataXMLEle(subEP)))
0103                     enforceArtificialHorizon = true;
0104             }
0105         }
0106         else if (!strcmp(tagXMLEle(ep), "CompletionCondition"))
0107         {
0108             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
0109             {
0110                 if (!strcmp("Sequence", pcdataXMLEle(subEP)))
0111                     completion = FINISH_SEQUENCE;
0112                 else if (!strcmp("Repeat", pcdataXMLEle(subEP)))
0113                 {
0114                     completion = FINISH_REPEAT;
0115                     completionRepeats = cLocale.toInt(findXMLAttValu(subEP, "value"));
0116                 }
0117                 else if (!strcmp("Loop", pcdataXMLEle(subEP)))
0118                     completion = FINISH_LOOP;
0119                 else if (!strcmp("At", pcdataXMLEle(subEP)))
0120                 {
0121                     completion = FINISH_AT;
0122                     completionTime = QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate);
0123                 }
0124             }
0125         }
0126         else if (!strcmp(tagXMLEle(ep), "Steps"))
0127         {
0128             XMLEle *module;
0129 
0130             for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0))
0131             {
0132                 const char *proc = pcdataXMLEle(module);
0133 
0134                 if (!strcmp(proc, "Track"))
0135                     track = true;
0136                 else if (!strcmp(proc, "Focus"))
0137                     focus = true;
0138                 else if (!strcmp(proc, "Align"))
0139                     align = true;
0140                 else if (!strcmp(proc, "Guide"))
0141                     guide = true;
0142             }
0143         }
0144     }
0145     SchedulerUtils::setupJob(*job, name, group, ra, dec,
0146                              KStarsData::Instance()->ut().djd(),
0147                              rotation, sequenceURL, fitsURL,
0148 
0149                              startup, startupTime,
0150                              completion, completionTime, completionRepeats,
0151 
0152                              minimumAltitude,
0153                              minimumMoonSeparation,
0154                              enforceWeather,
0155                              enforceTwilight,
0156                              enforceArtificialHorizon,
0157 
0158                              track,
0159                              focus,
0160                              align,
0161                              guide);
0162 
0163     return job;
0164 }
0165 
0166 void SchedulerUtils::setupJob(SchedulerJob &job, const QString &name, const QString &group, const dms &ra, const dms &dec, double djd, double rotation, const QUrl &sequenceUrl, const QUrl &fitsUrl, StartupCondition startup, const QDateTime &startupTime, CompletionCondition completion, const QDateTime &completionTime, int completionRepeats, double minimumAltitude, double minimumMoonSeparation, bool enforceWeather, bool enforceTwilight, bool enforceArtificialHorizon, bool track, bool focus, bool align, bool guide)
0167 {
0168     /* Configure or reconfigure the observation job */
0169 
0170     job.setName(name);
0171     job.setGroup(group);
0172     // djd should be ut.djd
0173     job.setTargetCoords(ra, dec, djd);
0174     job.setPositionAngle(rotation);
0175 
0176     /* Consider sequence file is new, and clear captured frames map */
0177     job.setCapturedFramesMap(CapturedFramesMap());
0178     job.setSequenceFile(sequenceUrl);
0179     job.setFITSFile(fitsUrl);
0180     // #1 Startup conditions
0181 
0182     job.setStartupCondition(startup);
0183     if (startup == START_AT)
0184     {
0185         job.setStartupTime(startupTime);
0186     }
0187     /* Store the original startup condition */
0188     job.setFileStartupCondition(job.getStartupCondition());
0189     job.setFileStartupTime(job.getStartupTime());
0190 
0191     // #2 Constraints
0192 
0193     job.setMinAltitude(minimumAltitude);
0194     job.setMinMoonSeparation(minimumMoonSeparation);
0195 
0196     // Check enforce weather constraints
0197     job.setEnforceWeather(enforceWeather);
0198     // twilight constraints
0199     job.setEnforceTwilight(enforceTwilight);
0200     job.setEnforceArtificialHorizon(enforceArtificialHorizon);
0201 
0202     job.setCompletionCondition(completion);
0203     if (completion == FINISH_AT)
0204         job.setCompletionTime(completionTime);
0205     else if (completion == FINISH_REPEAT)
0206     {
0207         job.setRepeatsRequired(completionRepeats);
0208         job.setRepeatsRemaining(completionRepeats);
0209     }
0210     // Job steps
0211     job.setStepPipeline(SchedulerJob::USE_NONE);
0212     if (track)
0213         job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_TRACK));
0214     if (focus)
0215         job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_FOCUS));
0216     if (align)
0217         job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_ALIGN));
0218     if (guide)
0219         job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_GUIDE));
0220 
0221     /* Store the original startup condition */
0222     job.setFileStartupCondition(job.getStartupCondition());
0223     job.setFileStartupTime(job.getStartupTime());
0224 
0225     /* Reset job state to evaluate the changes */
0226     job.reset();
0227 }
0228 
0229 uint16_t SchedulerUtils::fillCapturedFramesMap(const QMap<QString, uint16_t> &expected, const CapturedFramesMap &capturedFramesCount, SchedulerJob &schedJob, CapturedFramesMap &capture_map, int &completedIterations)
0230 {
0231     uint16_t totalCompletedCount = 0;
0232 
0233     // Figure out which repeat this is for the key with the least progress.
0234     int minIterationsCompleted = -1, currentIteration = 0;
0235     if (Options::rememberJobProgress())
0236     {
0237         completedIterations = 0;
0238         for (const QString &key : expected.keys())
0239         {
0240             const int iterationsCompleted = capturedFramesCount[key] / expected[key];
0241             if (minIterationsCompleted == -1 || iterationsCompleted < minIterationsCompleted)
0242                 minIterationsCompleted = iterationsCompleted;
0243         }
0244         // If this condition is FINISH_REPEAT, and we've already completed enough iterations
0245         // Then set the currentIteratiion as 1 more than required. No need to go higher.
0246         if (schedJob.getCompletionCondition() == FINISH_REPEAT
0247                 && minIterationsCompleted >= schedJob.getRepeatsRequired())
0248             currentIteration  = schedJob.getRepeatsRequired() + 1;
0249         else
0250             // Otherwise set it to one more than the number completed (i.e. the one it'll be working on).
0251             currentIteration = minIterationsCompleted + 1;
0252         completedIterations = std::max(0, currentIteration - 1);
0253     }
0254     else
0255         // If we are not remembering progress, we'll only know the iterations completed
0256         // by the current job's run.
0257         completedIterations = schedJob.getCompletedIterations();
0258 
0259     for (const QString &key : expected.keys())
0260     {
0261         if (Options::rememberJobProgress())
0262         {
0263             // If we're remembering progress, then figure out how many captures have not yet been captured.
0264             const int diff = expected[key] * currentIteration - capturedFramesCount[key];
0265 
0266             // Already captured more than required? Then don't capture any this round.
0267             if (diff <= 0)
0268                 capture_map[key] = expected[key];
0269             // Need more captures than one cycle could capture? If so, capture the full amount.
0270             else if (diff >= expected[key])
0271                 capture_map[key] = 0;
0272             // Otherwise we know that 0 < diff < expected[key]. Capture just the number needed.
0273             else
0274                 capture_map[key] = expected[key] - diff;
0275         }
0276         else
0277             // If we are not remembering progress, then the capture module, which reads this
0278             // Will capture all requirements in the .esq file.
0279             capture_map[key] = 0;
0280 
0281         // collect all captured frames counts
0282         if (schedJob.getCompletionCondition() == FINISH_LOOP)
0283             totalCompletedCount += capturedFramesCount[key];
0284         else
0285             totalCompletedCount += std::min(capturedFramesCount[key],
0286                                             static_cast<uint16_t>(expected[key] * schedJob.getRepeatsRequired()));
0287     }
0288     return totalCompletedCount;
0289 }
0290 
0291 void SchedulerUtils::updateLightFramesRequired(SchedulerJob *oneJob, const QList<SequenceJob *> &seqjobs, const CapturedFramesMap &framesCount)
0292 {
0293     bool lightFramesRequired = false;
0294     QMap<QString, uint16_t> expected;
0295     switch (oneJob->getCompletionCondition())
0296     {
0297         case FINISH_SEQUENCE:
0298         case FINISH_REPEAT:
0299             // Step 1: determine expected frames
0300             SchedulerUtils::calculateExpectedCapturesMap(seqjobs, expected);
0301             // Step 2: compare with already captured frames
0302             for (SequenceJob *oneSeqJob : seqjobs)
0303             {
0304                 QString const signature = oneSeqJob->getSignature();
0305                 /* If frame is LIGHT, how many do we have left? */
0306                 if (oneSeqJob->getFrameType() == FRAME_LIGHT && expected[signature] * oneJob->getRepeatsRequired() > framesCount[signature])
0307                 {
0308                     lightFramesRequired = true;
0309                     // exit the loop, one found is sufficient
0310                     break;
0311                 }
0312             }
0313             break;
0314         default:
0315             // in all other cases it does not depend on the number of captured frames
0316             lightFramesRequired = true;
0317     }
0318     oneJob->setLightFramesRequired(lightFramesRequired);
0319 }
0320 
0321 SequenceJob *SchedulerUtils::processSequenceJobInfo(XMLEle *root, SchedulerJob *schedJob)
0322 {
0323     SequenceJob *job = new SequenceJob(root, schedJob->getName());
0324     if (FRAME_LIGHT == job->getFrameType() && nullptr != schedJob)
0325         schedJob->setLightFramesRequired(true);
0326 
0327     auto placeholderPath = Ekos::PlaceholderPath();
0328     placeholderPath.processJobInfo(job);
0329 
0330     return job;
0331 }
0332 
0333 bool SchedulerUtils::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList<SequenceJob *> &jobs, bool &hasAutoFocus, ModuleLogger *logger)
0334 {
0335     QFile sFile;
0336     sFile.setFileName(fileURL);
0337 
0338     if (!sFile.open(QIODevice::ReadOnly))
0339     {
0340         if (logger != nullptr) logger->appendLogText(i18n("Unable to open sequence queue file '%1'", fileURL));
0341                 return false;
0342     }
0343 
0344     LilXML *xmlParser = newLilXML();
0345     char errmsg[MAXRBUF];
0346     XMLEle *root = nullptr;
0347     XMLEle *ep   = nullptr;
0348     char c;
0349 
0350     while (sFile.getChar(&c))
0351     {
0352         root = readXMLEle(xmlParser, c, errmsg);
0353 
0354         if (root)
0355         {
0356             for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
0357             {
0358                 if (!strcmp(tagXMLEle(ep), "Autofocus"))
0359                     hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true"));
0360                 else if (!strcmp(tagXMLEle(ep), "Job"))
0361                 {
0362                     SequenceJob *thisJob = processSequenceJobInfo(ep, schedJob);
0363                     jobs.append(thisJob);
0364                     if (jobs.count() == 1)
0365                     {
0366                         auto &firstJob = jobs.first();
0367                         if (FRAME_LIGHT == firstJob->getFrameType() && nullptr != schedJob)
0368                         {
0369                             schedJob->setInitialFilter(firstJob->getCoreProperty(SequenceJob::SJ_Filter).toString());
0370                         }
0371 
0372                     }
0373                 }
0374             }
0375             delXMLEle(root);
0376         }
0377         else if (errmsg[0])
0378         {
0379             if (logger != nullptr) logger->appendLogText(QString(errmsg));
0380             delLilXML(xmlParser);
0381             qDeleteAll(jobs);
0382             return false;
0383         }
0384     }
0385 
0386     return true;
0387 }
0388 
0389 bool SchedulerUtils::estimateJobTime(SchedulerJob *schedJob, const QMap<QString, uint16_t> &capturedFramesCount, ModuleLogger *logger)
0390 {
0391     static SchedulerJob *jobWarned = nullptr;
0392 
0393     // Load the sequence job associated with the argument scheduler job.
0394     QList<SequenceJob *> seqJobs;
0395     bool hasAutoFocus = false;
0396     bool result = loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus, logger);
0397     if (result == false)
0398     {
0399         qCWarning(KSTARS_EKOS_SCHEDULER) <<
0400                                          QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(
0401                                              schedJob->getSequenceFile().toLocalFile());
0402         return result;
0403     }
0404 
0405     // FIXME: setting in-sequence focus should be done in XML processing.
0406     schedJob->setInSequenceFocus(hasAutoFocus);
0407 
0408     // Stop spam of log on re-evaluation. If we display the warning once, then that's it.
0409     if (schedJob != jobWarned && hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS))
0410     {
0411         logger->appendLogText(i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.",
0412                      schedJob->getName()));
0413         jobWarned = schedJob;
0414     }
0415 
0416     /* This is the map of captured frames for this scheduler job, keyed per storage signature.
0417      * It will be forwarded to the Capture module in order to capture only what frames are required.
0418      * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally.
0419      */
0420     CapturedFramesMap capture_map;
0421     bool const rememberJobProgress = Options::rememberJobProgress();
0422 
0423     double totalImagingTime  = 0;
0424     double imagingTimePerRepeat = 0, imagingTimeLeftThisRepeat = 0;
0425 
0426     // Determine number of captures in the scheduler job
0427     QMap<QString, uint16_t> expected;
0428     uint16_t allCapturesPerRepeat = calculateExpectedCapturesMap(seqJobs, expected);
0429 
0430     // fill the captured frames map
0431     int completedIterations;
0432     uint16_t totalCompletedCount = fillCapturedFramesMap(expected, capturedFramesCount, *schedJob, capture_map, completedIterations);
0433     schedJob->setCompletedIterations(completedIterations);
0434     // Loop through sequence jobs to calculate the number of required frames and estimate duration.
0435     foreach (SequenceJob *seqJob, seqJobs)
0436     {
0437         // FIXME: find a way to actually display the filter name.
0438         QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt(),
0439                                seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(),
0440                                seqJob->getCoreProperty(SequenceJob::SJ_Filter).toString());
0441 
0442         if (seqJob->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
0443         {
0444             qCInfo(KSTARS_EKOS_SCHEDULER) <<
0445                                           QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName);
0446             schedJob->setEstimatedTime(-2);
0447             qDeleteAll(seqJobs);
0448             return true;
0449         }
0450 
0451         // Note that looping jobs will have zero repeats required.
0452         QString const signature      = seqJob->getSignature();
0453         QString const signature_path = QFileInfo(signature).path();
0454         int captures_required        = seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt() * schedJob->getRepeatsRequired();
0455         int captures_completed       = capturedFramesCount[signature];
0456         const int capturesRequiredPerRepeat = std::max(1, seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
0457         int capturesLeftThisRepeat   = std::max(0, capturesRequiredPerRepeat - (captures_completed % capturesRequiredPerRepeat));
0458         if (captures_completed >= (1 + completedIterations) * capturesRequiredPerRepeat)
0459         {
0460             // Something else is causing this iteration to be incomplete. Nothing left to do for this seqJob.
0461             capturesLeftThisRepeat = 0;
0462         }
0463 
0464         if (rememberJobProgress && schedJob->getCompletionCondition() != FINISH_LOOP)
0465         {
0466             /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count.
0467              *
0468              * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage.
0469              *
0470              * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location.
0471              * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job.
0472              *
0473              * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus
0474              * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are
0475              * missing, first because that's not how the sequence job is designed (completed count, not required count), and second
0476              * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence
0477              * jobs three times.
0478              *
0479              * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them.
0480              *
0481              * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages.
0482              * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0.
0483              * When that scheduler job executes, only B and L captures will be processed.
0484              *
0485              * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage.
0486              * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0.
0487              * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed.
0488              *
0489              * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets.
0490              * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage.
0491              * This is why it is important to manage the repeat count of the scheduler job, as stated earlier.
0492              */
0493 
0494             captures_required = expected[seqJob->getSignature()] * schedJob->getRepeatsRequired();
0495 
0496             qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg(
0497                                               captures_completed).arg(QFileInfo(signature).path());
0498 
0499             // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one
0500             foreach (SequenceJob *prevSeqJob, seqJobs)
0501             {
0502                 // Enumerate seqJobs up to the current one
0503                 if (seqJob == prevSeqJob)
0504                     break;
0505 
0506                 // If the previous sequence signature matches the current, skip counting to take duplicates into account
0507                 if (!signature.compare(prevSeqJob->getSignature()))
0508                     captures_required = 0;
0509 
0510                 // And break if no captures remain, this job does not need to be executed
0511                 if (captures_required == 0)
0512                     break;
0513             }
0514 
0515             qCDebug(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(
0516                                                seqName).arg(captures_completed).arg(captures_required).arg(signature_path);
0517 
0518         }
0519         // Else rely on the captures done during this session
0520         else if (0 < allCapturesPerRepeat)
0521         {
0522             captures_completed = schedJob->getCompletedCount() / allCapturesPerRepeat * seqJob->getCoreProperty(
0523                                      SequenceJob::SJ_Count).toInt();
0524         }
0525         else
0526         {
0527             captures_completed = 0;
0528         }
0529 
0530         // Check if we still need any light frames. Because light frames changes the flow of the observatory startup
0531         // Without light frames, there is no need to do focusing, alignment, guiding...etc
0532         // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required
0533         // OR if the completion condition is set to LOOP so it is never complete due to looping.
0534         // Note that looping jobs will have zero repeats required.
0535         // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job.
0536         bool const areJobCapturesComplete = (0 == captures_required || captures_completed >= captures_required);
0537         if (seqJob->getFrameType() == FRAME_LIGHT)
0538         {
0539             if(areJobCapturesComplete)
0540             {
0541                 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(
0542                                                   captures_required);
0543             }
0544         }
0545         else
0546         {
0547             qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName);
0548         }
0549 
0550         /* If captures are not complete, we have imaging time left */
0551         if (!areJobCapturesComplete || schedJob->getCompletionCondition() == FINISH_LOOP)
0552         {
0553             unsigned int const captures_to_go = captures_required - captures_completed;
0554             const double secsPerCapture = (seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() +
0555                                            (seqJob->getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000.0));
0556             totalImagingTime += fabs(secsPerCapture * captures_to_go);
0557             imagingTimePerRepeat += fabs(secsPerCapture * seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
0558             imagingTimeLeftThisRepeat += fabs(secsPerCapture * capturesLeftThisRepeat);
0559             /* If we have light frames to process, add focus/dithering delay */
0560             if (seqJob->getFrameType() == FRAME_LIGHT)
0561             {
0562                 // If inSequenceFocus is true
0563                 if (hasAutoFocus)
0564                 {
0565                     // Wild guess, 10s of autofocus for each capture required. Can vary a lot, but this is just a completion estimate.
0566                     constexpr int afSecsPerCapture = 10;
0567                     qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName);
0568                     totalImagingTime += captures_to_go * afSecsPerCapture;
0569                     imagingTimePerRepeat += capturesRequiredPerRepeat * afSecsPerCapture;
0570                     imagingTimeLeftThisRepeat += capturesLeftThisRepeat * afSecsPerCapture;
0571                 }
0572                 // If we're dithering after each exposure, that's another 10-20 seconds
0573                 if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled())
0574                 {
0575                     constexpr int ditherSecs = 15;
0576                     qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName);
0577                     totalImagingTime += (captures_to_go * ditherSecs) / Options::ditherFrames();
0578                     imagingTimePerRepeat += (capturesRequiredPerRepeat * ditherSecs) / Options::ditherFrames();
0579                     imagingTimeLeftThisRepeat += (capturesLeftThisRepeat * ditherSecs) / Options::ditherFrames();
0580                 }
0581             }
0582         }
0583     }
0584 
0585     schedJob->setCapturedFramesMap(capture_map);
0586     schedJob->setSequenceCount(allCapturesPerRepeat * schedJob->getRepeatsRequired());
0587 
0588     // only in case we remember the job progress, we change the completion count
0589     if (rememberJobProgress)
0590         schedJob->setCompletedCount(totalCompletedCount);
0591 
0592     qDeleteAll(seqJobs);
0593 
0594     schedJob->setEstimatedTimePerRepeat(imagingTimePerRepeat);
0595     schedJob->setEstimatedTimeLeftThisRepeat(imagingTimeLeftThisRepeat);
0596     if (schedJob->getLightFramesRequired())
0597         schedJob->setEstimatedStartupTime(timeHeuristics(schedJob));
0598 
0599     // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations!
0600 
0601     // We can't estimate times that do not finish when sequence is done
0602     if (schedJob->getCompletionCondition() == FINISH_LOOP)
0603     {
0604         // We can't know estimated time if it is looping indefinitely
0605         schedJob->setEstimatedTime(-2);
0606         qCDebug(KSTARS_EKOS_SCHEDULER) <<
0607                                        QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.")
0608                                        .arg(schedJob->getName());
0609     }
0610     // If we know startup and finish times, we can estimate time right away
0611     else if (schedJob->getStartupCondition() == START_AT &&
0612              schedJob->getCompletionCondition() == FINISH_AT)
0613     {
0614         // FIXME: SchedulerJob is probably doing this already
0615         qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime());
0616         schedJob->setEstimatedTime(diff);
0617 
0618         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.")
0619                                        .arg(schedJob->getName())
0620                                        .arg(dms(diff * 15.0 / 3600.0f).toHMSString());
0621     }
0622     // If we know finish time only, we can roughly estimate the time considering the job starts now
0623     else if (schedJob->getStartupCondition() != START_AT &&
0624              schedJob->getCompletionCondition() == FINISH_AT)
0625     {
0626         qint64 const diff = SchedulerModuleState::getLocalTime().secsTo(schedJob->getCompletionTime());
0627         schedJob->setEstimatedTime(diff);
0628         qCDebug(KSTARS_EKOS_SCHEDULER) <<
0629                                        QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.")
0630                                        .arg(schedJob->getName())
0631                                        .arg(dms(diff * 15.0 / 3600.0f).toHMSString());
0632     }
0633     // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null
0634     else if (totalImagingTime <= 0)
0635     {
0636         schedJob->setEstimatedTime(0);
0637         schedJob->setEstimatedTimePerRepeat(1);
0638         schedJob->setEstimatedTimeLeftThisRepeat(0);
0639 
0640         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.")
0641                                        .arg(schedJob->getName()).arg(schedJob->getCompletedCount()).arg(schedJob->getSequenceCount());
0642     }
0643     // Else consolidate with step durations
0644     else
0645     {
0646         if (schedJob->getLightFramesRequired())
0647         {
0648             totalImagingTime += timeHeuristics(schedJob);
0649             schedJob->setEstimatedStartupTime(timeHeuristics(schedJob));
0650         }
0651         dms const estimatedTime(totalImagingTime * 15.0 / 3600.0);
0652         schedJob->setEstimatedTime(std::ceil(totalImagingTime));
0653 
0654         qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(),
0655                                       estimatedTime.toHMSString());
0656     }
0657 
0658     return true;
0659 }
0660 
0661 int SchedulerUtils::timeHeuristics(const SchedulerJob *schedJob)
0662 {
0663     double imagingTime = 0;
0664     /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */
0665     // Are we doing tracking? It takes about 30 seconds
0666     if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK)
0667         imagingTime += 30;
0668     // Are we doing initial focusing? That can take about 2 minutes
0669     if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)
0670         imagingTime += 120;
0671     // Are we doing astrometry? That can take about 60 seconds
0672     if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
0673     {
0674         imagingTime += 60;
0675     }
0676     // Are we doing guiding?
0677     if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
0678     {
0679         // Looping, finding guide star, settling takes 15 sec
0680         imagingTime += 15;
0681 
0682         // Add guiding settle time from dither setting (used by phd2::guide())
0683         imagingTime += Options::ditherSettle();
0684         // Add guiding settle time from ekos sccheduler setting
0685         imagingTime += Options::guidingSettle();
0686 
0687         // If calibration always cleared
0688         // then calibration process can take about 2 mins
0689         if(Options::resetGuideCalibration())
0690             imagingTime += 120;
0691     }
0692     return imagingTime;
0693 
0694 }
0695 
0696 uint16_t SchedulerUtils::calculateExpectedCapturesMap(const QList<SequenceJob *> &seqJobs, QMap<QString, uint16_t> &expected)
0697 {
0698     uint16_t capturesPerRepeat = 0;
0699     for (auto &seqJob : seqJobs)
0700     {
0701         capturesPerRepeat += seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt();
0702         QString signature = seqJob->getCoreProperty(SequenceJob::SJ_Signature).toString();
0703         expected[signature] = static_cast<uint16_t>(seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt()) + (expected.contains(
0704                                   signature) ? expected[signature] : 0);
0705     }
0706     return capturesPerRepeat;
0707 }
0708 
0709 double SchedulerUtils::findAltitude(const SkyPoint &target, const QDateTime &when, bool *is_setting, bool debug)
0710 {
0711     // FIXME: block calculating target coordinates at a particular time is duplicated in several places
0712 
0713     // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
0714     KStarsDateTime ltWhen(when.isValid() ?
0715                           Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when :
0716                           SchedulerModuleState::getLocalTime());
0717 
0718     // Create a sky object with the target catalog coordinates
0719     SkyObject o;
0720     o.setRA0(target.ra0());
0721     o.setDec0(target.dec0());
0722 
0723     // Update RA/DEC of the target for the current fraction of the day
0724     KSNumbers numbers(ltWhen.djd());
0725     o.updateCoordsNow(&numbers);
0726 
0727     // Calculate alt/az coordinates using KStars instance's geolocation
0728     CachingDms const LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltWhen).gst());
0729     o.EquatorialToHorizontal(&LST, SchedulerModuleState::getGeo()->lat());
0730 
0731     // Hours are reduced to [0,24[, meridian being at 0
0732     double offset = LST.Hours() - o.ra().Hours();
0733     if (24.0 <= offset)
0734         offset -= 24.0;
0735     else if (offset < 0.0)
0736         offset += 24.0;
0737     bool const passed_meridian = 0.0 <= offset && offset < 12.0;
0738 
0739     if (debug)
0740         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7")
0741                                        .arg(o.ra().toHMSString())
0742                                        .arg(o.ra0().toHMSString())
0743                                        .arg(o.dec().toHMSString())
0744                                        .arg(o.dec0().toHMSString())
0745                                        .arg(o.alt().Degrees())
0746                                        .arg(passed_meridian ? "yes" : "no")
0747                                        .arg(o.ra().Hours())
0748                                        .arg(LST.toHMSString())
0749                                        .arg(ltWhen.toString("HH:mm:ss"));
0750 
0751     if (is_setting)
0752         *is_setting = passed_meridian;
0753 
0754     return o.alt().Degrees();
0755 }
0756 
0757 } // namespace