Kstars

schedulerjob.cpp
1 /* Ekos Scheduler Job
2  SPDX-FileCopyrightText: Jasem Mutlaq <[email protected]>
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "schedulerjob.h"
8 
9 #include "dms.h"
10 #include "artificialhorizoncomponent.h"
11 #include "kstarsdata.h"
12 #include "skymapcomposite.h"
13 #include "Options.h"
14 #include "scheduler.h"
15 #include "ksalmanac.h"
16 
17 #include <knotification.h>
18 
19 #include <QTableWidgetItem>
20 
21 #include <ekos_scheduler_debug.h>
22 
23 #define BAD_SCORE -1000
24 #define MIN_ALTITUDE 15.0
25 
26 bool SchedulerJob::m_UpdateGraphics = true;
27 
28 GeoLocation *SchedulerJob::storedGeo = nullptr;
29 KStarsDateTime *SchedulerJob::storedLocalTime = nullptr;
30 ArtificialHorizon *SchedulerJob::storedHorizon = nullptr;
31 
32 QString SchedulerJob::jobStatusString(JOBStatus state)
33 {
34  switch(state)
35  {
36  case SchedulerJob::JOB_IDLE:
37  return "IDLE";
38  case SchedulerJob::JOB_EVALUATION:
39  return "EVAL";
40  case SchedulerJob::JOB_SCHEDULED:
41  return "SCHEDULED";
42  case SchedulerJob::JOB_BUSY:
43  return "BUSY";
44  case SchedulerJob::JOB_ERROR:
45  return "ERROR";
46  case SchedulerJob::JOB_ABORTED:
47  return "ABORTED";
48  case SchedulerJob::JOB_INVALID:
49  return "INVALID";
50  case SchedulerJob::JOB_COMPLETE:
51  return "COMPLETE";
52  }
53  return QString("????");
54 }
55 
56 QString SchedulerJob::jobStageString(JOBStage state)
57 {
58  switch(state)
59  {
60  case SchedulerJob::STAGE_IDLE:
61  return "IDLE";
62  case SchedulerJob::STAGE_SLEWING:
63  return "SLEWING";
64  case SchedulerJob::STAGE_SLEW_COMPLETE:
65  return "SLEW_COMPLETE";
66  case SchedulerJob::STAGE_FOCUSING:
67  return "FOCUSING";
68  case SchedulerJob::STAGE_FOCUS_COMPLETE:
69  return "FOCUS_COMPLETE";
70  case SchedulerJob::STAGE_ALIGNING:
71  return "ALIGNING";
72  case SchedulerJob::STAGE_ALIGN_COMPLETE:
73  return "ALIGN_COMPLETE";
74  case SchedulerJob::STAGE_RESLEWING:
75  return "RESLEWING";
76  case SchedulerJob::STAGE_RESLEWING_COMPLETE:
77  return "RESLEWING_COMPLETE";
78  case SchedulerJob::STAGE_POSTALIGN_FOCUSING:
79  return "POSTALIGN_FOCUSING";
80  case SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE:
81  return "POSTALIGN_FOCUSING_COMPLETE";
82  case SchedulerJob::STAGE_GUIDING:
83  return "GUIDING";
84  case SchedulerJob::STAGE_GUIDING_COMPLETE:
85  return "GUIDING_COMPLETE";
86  case SchedulerJob::STAGE_CAPTURING:
87  return "CAPTURING";
88  case SchedulerJob::STAGE_COMPLETE:
89  return "COMPLETE";
90  }
91  return QString("????");
92 }
93 
94 QString SchedulerJob::startupConditionString(SchedulerJob::StartupCondition condition)
95 {
96  switch(condition)
97  {
98  case START_ASAP:
99  return "ASAP";
100  case START_CULMINATION:
101  return "CULM";
102  case START_AT:
103  return "AT";
104  }
105  return QString("????");
106 }
107 
108 QString SchedulerJob::jobStartupConditionString(SchedulerJob::StartupCondition condition) const
109 {
110  switch(condition)
111  {
112  case START_ASAP:
113  return "ASAP";
114  case START_CULMINATION:
115  return "CULM";
116  case START_AT:
117  return QString("AT %1").arg(getFileStartupTime().toString("MM/dd hh:mm"));
118  }
119  return QString("????");
120 }
121 
122 QString SchedulerJob::completionConditionString(SchedulerJob::CompletionCondition condition)
123 {
124  switch(condition)
125  {
126  case FINISH_SEQUENCE:
127  return "FINISH";
128  case FINISH_REPEAT:
129  return "REPEAT";
130  case FINISH_LOOP:
131  return "LOOP";
132  case FINISH_AT:
133  return "AT";
134  }
135  return QString("????");
136 }
137 
138 QString SchedulerJob::jobCompletionConditionString(SchedulerJob::CompletionCondition condition) const
139 {
140  switch(condition)
141  {
142  case FINISH_SEQUENCE:
143  return "FINISH";
144  case FINISH_REPEAT:
145  return "REPEAT";
146  case FINISH_LOOP:
147  return "LOOP";
148  case FINISH_AT:
149  return QString("AT %1").arg(getCompletionTime().toString("MM/dd hh:mm"));
150  }
151  return QString("????");
152 }
153 
154 SchedulerJob::SchedulerJob()
155 {
156  moon = dynamic_cast<KSMoon *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Moon")));
157 }
158 
159 // Private constructor for unit testing.
160 SchedulerJob::SchedulerJob(KSMoon *moonPtr)
161 {
162  moon = moonPtr;
163 }
164 
165 void SchedulerJob::setName(const QString &value)
166 {
167  name = value;
168  updateJobCells();
169 }
170 
171 KStarsDateTime SchedulerJob::getLocalTime()
172 {
174 }
175 
176 GeoLocation const *SchedulerJob::getGeo()
177 {
178  if (hasGeo())
179  return storedGeo;
180  return KStarsData::Instance()->geo();
181 }
182 
183 ArtificialHorizon const *SchedulerJob::getHorizon()
184 {
185  if (hasHorizon())
186  return storedHorizon;
187  if (KStarsData::Instance() == nullptr || KStarsData::Instance()->skyComposite() == nullptr
188  || KStarsData::Instance()->skyComposite()->artificialHorizon() == nullptr)
189  return nullptr;
190  return &KStarsData::Instance()->skyComposite()->artificialHorizon()->getHorizon();
191 }
192 
193 void SchedulerJob::setStartupCondition(const StartupCondition &value)
194 {
195  startupCondition = value;
196 
197  /* Keep startup time and condition valid */
198  if (value == START_ASAP)
199  startupTime = QDateTime();
200 
201  /* Refresh estimated time - which update job cells */
202  setEstimatedTime(estimatedTime);
203 
204  /* Refresh dawn and dusk for startup date */
205  calculateDawnDusk(startupTime, nextDawn, nextDusk);
206 }
207 
208 void SchedulerJob::setStartupTime(const QDateTime &value)
209 {
210  startupTime = value;
211 
212  /* Keep startup time and condition valid */
213  if (value.isValid())
214  startupCondition = START_AT;
215  else
216  startupCondition = fileStartupCondition;
217 
218  // Refresh altitude - invalid date/time is taken care of when rendering
219  altitudeAtStartup = findAltitude(targetCoords, startupTime, &isSettingAtStartup);
220 
221  /* Refresh estimated time - which update job cells */
222  setEstimatedTime(estimatedTime);
223 
224  /* Refresh dawn and dusk for startup date */
225  calculateDawnDusk(startupTime, nextDawn, nextDusk);
226 }
227 
228 void SchedulerJob::setSequenceFile(const QUrl &value)
229 {
230  sequenceFile = value;
231 }
232 
233 void SchedulerJob::setFITSFile(const QUrl &value)
234 {
235  fitsFile = value;
236 }
237 
238 void SchedulerJob::setMinAltitude(const double &value)
239 {
240  minAltitude = value;
241 }
242 
243 bool SchedulerJob::hasAltitudeConstraint() const
244 {
245  return hasMinAltitude() ||
246  (enforceArtificialHorizon && (getHorizon() != nullptr) && getHorizon()->altitudeConstraintsExist());
247 }
248 
249 void SchedulerJob::setMinMoonSeparation(const double &value)
250 {
251  minMoonSeparation = value;
252 }
253 
254 void SchedulerJob::setEnforceWeather(bool value)
255 {
256  enforceWeather = value;
257 }
258 
259 void SchedulerJob::setGreedyCompletionTime(const QDateTime &value)
260 {
261  greedyCompletionTime = value;
262 }
263 
264 void SchedulerJob::setCompletionTime(const QDateTime &value)
265 {
266  setGreedyCompletionTime(QDateTime());
267 
268  /* If completion time is valid, automatically switch condition to FINISH_AT */
269  if (value.isValid())
270  {
271  setCompletionCondition(FINISH_AT);
272  completionTime = value;
273  altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion);
274  setEstimatedTime(-1);
275  }
276  /* If completion time is invalid, and job is looping, keep completion time undefined */
277  else if (FINISH_LOOP == completionCondition)
278  {
279  completionTime = QDateTime();
280  altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion);
281  setEstimatedTime(-1);
282  }
283  /* If completion time is invalid, deduce completion from startup and duration */
284  else if (startupTime.isValid())
285  {
286  completionTime = startupTime.addSecs(estimatedTime);
287  altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion);
288  updateJobCells();
289  }
290  /* Else just refresh estimated time - which update job cells */
291  else setEstimatedTime(estimatedTime);
292 
293 
294  /* Invariants */
295  Q_ASSERT_X(completionTime.isValid() ?
296  (FINISH_AT == completionCondition || FINISH_REPEAT == completionCondition || FINISH_SEQUENCE == completionCondition) :
297  FINISH_LOOP == completionCondition,
298  __FUNCTION__, "Valid completion time implies job is FINISH_AT/REPEAT/SEQUENCE, else job is FINISH_LOOP.");
299 }
300 
301 void SchedulerJob::setCompletionCondition(const CompletionCondition &value)
302 {
303  completionCondition = value;
304 
305  // Update repeats requirement, looping jobs have none
306  switch (completionCondition)
307  {
308  case FINISH_LOOP:
309  setCompletionTime(QDateTime());
310  /* Fall through */
311  case FINISH_AT:
312  if (0 < getRepeatsRequired())
313  setRepeatsRequired(0);
314  break;
315 
316  case FINISH_SEQUENCE:
317  if (1 != getRepeatsRequired())
318  setRepeatsRequired(1);
319  break;
320 
321  case FINISH_REPEAT:
322  if (0 == getRepeatsRequired())
323  setRepeatsRequired(1);
324  break;
325 
326  default:
327  break;
328  }
329 
330  updateJobCells();
331 }
332 
333 void SchedulerJob::setStepPipeline(const StepPipeline &value)
334 {
335  stepPipeline = value;
336 }
337 
338 void SchedulerJob::setState(const JOBStatus &value)
339 {
340  state = value;
341  stateTime = getLocalTime();
342 
343  /* FIXME: move this to Scheduler, SchedulerJob is mostly a model */
344  if (JOB_ERROR == state)
345  {
346  lastErrorTime = getLocalTime();
347  KNotification::event(QLatin1String("EkosSchedulerJobFail"), i18n("Ekos job failed (%1)", getName()));
348  }
349 
350  /* If job becomes invalid or idle, automatically reset its startup characteristics, and force its duration to be reestimated */
351  if (JOB_INVALID == value || JOB_IDLE == value)
352  {
353  setStartupCondition(fileStartupCondition);
354  setStartupTime(fileStartupTime);
355  setEstimatedTime(-1);
356  }
357 
358  /* If job is aborted, automatically reset its startup characteristics */
359  if (JOB_ABORTED == value)
360  {
361  lastAbortTime = getLocalTime();
362  setStartupCondition(fileStartupCondition);
363  /* setStartupTime(fileStartupTime); */
364  }
365 
366  updateJobCells();
367 }
368 
369 
370 void SchedulerJob::setLeadTime(const int64_t &value)
371 {
372  leadTime = value;
373  updateJobCells();
374 }
375 
376 void SchedulerJob::setScore(int value)
377 {
378  score = value;
379  updateJobCells();
380 }
381 
382 void SchedulerJob::setCulminationOffset(const int16_t &value)
383 {
384  culminationOffset = value;
385 }
386 
387 void SchedulerJob::setSequenceCount(const int count)
388 {
389  sequenceCount = count;
390  updateJobCells();
391 }
392 
393 void SchedulerJob::setNameCell(QTableWidgetItem *value)
394 {
395  nameCell = value;
396 }
397 
398 void SchedulerJob::setCompletedCount(const int count)
399 {
400  completedCount = count;
401  updateJobCells();
402 }
403 
404 void SchedulerJob::setStatusCell(QTableWidgetItem *value)
405 {
406  statusCell = value;
407  if (nullptr != statusCell)
408  statusCell->setToolTip(i18n("Current status of job '%1', managed by the Scheduler.\n"
409  "If invalid, the Scheduler was not able to find a proper observation time for the target.\n"
410  "If aborted, the Scheduler missed the scheduled time or encountered transitory issues and will reschedule the job.\n"
411  "If complete, the Scheduler verified that all sequence captures requested were stored, including repeats.",
412  name));
413 }
414 
415 void SchedulerJob::setAltitudeCell(QTableWidgetItem *value)
416 {
417  altitudeCell = value;
418  if (nullptr != altitudeCell)
419  altitudeCell->setToolTip(i18n("Current altitude of the target of job '%1'.\n"
420  "A rising target is indicated with an arrow going up.\n"
421  "A setting target is indicated with an arrow going down.",
422  name));
423 }
424 
425 void SchedulerJob::setStartupCell(QTableWidgetItem *value)
426 {
427  startupCell = value;
428  if (nullptr != startupCell)
429  startupCell->setToolTip(i18n("Startup time for job '%1', as estimated by the Scheduler.\n"
430  "The altitude at startup, if available, is displayed too.\n"
431  "Fixed time from user or culmination time is marked with a chronometer symbol. ",
432  name));
433 }
434 
435 void SchedulerJob::setCompletionCell(QTableWidgetItem *value)
436 {
437  completionCell = value;
438  if (nullptr != completionCell)
439  completionCell->setToolTip(i18n("Completion time for job '%1', as estimated by the Scheduler.\n"
440  "You may specify a fixed time to limit duration of looping jobs. "
441  "A warning symbol indicates the altitude at completion may cause the job to abort before completion.\n",
442  name));
443 }
444 
445 void SchedulerJob::setCaptureCountCell(QTableWidgetItem *value)
446 {
447  captureCountCell = value;
448  if (nullptr != captureCountCell)
449  captureCountCell->setToolTip(i18n("Count of captures stored for job '%1', based on its sequence job.\n"
450  "This is a summary, additional specific frame types may be required to complete the job.",
451  name));
452 }
453 
454 void SchedulerJob::setScoreCell(QTableWidgetItem *value)
455 {
456  scoreCell = value;
457  if (nullptr != scoreCell)
458  scoreCell->setToolTip(i18n("Current score for job '%1', from its altitude, moon separation and sky darkness.\n"
459  "Negative if adequate altitude is not achieved yet or if there is no proper observation time today.\n"
460  "The Scheduler will refresh scores when picking a new candidate job.",
461  name));
462 }
463 
464 void SchedulerJob::setLeadTimeCell(QTableWidgetItem *value)
465 {
466  leadTimeCell = value;
467  if (nullptr != leadTimeCell)
468  leadTimeCell->setToolTip(i18n("Time interval from the job which precedes job '%1'.\n"
469  "Adjust the Lead Time in Ekos options to increase that duration and leave time for jobs to complete.\n"
470  "Rearrange jobs to minimize that duration and optimize your imaging time.",
471  name));
472 }
473 
474 void SchedulerJob::setDateTimeDisplayFormat(const QString &value)
475 {
476  dateTimeDisplayFormat = value;
477  updateJobCells();
478 }
479 
480 void SchedulerJob::setStage(const JOBStage &value)
481 {
482  stage = value;
483  updateJobCells();
484 }
485 
486 void SchedulerJob::setStageCell(QTableWidgetItem *cell)
487 {
488  stageCell = cell;
489  // FIXME: Add a tool tip if cell is used
490 }
491 
492 void SchedulerJob::setStageLabel(QLabel *label)
493 {
494  stageLabel = label;
495 }
496 
497 void SchedulerJob::setFileStartupCondition(const StartupCondition &value)
498 {
499  fileStartupCondition = value;
500 }
501 
502 void SchedulerJob::setFileStartupTime(const QDateTime &value)
503 {
504  fileStartupTime = value;
505 }
506 
507 void SchedulerJob::setEstimatedTime(const int64_t &value)
508 {
509  /* Estimated time is generally the difference between startup and completion times:
510  * - It is fixed when startup and completion times are fixed, that is, we disregard the argument
511  * - Else mostly it pushes completion time from startup time
512  *
513  * However it cannot advance startup time when completion time is fixed because of the way jobs are scheduled.
514  * This situation requires a warning in the user interface when there is not enough time for the job to process.
515  */
516 
517  /* If startup and completion times are fixed, estimated time cannot change - disregard the argument */
518  if (START_ASAP != fileStartupCondition && FINISH_AT == completionCondition)
519  {
520  estimatedTime = startupTime.secsTo(completionTime);
521  }
522  /* If completion time isn't fixed, estimated time adjusts completion time */
523  else if (FINISH_AT != completionCondition && FINISH_LOOP != completionCondition)
524  {
525  estimatedTime = value;
526  completionTime = startupTime.addSecs(value);
527  altitudeAtCompletion = findAltitude(targetCoords, completionTime, &isSettingAtCompletion);
528  }
529  /* Else estimated time is simply stored as is - covers FINISH_LOOP from setCompletionTime */
530  else estimatedTime = value;
531 
532  updateJobCells();
533 }
534 
535 void SchedulerJob::setInSequenceFocus(bool value)
536 {
537  inSequenceFocus = value;
538 }
539 
540 void SchedulerJob::setPriority(const uint8_t &value)
541 {
542  priority = value;
543 }
544 
545 void SchedulerJob::setEnforceTwilight(bool value)
546 {
547  enforceTwilight = value;
548  calculateDawnDusk(startupTime, nextDawn, nextDusk);
549 }
550 
551 void SchedulerJob::setEnforceArtificialHorizon(bool value)
552 {
553  enforceArtificialHorizon = value;
554 }
555 
556 void SchedulerJob::setEstimatedTimeCell(QTableWidgetItem *value)
557 {
558  estimatedTimeCell = value;
559  if (estimatedTimeCell)
560  estimatedTimeCell->setToolTip(i18n("Duration job '%1' will take to complete when started, as estimated by the Scheduler.\n"
561  "Depends on the actions to be run, and the sequence job to be processed.",
562  name));
563 }
564 
565 void SchedulerJob::setLightFramesRequired(bool value)
566 {
567  lightFramesRequired = value;
568 }
569 
570 void SchedulerJob::setRepeatsRequired(const uint16_t &value)
571 {
572  repeatsRequired = value;
573 
574  // Update completion condition to be compatible
575  if (1 < repeatsRequired)
576  {
577  if (FINISH_REPEAT != completionCondition)
578  setCompletionCondition(FINISH_REPEAT);
579  }
580  else if (0 < repeatsRequired)
581  {
582  if (FINISH_SEQUENCE != completionCondition)
583  setCompletionCondition(FINISH_SEQUENCE);
584  }
585  else
586  {
587  if (FINISH_LOOP != completionCondition)
588  setCompletionCondition(FINISH_LOOP);
589  }
590 
591  updateJobCells();
592 }
593 
594 void SchedulerJob::setRepeatsRemaining(const uint16_t &value)
595 {
596  repeatsRemaining = value;
597  updateJobCells();
598 }
599 
600 void SchedulerJob::setCapturedFramesMap(const CapturedFramesMap &value)
601 {
602  capturedFramesMap = value;
603 }
604 
605 void SchedulerJob::setTargetCoords(const dms &ra, const dms &dec, double djd)
606 {
607  targetCoords.setRA0(ra);
608  targetCoords.setDec0(dec);
609 
610  targetCoords.apparentCoord(static_cast<long double>(J2000), djd);
611 }
612 
613 void SchedulerJob::setPositionAngle(double value)
614 {
615  m_PositionAngle = value;
616 }
617 
618 void SchedulerJob::updateJobCells()
619 {
620  if (!m_UpdateGraphics) return;
621  if (nullptr != nameCell)
622  {
623  nameCell->setText(name);
624  if (nullptr != nameCell)
625  nameCell->tableWidget()->resizeColumnToContents(nameCell->column());
626  }
627 
628  if (nullptr != nameLabel)
629  {
630  nameLabel->setText(name + QString(":"));
631  }
632 
633  if (nullptr != statusCell)
634  {
635  static QMap<JOBStatus, QString> stateStrings;
636  static QString stateStringUnknown;
637  if (stateStrings.isEmpty())
638  {
639  stateStrings[JOB_IDLE] = i18n("Idle");
640  stateStrings[JOB_EVALUATION] = i18n("Evaluating");
641  stateStrings[JOB_SCHEDULED] = i18n("Scheduled");
642  stateStrings[JOB_BUSY] = i18n("Running");
643  stateStrings[JOB_INVALID] = i18n("Invalid");
644  stateStrings[JOB_COMPLETE] = i18n("Complete");
645  stateStrings[JOB_ABORTED] = i18n("Aborted");
646  stateStrings[JOB_ERROR] = i18n("Error");
647  stateStringUnknown = i18n("Unknown");
648  }
649  statusCell->setText(stateStrings.value(state, stateStringUnknown));
650 
651  if (nullptr != statusCell->tableWidget())
652  statusCell->tableWidget()->resizeColumnToContents(statusCell->column());
653  }
654 
655  if (nullptr != stageCell || nullptr != stageLabel)
656  {
657  /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case should ; also, not thread-safe */
658  /* FIXME: this should work with a static initializer in C++11, but QT versions are touchy on this, and perhaps i18n can't be used? */
659  static QMap<JOBStage, QString> stageStrings;
660  static QString stageStringUnknown;
661  if (stageStrings.isEmpty())
662  {
663  stageStrings[STAGE_IDLE] = i18n("Idle");
664  stageStrings[STAGE_SLEWING] = i18n("Slewing");
665  stageStrings[STAGE_SLEW_COMPLETE] = i18n("Slew complete");
666  stageStrings[STAGE_FOCUSING] =
667  stageStrings[STAGE_POSTALIGN_FOCUSING] = i18n("Focusing");
668  stageStrings[STAGE_FOCUS_COMPLETE] =
669  stageStrings[STAGE_POSTALIGN_FOCUSING_COMPLETE ] = i18n("Focus complete");
670  stageStrings[STAGE_ALIGNING] = i18n("Aligning");
671  stageStrings[STAGE_ALIGN_COMPLETE] = i18n("Align complete");
672  stageStrings[STAGE_RESLEWING] = i18n("Repositioning");
673  stageStrings[STAGE_RESLEWING_COMPLETE] = i18n("Repositioning complete");
674  /*stageStrings[STAGE_CALIBRATING] = i18n("Calibrating");*/
675  stageStrings[STAGE_GUIDING] = i18n("Guiding");
676  stageStrings[STAGE_GUIDING_COMPLETE] = i18n("Guiding complete");
677  stageStrings[STAGE_CAPTURING] = i18n("Capturing");
678  stageStringUnknown = i18n("Unknown");
679  }
680  if (nullptr != stageCell)
681  {
682  stageCell->setText(stageStrings.value(stage, stageStringUnknown));
683  if (nullptr != stageCell->tableWidget())
684  stageCell->tableWidget()->resizeColumnToContents(stageCell->column());
685  }
686  if (nullptr != stageLabel)
687  {
688  stageLabel->setText(QString("%1: %2").arg(name, stageStrings.value(stage, stageStringUnknown)));
689  }
690  }
691 
692  if (nullptr != startupCell)
693  {
694  /* Display startup time if it is valid */
695  if (startupTime.isValid())
696  {
697  startupCell->setText(QString("%1%2%L3° %4")
698  .arg(altitudeAtStartup < minAltitude ? QString(QChar(0x26A0)) : "")
699  .arg(QChar(isSettingAtStartup ? 0x2193 : 0x2191))
700  .arg(altitudeAtStartup, 0, 'f', 1)
701  .arg(startupTime.toString(dateTimeDisplayFormat)));
702 
703  switch (fileStartupCondition)
704  {
705  /* If the original condition is START_AT/START_CULMINATION, startup time is fixed */
706  case START_AT:
707  case START_CULMINATION:
708  startupCell->setIcon(QIcon::fromTheme("chronometer"));
709  break;
710 
711  /* If the original condition is START_ASAP, startup time is informational */
712  case START_ASAP:
713  startupCell->setIcon(QIcon());
714  break;
715 
716  default:
717  break;
718  }
719  }
720  /* Else do not display any startup time */
721  else
722  {
723  startupCell->setText("-");
724  startupCell->setIcon(QIcon());
725  }
726 
727  if (nullptr != startupCell->tableWidget())
728  startupCell->tableWidget()->resizeColumnToContents(startupCell->column());
729  }
730 
731  if (nullptr != altitudeCell)
732  {
733  // FIXME: Cache altitude calculations
734  bool is_setting = false;
735  double const alt = findAltitude(targetCoords, QDateTime(), &is_setting);
736 
737  altitudeCell->setText(QString("%1%L2°")
738  .arg(QChar(is_setting ? 0x2193 : 0x2191))
739  .arg(alt, 0, 'f', 1));
740 
741  if (nullptr != altitudeCell->tableWidget())
742  altitudeCell->tableWidget()->resizeColumnToContents(altitudeCell->column());
743  }
744 
745  if (nullptr != completionCell)
746  {
747  if (greedyCompletionTime.isValid())
748  {
749  completionCell->setText(QString("%1")
750  .arg(greedyCompletionTime.toString("hh:mm")));
751  }
752  else
753  /* Display completion time if it is valid and job is not looping */
754  if (FINISH_LOOP != completionCondition && completionTime.isValid())
755  {
756  completionCell->setText(QString("%1%2%L3° %4")
757  .arg(altitudeAtCompletion < minAltitude ? QString(QChar(0x26A0)) : "")
758  .arg(QChar(isSettingAtCompletion ? 0x2193 : 0x2191))
759  .arg(altitudeAtCompletion, 0, 'f', 1)
760  .arg(completionTime.toString(dateTimeDisplayFormat)));
761 
762  switch (completionCondition)
763  {
764  case FINISH_AT:
765  completionCell->setIcon(QIcon::fromTheme("chronometer"));
766  break;
767 
768  case FINISH_SEQUENCE:
769  case FINISH_REPEAT:
770  default:
771  completionCell->setIcon(QIcon());
772  break;
773  }
774  }
775  /* Else do not display any completion time */
776  else
777  {
778  completionCell->setText("-");
779  completionCell->setIcon(QIcon());
780  }
781 
782  if (nullptr != completionCell->tableWidget())
783  completionCell->tableWidget()->resizeColumnToContents(completionCell->column());
784  }
785 
786  if (nullptr != estimatedTimeCell)
787  {
788  if (0 < estimatedTime)
789  /* Seconds to ms - this doesn't follow dateTimeDisplayFormat, which renders YMD too */
790  estimatedTimeCell->setText(QTime::fromMSecsSinceStartOfDay(estimatedTime * 1000).toString("HH:mm:ss"));
791 #if 0
792  else if(0 == estimatedTime)
793  /* FIXME: this special case could be merged with the previous, kept for future to indicate actual duration */
794  estimatedTimeCell->setText("00:00:00");
795 #endif
796  else
797  /* Invalid marker */
798  estimatedTimeCell->setText("-");
799 
800  /* Warn the end-user if estimated time doesn't fit in the startup/completion interval */
801  if (estimatedTime < startupTime.secsTo(completionTime))
802  estimatedTimeCell->setIcon(QIcon::fromTheme("document-find"));
803  else
804  estimatedTimeCell->setIcon(QIcon());
805 
806  if (nullptr != estimatedTimeCell->tableWidget())
807  estimatedTimeCell->tableWidget()->resizeColumnToContents(estimatedTimeCell->column());
808  }
809 
810  if (nullptr != captureCountCell)
811  {
812  switch (completionCondition)
813  {
814  case FINISH_AT:
815  // FIXME: Attempt to calculate the number of frames until end - requires detailed imaging time
816 
817  case FINISH_LOOP:
818  // If looping, display the count of completed frames
819  captureCountCell->setText(QString("%L1/-").arg(completedCount));
820  break;
821 
822  case FINISH_SEQUENCE:
823  case FINISH_REPEAT:
824  default:
825  // If repeating, display the count of completed frames to the count of requested frames
826  captureCountCell->setText(QString("%L1/%L2").arg(completedCount).arg(sequenceCount));
827  break;
828  }
829 
830  if (nullptr != captureCountCell->tableWidget())
831  captureCountCell->tableWidget()->resizeColumnToContents(captureCountCell->column());
832  }
833 
834  if (nullptr != scoreCell)
835  {
836  if (0 <= score)
837  scoreCell->setText(QString("%L1").arg(score));
838  else
839  /* FIXME: negative scores are just weird for the end-user */
840  scoreCell->setText("<0");
841 
842  if (nullptr != scoreCell->tableWidget())
843  scoreCell->tableWidget()->resizeColumnToContents(scoreCell->column());
844  }
845 
846  if (nullptr != leadTimeCell)
847  {
848  // Display lead time, plus a warning if lead time is more than twice the lead time of the Ekos options
849  switch (state)
850  {
851  case JOB_INVALID:
852  case JOB_ERROR:
853  case JOB_COMPLETE:
854  leadTimeCell->setText("-");
855  break;
856 
857  default:
858  leadTimeCell->setText(QString("%1%2")
859  .arg(Options::leadTime() * 60 * 2 < leadTime ? QString(QChar(0x26A0)) : "")
860  .arg(QTime::fromMSecsSinceStartOfDay(leadTime * 1000).toString("HH:mm:ss")));
861  break;
862  }
863 
864  if (nullptr != leadTimeCell->tableWidget())
865  leadTimeCell->tableWidget()->resizeColumnToContents(leadTimeCell->column());
866  }
867 }
868 
869 void SchedulerJob::reset()
870 {
871  state = JOB_IDLE;
872  stage = STAGE_IDLE;
873  stateTime = getLocalTime();
874  lastAbortTime = QDateTime();
875  lastErrorTime = QDateTime();
876  estimatedTime = -1;
877  leadTime = 0;
878  startupCondition = fileStartupCondition;
879  startupTime = fileStartupCondition == START_AT ? fileStartupTime : QDateTime();
880 
881  /* Refresh dawn and dusk for startup date */
882  calculateDawnDusk(startupTime, nextDawn, nextDusk);
883 
884  greedyCompletionTime = QDateTime();
885  stopReason.clear();
886 
887  /* No change to culmination offset */
888  repeatsRemaining = repeatsRequired;
889  updateJobCells();
890  clearCache();
891 }
892 
893 bool SchedulerJob::decreasingScoreOrder(SchedulerJob const *job1, SchedulerJob const *job2)
894 {
895  return job1->getScore() > job2->getScore();
896 }
897 
898 bool SchedulerJob::increasingPriorityOrder(SchedulerJob const *job1, SchedulerJob const *job2)
899 {
900  return job1->getPriority() < job2->getPriority();
901 }
902 
903 bool SchedulerJob::decreasingAltitudeOrder(SchedulerJob const *job1, SchedulerJob const *job2, QDateTime const &when)
904 {
905  bool A_is_setting = job1->isSettingAtStartup;
906  double const altA = when.isValid() ?
907  findAltitude(job1->getTargetCoords(), when, &A_is_setting) :
908  job1->altitudeAtStartup;
909 
910  bool B_is_setting = job2->isSettingAtStartup;
911  double const altB = when.isValid() ?
912  findAltitude(job2->getTargetCoords(), when, &B_is_setting) :
913  job2->altitudeAtStartup;
914 
915  // Sort with the setting target first
916  if (A_is_setting && !B_is_setting)
917  return true;
918  else if (!A_is_setting && B_is_setting)
919  return false;
920 
921  // If both targets rise or set, sort by decreasing altitude, considering a setting target is prioritary
922  return (A_is_setting && B_is_setting) ? altA < altB : altB < altA;
923 }
924 
925 bool SchedulerJob::increasingStartupTimeOrder(SchedulerJob const *job1, SchedulerJob const *job2)
926 {
927  return job1->getStartupTime() < job2->getStartupTime();
928 }
929 
930 // This uses both the user-setting minAltitude, as well as any artificial horizon
931 // constraints the user might have setup.
932 double SchedulerJob::getMinAltitudeConstraint(double azimuth, bool *artificialHorizon) const
933 {
934  double constraint = getMinAltitude();
935  if (artificialHorizon) *artificialHorizon = false;
936  if (getHorizon() != nullptr && enforceArtificialHorizon)
937  {
938  double artificialHorizonConstraint = getHorizon()->altitudeConstraint(azimuth);
939  if (artificialHorizon) *artificialHorizon = artificialHorizonConstraint > constraint;
940  constraint = std::max(constraint, artificialHorizonConstraint);
941  }
942  return constraint;
943 }
944 
945 int16_t SchedulerJob::getAltitudeScore(QDateTime const &when, double *altPtr) const
946 {
947  // FIXME: block calculating target coordinates at a particular time is duplicated in several places
948 
949  // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
950  KStarsDateTime ltWhen(when.isValid() ?
951  Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
952  getLocalTime());
953 
954  // Create a sky object with the target catalog coordinates
955  SkyPoint const target = getTargetCoords();
956  SkyObject o;
957  o.setRA0(target.ra0());
958  o.setDec0(target.dec0());
959 
960  // Update RA/DEC of the target for the current fraction of the day
961  KSNumbers numbers(ltWhen.djd());
962  o.updateCoordsNow(&numbers);
963 
964  // Compute local sidereal time for the current fraction of the day, calculate altitude
965  CachingDms const LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltWhen).gst());
966  o.EquatorialToHorizontal(&LST, getGeo()->lat());
967  double const altitude = o.alt().Degrees();
968  double const azimuth = o.az().Degrees();
969  if (altPtr != nullptr)
970  *altPtr = altitude;
971 
972  double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff();
973  int16_t score = BAD_SCORE - 1;
974 
975  const double minAlt = getMinAltitudeConstraint(azimuth);
976 
977  // If altitude is negative, bad score
978  // FIXME: some locations may allow negative altitudes
979  if (altitude < 0)
980  {
981  score = BAD_SCORE;
982  }
983  else if (hasAltitudeConstraint())
984  {
985  // If under altitude constraint, bad score
986  if (altitude < minAlt)
987  score = BAD_SCORE;
988  // Else if setting and under altitude cutoff, job would end soon after starting, bad score
989  // FIXME: half bad score when under altitude cutoff risk getting positive again
990  else
991  {
992  double offset = LST.Hours() - o.ra().Hours();
993  if (24.0 <= offset)
994  offset -= 24.0;
995  else if (offset < 0.0)
996  offset += 24.0;
997  if (0.0 <= offset && offset < 12.0)
998  if (altitude - SETTING_ALTITUDE_CUTOFF < minAlt)
999  score = BAD_SCORE / 2;
1000  }
1001  }
1002  // If not constrained but below minimum hard altitude, set score to 10% of altitude value
1003  else if (altitude < MIN_ALTITUDE)
1004  {
1005  score = static_cast <int16_t> (altitude / 10.0);
1006  }
1007 
1008  // Else default score calculation without altitude constraint
1009  if (score < BAD_SCORE)
1010  score = static_cast <int16_t> ((1.5 * pow(1.06, altitude)) - (MIN_ALTITUDE / 10.0));
1011 
1012  //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target altitude is %3 degrees at %2 (score %4).")
1013  // .arg(getName())
1014  // .arg(when.toString(getDateTimeDisplayFormat()))
1015  // .arg(currentAlt, 0, 'f', minAltitude->decimals())
1016  // .arg(QString::asprintf("%+d", score));
1017 
1018  return score;
1019 }
1020 
1021 int16_t SchedulerJob::getMoonSeparationScore(QDateTime const &when) const
1022 {
1023  if (moon == nullptr) return 100;
1024 
1025  // FIXME: block calculating target coordinates at a particular time is duplicated in several places
1026 
1027  // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1028  KStarsDateTime ltWhen(when.isValid() ?
1029  Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
1030  getLocalTime());
1031 
1032  // Create a sky object with the target catalog coordinates
1033  SkyPoint const target = getTargetCoords();
1034  SkyObject o;
1035  o.setRA0(target.ra0());
1036  o.setDec0(target.dec0());
1037 
1038  // Update RA/DEC of the target for the current fraction of the day
1039  KSNumbers numbers(ltWhen.djd());
1040  o.updateCoordsNow(&numbers);
1041 
1042  // Update moon
1043  //ut = getGeo()->LTtoUT(ltWhen);
1044  //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation
1045  //LST = getGeo()->GSTtoLST(ut.gst());
1046  CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltWhen).gst());
1047  moon->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1048 
1049  double const moonAltitude = moon->alt().Degrees();
1050 
1051  // Lunar illumination %
1052  double const illum = moon->illum() * 100.0;
1053 
1054  // Moon/Sky separation p
1055  double const separation = moon->angularDistanceTo(&o).Degrees();
1056 
1057  // Zenith distance of the moon
1058  double const zMoon = (90 - moonAltitude);
1059  // Zenith distance of target
1060  double const zTarget = (90 - o.alt().Degrees());
1061 
1062  int16_t score = 0;
1063 
1064  // If target = Moon, or no illuminiation, or moon below horizon, return static score.
1065  if (zMoon == zTarget || illum == 0 || zMoon >= 90)
1066  score = 100;
1067  else
1068  {
1069  // JM: Some magic voodoo formula I came up with!
1070  double moonEffect = (pow(separation, 1.7) * pow(zMoon, 0.5)) / (pow(zTarget, 1.1) * pow(illum, 0.5));
1071 
1072  // Limit to 0 to 100 range.
1073  moonEffect = KSUtils::clamp(moonEffect, 0.0, 100.0);
1074 
1075  if (getMinMoonSeparation() > 0)
1076  {
1077  if (separation < getMinMoonSeparation())
1078  score = BAD_SCORE * 5;
1079  else
1080  score = moonEffect;
1081  }
1082  else
1083  score = moonEffect;
1084  }
1085 
1086  // Limit to 0 to 20
1087  score /= 5.0;
1088 
1089  //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target is %L3 degrees from Moon (score %2).")
1090  // .arg(getName())
1091  // .arg(separation, 0, 'f', 3)
1092  // .arg(QString::asprintf("%+d", score));
1093 
1094  return score;
1095 }
1096 
1097 
1098 double SchedulerJob::getCurrentMoonSeparation() const
1099 {
1100  if (moon == nullptr) return 180.0;
1101 
1102  // FIXME: block calculating target coordinates at a particular time is duplicated in several places
1103 
1104  // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1105  KStarsDateTime ltWhen(getLocalTime());
1106 
1107  // Create a sky object with the target catalog coordinates
1108  SkyPoint const target = getTargetCoords();
1109  SkyObject o;
1110  o.setRA0(target.ra0());
1111  o.setDec0(target.dec0());
1112 
1113  // Update RA/DEC of the target for the current fraction of the day
1114  KSNumbers numbers(ltWhen.djd());
1115  o.updateCoordsNow(&numbers);
1116 
1117  // Update moon
1118  //ut = getGeo()->LTtoUT(ltWhen);
1119  //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation
1120  //LST = getGeo()->GSTtoLST(ut.gst());
1121  CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltWhen).gst());
1122  moon->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1123 
1124  // Moon/Sky separation p
1125  return moon->angularDistanceTo(&o).Degrees();
1126 }
1127 
1128 QDateTime SchedulerJob::calculateNextTime(QDateTime const &when, bool checkIfConstraintsAreMet, int increment,
1129  QString *reason, bool runningJob, const QDateTime &until) const
1130 {
1131  // FIXME: block calculating target coordinates at a particular time is duplicated in several places
1132 
1133  // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1134  KStarsDateTime ltWhen(when.isValid() ?
1135  Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
1136  getLocalTime());
1137 
1138  // Create a sky object with the target catalog coordinates
1139  SkyPoint const target = getTargetCoords();
1140  SkyObject o;
1141  o.setRA0(target.ra0());
1142  o.setDec0(target.dec0());
1143 
1144  // Calculate the UT at the argument time
1145  KStarsDateTime const ut = getGeo()->LTtoUT(ltWhen);
1146 
1147  double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff();
1148 
1149  auto maxMinute = 1e8;
1150  if (!runningJob && until.isValid())
1151  maxMinute = when.secsTo(until) / 60;
1152 
1153  if (maxMinute > 24 * 60)
1154  maxMinute = 24 * 60;
1155 
1156  // Within the next 24 hours, search when the job target matches the altitude and moon constraints
1157  for (unsigned int minute = 0; minute < maxMinute; minute += increment)
1158  {
1159  KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60));
1160 
1161  // Is this violating twilight?
1162  QDateTime nextSuccess;
1163  if (getEnforceTwilight() && !runsDuringAstronomicalNightTime(ltOffset, &nextSuccess))
1164  {
1165  if (checkIfConstraintsAreMet)
1166  {
1167  // Change the minute to increment-minutes before next success.
1168  if (nextSuccess.isValid())
1169  {
1170  const int minutesToSuccess = ltOffset.secsTo(nextSuccess) / 60 - increment;
1171  if (minutesToSuccess > 0)
1172  minute += minutesToSuccess;
1173  }
1174  continue;
1175  }
1176  else
1177  {
1178  if (reason) *reason = "twilight";
1179  return ltOffset;
1180  }
1181  }
1182 
1183  // Update RA/DEC of the target for the current fraction of the day
1184  KSNumbers numbers(ltOffset.djd());
1185  o.updateCoordsNow(&numbers);
1186 
1187  // Compute local sidereal time for the current fraction of the day, calculate altitude
1188  CachingDms const LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltOffset).gst());
1189  o.EquatorialToHorizontal(&LST, getGeo()->lat());
1190  double const altitude = o.alt().Degrees();
1191  double const azimuth = o.az().Degrees();
1192  bool artificialHorizonConstrains = false;
1193  double const minAlt = getMinAltitudeConstraint(azimuth, &artificialHorizonConstrains);
1194 
1195  if (minAlt <= altitude)
1196  {
1197  // Don't test proximity to dawn in this situation, we only cater for altitude here
1198 
1199  // Continue searching if Moon separation is not good enough
1200  if (0 < getMinMoonSeparation() && getMoonSeparationScore(ltOffset) < 0)
1201  {
1202  if (checkIfConstraintsAreMet)
1203  continue;
1204  else
1205  {
1206  if (reason) *reason = QString("moon separation");
1207  return ltOffset;
1208  }
1209  }
1210 
1211  // Continue searching if target is setting and under the cutoff
1212  if (checkIfConstraintsAreMet)
1213  {
1214  if (!runningJob)
1215  {
1216  double offset = LST.Hours() - o.ra().Hours();
1217  if (24.0 <= offset)
1218  offset -= 24.0;
1219  else if (offset < 0.0)
1220  offset += 24.0;
1221  if (0.0 <= offset && offset < 12.0)
1222  if (altitude - SETTING_ALTITUDE_CUTOFF < minAlt)
1223  continue;
1224  }
1225  return ltOffset;
1226  }
1227  }
1228  else if (!checkIfConstraintsAreMet)
1229  {
1230  if (reason)
1231  *reason = QString("altitude %1 < %2%3").arg(altitude, 0, 'f', 1)
1232  .arg(artificialHorizonConstrains ? "artificial horizon " : "").arg(minAlt, 0, 'f', 1);
1233  return ltOffset;
1234  }
1235  }
1236 
1237  return QDateTime();
1238 }
1239 
1240 QDateTime SchedulerJob::calculateCulmination(QDateTime const &when) const
1241 {
1242  // FIXME: culmination calculation is a min altitude requirement, should be an interval altitude requirement
1243 
1244  // FIXME: block calculating target coordinates at a particular time is duplicated in calculateCulmination
1245 
1246  // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1247  KStarsDateTime ltWhen(when.isValid() ?
1248  Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
1249  getLocalTime());
1250 
1251  // Create a sky object with the target catalog coordinates
1252  SkyPoint const target = getTargetCoords();
1253  SkyObject o;
1254  o.setRA0(target.ra0());
1255  o.setDec0(target.dec0());
1256 
1257  // Update RA/DEC for the argument date/time
1258  KSNumbers numbers(ltWhen.djd());
1259  o.updateCoordsNow(&numbers);
1260 
1261  // Calculate transit date/time at the argument date - transitTime requires UT and returns LocalTime
1262  KStarsDateTime transitDateTime(ltWhen.date(), o.transitTime(getGeo()->LTtoUT(ltWhen), getGeo()), Qt::LocalTime);
1263 
1264  // Shift transit date/time by the argument offset
1265  KStarsDateTime observationDateTime = transitDateTime.addSecs(getCulminationOffset() * 60);
1266 
1267  // Relax observation time, culmination calculation is stable at minute only
1268  KStarsDateTime relaxedDateTime = observationDateTime.addSecs(Options::leadTime() * 60);
1269 
1270  // Verify resulting observation time is under lead time vs. argument time
1271  // If sooner, delay by 8 hours to get to the next transit - perhaps in a third call
1272  if (relaxedDateTime < ltWhen)
1273  {
1274  qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' startup %2 is posterior to transit %3, shifting by 8 hours.")
1275  .arg(getName())
1276  .arg(ltWhen.toString(getDateTimeDisplayFormat()))
1277  .arg(relaxedDateTime.toString(getDateTimeDisplayFormat()));
1278 
1279  return calculateCulmination(when.addSecs(8 * 60 * 60));
1280  }
1281 
1282  // Guarantees - culmination calculation is stable at minute level, so relax by lead time
1283  Q_ASSERT_X(observationDateTime.isValid(), __FUNCTION__, "Observation time for target culmination is valid.");
1284  Q_ASSERT_X(ltWhen <= relaxedDateTime, __FUNCTION__,
1285  "Observation time for target culmination is at or after than argument time");
1286 
1287  // Return consolidated culmination time
1288  return Qt::UTC == observationDateTime.timeSpec() ? getGeo()->UTtoLT(observationDateTime) : observationDateTime;
1289 }
1290 
1291 double SchedulerJob::findAltitude(const SkyPoint &target, const QDateTime &when, bool * is_setting, bool debug)
1292 {
1293  // FIXME: block calculating target coordinates at a particular time is duplicated in several places
1294 
1295  // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
1296  KStarsDateTime ltWhen(when.isValid() ?
1297  Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when :
1298  getLocalTime());
1299 
1300  // Create a sky object with the target catalog coordinates
1301  SkyObject o;
1302  o.setRA0(target.ra0());
1303  o.setDec0(target.dec0());
1304 
1305  // Update RA/DEC of the target for the current fraction of the day
1306  KSNumbers numbers(ltWhen.djd());
1307  o.updateCoordsNow(&numbers);
1308 
1309  // Calculate alt/az coordinates using KStars instance's geolocation
1310  CachingDms const LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(ltWhen).gst());
1311  o.EquatorialToHorizontal(&LST, getGeo()->lat());
1312 
1313  // Hours are reduced to [0,24[, meridian being at 0
1314  double offset = LST.Hours() - o.ra().Hours();
1315  if (24.0 <= offset)
1316  offset -= 24.0;
1317  else if (offset < 0.0)
1318  offset += 24.0;
1319  bool const passed_meridian = 0.0 <= offset && offset < 12.0;
1320 
1321  if (debug)
1322  qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7")
1323  .arg(o.ra().toHMSString())
1324  .arg(o.ra0().toHMSString())
1325  .arg(o.dec().toHMSString())
1326  .arg(o.dec0().toHMSString())
1327  .arg(o.alt().Degrees())
1328  .arg(passed_meridian ? "yes" : "no")
1329  .arg(o.ra().Hours())
1330  .arg(LST.toHMSString())
1331  .arg(ltWhen.toString("HH:mm:ss"));
1332 
1333  if (is_setting)
1334  *is_setting = passed_meridian;
1335 
1336  return o.alt().Degrees();
1337 }
1338 
1339 void SchedulerJob::calculateDawnDusk(QDateTime const &when, QDateTime &nDawn, QDateTime &nDusk)
1340 {
1341  QDateTime startup = when;
1342 
1343  if (!startup.isValid())
1344  startup = getLocalTime();
1345 
1346  // Our local midnight - the KStarsDateTime date+time constructor is safe for local times
1347  // Exact midnight seems unreliable--offset it by a minute.
1348  KStarsDateTime midnight(startup.date(), QTime(0, 1), Qt::LocalTime);
1349 
1350  QDateTime dawn = startup, dusk = startup;
1351 
1352  // Loop dawn and dusk calculation until the events found are the next events
1353  for ( ; dawn <= startup || dusk <= startup ; midnight = midnight.addDays(1))
1354  {
1355  // KSAlmanac computes the closest dawn and dusk events from the local sidereal time corresponding to the midnight argument
1356 
1357 #if 0
1358  KSAlmanac const ksal(midnight, getGeo());
1359  // If dawn is in the past compared to this observation, fetch the next dawn
1360  if (dawn <= startup)
1361  dawn = getGeo()->UTtoLT(ksal.getDate().addSecs((ksal.getDawnAstronomicalTwilight() * 24.0 + Options::dawnOffset()) *
1362  3600.0));
1363  // If dusk is in the past compared to this observation, fetch the next dusk
1364  if (dusk <= startup)
1365  dusk = getGeo()->UTtoLT(ksal.getDate().addSecs((ksal.getDuskAstronomicalTwilight() * 24.0 + Options::duskOffset()) *
1366  3600.0));
1367 #else
1368  // Creating these almanac instances seems expensive.
1369  static QMap<QString, KSAlmanac const * > almanacMap;
1370  const QString key = QString("%1 %2 %3").arg(midnight.toString()).arg(getGeo()->lat()->Degrees()).arg(
1371  getGeo()->lng()->Degrees());
1372  KSAlmanac const * ksal = almanacMap.value(key, nullptr);
1373  if (ksal == nullptr)
1374  {
1375  if (almanacMap.size() > 5)
1376  {
1377  // don't allow this to grow too large.
1378  qDeleteAll(almanacMap);
1379  almanacMap.clear();
1380  }
1381  ksal = new KSAlmanac(midnight, getGeo());
1382  almanacMap[key] = ksal;
1383  }
1384 
1385  // If dawn is in the past compared to this observation, fetch the next dawn
1386  if (dawn <= startup)
1387  dawn = getGeo()->UTtoLT(ksal->getDate().addSecs((ksal->getDawnAstronomicalTwilight() * 24.0 + Options::dawnOffset()) *
1388  3600.0));
1389 
1390  // If dusk is in the past compared to this observation, fetch the next dusk
1391  if (dusk <= startup)
1392  dusk = getGeo()->UTtoLT(ksal->getDate().addSecs((ksal->getDuskAstronomicalTwilight() * 24.0 + Options::duskOffset()) *
1393  3600.0));
1394 #endif
1395  }
1396 
1397  // Now we have the next events:
1398  // - if dawn comes first, observation runs during the night
1399  // - if dusk comes first, observation runs during the day
1400  nDawn = dawn;
1401  nDusk = dusk;
1402 }
1403 
1404 bool SchedulerJob::runsDuringAstronomicalNightTime(const QDateTime &time,
1405  QDateTime *nextPossibleSuccess) const
1406 {
1407  // We call this very frequently in the Greedy Algorithm, and the calls
1408  // below are expensive. Almost all the calls are redundent (e.g. if it's not nighttime
1409  // now, it's not nighttime in 10 minutes). So, cache the answer and return it if the next
1410  // call is for a time between this time and the next dawn/dusk (whichever is sooner).
1411 
1412  static QDateTime previousMinDawnDusk, previousTime;
1413  static GeoLocation const *previousGeo = nullptr; // A dangling pointer, I suppose, but we never reference it.
1414  static bool previousAnswer;
1415  static double previousPreDawnTime = 0;
1416  static QDateTime nextSuccess;
1417 
1418  // Lock this method because of all the statics
1419  static std::mutex nightTimeMutex;
1420  const std::lock_guard<std::mutex> lock(nightTimeMutex);
1421 
1422  // We likely can rely on the previous calculations.
1423  if (previousTime.isValid() && previousMinDawnDusk.isValid() &&
1424  time >= previousTime && time < previousMinDawnDusk &&
1425  getGeo() == previousGeo &&
1426  Options::preDawnTime() == previousPreDawnTime)
1427  {
1428  if (!previousAnswer && nextPossibleSuccess != nullptr)
1429  *nextPossibleSuccess = nextSuccess;
1430  return previousAnswer;
1431  }
1432  else
1433  {
1434  previousAnswer = runsDuringAstronomicalNightTimeInternal(time, &previousMinDawnDusk, &nextSuccess);
1435  previousTime = time;
1436  previousGeo = getGeo();
1437  previousPreDawnTime = Options::preDawnTime();
1438  if (!previousAnswer && nextPossibleSuccess != nullptr)
1439  *nextPossibleSuccess = nextSuccess;
1440  return previousAnswer;
1441  }
1442 }
1443 
1444 
1445 bool SchedulerJob::runsDuringAstronomicalNightTimeInternal(const QDateTime &time, QDateTime *minDawnDusk,
1446  QDateTime *nextPossibleSuccess) const
1447 {
1448  QDateTime t;
1449  QDateTime nDawn = nextDawn, nDusk = nextDusk;
1450  if (time.isValid())
1451  {
1452  // Can't rely on the pre-computed dawn/dusk if we're giving it an arbitary time.
1453  calculateDawnDusk(time, nDawn, nDusk);
1454  t = time;
1455  }
1456  else
1457  {
1458  t = startupTime;
1459  }
1460 
1461  // Calculate the next astronomical dawn time, adjusted with the Ekos pre-dawn offset
1462  QDateTime const earlyDawn = nDawn.addSecs(-60.0 * abs(Options::preDawnTime()));
1463 
1464  *minDawnDusk = earlyDawn < nDusk ? earlyDawn : nDusk;
1465 
1466  // Dawn and dusk are ordered as the immediate next events following the observation time
1467  // Thus if dawn comes first, the job startup time occurs during the dusk/dawn interval.
1468  bool result = nDawn < nDusk && t <= earlyDawn;
1469 
1470  // Return a hint about when it might succeed.
1471  if (nextPossibleSuccess != nullptr)
1472  {
1473  if (result) *nextPossibleSuccess = QDateTime();
1474  else *nextPossibleSuccess = nDusk;
1475  }
1476 
1477  return result;
1478 }
1479 
1480 void SchedulerJob::setInitialFilter(const QString &value)
1481 {
1482  m_InitialFilter = value;
1483 }
1484 
1485 const QString &SchedulerJob::getInitialFilter() const
1486 {
1487  return m_InitialFilter;
1488 }
1489 
1490 bool SchedulerJob::StartTimeCache::check(const QDateTime &from, const QDateTime &until,
1491  QDateTime *result, QDateTime *newFrom) const
1492 {
1493  // Look at the cached results from getNextPossibleStartTime.
1494  // If the desired 'from' time is in one of them, that is, between computation.from and computation.until,
1495  // then we can re-use that result (as long as the desired until time is < computation.until).
1496  foreach (const StartTimeComputation &computation, startComputations)
1497  {
1498  if (from >= computation.from &&
1499  (!computation.until.isValid() || from < computation.until) &&
1500  (!computation.result.isValid() || from < computation.result))
1501  {
1502  if (computation.result.isValid() || until <= computation.until)
1503  {
1504  // We have a cached result.
1505  *result = computation.result;
1506  *newFrom = QDateTime();
1507  return true;
1508  }
1509  else
1510  {
1511  // No cached result, but at least we can constrain the search.
1512  *result = QDateTime();
1513  *newFrom = computation.until;
1514  return true;
1515  }
1516  }
1517  }
1518  return false;
1519 }
1520 
1521 void SchedulerJob::StartTimeCache::clear() const
1522 {
1523  startComputations.clear();
1524 }
1525 
1526 void SchedulerJob::StartTimeCache::add(const QDateTime &from, const QDateTime &until, const QDateTime &result) const
1527 {
1528  // Manage the cache size.
1529  if (startComputations.size() > 10)
1530  startComputations.clear();
1531 
1532  // The getNextPossibleStartTime computation (which calls calculateNextTime) searches ahead at most 24 hours.
1533  QDateTime endTime;
1534  if (!until.isValid())
1535  endTime = from.addSecs(24 * 3600);
1536  else
1537  {
1538  QDateTime oneDay = from.addSecs(24 * 3600);
1539  if (until > oneDay)
1540  endTime = oneDay;
1541  else
1542  endTime = until;
1543  }
1544 
1545  StartTimeComputation c;
1546  c.from = from;
1547  c.until = endTime;
1548  c.result = result;
1549  startComputations.push_back(c);
1550 }
1551 
1552 // When can this job start? For now ignores culmination constraint.
1553 QDateTime SchedulerJob::getNextPossibleStartTime(const QDateTime &when, int increment, bool runningJob,
1554  const QDateTime &until) const
1555 {
1556  QDateTime ltWhen(
1557  when.isValid() ? (Qt::UTC == when.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(when)) : when)
1558  : getLocalTime());
1559 
1560  // We do not consider job state here. It is the responsibility of the caller
1561  // to filter for that, if desired.
1562 
1563  if (SchedulerJob::START_AT == getFileStartupCondition())
1564  {
1565  int secondsFromNow = ltWhen.secsTo(getFileStartupTime());
1566  if (secondsFromNow < -500)
1567  // We missed it.
1568  return QDateTime();
1569  ltWhen = secondsFromNow > 0 ? getFileStartupTime() : ltWhen;
1570  }
1571 
1572  // Can't start if we're past the finish time.
1573  if (getCompletionCondition() == FINISH_AT)
1574  {
1575  const QDateTime &t = getCompletionTime();
1576  if (t.isValid() && t < ltWhen)
1577  return QDateTime(); // return an invalid time.
1578  }
1579 
1580  if (runningJob)
1581  return calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
1582  else
1583  {
1584  QDateTime result, newFrom;
1585  if (startTimeCache.check(ltWhen, until, &result, &newFrom))
1586  {
1587  if (result.isValid() || !newFrom.isValid())
1588  return result;
1589  if (newFrom.isValid())
1590  ltWhen = newFrom;
1591  }
1592  result = calculateNextTime(ltWhen, true, increment, nullptr, runningJob, until);
1593  startTimeCache.add(ltWhen, until, result);
1594  return result;
1595  }
1596 }
1597 
1598 // When will this job end (not looking at capture plan)?
1599 QDateTime SchedulerJob::getNextEndTime(const QDateTime &start, int increment, QString *reason, const QDateTime &until) const
1600 {
1601  QDateTime ltStart(
1602  start.isValid() ? (Qt::UTC == start.timeSpec() ? getGeo()->UTtoLT(KStarsDateTime(start)) : start)
1603  : getLocalTime());
1604 
1605  // We do not consider job state here. It is the responsibility of the caller
1606  // to filter for that, if desired.
1607 
1608  if (SchedulerJob::START_AT == getFileStartupCondition())
1609  {
1610  if (getFileStartupTime().secsTo(ltStart) < 60)
1611  {
1612  // if the file startup time is in the future, then end now.
1613  // This case probably wouldn't happen in the running code.
1614  if (reason) *reason = "before start-at time";
1615  return QDateTime();
1616  }
1617  // otherwise, test from now.
1618  }
1619 
1620  // Can't start if we're past the finish time.
1621  if (getCompletionCondition() == FINISH_AT)
1622  {
1623  const QDateTime &t = getCompletionTime();
1624  if (t.isValid() && t < ltStart)
1625  {
1626  if (reason) *reason = "end-at time";
1627  return QDateTime(); // return an invalid time.
1628  }
1629  auto result = calculateNextTime(ltStart, false, increment, reason, false, until);
1630  if (!result.isValid() || result.secsTo(getCompletionTime()) < 0)
1631  {
1632  if (reason) *reason = "end-at time";
1633  return getCompletionTime();
1634  }
1635  else return result;
1636  }
1637 
1638  return calculateNextTime(ltStart, false, increment, reason, false, until);
1639 }
1640 
1641 QJsonObject SchedulerJob::toJson() const
1642 {
1643  return
1644  {
1645  {"name", name},
1646  {"pa", m_PositionAngle},
1647  {"targetRA", targetCoords.ra0().Hours()},
1648  {"targetDEC", targetCoords.dec0().Degrees()},
1649  {"state", state},
1650  {"stage", stage},
1651  {"sequenceCount", sequenceCount},
1652  {"completedCount", completedCount},
1653  {"minAltitude", minAltitude},
1654  {"minMoonSeparation", minMoonSeparation},
1655  // Warning: Qt JSON does not natively support 64bit integers
1656  {"estimatedTime", static_cast<double>(estimatedTime)},
1657  {"culminationOffset", culminationOffset},
1658  {"priority", priority},
1659  // Warning: Qt JSON does not natively support 64bit integers
1660  {"leadTime", static_cast<double>(leadTime)},
1661  {"repeatsRequired", repeatsRequired},
1662  {"repeatsRemaining", repeatsRemaining},
1663  {"inSequenceFocus", inSequenceFocus},
1664  {"score", score},
1665 
1666  };
1667 }
const dms & alt() const
Definition: skypoint.h:281
Extension of QDateTime for KStars KStarsDateTime can represent the date/time as a Julian Day,...
QDateTime addSecs(qint64 s) const const
int size() const const
void clear()
QTime fromMSecsSinceStartOfDay(int msecs)
const T value(const Key &key, const T &defaultValue) const const
Stores dms coordinates for a point in the sky. for converting between coordinate systems.
Definition: skypoint.h:44
a dms subclass that caches its sine and cosine values every time the angle is changed.
Definition: cachingdms.h:18
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition: skypoint.h:119
QDateTime addDays(qint64 ndays) const const
QIcon fromTheme(const QString &name)
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition: skypoint.h:94
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition: skypoint.cpp:77
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition: dms.cpp:370
Provides necessary information about the Moon. A subclass of SkyObject that provides information need...
Definition: ksmoon.h:25
KStarsDateTime addSecs(double s) const
SkyObject * findByName(const QString &name, bool exact=true) override
Search the children of this SkyMapComposite for a SkyObject whose name matches the argument.
QString i18n(const char *text, const TYPE &arg...)
Store several time-dependent astronomical quantities.
Definition: ksnumbers.h:42
const CachingDms & dec() const
Definition: skypoint.h:269
qint64 secsTo(const QDateTime &other) const const
char * toString(const T &value)
GeoLocation * geo()
Definition: kstarsdata.h:229
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition: skypoint.h:382
SkyMapComposite * skyComposite()
Definition: kstarsdata.h:165
QString label(StandardShortcut id)
An angle, stored as degrees, but expressible in many ways.
Definition: dms.h:37
const CachingDms & ra() const
Definition: skypoint.h:263
Implement methods to find important times in a day.
Definition: ksalmanac.h:26
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
const CachingDms & dec0() const
Definition: skypoint.h:257
const double & Degrees() const
Definition: dms.h:141
Qt::TimeSpec timeSpec() const const
void setToolTip(const QString &toolTip)
const char * name(StandardAction id)
QDate date() const const
const CachingDms & ra0() const
Definition: skypoint.h:251
bool isValid() const const
static KStarsDateTime getLocalTime()
Setter used in testing to fix the local time.
Definition: scheduler.cpp:274
QTime transitTime(const KStarsDateTime &dt, const GeoLocation *geo) const
The same iteration technique described in riseSetTime() is used here.
Definition: skyobject.cpp:239
QString toString(Qt::DateFormat format) const const
static KNotification * event(const QString &eventId, const QString &text=QString(), const QPixmap &pixmap=QPixmap(), QWidget *widget=nullptr, const NotificationFlags &flags=CloseOnTimeout, const QString &componentName=QString())
Information about an object in the sky.
Definition: skyobject.h:41
double Hours() const
Definition: dms.h:168
Relevant data about an observing location on Earth.
Definition: geolocation.h:27
bool isEmpty() const const
const dms & az() const
Definition: skypoint.h:275
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Fri Aug 12 2022 04:00:57 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.