diff --git a/kstars/ekos/scheduler/scheduler.h b/kstars/ekos/scheduler/scheduler.h --- a/kstars/ekos/scheduler/scheduler.h +++ b/kstars/ekos/scheduler/scheduler.h @@ -27,7 +27,6 @@ class QProgressIndicator; class GeoLocation; -class KSMoon; class SchedulerJob; class SkyObject; @@ -167,17 +166,6 @@ */ void setSolverAction(Align::GotoMode mode); - /** - * @brief findAltitude Find altitude given a specific time - * @param target Target - * @param when date time to find altitude - * @param is_setting whether target is setting at the argument time (optional). - * @param debug outputs calculation to log file (optional). - * @return Altitude of the target at the specific date and time given. - * @warning This function uses the current KStars geolocation. - */ - static double findAltitude(const SkyPoint &target, const QDateTime &when, bool *is_setting = nullptr, bool debug = false); - /** @defgroup SchedulerDBusInterface Ekos DBus Interface - Scheduler Module * Ekos::Align interface provides primary functions to run and stop the scheduler. */ @@ -465,22 +453,6 @@ int16_t getDarkSkyScore(QDateTime const &when = QDateTime()) const; /** - * @brief getAltitudeScore Get the altitude score of an object. The higher the better - * @param job Target job - * @param when date and time to check the target altitude, now if omitted. - * @return Altitude score. Target altitude below minimum altitude required by job or setting target under 3 degrees below minimum altitude get bad score. - */ - int16_t getAltitudeScore(SchedulerJob const *job, QDateTime const &when = QDateTime()) const; - - /** - * @brief getMoonSeparationScore Get moon separation score. The further apart, the better, up a maximum score of 20. - * @param job Target job - * @param when date and time to check the moon separation, now if omitted. - * @return Moon separation score - */ - int16_t getMoonSeparationScore(SchedulerJob const *job, QDateTime const &when = QDateTime()) const; - - /** * @brief calculateJobScore Calculate job dark sky score, altitude score, and moon separation scores and returns the sum. * @param job Target * @param when date and time to evaluate constraints, now if omitted. @@ -495,25 +467,6 @@ int16_t getWeatherScore() const; /** - * @brief calculateAltitudeTime calculate the altitude time given the minimum altitude given. - * @param job active target - * @param minAltitude minimum altitude required - * @param minMoonAngle minimum separation from the moon. -1 to ignore. - * @param when date and time to start searching from, now if omitted. - * @return The date and time the target is at or above the argument altitude, valid if found, invalid if not achievable (always under altitude). - */ - QDateTime calculateAltitudeTime(SchedulerJob const *job, double minAltitude, double minMoonAngle = -1, QDateTime const &when = QDateTime()) const; - - /** - * @brief calculateCulmination find culmination time adjust for the job offset - * @param job active target - * @param offset_minutes offset in minutes before culmination to search for. - * @param when date and time to start searching from, now if omitted - * @return The date and time the target is in entering the culmination interval, valid if found, invalid if not achievable (currently always valid). - */ - QDateTime calculateCulmination(SchedulerJob const *job, int offset_minutes, QDateTime const &when = QDateTime()) const; - - /** * @brief calculateDawnDusk Get dawn and dusk times for today */ void calculateDawnDusk(); @@ -624,13 +577,6 @@ bool processJobInfo(XMLEle *root); /** - * @brief getCurrentMoonSeparation Get current moon separation in degrees at current time for the given job - * @param job scheduler job - * @return Separation in degrees - */ - double getCurrentMoonSeparation(SchedulerJob const *job) const; - - /** * @brief updatePreDawn Update predawn time depending on current time and user offset */ void updatePreDawn(); @@ -713,8 +659,6 @@ QProgressIndicator *pi { nullptr }; /// Are we editing a job right now? Job row index int jobUnderEdit { -1 }; - /// Pointer to Moon object - KSMoon *moon { nullptr }; /// Pointer to Geograpic locatoin GeoLocation *geo { nullptr }; /// How many repeated job batches did we complete thus far? diff --git a/kstars/ekos/scheduler/scheduler.cpp b/kstars/ekos/scheduler/scheduler.cpp --- a/kstars/ekos/scheduler/scheduler.cpp +++ b/kstars/ekos/scheduler/scheduler.cpp @@ -76,8 +76,6 @@ QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "indiStatusChanged", this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "ekosStatusChanged", this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); - moon = dynamic_cast(KStarsData::Instance()->skyComposite()->findByName("Moon")); - sleepLabel->setPixmap( QIcon::fromTheme("chronometer").pixmap(QSize(32, 32))); sleepLabel->hide(); @@ -1598,7 +1596,7 @@ break; } // Check whether the current job has a positive altitude score at the time of startup - else if (-90 < currentJob->getMinAltitude() && getAltitudeScore(currentJob, currentJob->getStartupTime()) < 0) + else if (-90 < currentJob->getMinAltitude() && currentJob->getAltitudeScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); @@ -1608,7 +1606,7 @@ break; } // Check whether the current job has a positive Moon separation score at the time of startup - else if (0 < currentJob->getMinMoonSeparation() && getMoonSeparationScore(currentJob, currentJob->getStartupTime()) < 0) + else if (0 < currentJob->getMinMoonSeparation() && currentJob->getMoonSeparationScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); @@ -1765,7 +1763,7 @@ if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition()) { // Consolidate the culmination time, with offset, of the current job - QDateTime const nextCulminationTime = calculateCulmination(currentJob, currentJob->getCulminationOffset(), currentJob->getStartupTime()); + QDateTime const nextCulminationTime = currentJob->calculateCulmination(currentJob->getStartupTime()); if (nextCulminationTime.isValid()) // Guaranteed { @@ -1806,7 +1804,7 @@ if (-90 < currentJob->getMinAltitude()) { // Consolidate a new altitude time from the startup time of the current job - QDateTime const nextAltitudeTime = calculateAltitudeTime(currentJob, currentJob->getMinAltitude(), currentJob->getMinMoonSeparation(), currentJob->getStartupTime()); + QDateTime const nextAltitudeTime = currentJob->calculateAltitudeTime(currentJob->getStartupTime()); if (nextAltitudeTime.isValid()) { @@ -1835,7 +1833,7 @@ break; } - Q_ASSERT_X(0 <= getAltitudeScore(currentJob, currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); + Q_ASSERT_X(0 <= currentJob->getAltitudeScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); } @@ -2042,164 +2040,6 @@ } } -double Scheduler::findAltitude(const SkyPoint &target, const QDateTime &when, bool * is_setting, bool debug) -{ - // FIXME: block calculating target coordinates at a particular time is duplicated in several places - - GeoLocation * const geo = KStarsData::Instance()->geo(); - - // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! - KStarsDateTime ltWhen(when.isValid() ? - Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : - KStarsData::Instance()->lt()); - - // Create a sky object with the target catalog coordinates - SkyObject o; - o.setRA0(target.ra0()); - o.setDec0(target.dec0()); - - // Update RA/DEC of the target for the current fraction of the day - KSNumbers numbers(ltWhen.djd()); - o.updateCoordsNow(&numbers); - - // Calculate alt/az coordinates using KStars instance's geolocation - CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); - o.EquatorialToHorizontal(&LST, geo->lat()); - - // Hours are reduced to [0,24[, meridian being at 0 - double offset = LST.Hours() - o.ra().Hours(); - if (24.0 <= offset) - offset -= 24.0; - else if (offset < 0.0) - offset += 24.0; - bool const passed_meridian = 0.0 <= offset && offset < 12.0; - - if (debug) - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7") - .arg(o.ra().toHMSString()) - .arg(o.ra0().toHMSString()) - .arg(o.dec().toHMSString()) - .arg(o.dec0().toHMSString()) - .arg(o.alt().Degrees()) - .arg(passed_meridian ? "yes" : "no") - .arg(o.ra().Hours()) - .arg(LST.toHMSString()) - .arg(ltWhen.toString("HH:mm:ss")); - - if (is_setting) - *is_setting = passed_meridian; - - return o.alt().Degrees(); -} - -QDateTime Scheduler::calculateAltitudeTime(SchedulerJob const *job, double minAltitude, double minMoonAngle, QDateTime const &when) const -{ - // FIXME: block calculating target coordinates at a particular time is duplicated in several places - - // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! - KStarsDateTime ltWhen(when.isValid() ? - Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : - KStarsData::Instance()->lt()); - - // Create a sky object with the target catalog coordinates - SkyPoint const target = job->getTargetCoords(); - SkyObject o; - o.setRA0(target.ra0()); - o.setDec0(target.dec0()); - - // Calculate the UT at the argument time - KStarsDateTime const ut = geo->LTtoUT(ltWhen); - - double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff(); - - // Within the next 24 hours, search when the job target matches the altitude and moon constraints - for (unsigned int minute = 0; minute < 24 * 60; minute++) - { - KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60)); - - // Update RA/DEC of the target for the current fraction of the day - KSNumbers numbers(ltOffset.djd()); - o.updateCoordsNow(&numbers); - - // Compute local sidereal time for the current fraction of the day, calculate altitude - CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltOffset).gst()); - o.EquatorialToHorizontal(&LST, geo->lat()); - double const altitude = o.alt().Degrees(); - - if (minAltitude <= altitude) - { - // Don't test proximity to dawn in this situation, we only cater for altitude here - - // Continue searching if Moon separation is not good enough - if (0 < minMoonAngle && getMoonSeparationScore(job, ltOffset) < 0) - continue; - - // Continue searching if target is setting and under the cutoff - double offset = LST.Hours() - o.ra().Hours(); - if (24.0 <= offset) - offset -= 24.0; - else if (offset < 0.0) - offset += 24.0; - if (0.0 <= offset && offset < 12.0) - if (altitude - SETTING_ALTITUDE_CUTOFF < minAltitude) - continue; - - return ltOffset; - } - } - - return QDateTime(); -} - -QDateTime Scheduler::calculateCulmination(SchedulerJob const *job, int offset_minutes, QDateTime const &when) const -{ - // FIXME: culmination calculation is a min altitude requirement, should be an interval altitude requirement - // FIXME: block calculating target coordinates at a particular time is duplicated in calculateCulmination - - // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! - KStarsDateTime ltWhen(when.isValid() ? - Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : - KStarsData::Instance()->lt()); - - // Create a sky object with the target catalog coordinates - SkyPoint const target = job->getTargetCoords(); - SkyObject o; - o.setRA0(target.ra0()); - o.setDec0(target.dec0()); - - // Update RA/DEC for the argument date/time - KSNumbers numbers(ltWhen.djd()); - o.updateCoordsNow(&numbers); - - // Calculate transit date/time at the argument date - transitTime requires UT and returns LocalTime - KStarsDateTime transitDateTime(ltWhen.date(), o.transitTime(geo->LTtoUT(ltWhen), geo), Qt::LocalTime); - - // Shift transit date/time by the argument offset - KStarsDateTime observationDateTime = transitDateTime.addSecs(offset_minutes * 60); - - // Relax observation time, culmination calculation is stable at minute only - KStarsDateTime relaxedDateTime = observationDateTime.addSecs(Options::leadTime() * 60); - - // Verify resulting observation time is under lead time vs. argument time - // If sooner, delay by 8 hours to get to the next transit - perhaps in a third call - if (relaxedDateTime < ltWhen) - { - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' startup %2 is posterior to transit %3, shifting by 8 hours.") - .arg(job->getName()) - .arg(ltWhen.toString(job->getDateTimeDisplayFormat())) - .arg(relaxedDateTime.toString(job->getDateTimeDisplayFormat())); - - return calculateCulmination(job, offset_minutes, when.addSecs(8 * 60 * 60)); - } - - // Guarantees - culmination calculation is stable at minute level, so relax by lead time - Q_ASSERT_X(observationDateTime.isValid(), __FUNCTION__, "Observation time for target culmination is valid."); - Q_ASSERT_X(ltWhen <= relaxedDateTime, __FUNCTION__, "Observation time for target culmination is at or after than argument time"); - - // Return consolidated culmination time - return Qt::UTC == observationDateTime.timeSpec() ? geo->UTtoLT(observationDateTime) : observationDateTime; -} - int16_t Scheduler::getWeatherScore() const { if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) @@ -2304,7 +2144,7 @@ */ if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/) { - int16_t const altitudeScore = getAltitudeScore(job, when); + int16_t const altitudeScore = job->getAltitudeScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' altitude score is %2 at %3") .arg(job->getName()) @@ -2316,7 +2156,7 @@ if (0 <= total) { - int16_t const moonSeparationScore = getMoonSeparationScore(job, when); + int16_t const moonSeparationScore = job->getMoonSeparationScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' Moon separation score is %2 at %3") .arg(job->getName()) @@ -2334,179 +2174,6 @@ return total; } -int16_t Scheduler::getAltitudeScore(SchedulerJob const *job, QDateTime const &when) const -{ - // FIXME: block calculating target coordinates at a particular time is duplicated in several places - - // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! - KStarsDateTime ltWhen(when.isValid() ? - Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : - KStarsData::Instance()->lt()); - - // Create a sky object with the target catalog coordinates - SkyPoint const target = job->getTargetCoords(); - SkyObject o; - o.setRA0(target.ra0()); - o.setDec0(target.dec0()); - - // Update RA/DEC of the target for the current fraction of the day - KSNumbers numbers(ltWhen.djd()); - o.updateCoordsNow(&numbers); - - // Compute local sidereal time for the current fraction of the day, calculate altitude - CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); - o.EquatorialToHorizontal(&LST, geo->lat()); - double const altitude = o.alt().Degrees(); - - double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff(); - int16_t score = BAD_SCORE - 1; - - // If altitude is negative, bad score - // FIXME: some locations may allow negative altitudes - if (altitude < 0) - { - score = BAD_SCORE; - } - else if (-90 < job->getMinAltitude()) - { - // If under altitude constraint, bad score - if (altitude < job->getMinAltitude()) - score = BAD_SCORE; - // Else if setting and under altitude cutoff, job would end soon after starting, bad score - // FIXME: half bad score when under altitude cutoff risk getting positive again - else - { - double offset = LST.Hours() - o.ra().Hours(); - if (24.0 <= offset) - offset -= 24.0; - else if (offset < 0.0) - offset += 24.0; - if (0.0 <= offset && offset < 12.0) - if (altitude - SETTING_ALTITUDE_CUTOFF < job->getMinAltitude()) - score = BAD_SCORE / 2; - } - } - // If not constrained but below minimum hard altitude, set score to 10% of altitude value - else if (altitude < minAltitude->minimum()) - { - score = static_cast (altitude / 10.0); - } - - // Else default score calculation without altitude constraint - if (score < BAD_SCORE) - score = static_cast ((1.5 * pow(1.06, altitude)) - (minAltitude->minimum() / 10.0)); - - //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target altitude is %3 degrees at %2 (score %4).") - // .arg(job->getName()) - // .arg(when.toString(job->getDateTimeDisplayFormat())) - // .arg(currentAlt, 0, 'f', minAltitude->decimals()) - // .arg(QString::asprintf("%+d", score)); - - return score; -} - -double Scheduler::getCurrentMoonSeparation(SchedulerJob const *job) const -{ - // FIXME: block calculating target coordinates at a particular time is duplicated in several places - - // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! - KStarsDateTime ltWhen(KStarsData::Instance()->lt()); - - // Create a sky object with the target catalog coordinates - SkyPoint const target = job->getTargetCoords(); - SkyObject o; - o.setRA0(target.ra0()); - o.setDec0(target.dec0()); - - // Update RA/DEC of the target for the current fraction of the day - KSNumbers numbers(ltWhen.djd()); - o.updateCoordsNow(&numbers); - - // Update moon - //ut = geo->LTtoUT(ltWhen); - //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation - //LST = geo->GSTtoLST(ut.gst()); - CachingDms LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); - moon->updateCoords(&numbers, true, geo->lat(), &LST, true); - - // Moon/Sky separation p - return moon->angularDistanceTo(&o).Degrees(); -} - -int16_t Scheduler::getMoonSeparationScore(SchedulerJob const *job, QDateTime const &when) const -{ - // FIXME: block calculating target coordinates at a particular time is duplicated in several places - - // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! - KStarsDateTime ltWhen(when.isValid() ? - Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : - KStarsData::Instance()->lt()); - - // Create a sky object with the target catalog coordinates - SkyPoint const target = job->getTargetCoords(); - SkyObject o; - o.setRA0(target.ra0()); - o.setDec0(target.dec0()); - - // Update RA/DEC of the target for the current fraction of the day - KSNumbers numbers(ltWhen.djd()); - o.updateCoordsNow(&numbers); - - // Update moon - //ut = geo->LTtoUT(ltWhen); - //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation - //LST = geo->GSTtoLST(ut.gst()); - CachingDms LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); - moon->updateCoords(&numbers, true, geo->lat(), &LST, true); - - double const moonAltitude = moon->alt().Degrees(); - - // Lunar illumination % - double const illum = moon->illum() * 100.0; - - // Moon/Sky separation p - double const separation = moon->angularDistanceTo(&o).Degrees(); - - // Zenith distance of the moon - double const zMoon = (90 - moonAltitude); - // Zenith distance of target - double const zTarget = (90 - o.alt().Degrees()); - - int16_t score = 0; - - // If target = Moon, or no illuminiation, or moon below horizon, return static score. - if (zMoon == zTarget || illum == 0 || zMoon >= 90) - score = 100; - else - { - // JM: Some magic voodoo formula I came up with! - double moonEffect = (pow(separation, 1.7) * pow(zMoon, 0.5)) / (pow(zTarget, 1.1) * pow(illum, 0.5)); - - // Limit to 0 to 100 range. - moonEffect = KSUtils::clamp(moonEffect, 0.0, 100.0); - - if (job->getMinMoonSeparation() > 0) - { - if (separation < job->getMinMoonSeparation()) - score = BAD_SCORE * 5; - else - score = moonEffect; - } - else - score = moonEffect; - } - - // Limit to 0 to 20 - score /= 5.0; - - //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target is %L3 degrees from Moon (score %2).") - // .arg(job->getName()) - // .arg(separation, 0, 'f', 3) - // .arg(QString::asprintf("%+d", score)); - - return score; -} - void Scheduler::calculateDawnDusk() { KSAlmanac ksal; @@ -3396,7 +3063,7 @@ SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); - double moonSeparation = getCurrentMoonSeparation(currentJob); + double moonSeparation = currentJob->getCurrentMoonSeparation(); if (moonSeparation < currentJob->getMinMoonSeparation()) { diff --git a/kstars/ekos/scheduler/schedulerjob.h b/kstars/ekos/scheduler/schedulerjob.h --- a/kstars/ekos/scheduler/schedulerjob.h +++ b/kstars/ekos/scheduler/schedulerjob.h @@ -13,16 +13,18 @@ #include #include +#include "ksmoon.h" class QTableWidgetItem; class QLabel; +class KSMoon; class dms; class SchedulerJob { public: - SchedulerJob() = default; + SchedulerJob(); /** @brief States of a SchedulerJob. */ typedef enum { @@ -384,6 +386,53 @@ */ static bool increasingStartupTimeOrder(SchedulerJob const *a, SchedulerJob const *b); + /** + * @brief getAltitudeScore Get the altitude score of an object. The higher the better + * @param when date and time to check the target altitude, now if omitted. + * @return Altitude score. Target altitude below minimum altitude required by job or setting target under 3 degrees below minimum altitude get bad score. + */ + int16_t getAltitudeScore(QDateTime const &when = QDateTime()) const; + + /** + * @brief getMoonSeparationScore Get moon separation score. The further apart, the better, up a maximum score of 20. + * @param when date and time to check the moon separation, now if omitted. + * @return Moon separation score + */ + int16_t getMoonSeparationScore(QDateTime const &when = QDateTime()) const; + + /** + * @brief getCurrentMoonSeparation Get current moon separation in degrees at current time for the given job + * @param job scheduler job + * @return Separation in degrees + */ + double getCurrentMoonSeparation() const; + + /** + * @brief calculateAltitudeTime calculate the altitude time given the minimum altitude given. + * @param when date and time to start searching from, now if omitted. + * @return The date and time the target is at or above the argument altitude, valid if found, invalid if not achievable (always under altitude). + */ + QDateTime calculateAltitudeTime(QDateTime const &when = QDateTime()) const; + + /** + * @brief calculateCulmination find culmination time adjust for the job offset + * @param when date and time to start searching from, now if omitted + * @return The date and time the target is in entering the culmination interval, valid if found, invalid if not achievable (currently always valid). + */ + QDateTime calculateCulmination(QDateTime const &when = QDateTime()) const; + + + /** + * @brief findAltitude Find altitude given a specific time + * @param target Target + * @param when date time to find altitude + * @param is_setting whether target is setting at the argument time (optional). + * @param debug outputs calculation to log file (optional). + * @return Altitude of the target at the specific date and time given. + * @warning This function uses the current KStars geolocation. + */ + static double findAltitude(const SkyPoint &target, const QDateTime &when, bool *is_setting = nullptr, bool debug = false); + private: QString name; SkyPoint targetCoords; @@ -450,4 +499,7 @@ bool lightFramesRequired { false }; QMap capturedFramesMap; + + /// Pointer to Moon object + KSMoon *moon { nullptr }; }; diff --git a/kstars/ekos/scheduler/schedulerjob.cpp b/kstars/ekos/scheduler/schedulerjob.cpp --- a/kstars/ekos/scheduler/schedulerjob.cpp +++ b/kstars/ekos/scheduler/schedulerjob.cpp @@ -11,13 +11,24 @@ #include "dms.h" #include "kstarsdata.h" +#include "skymapcomposite.h" #include "Options.h" #include "scheduler.h" #include #include +#include + +#define BAD_SCORE -1000 +#define MIN_ALTITUDE 15.0 + +SchedulerJob::SchedulerJob() +{ + moon = dynamic_cast(KStarsData::Instance()->skyComposite()->findByName("Moon")); +} + void SchedulerJob::setName(const QString &value) { name = value; @@ -47,7 +58,7 @@ startupCondition = fileStartupCondition; // Refresh altitude - invalid date/time is taken care of when rendering - altitudeAtStartup = Ekos::Scheduler::findAltitude(targetCoords, startupTime, &isSettingAtStartup); + altitudeAtStartup = findAltitude(targetCoords, startupTime, &isSettingAtStartup); /* Refresh estimated time - which update job cells */ setEstimatedTime(estimatedTime); @@ -85,21 +96,21 @@ { setCompletionCondition(FINISH_AT); completionTime = value; - altitudeAtCompletion = Ekos::Scheduler::findAltitude(targetCoords, completionTime, &isSettingAtCompletion); + altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion); setEstimatedTime(-1); } /* If completion time is invalid, and job is looping, keep completion time undefined */ else if (FINISH_LOOP == completionCondition) { completionTime = QDateTime(); - altitudeAtCompletion = Ekos::Scheduler::findAltitude(targetCoords, completionTime, &isSettingAtCompletion); + altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion); setEstimatedTime(-1); } /* If completion time is invalid, deduce completion from startup and duration */ else if (startupTime.isValid()) { completionTime = startupTime.addSecs(estimatedTime); - altitudeAtCompletion = Ekos::Scheduler::findAltitude(targetCoords, completionTime, &isSettingAtCompletion); + altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion); updateJobCells(); } /* Else just refresh estimated time - which update job cells */ @@ -332,7 +343,7 @@ { estimatedTime = value; completionTime = startupTime.addSecs(value); - altitudeAtCompletion = Ekos::Scheduler::findAltitude(targetCoords, completionTime, &isSettingAtCompletion); + altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion); } /* Else estimated time is simply stored as is - covers FINISH_LOOP from setCompletionTime */ else estimatedTime = value; @@ -527,7 +538,7 @@ { // FIXME: Cache altitude calculations bool is_setting = false; - double const alt = Ekos::Scheduler::findAltitude(targetCoords, QDateTime(), &is_setting); + double const alt = findAltitude(targetCoords, QDateTime(), &is_setting); altitudeCell->setText(QString("%1%L2°") .arg(QChar(is_setting ? 0x2193 : 0x2191)) @@ -682,12 +693,12 @@ { bool A_is_setting = job1->isSettingAtStartup; double const altA = when.isValid() ? - Ekos::Scheduler::findAltitude(job1->getTargetCoords(), when, &A_is_setting) : + findAltitude(job1->getTargetCoords(), when, &A_is_setting) : job1->altitudeAtStartup; bool B_is_setting = job2->isSettingAtStartup; double const altB = when.isValid() ? - Ekos::Scheduler::findAltitude(job2->getTargetCoords(), when, &B_is_setting) : + findAltitude(job2->getTargetCoords(), when, &B_is_setting) : job2->altitudeAtStartup; // Sort with the setting target first @@ -704,3 +715,340 @@ { return job1->getStartupTime() < job2->getStartupTime(); } + +int16_t SchedulerJob::getAltitudeScore(QDateTime const &when) const +{ + // FIXME: block calculating target coordinates at a particular time is duplicated in several places + GeoLocation *geo = KStarsData::Instance()->geo(); + + // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! + KStarsDateTime ltWhen(when.isValid() ? + Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : + KStarsData::Instance()->lt()); + + // Create a sky object with the target catalog coordinates + SkyPoint const target = getTargetCoords(); + SkyObject o; + o.setRA0(target.ra0()); + o.setDec0(target.dec0()); + + // Update RA/DEC of the target for the current fraction of the day + KSNumbers numbers(ltWhen.djd()); + o.updateCoordsNow(&numbers); + + // Compute local sidereal time for the current fraction of the day, calculate altitude + CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); + o.EquatorialToHorizontal(&LST, geo->lat()); + double const altitude = o.alt().Degrees(); + + double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff(); + int16_t score = BAD_SCORE - 1; + + // If altitude is negative, bad score + // FIXME: some locations may allow negative altitudes + if (altitude < 0) + { + score = BAD_SCORE; + } + else if (-90 < getMinAltitude()) + { + // If under altitude constraint, bad score + if (altitude < getMinAltitude()) + score = BAD_SCORE; + // Else if setting and under altitude cutoff, job would end soon after starting, bad score + // FIXME: half bad score when under altitude cutoff risk getting positive again + else + { + double offset = LST.Hours() - o.ra().Hours(); + if (24.0 <= offset) + offset -= 24.0; + else if (offset < 0.0) + offset += 24.0; + if (0.0 <= offset && offset < 12.0) + if (altitude - SETTING_ALTITUDE_CUTOFF < getMinAltitude()) + score = BAD_SCORE / 2; + } + } + // If not constrained but below minimum hard altitude, set score to 10% of altitude value + else if (altitude < MIN_ALTITUDE) + { + score = static_cast (altitude / 10.0); + } + + // Else default score calculation without altitude constraint + if (score < BAD_SCORE) + score = static_cast ((1.5 * pow(1.06, altitude)) - (MIN_ALTITUDE / 10.0)); + + //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target altitude is %3 degrees at %2 (score %4).") + // .arg(getName()) + // .arg(when.toString(getDateTimeDisplayFormat())) + // .arg(currentAlt, 0, 'f', minAltitude->decimals()) + // .arg(QString::asprintf("%+d", score)); + + return score; +} + +int16_t SchedulerJob::getMoonSeparationScore(QDateTime const &when) const +{ + // FIXME: block calculating target coordinates at a particular time is duplicated in several places + GeoLocation *geo = KStarsData::Instance()->geo(); + + // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! + KStarsDateTime ltWhen(when.isValid() ? + Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : + KStarsData::Instance()->lt()); + + // Create a sky object with the target catalog coordinates + SkyPoint const target = getTargetCoords(); + SkyObject o; + o.setRA0(target.ra0()); + o.setDec0(target.dec0()); + + // Update RA/DEC of the target for the current fraction of the day + KSNumbers numbers(ltWhen.djd()); + o.updateCoordsNow(&numbers); + + // Update moon + //ut = geo->LTtoUT(ltWhen); + //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation + //LST = geo->GSTtoLST(ut.gst()); + CachingDms LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); + moon->updateCoords(&numbers, true, geo->lat(), &LST, true); + + double const moonAltitude = moon->alt().Degrees(); + + // Lunar illumination % + double const illum = moon->illum() * 100.0; + + // Moon/Sky separation p + double const separation = moon->angularDistanceTo(&o).Degrees(); + + // Zenith distance of the moon + double const zMoon = (90 - moonAltitude); + // Zenith distance of target + double const zTarget = (90 - o.alt().Degrees()); + + int16_t score = 0; + + // If target = Moon, or no illuminiation, or moon below horizon, return static score. + if (zMoon == zTarget || illum == 0 || zMoon >= 90) + score = 100; + else + { + // JM: Some magic voodoo formula I came up with! + double moonEffect = (pow(separation, 1.7) * pow(zMoon, 0.5)) / (pow(zTarget, 1.1) * pow(illum, 0.5)); + + // Limit to 0 to 100 range. + moonEffect = KSUtils::clamp(moonEffect, 0.0, 100.0); + + if (getMinMoonSeparation() > 0) + { + if (separation < getMinMoonSeparation()) + score = BAD_SCORE * 5; + else + score = moonEffect; + } + else + score = moonEffect; + } + + // Limit to 0 to 20 + score /= 5.0; + + //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target is %L3 degrees from Moon (score %2).") + // .arg(getName()) + // .arg(separation, 0, 'f', 3) + // .arg(QString::asprintf("%+d", score)); + + return score; +} + + +double SchedulerJob::getCurrentMoonSeparation() const +{ + // FIXME: block calculating target coordinates at a particular time is duplicated in several places + GeoLocation *geo = KStarsData::Instance()->geo(); + + // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! + KStarsDateTime ltWhen(KStarsData::Instance()->lt()); + + // Create a sky object with the target catalog coordinates + SkyPoint const target = getTargetCoords(); + SkyObject o; + o.setRA0(target.ra0()); + o.setDec0(target.dec0()); + + // Update RA/DEC of the target for the current fraction of the day + KSNumbers numbers(ltWhen.djd()); + o.updateCoordsNow(&numbers); + + // Update moon + //ut = geo->LTtoUT(ltWhen); + //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation + //LST = geo->GSTtoLST(ut.gst()); + CachingDms LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); + moon->updateCoords(&numbers, true, geo->lat(), &LST, true); + + // Moon/Sky separation p + return moon->angularDistanceTo(&o).Degrees(); +} + +QDateTime SchedulerJob::calculateAltitudeTime(QDateTime const &when) const +{ + // FIXME: block calculating target coordinates at a particular time is duplicated in several places + GeoLocation *geo = KStarsData::Instance()->geo(); + + // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! + KStarsDateTime ltWhen(when.isValid() ? + Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : + KStarsData::Instance()->lt()); + + // Create a sky object with the target catalog coordinates + SkyPoint const target = getTargetCoords(); + SkyObject o; + o.setRA0(target.ra0()); + o.setDec0(target.dec0()); + + // Calculate the UT at the argument time + KStarsDateTime const ut = geo->LTtoUT(ltWhen); + + double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff(); + + // Within the next 24 hours, search when the job target matches the altitude and moon constraints + for (unsigned int minute = 0; minute < 24 * 60; minute++) + { + KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60)); + + // Update RA/DEC of the target for the current fraction of the day + KSNumbers numbers(ltOffset.djd()); + o.updateCoordsNow(&numbers); + + // Compute local sidereal time for the current fraction of the day, calculate altitude + CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltOffset).gst()); + o.EquatorialToHorizontal(&LST, geo->lat()); + double const altitude = o.alt().Degrees(); + + if (getMinAltitude() <= altitude) + { + // Don't test proximity to dawn in this situation, we only cater for altitude here + + // Continue searching if Moon separation is not good enough + if (0 < getMinMoonSeparation() && getMoonSeparationScore(ltOffset) < 0) + continue; + + // Continue searching if target is setting and under the cutoff + double offset = LST.Hours() - o.ra().Hours(); + if (24.0 <= offset) + offset -= 24.0; + else if (offset < 0.0) + offset += 24.0; + if (0.0 <= offset && offset < 12.0) + if (altitude - SETTING_ALTITUDE_CUTOFF < getMinAltitude()) + continue; + + return ltOffset; + } + } + + return QDateTime(); +} + +QDateTime SchedulerJob::calculateCulmination(QDateTime const &when) const +{ + // FIXME: culmination calculation is a min altitude requirement, should be an interval altitude requirement + GeoLocation *geo = KStarsData::Instance()->geo(); + // FIXME: block calculating target coordinates at a particular time is duplicated in calculateCulmination + + // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! + KStarsDateTime ltWhen(when.isValid() ? + Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : + KStarsData::Instance()->lt()); + + // Create a sky object with the target catalog coordinates + SkyPoint const target = getTargetCoords(); + SkyObject o; + o.setRA0(target.ra0()); + o.setDec0(target.dec0()); + + // Update RA/DEC for the argument date/time + KSNumbers numbers(ltWhen.djd()); + o.updateCoordsNow(&numbers); + + // Calculate transit date/time at the argument date - transitTime requires UT and returns LocalTime + KStarsDateTime transitDateTime(ltWhen.date(), o.transitTime(geo->LTtoUT(ltWhen), geo), Qt::LocalTime); + + // Shift transit date/time by the argument offset + KStarsDateTime observationDateTime = transitDateTime.addSecs(getCulminationOffset() * 60); + + // Relax observation time, culmination calculation is stable at minute only + KStarsDateTime relaxedDateTime = observationDateTime.addSecs(Options::leadTime() * 60); + + // Verify resulting observation time is under lead time vs. argument time + // If sooner, delay by 8 hours to get to the next transit - perhaps in a third call + if (relaxedDateTime < ltWhen) + { + qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' startup %2 is posterior to transit %3, shifting by 8 hours.") + .arg(getName()) + .arg(ltWhen.toString(getDateTimeDisplayFormat())) + .arg(relaxedDateTime.toString(getDateTimeDisplayFormat())); + + return calculateCulmination(when.addSecs(8 * 60 * 60)); + } + + // Guarantees - culmination calculation is stable at minute level, so relax by lead time + Q_ASSERT_X(observationDateTime.isValid(), __FUNCTION__, "Observation time for target culmination is valid."); + Q_ASSERT_X(ltWhen <= relaxedDateTime, __FUNCTION__, "Observation time for target culmination is at or after than argument time"); + + // Return consolidated culmination time + return Qt::UTC == observationDateTime.timeSpec() ? geo->UTtoLT(observationDateTime) : observationDateTime; +} + +double SchedulerJob::findAltitude(const SkyPoint &target, const QDateTime &when, bool * is_setting, bool debug) +{ + // FIXME: block calculating target coordinates at a particular time is duplicated in several places + + GeoLocation * const geo = KStarsData::Instance()->geo(); + + // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! + KStarsDateTime ltWhen(when.isValid() ? + Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : + KStarsData::Instance()->lt()); + + // Create a sky object with the target catalog coordinates + SkyObject o; + o.setRA0(target.ra0()); + o.setDec0(target.dec0()); + + // Update RA/DEC of the target for the current fraction of the day + KSNumbers numbers(ltWhen.djd()); + o.updateCoordsNow(&numbers); + + // Calculate alt/az coordinates using KStars instance's geolocation + CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); + o.EquatorialToHorizontal(&LST, geo->lat()); + + // Hours are reduced to [0,24[, meridian being at 0 + double offset = LST.Hours() - o.ra().Hours(); + if (24.0 <= offset) + offset -= 24.0; + else if (offset < 0.0) + offset += 24.0; + bool const passed_meridian = 0.0 <= offset && offset < 12.0; + + if (debug) + qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7") + .arg(o.ra().toHMSString()) + .arg(o.ra0().toHMSString()) + .arg(o.dec().toHMSString()) + .arg(o.dec0().toHMSString()) + .arg(o.alt().Degrees()) + .arg(passed_meridian ? "yes" : "no") + .arg(o.ra().Hours()) + .arg(LST.toHMSString()) + .arg(ltWhen.toString("HH:mm:ss")); + + if (is_setting) + *is_setting = passed_meridian; + + return o.alt().Degrees(); +}