Changeset View
Standalone View
kstars/ekos/scheduler/scheduler.cpp
- This file is larger than 256 KB, so syntax highlighting is disabled by default.
Show First 20 Lines • Show All 113 Lines • ▼ Show 20 Line(s) | 46 | { | |||
---|---|---|---|---|---|
114 | queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n" | 114 | queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n" | ||
115 | "Order only affect observation jobs that are scheduled to start at the same time.\n" | 115 | "Order only affect observation jobs that are scheduled to start at the same time.\n" | ||
116 | "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); | 116 | "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); | ||
117 | queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | 117 | queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | ||
118 | 118 | | |||
119 | evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot")); | 119 | evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot")); | ||
120 | evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs.")); | 120 | evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs.")); | ||
121 | evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | 121 | evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | ||
122 | sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical")); | ||||
123 | sortJobsB->setToolTip(i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n" | ||||
124 | "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n" | ||||
125 | "Option \"Sort Jobs by Altitude and Priority\" keeps the job list sorted this way, but with current time as reference.\n" | ||||
126 | "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs.")); | ||||
127 | sortJobsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | ||||
122 | mosaicB->setIcon(QIcon::fromTheme("zoom-draw")); | 128 | mosaicB->setIcon(QIcon::fromTheme("zoom-draw")); | ||
123 | mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | 129 | mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | ||
124 | 130 | | |||
125 | queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); | 131 | queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); | ||
126 | queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | 132 | queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | ||
127 | queueSaveB->setIcon(QIcon::fromTheme("document-save")); | 133 | queueSaveB->setIcon(QIcon::fromTheme("document-save")); | ||
128 | queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | 134 | queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | ||
129 | queueLoadB->setIcon(QIcon::fromTheme("document-open")); | 135 | queueLoadB->setIcon(QIcon::fromTheme("document-open")); | ||
Show All 27 Lines | |||||
157 | connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript); | 163 | connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript); | ||
158 | 164 | | |||
159 | connect(mosaicB, &QPushButton::clicked, this, &Scheduler::startMosaicTool); | 165 | connect(mosaicB, &QPushButton::clicked, this, &Scheduler::startMosaicTool); | ||
160 | connect(addToQueueB, &QPushButton::clicked, this, &Scheduler::addJob); | 166 | connect(addToQueueB, &QPushButton::clicked, this, &Scheduler::addJob); | ||
161 | connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob); | 167 | connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob); | ||
162 | connect(queueUpB, &QPushButton::clicked, this, &Scheduler::moveJobUp); | 168 | connect(queueUpB, &QPushButton::clicked, this, &Scheduler::moveJobUp); | ||
163 | connect(queueDownB, &QPushButton::clicked, this, &Scheduler::moveJobDown); | 169 | connect(queueDownB, &QPushButton::clicked, this, &Scheduler::moveJobDown); | ||
164 | connect(evaluateOnlyB, &QPushButton::clicked, this, &Scheduler::startJobEvaluation); | 170 | connect(evaluateOnlyB, &QPushButton::clicked, this, &Scheduler::startJobEvaluation); | ||
171 | connect(sortJobsB, &QPushButton::clicked, this, &Scheduler::sortJobsPerAltitude); | ||||
165 | connect(queueTable, &QAbstractItemView::clicked, this, &Scheduler::clickQueueTable); | 172 | connect(queueTable, &QAbstractItemView::clicked, this, &Scheduler::clickQueueTable); | ||
166 | connect(queueTable, &QAbstractItemView::doubleClicked, this, &Scheduler::loadJob); | 173 | connect(queueTable, &QAbstractItemView::doubleClicked, this, &Scheduler::loadJob); | ||
167 | 174 | | |||
168 | startB->setIcon(QIcon::fromTheme("media-playback-start")); | 175 | startB->setIcon(QIcon::fromTheme("media-playback-start")); | ||
169 | startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | 176 | startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | ||
170 | pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); | 177 | pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); | ||
171 | pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | 178 | pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); | ||
172 | 179 | | |||
▲ Show 20 Lines • Show All 291 Lines • ▼ Show 20 Line(s) | 435 | { | |||
464 | if (decOk == false) | 471 | if (decOk == false) | ||
465 | { | 472 | { | ||
466 | appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); | 473 | appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); | ||
467 | return; | 474 | return; | ||
468 | } | 475 | } | ||
469 | 476 | | |||
470 | watchJobChanges(false); | 477 | watchJobChanges(false); | ||
471 | 478 | | |||
472 | /* Warn if appending a job after infinite repeat */ | | |||
473 | /* FIXME: alter looping job priorities so that they are rescheduled later */ | | |||
474 | foreach(SchedulerJob * job, jobs) | | |||
475 | if(SchedulerJob::FINISH_LOOP == job->getCompletionCondition()) | | |||
476 | appendLogText(i18n("Warning: Job '%1' has completion condition set to infinite repeat, other jobs may not execute.",job->getName())); | | |||
477 | | ||||
478 | /* Create or Update a scheduler job */ | 479 | /* Create or Update a scheduler job */ | ||
479 | int currentRow = queueTable->currentRow(); | 480 | int currentRow = queueTable->currentRow(); | ||
480 | SchedulerJob * job = nullptr; | 481 | SchedulerJob * job = nullptr; | ||
481 | 482 | | |||
482 | /* If no row is selected for insertion, append at end of list. */ | 483 | /* If no row is selected for insertion, append at end of list. */ | ||
483 | if (currentRow < 0) | 484 | if (currentRow < 0) | ||
484 | currentRow = queueTable->rowCount(); | 485 | currentRow = queueTable->rowCount(); | ||
485 | 486 | | |||
▲ Show 20 Lines • Show All 54 Lines • ▼ Show 20 Line(s) | |||||
540 | 541 | | |||
541 | 542 | | |||
542 | // #2 Constraints | 543 | // #2 Constraints | ||
543 | 544 | | |||
544 | // Do we have minimum altitude constraint? | 545 | // Do we have minimum altitude constraint? | ||
545 | if (altConstraintCheck->isChecked()) | 546 | if (altConstraintCheck->isChecked()) | ||
546 | job->setMinAltitude(minAltitude->value()); | 547 | job->setMinAltitude(minAltitude->value()); | ||
547 | else | 548 | else | ||
548 | job->setMinAltitude(-1); | 549 | job->setMinAltitude(-90); | ||
549 | // Do we have minimum moon separation constraint? | 550 | // Do we have minimum moon separation constraint? | ||
550 | if (moonSeparationCheck->isChecked()) | 551 | if (moonSeparationCheck->isChecked()) | ||
551 | job->setMinMoonSeparation(minMoonSeparation->value()); | 552 | job->setMinMoonSeparation(minMoonSeparation->value()); | ||
552 | else | 553 | else | ||
553 | job->setMinMoonSeparation(-1); | 554 | job->setMinMoonSeparation(-1); | ||
554 | 555 | | |||
555 | // Check enforce weather constraints | 556 | // Check enforce weather constraints | ||
556 | job->setEnforceWeather(weatherCheck->isChecked()); | 557 | job->setEnforceWeather(weatherCheck->isChecked()); | ||
557 | // twilight constraints | 558 | // twilight constraints | ||
558 | job->setEnforceTwilight(twilightCheck->isChecked()); | 559 | job->setEnforceTwilight(twilightCheck->isChecked()); | ||
559 | 560 | | |||
560 | /* Verifications */ | 561 | /* Verifications */ | ||
561 | /* FIXME: perhaps use a method more visible to the end-user */ | 562 | /* FIXME: perhaps use a method more visible to the end-user */ | ||
562 | if (SchedulerJob::START_AT == job->getFileStartupCondition()) | 563 | if (SchedulerJob::START_AT == job->getFileStartupCondition()) | ||
563 | { | 564 | { | ||
564 | /* Warn if appending a job which startup time doesn't allow proper score */ | 565 | /* Warn if appending a job which startup time doesn't allow proper score */ | ||
565 | if (calculateJobScore(job, job->getStartupTime()) < 0) | 566 | if (calculateJobScore(job, job->getStartupTime()) < 0) | ||
566 | appendLogText(i18n("Warning: job '%1' has startup time %2 resulting in a negative score, and will be marked invalid when processed.", | 567 | appendLogText(i18n("Warning: job '%1' has startup time %2 resulting in a negative score, and will be marked invalid when processed.", | ||
567 | job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); | 568 | job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); | ||
568 | 569 | | |||
569 | /* Warn if appending a job with a startup time that is in the past */ | | |||
570 | if (job->getStartupTime() < KStarsData::Instance()->lt()) | | |||
571 | appendLogText(i18n("Warning: job '%1' has fixed startup time %2 set in the past, and will be marked invalid when evaluated.", | | |||
572 | job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); | | |||
573 | } | 570 | } | ||
574 | 571 | | |||
575 | // #3 Completion conditions | 572 | // #3 Completion conditions | ||
576 | if (sequenceCompletionR->isChecked()) | 573 | if (sequenceCompletionR->isChecked()) | ||
577 | { | 574 | { | ||
578 | job->setCompletionCondition(SchedulerJob::FINISH_SEQUENCE); | 575 | job->setCompletionCondition(SchedulerJob::FINISH_SEQUENCE); | ||
579 | } | 576 | } | ||
580 | else if (repeatCompletionR->isChecked()) | 577 | else if (repeatCompletionR->isChecked()) | ||
▲ Show 20 Lines • Show All 54 Lines • ▼ Show 20 Line(s) | 626 | { | |||
635 | if (a_job->getStartupTime() == a_job->getStartupTime() && a_job->getPriority() == job->getPriority()) | 632 | if (a_job->getStartupTime() == a_job->getStartupTime() && a_job->getPriority() == job->getPriority()) | ||
636 | appendLogText(i18n("Warning: job '%1' at row %2 might require a specific startup time or a different priority, " | 633 | appendLogText(i18n("Warning: job '%1' at row %2 might require a specific startup time or a different priority, " | ||
637 | "as currently they will start in order of insertion in the table", | 634 | "as currently they will start in order of insertion in the table", | ||
638 | job->getName(), currentRow)); | 635 | job->getName(), currentRow)); | ||
639 | } | 636 | } | ||
640 | } | 637 | } | ||
641 | } | 638 | } | ||
642 | 639 | | |||
643 | /* FIXME: Move part of the new job cell-wiring to setJobStatusCells */ | 640 | if (-1 == jobUnderEdit) | ||
644 | 641 | { | |||
645 | QTableWidgetItem *nameCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, static_cast<int>(SCHEDCOL_NAME)) : new QTableWidgetItem(); | 642 | QTableWidgetItem *nameCell = new QTableWidgetItem(); | ||
mutlaqja: Would this lead to memory leak or is it reparented when setItem(..) is used? | |||||
Not sure I understand the question. This code is only executed when adding a new line at currentRow. TallFurryMan: Not sure I understand the question. This code is only executed when adding a new line at… | |||||
646 | if (jobUnderEdit == -1) queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_NAME), nameCell); | 643 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_NAME), nameCell); | ||
647 | nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | 644 | nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||
648 | nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | 645 | nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||
649 | job->setNameCell(nameCell); | | |||
650 | 646 | | |||
651 | QTableWidgetItem *statusCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, static_cast<int>(SCHEDCOL_STATUS)) : new QTableWidgetItem(); | 647 | QTableWidgetItem *statusCell = new QTableWidgetItem(); | ||
652 | if (jobUnderEdit == -1) queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_STATUS), statusCell); | 648 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_STATUS), statusCell); | ||
653 | statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | 649 | statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||
654 | statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | 650 | statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||
655 | job->setStatusCell(statusCell); | | |||
656 | 651 | | |||
657 | QTableWidgetItem *captureCount = (jobUnderEdit >= 0) ? queueTable->item(currentRow, static_cast<int>(SCHEDCOL_CAPTURES)) : new QTableWidgetItem(); | 652 | QTableWidgetItem *captureCount = new QTableWidgetItem(); | ||
658 | if (jobUnderEdit == -1) queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_CAPTURES), captureCount); | 653 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_CAPTURES), captureCount); | ||
659 | captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | 654 | captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||
660 | captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | 655 | captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||
661 | job->setCaptureCountCell(captureCount); | | |||
662 | 656 | | |||
663 | QTableWidgetItem *scoreValue = (jobUnderEdit >= 0) ? queueTable->item(currentRow, static_cast<int>(SCHEDCOL_SCORE)) : new QTableWidgetItem(); | 657 | QTableWidgetItem *scoreValue = new QTableWidgetItem(); | ||
664 | if (jobUnderEdit == -1) queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_SCORE), scoreValue); | 658 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_SCORE), scoreValue); | ||
665 | scoreValue->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | 659 | scoreValue->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||
666 | scoreValue->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | 660 | scoreValue->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||
667 | job->setScoreCell(scoreValue); | | |||
668 | 661 | | |||
669 | QTableWidgetItem *startupCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, static_cast<int>(SCHEDCOL_STARTTIME)) : new QTableWidgetItem(); | 662 | QTableWidgetItem *startupCell = new QTableWidgetItem(); | ||
670 | if (jobUnderEdit == -1) queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_STARTTIME), startupCell); | 663 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_STARTTIME), startupCell); | ||
671 | startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | 664 | startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||
672 | startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | 665 | startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||
673 | job->setStartupCell(startupCell); | | |||
674 | 666 | | |||
675 | QTableWidgetItem *completionCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, static_cast<int>(SCHEDCOL_ENDTIME)) : new QTableWidgetItem(); | 667 | QTableWidgetItem *altitudeCell = new QTableWidgetItem(); | ||
676 | if (jobUnderEdit == -1) queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_ENDTIME), completionCell); | 668 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_ALTITUDE), altitudeCell); | ||
669 | altitudeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||||
670 | altitudeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||||
671 | | ||||
672 | QTableWidgetItem *completionCell = new QTableWidgetItem(); | ||||
673 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_ENDTIME), completionCell); | ||||
677 | completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | 674 | completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||
678 | completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | 675 | completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||
679 | job->setCompletionCell(completionCell); | | |||
680 | 676 | | |||
681 | QTableWidgetItem *estimatedTimeCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, static_cast<int>(SCHEDCOL_DURATION)) : new QTableWidgetItem(); | 677 | QTableWidgetItem *estimatedTimeCell = new QTableWidgetItem(); | ||
682 | if (jobUnderEdit == -1) queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_DURATION), estimatedTimeCell); | 678 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_DURATION), estimatedTimeCell); | ||
683 | estimatedTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | 679 | estimatedTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||
684 | estimatedTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | 680 | estimatedTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||
685 | job->setEstimatedTimeCell(estimatedTimeCell); | 681 | | ||
682 | QTableWidgetItem *leadTimeCell = new QTableWidgetItem(); | ||||
683 | queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_LEADTIME), leadTimeCell); | ||||
684 | leadTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | ||||
685 | leadTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); | ||||
686 | } | ||||
687 | | ||||
688 | setJobStatusCells(currentRow); | ||||
686 | 689 | | |||
687 | /* We just added or saved a job, so we have a job in the list - enable relevant buttons */ | 690 | /* We just added or saved a job, so we have a job in the list - enable relevant buttons */ | ||
688 | queueSaveAsB->setEnabled(true); | 691 | queueSaveAsB->setEnabled(true); | ||
689 | queueSaveB->setEnabled(true); | 692 | queueSaveB->setEnabled(true); | ||
690 | startB->setEnabled(true); | 693 | startB->setEnabled(true); | ||
691 | evaluateOnlyB->setEnabled(true); | 694 | evaluateOnlyB->setEnabled(true); | ||
692 | setJobManipulation(!Options::sortSchedulerJobs(), true); | 695 | setJobManipulation(!Options::sortSchedulerJobs(), true); | ||
693 | 696 | | |||
▲ Show 20 Lines • Show All 69 Lines • ▼ Show 20 Line(s) | 756 | { | |||
763 | 766 | | |||
764 | case SchedulerJob::START_AT: | 767 | case SchedulerJob::START_AT: | ||
765 | startupTimeConditionR->setChecked(true); | 768 | startupTimeConditionR->setChecked(true); | ||
766 | startupTimeEdit->setDateTime(job->getStartupTime()); | 769 | startupTimeEdit->setDateTime(job->getStartupTime()); | ||
767 | culminationOffset->setValue(DEFAULT_CULMINATION_TIME); | 770 | culminationOffset->setValue(DEFAULT_CULMINATION_TIME); | ||
768 | break; | 771 | break; | ||
769 | } | 772 | } | ||
770 | 773 | | |||
771 | if (job->getMinAltitude() >= 0) | 774 | if (-90 < job->getMinAltitude()) | ||
772 | { | 775 | { | ||
773 | altConstraintCheck->setChecked(true); | 776 | altConstraintCheck->setChecked(true); | ||
774 | minAltitude->setValue(job->getMinAltitude()); | 777 | minAltitude->setValue(job->getMinAltitude()); | ||
775 | } | 778 | } | ||
776 | else | 779 | else | ||
777 | { | 780 | { | ||
778 | altConstraintCheck->setChecked(false); | 781 | altConstraintCheck->setChecked(false); | ||
779 | minAltitude->setValue(DEFAULT_MIN_ALTITUDE); | 782 | minAltitude->setValue(DEFAULT_MIN_ALTITUDE); | ||
▲ Show 20 Lines • Show All 84 Lines • ▼ Show 20 Line(s) | 865 | { | |||
864 | queueUpB->setEnabled(0 < currentRow); | 867 | queueUpB->setEnabled(0 < currentRow); | ||
865 | queueDownB->setEnabled(currentRow < queueTable->rowCount() - 1); | 868 | queueDownB->setEnabled(currentRow < queueTable->rowCount() - 1); | ||
866 | } | 869 | } | ||
867 | else | 870 | else | ||
868 | { | 871 | { | ||
869 | queueUpB->setEnabled(false); | 872 | queueUpB->setEnabled(false); | ||
870 | queueDownB->setEnabled(false); | 873 | queueDownB->setEnabled(false); | ||
871 | } | 874 | } | ||
875 | sortJobsB->setEnabled(can_reorder); | ||||
872 | removeFromQueueB->setEnabled(can_delete); | 876 | removeFromQueueB->setEnabled(can_delete); | ||
873 | } | 877 | } | ||
874 | 878 | | |||
879 | bool Scheduler::reorderJobs(QList<SchedulerJob*> reordered_sublist) | ||||
880 | { | ||||
881 | /* Add jobs not reordered at the end of the list, in initial order */ | ||||
882 | foreach (SchedulerJob* job, jobs) | ||||
883 | if (!reordered_sublist.contains(job)) | ||||
884 | reordered_sublist.append(job); | ||||
885 | | ||||
886 | if (jobs != reordered_sublist) | ||||
887 | { | ||||
888 | /* Remember job currently selected */ | ||||
889 | int const selectedRow = queueTable->currentRow(); | ||||
890 | SchedulerJob * const selectedJob = 0 <= selectedRow ? jobs.at(selectedRow) : nullptr; | ||||
891 | | ||||
892 | /* Reassign list */ | ||||
893 | jobs = reordered_sublist; | ||||
894 | | ||||
895 | /* Reassign status cells for all jobs, and reset them */ | ||||
896 | for (int row = 0; row < jobs.size(); row++) | ||||
897 | setJobStatusCells(row); | ||||
898 | | ||||
899 | /* Reselect previously selected job */ | ||||
900 | if (nullptr != selectedJob) | ||||
901 | queueTable->selectRow(jobs.indexOf(selectedJob)); | ||||
902 | | ||||
903 | return true; | ||||
904 | } | ||||
905 | else return false; | ||||
906 | } | ||||
907 | | ||||
875 | void Scheduler::moveJobUp() | 908 | void Scheduler::moveJobUp() | ||
876 | { | 909 | { | ||
910 | /* No move if jobs are sorted automatically */ | ||||
911 | if (Options::sortSchedulerJobs()) | ||||
912 | return; | ||||
913 | | ||||
877 | int const rowCount = queueTable->rowCount(); | 914 | int const rowCount = queueTable->rowCount(); | ||
878 | int const currentRow = queueTable->currentRow(); | 915 | int const currentRow = queueTable->currentRow(); | ||
879 | int const destinationRow = currentRow - 1; | 916 | int const destinationRow = currentRow - 1; | ||
880 | 917 | | |||
881 | /* No move if no job selected, if table has one line or less or if destination is out of table */ | 918 | /* No move if no job selected, if table has one line or less or if destination is out of table */ | ||
882 | if (currentRow < 0 || rowCount <= 1 || destinationRow < 0) | 919 | if (currentRow < 0 || rowCount <= 1 || destinationRow < 0) | ||
883 | return; | 920 | return; | ||
884 | 921 | | |||
885 | /* Swap jobs in the list */ | 922 | /* Swap jobs in the list */ | ||
886 | jobs.swap(currentRow, destinationRow); | 923 | jobs.swap(currentRow, destinationRow); | ||
887 | 924 | | |||
888 | /* Reassign status cells */ | 925 | /* Reassign status cells */ | ||
889 | setJobStatusCells(currentRow); | 926 | setJobStatusCells(currentRow); | ||
890 | setJobStatusCells(destinationRow); | 927 | setJobStatusCells(destinationRow); | ||
891 | 928 | | |||
892 | /* Move selection to destination row */ | 929 | /* Move selection to destination row */ | ||
893 | queueTable->selectRow(destinationRow); | 930 | queueTable->selectRow(destinationRow); | ||
894 | setJobManipulation(!Options::sortSchedulerJobs(), true); | 931 | setJobManipulation(!Options::sortSchedulerJobs(), true); | ||
895 | 932 | | |||
896 | /* Make list modified */ | 933 | /* Jobs are now sorted, so reset all later jobs */ | ||
897 | setDirty(); | 934 | for (int row = destinationRow; row < jobs.size(); row++) | ||
898 | 935 | jobs.at(row)->reset(); | |||
899 | /* Reset all jobs starting from the one moved */ | | |||
900 | for (int i = currentRow; i < jobs.size(); i++) | | |||
901 | jobs.at(i)->reset(); | | |||
902 | 936 | | |||
903 | /* Run evaluation as jobs that can run now changed order - saveJob will only evaluate if a job is edited */ | 937 | /* Make list modified and evaluate jobs */ | ||
938 | mDirty = true; | ||||
904 | jobEvaluationOnly = true; | 939 | jobEvaluationOnly = true; | ||
905 | evaluateJobs(); | 940 | evaluateJobs(); | ||
mutlaqja: extra ; | |||||
906 | } | 941 | } | ||
907 | 942 | | |||
908 | void Scheduler::moveJobDown() | 943 | void Scheduler::moveJobDown() | ||
909 | { | 944 | { | ||
945 | /* No move if jobs are sorted automatically */ | ||||
946 | if (Options::sortSchedulerJobs()) | ||||
947 | return; | ||||
948 | | ||||
910 | int const rowCount = queueTable->rowCount(); | 949 | int const rowCount = queueTable->rowCount(); | ||
911 | int const currentRow = queueTable->currentRow(); | 950 | int const currentRow = queueTable->currentRow(); | ||
912 | int const destinationRow = currentRow + 1; | 951 | int const destinationRow = currentRow + 1; | ||
913 | 952 | | |||
914 | /* No move if no job selected, if table has one line or less or if destination is out of table */ | 953 | /* No move if no job selected, if table has one line or less or if destination is out of table */ | ||
915 | if (currentRow < 0 || rowCount <= 1 || destinationRow == rowCount) | 954 | if (currentRow < 0 || rowCount <= 1 || destinationRow == rowCount) | ||
916 | return; | 955 | return; | ||
917 | 956 | | |||
918 | /* Swap jobs in the list */ | 957 | /* Swap jobs in the list */ | ||
919 | jobs.swap(currentRow, destinationRow); | 958 | jobs.swap(currentRow, destinationRow); | ||
920 | 959 | | |||
921 | /* Reassign status cells */ | 960 | /* Reassign status cells */ | ||
922 | setJobStatusCells(currentRow); | 961 | setJobStatusCells(currentRow); | ||
923 | setJobStatusCells(destinationRow); | 962 | setJobStatusCells(destinationRow); | ||
924 | 963 | | |||
925 | /* Move selection to destination row */ | 964 | /* Move selection to destination row */ | ||
926 | queueTable->selectRow(destinationRow); | 965 | queueTable->selectRow(destinationRow); | ||
927 | setJobManipulation(!Options::sortSchedulerJobs(), true); | 966 | setJobManipulation(!Options::sortSchedulerJobs(), true); | ||
928 | 967 | | |||
929 | /* Make list modified */ | 968 | /* Jobs are now sorted, so reset all later jobs */ | ||
930 | setDirty(); | 969 | for (int row = currentRow; row < jobs.size(); row++) | ||
931 | 970 | jobs.at(row)->reset(); | |||
932 | /* Reset all jobs starting from the one moved */ | | |||
933 | for (int i = currentRow; i < jobs.size(); i++) | | |||
934 | jobs.at(i)->reset(); | | |||
935 | 971 | | |||
936 | /* Run evaluation as jobs that can run now changed order - saveJob will only evaluate if a job is edited */ | 972 | /* Make list modified and evaluate jobs */ | ||
973 | mDirty = true; | ||||
937 | jobEvaluationOnly = true; | 974 | jobEvaluationOnly = true; | ||
938 | evaluateJobs(); | 975 | evaluateJobs(); | ||
mutlaqja: extra ; | |||||
yurchor: Is this second semicolon important? | |||||
TallFurryMan: Seems to be a copy&paste error, thanks! | |||||
939 | } | 976 | } | ||
940 | 977 | | |||
941 | void Scheduler::setJobStatusCells(int row) | 978 | void Scheduler::setJobStatusCells(int row) | ||
942 | { | 979 | { | ||
943 | if (row < 0 || jobs.size() <= row) | 980 | if (row < 0 || jobs.size() <= row) | ||
944 | return; | 981 | return; | ||
945 | 982 | | |||
946 | SchedulerJob * const job = jobs.at(row); | 983 | SchedulerJob * const job = jobs.at(row); | ||
947 | 984 | | |||
948 | job->setNameCell(queueTable->item(row, static_cast<int>(SCHEDCOL_NAME))); | 985 | job->setNameCell(queueTable->item(row, static_cast<int>(SCHEDCOL_NAME))); | ||
949 | job->setStatusCell(queueTable->item(row, static_cast<int>(SCHEDCOL_STATUS))); | 986 | job->setStatusCell(queueTable->item(row, static_cast<int>(SCHEDCOL_STATUS))); | ||
950 | job->setCaptureCountCell(queueTable->item(row, static_cast<int>(SCHEDCOL_CAPTURES))); | 987 | job->setCaptureCountCell(queueTable->item(row, static_cast<int>(SCHEDCOL_CAPTURES))); | ||
951 | job->setScoreCell(queueTable->item(row, static_cast<int>(SCHEDCOL_SCORE))); | 988 | job->setScoreCell(queueTable->item(row, static_cast<int>(SCHEDCOL_SCORE))); | ||
989 | job->setAltitudeCell(queueTable->item(row, static_cast<int>(SCHEDCOL_ALTITUDE))); | ||||
952 | job->setStartupCell(queueTable->item(row, static_cast<int>(SCHEDCOL_STARTTIME))); | 990 | job->setStartupCell(queueTable->item(row, static_cast<int>(SCHEDCOL_STARTTIME))); | ||
953 | job->setCompletionCell(queueTable->item(row, static_cast<int>(SCHEDCOL_ENDTIME))); | 991 | job->setCompletionCell(queueTable->item(row, static_cast<int>(SCHEDCOL_ENDTIME))); | ||
954 | job->setEstimatedTimeCell(queueTable->item(row, static_cast<int>(SCHEDCOL_DURATION))); | 992 | job->setEstimatedTimeCell(queueTable->item(row, static_cast<int>(SCHEDCOL_DURATION))); | ||
993 | job->setLeadTimeCell(queueTable->item(row, static_cast<int>(SCHEDCOL_LEADTIME))); | ||||
994 | job->updateJobCells(); | ||||
955 | } | 995 | } | ||
956 | 996 | | |||
957 | void Scheduler::resetJobEdit() | 997 | void Scheduler::resetJobEdit() | ||
958 | { | 998 | { | ||
959 | if (jobUnderEdit < 0) | 999 | if (jobUnderEdit < 0) | ||
960 | return; | 1000 | return; | ||
961 | 1001 | | |||
962 | SchedulerJob * const job = jobs.at(jobUnderEdit); | 1002 | SchedulerJob * const job = jobs.at(jobUnderEdit); | ||
▲ Show 20 Lines • Show All 273 Lines • ▼ Show 20 Line(s) | 1270 | { | |||
1236 | 1276 | | |||
1237 | /* Set current job */ | 1277 | /* Set current job */ | ||
1238 | currentJob = job; | 1278 | currentJob = job; | ||
1239 | 1279 | | |||
1240 | /* Reassign job widgets, or reset to defaults */ | 1280 | /* Reassign job widgets, or reset to defaults */ | ||
1241 | if (currentJob) | 1281 | if (currentJob) | ||
1242 | { | 1282 | { | ||
1243 | currentJob->setStageLabel(jobStatus); | 1283 | currentJob->setStageLabel(jobStatus); | ||
1244 | queueTable->selectRow(currentJob->getStartupCell()->row()); | 1284 | queueTable->selectRow(jobs.indexOf(currentJob)); | ||
1245 | } | 1285 | } | ||
1246 | else | 1286 | else | ||
1247 | { | 1287 | { | ||
1248 | jobStatus->setText(i18n("No job running")); | 1288 | jobStatus->setText(i18n("No job running")); | ||
1249 | queueTable->clearSelection(); | 1289 | //queueTable->clearSelection(); | ||
1250 | } | 1290 | } | ||
1251 | } | 1291 | } | ||
1252 | 1292 | | |||
1253 | void Scheduler::evaluateJobs() | 1293 | void Scheduler::evaluateJobs() | ||
1254 | { | 1294 | { | ||
1295 | /* Don't evaluate if list is empty */ | ||||
1296 | if (jobs.isEmpty()) | ||||
1297 | return; | ||||
1298 | | ||||
1255 | /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */ | 1299 | /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */ | ||
1256 | QDateTime const now = KStarsData::Instance()->lt(); | 1300 | QDateTime const now = KStarsData::Instance()->lt(); | ||
1257 | 1301 | | |||
1258 | /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */ | 1302 | /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */ | ||
1259 | if (Options::rememberJobProgress()) | 1303 | if (Options::rememberJobProgress()) | ||
1260 | updateCompletedJobsCount(); | 1304 | updateCompletedJobsCount(); | ||
1261 | 1305 | | |||
1262 | /* Update dawn and dusk astronomical times - unconditionally in case date changed */ | 1306 | /* Update dawn and dusk astronomical times - unconditionally in case date changed */ | ||
1263 | calculateDawnDusk(); | 1307 | calculateDawnDusk(); | ||
1264 | 1308 | | |||
1265 | /* First, filter out non-schedulable jobs */ | 1309 | /* First, filter out non-schedulable jobs */ | ||
1266 | /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */ | 1310 | /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */ | ||
1267 | QList<SchedulerJob *> sortedJobs = jobs; | 1311 | QList<SchedulerJob *> sortedJobs = jobs; | ||
1268 | sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(),[](SchedulerJob* job) | 1312 | sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(),[](SchedulerJob* job) | ||
1269 | { return SchedulerJob::JOB_ABORTED < job->getState(); }), sortedJobs.end()); | 1313 | { return SchedulerJob::JOB_ABORTED < job->getState(); }), sortedJobs.end()); | ||
1270 | 1314 | | |||
1271 | /* Then reorder jobs by priority */ | 1315 | /* Then enumerate SchedulerJobs to consolidate imaging time */ | ||
1272 | /* FIXME: refactor so all sorts are using the same predicates */ | | |||
1273 | /* FIXME: use std::stable_sort as qStableSort is deprecated */ | | |||
1274 | if (Options::sortSchedulerJobs()) | | |||
1275 | qStableSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); | | |||
1276 | | ||||
1277 | /* Then enumerate SchedulerJobs, scheduling only what is required */ | | |||
1278 | foreach (SchedulerJob *job, sortedJobs) | 1316 | foreach (SchedulerJob *job, sortedJobs) | ||
1279 | { | 1317 | { | ||
1280 | /* Let aborted jobs be rescheduled later instead of forgetting them */ | 1318 | /* Let aborted jobs be rescheduled later instead of forgetting them */ | ||
1281 | /* FIXME: minimum altitude and altitude cutoff may cause loops here */ | | |||
1282 | switch (job->getState()) | 1319 | switch (job->getState()) | ||
1283 | { | 1320 | { | ||
1284 | /* If job is idle, set it for evaluation */ | | |||
1285 | case SchedulerJob::JOB_IDLE: | | |||
1286 | job->setState(SchedulerJob::JOB_EVALUATION); | | |||
1287 | job->setEstimatedTime(-1); | | |||
1288 | break; | | |||
1289 | | ||||
1290 | /* If job is aborted, reset it for evaluation */ | | |||
1291 | case SchedulerJob::JOB_ABORTED: | | |||
1292 | job->setState(SchedulerJob::JOB_EVALUATION); | | |||
1293 | break; | | |||
1294 | | ||||
1295 | /* If job is scheduled, quick-check startup and bypass evaluation if in future */ | | |||
1296 | case SchedulerJob::JOB_SCHEDULED: | 1321 | case SchedulerJob::JOB_SCHEDULED: | ||
1297 | if (job->getStartupTime() < now) | 1322 | /* If job is scheduled, bypass if set to start later with a fixed time */ | ||
1298 | break; | 1323 | if (SchedulerJob::START_AT == job->getFileStartupCondition()) | ||
1324 | if (now < job->getStartupTime()) | ||||
1299 | continue; | 1325 | continue; | ||
1326 | break; | ||||
1300 | 1327 | | |||
1301 | /* If job is in error, invalid or complete, bypass evaluation */ | | |||
1302 | case SchedulerJob::JOB_ERROR: | 1328 | case SchedulerJob::JOB_ERROR: | ||
1303 | case SchedulerJob::JOB_INVALID: | 1329 | case SchedulerJob::JOB_INVALID: | ||
1304 | case SchedulerJob::JOB_COMPLETE: | 1330 | case SchedulerJob::JOB_COMPLETE: | ||
1331 | /* If job is in error, invalid or complete, bypass evaluation */ | ||||
1305 | continue; | 1332 | continue; | ||
1306 | 1333 | | |||
1307 | /* If job is busy, edge case, bypass evaluation */ | | |||
1308 | case SchedulerJob::JOB_BUSY: | 1334 | case SchedulerJob::JOB_BUSY: | ||
1335 | /* If job is busy, edge case, bypass evaluation */ | ||||
1309 | continue; | 1336 | continue; | ||
1310 | 1337 | | |||
1311 | /* Else evaluate */ | 1338 | case SchedulerJob::JOB_IDLE: | ||
1339 | case SchedulerJob::JOB_ABORTED: | ||||
1312 | case SchedulerJob::JOB_EVALUATION: | 1340 | case SchedulerJob::JOB_EVALUATION: | ||
1341 | default: | ||||
1342 | /* If job is idle or aborted, re-evaluate completely */ | ||||
1343 | job->setEstimatedTime(-1); | ||||
1313 | break; | 1344 | break; | ||
1314 | } | 1345 | } | ||
1315 | 1346 | | |||
1347 | switch (job->getCompletionCondition()) | ||||
1348 | { | ||||
1349 | case SchedulerJob::FINISH_AT: | ||||
1350 | /* Job is complete if its fixed completion time is passed */ | ||||
1351 | if (job->getCompletionTime().isValid() && job->getCompletionTime() < now) | ||||
1352 | { | ||||
1353 | job->setState(SchedulerJob::JOB_COMPLETE); | ||||
1354 | continue; | ||||
1355 | } | ||||
1356 | break; | ||||
1357 | | ||||
1358 | case SchedulerJob::FINISH_REPEAT: | ||||
1316 | // In case of a repeating jobs, let's make sure we have more runs left to go | 1359 | // In case of a repeating jobs, let's make sure we have more runs left to go | ||
1317 | // If we don't, re-estimate imaging time for the scheduler job before concluding | 1360 | // If we don't, re-estimate imaging time for the scheduler job before concluding | ||
1318 | if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) | | |||
1319 | { | | |||
1320 | if (job->getRepeatsRemaining() == 0) | 1361 | if (job->getRepeatsRemaining() == 0) | ||
1321 | { | 1362 | { | ||
1322 | appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName())); | 1363 | appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName())); | ||
1323 | if (Options::rememberJobProgress()) | 1364 | if (Options::rememberJobProgress()) | ||
1324 | { | 1365 | { | ||
1325 | job->setState(SchedulerJob::JOB_EVALUATION); | | |||
1326 | job->setEstimatedTime(-1); | 1366 | job->setEstimatedTime(-1); | ||
1327 | } | 1367 | } | ||
1328 | else | 1368 | else | ||
1329 | { | 1369 | { | ||
1330 | job->setState(SchedulerJob::JOB_COMPLETE); | 1370 | job->setState(SchedulerJob::JOB_COMPLETE); | ||
1331 | job->setEstimatedTime(0); | 1371 | job->setEstimatedTime(0); | ||
1332 | continue; | 1372 | continue; | ||
1333 | } | 1373 | } | ||
1334 | } | 1374 | } | ||
1375 | break; | ||||
1376 | | ||||
1377 | default: | ||||
1378 | break; | ||||
1335 | } | 1379 | } | ||
1336 | 1380 | | |||
1337 | // -1 = Job is not estimated yet | 1381 | // -1 = Job is not estimated yet | ||
1338 | // -2 = Job is estimated but time is unknown | 1382 | // -2 = Job is estimated but time is unknown | ||
1339 | // > 0 Job is estimated and time is known | 1383 | // > 0 Job is estimated and time is known | ||
1340 | if (job->getEstimatedTime() == -1) | 1384 | if (job->getEstimatedTime() == -1) | ||
1341 | { | 1385 | { | ||
1342 | if (estimateJobTime(job) == false) | 1386 | if (estimateJobTime(job) == false) | ||
1343 | { | 1387 | { | ||
1344 | job->setState(SchedulerJob::JOB_INVALID); | 1388 | job->setState(SchedulerJob::JOB_INVALID); | ||
1345 | continue; | 1389 | continue; | ||
1346 | } | 1390 | } | ||
1347 | } | 1391 | } | ||
1348 | 1392 | | |||
1349 | if (job->getEstimatedTime() == 0) | 1393 | if (job->getEstimatedTime() == 0) | ||
1350 | { | 1394 | { | ||
1351 | job->setRepeatsRemaining(0); | 1395 | job->setRepeatsRemaining(0); | ||
1352 | job->setState(SchedulerJob::JOB_COMPLETE); | 1396 | job->setState(SchedulerJob::JOB_COMPLETE); | ||
1353 | continue; | 1397 | continue; | ||
1354 | } | 1398 | } | ||
1355 | 1399 | | |||
1356 | // #1 Check startup conditions | 1400 | // In any other case, evaluate | ||
1357 | switch (job->getStartupCondition()) | 1401 | job->setState(SchedulerJob::JOB_EVALUATION); | ||
1358 | { | 1402 | } | ||
1359 | // #1.1 ASAP? | | |||
1360 | case SchedulerJob::START_ASAP: | | |||
1361 | { | | |||
1362 | /* Job is to be started as soon as possible, so check its current score */ | | |||
1363 | int16_t const score = calculateJobScore(job, now); | | |||
1364 | 1403 | | |||
1365 | /* If it's not possible to run the job now, find proper altitude time */ | 1404 | /* | ||
1366 | if (score < 0) | 1405 | * At this step, we prepare scheduling of jobs. | ||
1367 | { | 1406 | * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time. | ||
1368 | // If Altitude or Dark score are negative, we try to schedule a better time for altitude and dark sky period. | 1407 | */ | ||
1369 | if (calculateAltitudeTime(job, job->getMinAltitude() > 0 ? job->getMinAltitude() : 0, | 1408 | | ||
1370 | job->getMinMoonSeparation())) | 1409 | updatePreDawn(); | ||
1410 | | ||||
1411 | /* Remove jobs that don't need evaluation */ | ||||
1412 | sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(),[](SchedulerJob* job) | ||||
1413 | { return SchedulerJob::JOB_EVALUATION != job->getState(); }), sortedJobs.end()); | ||||
1414 | | ||||
1415 | /* If there are no jobs left to run in the filtered list, stop evaluation */ | ||||
1416 | if (sortedJobs.isEmpty()) | ||||
1371 | { | 1417 | { | ||
1372 | job->setState(SchedulerJob::JOB_SCHEDULED); | 1418 | appendLogText(i18n("No jobs left in the scheduler queue.")); | ||
1419 | setCurrentJob(nullptr); | ||||
1420 | jobEvaluationOnly = false; | ||||
1421 | return; | ||||
1373 | } | 1422 | } | ||
1374 | else | 1423 | | ||
1424 | /* If option says so, reorder by altitude and priority before sequencing */ | ||||
1425 | /* FIXME: refactor so all sorts are using the same predicates */ | ||||
1426 | /* FIXME: use std::stable_sort as qStableSort is deprecated */ | ||||
1427 | /* FIXME: dissociate altitude and priority, it's difficult to choose which predicate to use first */ | ||||
1428 | qCInfo(KSTARS_EKOS_SCHEDULER) << "Option to sort jobs based on priority and altitude is" << Options::sortSchedulerJobs(); | ||||
1429 | if (Options::sortSchedulerJobs()) | ||||
1375 | { | 1430 | { | ||
1376 | job->setState(SchedulerJob::JOB_INVALID); | 1431 | using namespace std::placeholders; | ||
1432 | std::stable_sort(sortedJobs.begin(), sortedJobs.end(), | ||||
1433 | std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, KStarsData::Instance()->lt())); | ||||
1434 | std::stable_sort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); | ||||
1377 | } | 1435 | } | ||
1378 | 1436 | | |||
1379 | /* Keep the job score for current time, score will refresh as scheduler progresses */ | 1437 | /* The first reordered job has no lead time - this could also be the delay from now to startup */ | ||
1380 | /* score = calculateJobScore(job, job->getStartupTime()); */ | 1438 | sortedJobs.first()->setLeadTime(0); | ||
1381 | job->setScore(score); | 1439 | | ||
1382 | } | 1440 | /* The objective of the following block is to make sure jobs are sequential in the list filtered previously. | ||
1383 | /* If it's possible to run the job now, check weather */ | 1441 | * | ||
1384 | else if (isWeatherOK(job) == false) | 1442 | * The algorithm manages overlap between jobs by stating that scheduled jobs that start sooner are non-movable. | ||
1443 | * If the completion time of the previous job overlaps the current job, we offset the startup of the current job. | ||||
1444 | * Jobs that have no valid startup time when evaluated (ASAP jobs) are assigned an immediate startup time. | ||||
1445 | * The lead time from the Options registry is used as a buffer between jobs. | ||||
1446 | * | ||||
1447 | * Note about the situation where the current job overlaps the next job, and the next job is not movable: | ||||
1448 | * - If we mark the current job invalid, it will not be processed at all. Dropping is not satisfactory. | ||||
1449 | * - If we move the current job after the fixed job, we need to restart evaluation with a new list, and risk an | ||||
1450 | * infinite loop eventually. This means swapping schedules, and is incompatible with altitude/priority sort. | ||||
1451 | * - If we mark the current job aborted, it will be re-evaluated each time a job is complete to see if it can fit. | ||||
1452 | * Although puzzling for the end-user, this solution is dynamic: the aborted job might or might not be scheduled | ||||
1453 | * at the planned time slot. But as the end-user did not enforce the start time, this is acceptable. Moreover, the | ||||
1454 | * schedule will be altered by external events during the execution. | ||||
1455 | * | ||||
1456 | * Here are the constraints that have an effect on the job being examined, and indirectly on all subsequent jobs: | ||||
1457 | * - Twilight constraint moves jobs to the next dark sky interval. | ||||
1458 | * - Altitude constraint, currently linked with Moon separation, moves jobs to the next acceptable altitude time. | ||||
1459 | * - Culmination constraint moves jobs to the next transit time, with arbitrary offset. | ||||
1460 | * - Fixed startup time moves jobs to a fixed time, essentially making them non-movable, or invalid if in the past. | ||||
1461 | * | ||||
1462 | * Here are the constraints that have an effect on jobs following the job being examined: | ||||
1463 | * - Repeats requirement increases the duration of the current job, pushing subsequent jobs. | ||||
1464 | * - Looping requirement causes subsequent jobs to become invalid (until dynamic priority is implemented). | ||||
1465 | * - Fixed completion makes subsequent jobs start after that boundary time. | ||||
1466 | * | ||||
1467 | * However, we need a way to inform the end-user about failed schedules clearly in the UI. | ||||
1468 | * The message to get through is that if jobs are not sorted by altitude/priority, the aborted or invalid jobs | ||||
1469 | * should be modified or manually moved to a better position. If jobs are sorted automatically, aborted jobs will | ||||
1470 | * be processed when possible, probably not at the expected moment. | ||||
1471 | */ | ||||
1472 | | ||||
1473 | // Make sure no two jobs have the same scheduled time or overlap with other jobs | ||||
1474 | for (int index = 0; index < sortedJobs.size(); index++) | ||||
1385 | { | 1475 | { | ||
1386 | appendLogText(i18n("Job '%1' cannot run now because of bad weather.", job->getName())); | 1476 | SchedulerJob * const currentJob = sortedJobs.at(index); | ||
1387 | job->setState(SchedulerJob::JOB_ABORTED); | 1477 | | ||
1388 | job->setScore(BAD_SCORE); | 1478 | // At this point, a job with no valid start date is a problem, so consider invalid startup time is now | ||
1389 | } | 1479 | if (!currentJob->getStartupTime().isValid()) | ||
1390 | /* If weather is ok, schedule the job to run now */ | 1480 | currentJob->setStartupTime(now); | ||
1391 | else | 1481 | | ||
1482 | // Locate the previous scheduled job, so that a full schedule plan may be actually consolidated | ||||
1483 | SchedulerJob const * previousJob = nullptr; | ||||
1484 | for (int i = index - 1; 0 <= i; i--) | ||||
1392 | { | 1485 | { | ||
1393 | appendLogText(i18n("Job '%1' is due to run as soon as possible.", job->getName())); | 1486 | SchedulerJob const * const a_job = sortedJobs.at(i); | ||
1394 | /* Give a proper start time, so that job can be rescheduled if others also start asap */ | 1487 | | ||
1395 | job->setStartupTime(now); | 1488 | if (SchedulerJob::JOB_SCHEDULED == a_job->getState()) | ||
1396 | job->setState(SchedulerJob::JOB_SCHEDULED); | 1489 | { | ||
1397 | job->setScore(score); | 1490 | previousJob = a_job; | ||
1491 | break; | ||||
1398 | } | 1492 | } | ||
1399 | } | 1493 | } | ||
1400 | break; | | |||
1401 | 1494 | | |||
1402 | // #1.2 Culmination? | 1495 | Q_ASSERT_X(nullptr == previousJob || previousJob != currentJob, __FUNCTION__, "Previous job considered for schedule is either undefined or not equal to current."); | ||
1403 | case SchedulerJob::START_CULMINATION: | 1496 | | ||
1497 | // Locate the next job - nothing special required except end of list check | ||||
1498 | SchedulerJob const * const nextJob = index + 1 < sortedJobs.size() ? sortedJobs.at(index + 1) : nullptr; | ||||
1499 | | ||||
1500 | Q_ASSERT_X(nullptr == nextJob || nextJob != currentJob, __FUNCTION__, "Next job considered for schedule is either undefined or not equal to current."); | ||||
1501 | | ||||
1502 | // We're attempting to schedule the job 10 times before making it invalid | ||||
1503 | for (int attempt = 1; attempt < 11; attempt++) | ||||
1404 | { | 1504 | { | ||
1405 | if (calculateCulmination(job)) | 1505 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Schedule attempt #%1 for %2-second job '%3' on row #%4 starting at %5, completing at %6.") | ||
1506 | .arg(attempt) | ||||
1507 | .arg(static_cast<int>(currentJob->getEstimatedTime())) | ||||
1508 | .arg(currentJob->getName()) | ||||
1509 | .arg(index + 1) | ||||
1510 | .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) | ||||
1511 | .arg(currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat())); | ||||
1512 | | ||||
1513 | | ||||
1514 | // ----- #1 Should we reject the current job because of its fixed startup time? | ||||
1515 | // | ||||
1516 | // A job with fixed startup time must be processed at the time of startup, and may be late up to leadTime. | ||||
1517 | // When such a job repeats, its startup time is reinitialized to prevent abort - see completion algorithm. | ||||
1518 | // If such a job requires night time, minimum altitude or Moon separation, the consolidated startup time is checked for errors. | ||||
1519 | // If all restrictions are complied with, we bypass the rest of the verifications as the job cannot be moved. | ||||
1520 | | ||||
1521 | if (SchedulerJob::START_AT == currentJob->getFileStartupCondition()) | ||||
1522 | { | ||||
1523 | // Check whether the current job is too far in the past to be processed - if job is repeating, its startup time is already now | ||||
1524 | if (currentJob->getStartupTime().addSecs(static_cast <int> (ceil(Options::leadTime()*60))) < now) | ||||
1406 | { | 1525 | { | ||
1407 | appendLogText(i18n("Job '%1' is scheduled at %2 for culmination.", job->getName(), | 1526 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||
1408 | job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); | 1527 | | ||
1409 | job->setState(SchedulerJob::JOB_SCHEDULED); | 1528 | | ||
1529 | appendLogText(i18n("Warning: job '%1' has fixed startup time %2 set in the past, marking invalid.", | ||||
1530 | currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); | ||||
1531 | | ||||
1532 | break; | ||||
1410 | } | 1533 | } | ||
1411 | else | 1534 | // Check whether the current job has a positive dark sky score at the time of startup | ||
1535 | else if (true == currentJob->getEnforceTwilight() && getDarkSkyScore(currentJob->getStartupTime()) < 0) | ||||
1412 | { | 1536 | { | ||
1413 | appendLogText(i18n("Job '%1' culmination cannot be scheduled, marking invalid.", job->getName())); | 1537 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||
1414 | job->setState(SchedulerJob::JOB_INVALID); | 1538 | | ||
1539 | appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its twilight restriction, marking invalid.", | ||||
1540 | currentJob->getName())); | ||||
1541 | | ||||
1542 | break; | ||||
mutlaqja: can such jobs be scheduled later than 24 hours later? | |||||
There is no temporal boundary anymore, jobs can be even be scheduled a few years in advance. TallFurryMan: There is no temporal boundary anymore, jobs can be even be scheduled a few years in advance.
So… | |||||
1415 | } | 1543 | } | ||
1544 | // Check whether the current job has a positive altitude score at the time of startup | ||||
1545 | else if (-90 < currentJob->getMinAltitude() && getAltitudeScore(currentJob, currentJob->getStartupTime()) < 0) | ||||
1546 | { | ||||
1547 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||||
1548 | | ||||
1549 | appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its altitude restriction, marking invalid.", | ||||
1550 | currentJob->getName())); | ||||
1551 | | ||||
1552 | break; | ||||
1553 | } | ||||
1554 | // Check whether the current job has a positive Moon separation score at the time of startup | ||||
1555 | else if (0 < currentJob->getMinMoonSeparation() && getMoonSeparationScore(currentJob, currentJob->getStartupTime()) < 0) | ||||
1556 | { | ||||
1557 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||||
1558 | | ||||
1559 | appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its Moon separation restriction, marking invalid.", | ||||
1560 | currentJob->getName())); | ||||
1561 | | ||||
1562 | break; | ||||
1416 | } | 1563 | } | ||
1564 | | ||||
1565 | // This job is non-movable, we're done | ||||
1566 | currentJob->setState(SchedulerJob::JOB_SCHEDULED); | ||||
1567 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with fixed startup time requirement.") | ||||
1568 | .arg(currentJob->getName()) | ||||
1569 | .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); | ||||
1570 | | ||||
1417 | break; | 1571 | break; | ||
1572 | } | ||||
1418 | 1573 | | |||
1419 | // #1.3 Start at? | 1574 | // ----- #2 Should we delay the current job because it overlaps the previous job? | ||
1420 | case SchedulerJob::START_AT: | 1575 | // | ||
1576 | // The previous job is considered non-movable, and its completion, plus lead time, is the origin for the current job. | ||||
1577 | // If no previous job exists, or if all prior jobs in the list are rejected, there is no overlap. | ||||
1578 | // If there is a previous job, the current job is simply delayed to avoid an eventual overlap. | ||||
1579 | // IF there is a previous job but it never finishes, the current job is rejected. | ||||
1580 | // This scheduling obviously relies on imaging time estimation: because errors stack up, future startup times are less and less reliable. | ||||
1581 | | ||||
1582 | if (nullptr != previousJob) | ||||
1421 | { | 1583 | { | ||
1422 | if (job->getCompletionCondition() == SchedulerJob::FINISH_AT) | 1584 | if (previousJob->getCompletionTime().isValid()) | ||
1423 | { | 1585 | { | ||
1424 | if (job->getCompletionTime() <= job->getStartupTime()) | 1586 | // Calculate time we should be at after finishing the previous job | ||
1587 | QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast <int> (ceil(Options::leadTime()*60.0))); | ||||
1588 | | ||||
1589 | // Delay the current job to completion of its previous sibling if needed - this updates the completion time automatically | ||||
1590 | if (currentJob->getStartupTime() < previousCompletionTime) | ||||
1425 | { | 1591 | { | ||
1426 | appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3), marking invalid", job->getName(), | 1592 | currentJob->setStartupTime(previousCompletionTime); | ||
1427 | job->getCompletionTime().toString(job->getDateTimeDisplayFormat()), | 1593 | | ||
1428 | job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); | 1594 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, %3 seconds after %4, in compliance with previous job completion requirement.") | ||
1429 | job->setState(SchedulerJob::JOB_INVALID); | 1595 | .arg(currentJob->getName()) | ||
1596 | .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) | ||||
1597 | .arg(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())) | ||||
1598 | .arg(previousJob->getCompletionTime().toString(previousJob->getDateTimeDisplayFormat())); | ||||
1599 | | ||||
1600 | // If the job is repeating or looping, re-estimate imaging duration - error case may be a bug | ||||
1601 | if (SchedulerJob::FINISH_SEQUENCE != currentJob->getCompletionCondition()) | ||||
1602 | if (false == estimateJobTime(currentJob)) | ||||
1603 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||||
1604 | | ||||
1430 | continue; | 1605 | continue; | ||
1431 | } | 1606 | } | ||
1432 | } | 1607 | } | ||
1608 | else | ||||
1609 | { | ||||
1610 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||||
1433 | 1611 | | |||
1434 | int const timeUntil = now.secsTo(job->getStartupTime()); | 1612 | appendLogText(i18n("Warning: Job '%1' cannot start because its previous sibling has no completion time, marking invalid.", | ||
1613 | currentJob->getName())); | ||||
1435 | 1614 | | |||
1436 | // If starting time already passed by 5 minutes (default), we mark the job as invalid or aborted | 1615 | break; | ||
1437 | if (timeUntil < (-1 * Options::leadTime() * 60)) | 1616 | } | ||
1438 | { | | |||
1439 | dms const passedUp(-timeUntil * 15.0 / 3600.0); | | |||
1440 | 1617 | | |||
1441 | /* Mark the job invalid only if its startup time was a user request, else just abort it for later reschedule */ | 1618 | currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); | ||
1442 | if (job->getFileStartupCondition() == SchedulerJob::START_AT) | 1619 | | ||
1443 | { | 1620 | Q_ASSERT_X(previousJob->getCompletionTime() < currentJob->getStartupTime(), __FUNCTION__, "Previous and current jobs do not overlap."); | ||
1444 | appendLogText(i18n("Job '%1' startup time was fixed at %2, and is already passed by %3, marking invalid.", | | |||
1445 | job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()), passedUp.toHMSString())); | | |||
1446 | job->setState(SchedulerJob::JOB_INVALID); | | |||
1447 | } | 1621 | } | ||
1448 | /* Don't abort a job that is repeating because it started long ago, that delay is expected */ | 1622 | | ||
1449 | else if (job->getRepeatsRequired() <= 1) | 1623 | | ||
1624 | // ----- #3 Should we delay the current job because it overlaps daylight? | ||||
1625 | // | ||||
1626 | // Pre-dawn time rules whether a job may be started before dawn, or delayed to next night. | ||||
1627 | // Note that the case of START_AT jobs is considered earlier in the algorithm, thus may be omitted here. | ||||
1628 | // In addition to be hardcoded currently, the imaging duration is not reliable enough to start a short job during pre-dawn. | ||||
1629 | // However, completion time during daylight only causes a warning, as this case will be processed as the job runs. | ||||
1630 | | ||||
1631 | if (currentJob->getEnforceTwilight()) | ||||
1450 | { | 1632 | { | ||
1451 | appendLogText(i18n("Job '%1' startup time was %2, and is already passed by %3, marking aborted.", | 1633 | // During that check, we don't verify the current job can actually complete before dawn. | ||
1452 | job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()), passedUp.toHMSString())); | 1634 | // If the job is interrupted while running, it will be aborted and rescheduled at a later time. | ||
1453 | job->setState(SchedulerJob::JOB_ABORTED); | 1635 | | ||
1454 | } | 1636 | // We wouldn't start observation 30 mins (default) before dawn. | ||
1455 | } | 1637 | // FIXME: Refactor duplicated dawn/dusk calculations | ||
1456 | // Start scoring once we reach startup time | 1638 | double const earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); | ||
1457 | else if (timeUntil <= 0) | 1639 | | ||
1640 | // Compute dawn time for the startup date of the job | ||||
1641 | // FIXME: Use KAlmanac to find the real dawn/dusk time for the day the job is supposed to be processed | ||||
1642 | QDateTime const dawnDateTime(currentJob->getStartupTime().date(), QTime(0,0).addSecs(earlyDawn * 24 * 3600)); | ||||
1643 | | ||||
1644 | // Check if the job starts after dawn | ||||
1645 | if (dawnDateTime < currentJob->getStartupTime()) | ||||
1458 | { | 1646 | { | ||
1459 | /* Consolidate altitude, moon separation and sky darkness scores */ | 1647 | // Compute dusk time for the startup date of the job - no lead time on dusk | ||
1460 | int16_t const score = calculateJobScore(job, now); | 1648 | QDateTime const duskDateTime(currentJob->getStartupTime().date(), QTime(0,0).addSecs(Dusk * 24 * 3600)); | ||
1461 | 1649 | | |||
1462 | if (score < 0) | 1650 | // Check if the job starts before dusk | ||
1651 | if (currentJob->getStartupTime() < duskDateTime) | ||||
1463 | { | 1652 | { | ||
1464 | /* If job score is already negative, silently abort the job to avoid spamming the user */ | 1653 | // Delay job to next dusk - we will check other requirements later on | ||
1465 | if (0 < job->getScore()) | 1654 | currentJob->setStartupTime(duskDateTime); | ||
1466 | { | | |||
1467 | if (job->getState() == SchedulerJob::JOB_EVALUATION) | | |||
1468 | appendLogText(i18n("Job '%1' evaluation failed with a score of %2, marking aborted.", | | |||
1469 | job->getName(), score)); | | |||
1470 | else if (timeUntil == 0) | | |||
1471 | appendLogText(i18n("Job '%1' updated score is %2 at startup time, marking aborted.", | | |||
1472 | job->getName(), score)); | | |||
1473 | else | | |||
1474 | appendLogText(i18n("Job '%1' updated score is %2 %3 seconds after startup time, marking aborted.", | | |||
1475 | job->getName(), score, abs(timeUntil))); | | |||
1476 | } | | |||
1477 | 1655 | | |||
1478 | job->setState(SchedulerJob::JOB_ABORTED); | 1656 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with night time requirement.") | ||
1479 | job->setScore(score); | 1657 | .arg(currentJob->getName()) | ||
1658 | .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); | ||||
1659 | | ||||
1660 | continue; | ||||
1480 | } | 1661 | } | ||
1481 | /* Positive score means job is already scheduled, so we check the weather, and if it is not OK, we set bad score until weather improves. */ | | |||
1482 | else if (isWeatherOK(job) == false) | | |||
1483 | { | | |||
1484 | appendLogText(i18n("Job '%1' cannot run now because of bad weather.", job->getName())); | | |||
1485 | job->setState(SchedulerJob::JOB_ABORTED); | | |||
1486 | job->setScore(BAD_SCORE); | | |||
1487 | } | 1662 | } | ||
1488 | /* Else record current score */ | 1663 | | ||
1489 | else | 1664 | // Compute dawn time for the day following the startup time, but disregard the pre-dawn offset as we'll consider completion | ||
1665 | // FIXME: Use KAlmanac to find the real dawn/dusk time for the day next to the day the job is supposed to be processed | ||||
1666 | QDateTime const nextDawnDateTime(currentJob->getStartupTime().date().addDays(1), QTime(0,0).addSecs(Dawn * 24 * 3600)); | ||||
1667 | | ||||
1668 | // Check if the completion date overlaps the next dawn, and issue a warning if so | ||||
1669 | if (nextDawnDateTime < currentJob->getCompletionTime()) | ||||
1490 | { | 1670 | { | ||
1491 | appendLogText(i18n("Job '%1' will be run at %2.", job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); | 1671 | appendLogText(i18n("Warning: job '%1' execution overlaps daylight, it will be interrupted at dawn and rescheduled on next night time.", | ||
1492 | job->setState(SchedulerJob::JOB_SCHEDULED); | 1672 | currentJob->getName())); | ||
1493 | job->setScore(score); | | |||
1494 | } | 1673 | } | ||
1674 | | ||||
1675 | | ||||
1676 | Q_ASSERT_X(0 <= getDarkSkyScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated startup time results in a positive dark sky score."); | ||||
1495 | } | 1677 | } | ||
1496 | #if 0 | 1678 | | ||
1497 | // If it is in the future and originally was designated as ASAP job | 1679 | | ||
1498 | // Job must be less than 12 hours away to be considered for re-evaluation | 1680 | // ----- #4 Should we delay the current job because of its target culmination? | ||
1499 | else if (timeUntil > (Options::leadTime() * 60) && (timeUntil < 12 * 3600) && | 1681 | // | ||
1500 | job->getFileStartupCondition() == SchedulerJob::START_ASAP) | 1682 | // Culmination uses the transit time, and fixes the startup time of the job to a particular offset around this transit time. | ||
1501 | { | 1683 | // This restriction may be used to start a job at the least air mass, or after a meridian flip. | ||
1502 | QDateTime nextJobTime = now.addSecs(Options::leadTime() * 60); | 1684 | // Culmination is scheduled before altitude restriction because it is normally more restrictive for the resulting startup time. | ||
1503 | if (job->getEnforceTwilight() == false || (now > duskDateTime && now < preDawnDateTime)) | 1685 | // It may happen that a target cannot rise enough to comply with the altitude restriction, but a culmination time is always valid. | ||
1686 | | ||||
1687 | if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition()) | ||||
1504 | { | 1688 | { | ||
1505 | appendLogText(i18n("Job '%1' can be scheduled under 12 hours, but will be re-evaluated at %2.", | 1689 | // Consolidate the culmination time, with offset, of the current job | ||
1506 | job->getName(), nextJobTime.toString(job->getDateTimeDisplayFormat()))); | 1690 | QDateTime const nextCulminationTime = calculateCulmination(currentJob, currentJob->getCulminationOffset(), currentJob->getStartupTime()); | ||
1507 | job->setStartupTime(nextJobTime); | 1691 | | ||
1508 | } | 1692 | if (nextCulminationTime.isValid()) // Guaranteed | ||
1509 | job->setScore(BAD_SCORE); | | |||
1510 | } | | |||
1511 | // If time is far in the future, we make the score negative | | |||
1512 | else | | |||
1513 | { | 1693 | { | ||
1514 | if (job->getState() == SchedulerJob::JOB_EVALUATION && | 1694 | if (currentJob->getStartupTime() < nextCulminationTime) | ||
1515 | calculateJobScore(job, job->getStartupTime()) < 0) | | |||
1516 | { | 1695 | { | ||
1517 | appendLogText(i18n("Job '%1' can only be scheduled in more than 12 hours, marking aborted.", | 1696 | currentJob->setStartupTime(nextCulminationTime); | ||
1518 | job->getName())); | 1697 | | ||
1519 | job->setState(SchedulerJob::JOB_ABORTED); | 1698 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with culmination requirements.") | ||
1699 | .arg(currentJob->getName()) | ||||
1700 | .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); | ||||
1701 | | ||||
1520 | continue; | 1702 | continue; | ||
1521 | } | 1703 | } | ||
1522 | | ||||
1523 | /*score += BAD_SCORE;*/ | | |||
1524 | } | 1704 | } | ||
1525 | #endif | | |||
1526 | /* Else simply refresh job score */ | | |||
1527 | else | 1705 | else | ||
1528 | { | 1706 | { | ||
1529 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' unmodified, will be run at %2.") | 1707 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||
1530 | .arg(job->getName()) | 1708 | | ||
1531 | .arg(job->getStartupTime().toString(job->getDateTimeDisplayFormat())); | 1709 | appendLogText(i18n("Warning: job '%1' requires culmination offset of %2 minutes, not achievable, marking invalid.", | ||
1532 | job->setState(SchedulerJob::JOB_SCHEDULED); | 1710 | currentJob->getName(), | ||
1533 | job->setScore(calculateJobScore(job, now)); | 1711 | QString("%L1").arg(currentJob->getCulminationOffset()))); | ||
1534 | } | | |||
1535 | 1712 | | |||
1536 | } | | |||
1537 | break; | 1713 | break; | ||
1538 | } | 1714 | } | ||
1539 | 1715 | | |||
1540 | if (job->getState() == SchedulerJob::JOB_EVALUATION) | 1716 | // Don't test altitude here, because we will push the job during the next check step | ||
1541 | { | 1717 | // Q_ASSERT_X(0 <= getAltitudeScore(currentJob, currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); | ||
1542 | qCDebug(KSTARS_EKOS_SCHEDULER) << "BUGBUG! Job '" << job->getName() << "' was unexpectedly not scheduled by evaluation."; | | |||
1543 | } | | |||
1544 | } | 1718 | } | ||
1545 | 1719 | | |||
1546 | /* | | |||
1547 | * At this step, we scheduled all jobs that had to be scheduled because they could not start as soon as possible. | | |||
1548 | * Now we check the amount of jobs we have to run. | | |||
1549 | */ | | |||
1550 | 1720 | | |||
1551 | int invalidJobs = 0, completedJobs = 0, abortedJobs = 0, upcomingJobs = 0; | 1721 | // ----- #5 Should we delay the current job because its altitude is incorrect? | ||
1722 | // | ||||
1723 | // Altitude time ensures the job is assigned a startup time when its target is high enough. | ||||
1724 | // As other restrictions, the altitude is only considered for startup time, completion time is managed while the job is running. | ||||
1725 | // Because a target setting down is a problem for the schedule, a cutoff altitude is added in the case the job target is past the meridian at startup time. | ||||
1726 | // FIXME: though arguable, Moon separation is also considered in that restriction check - move it to a separate case. | ||||
1552 | 1727 | | |||
1553 | /* Partition jobs into invalid/aborted/completed/upcoming jobs */ | 1728 | if (-90 < currentJob->getMinAltitude()) | ||
1554 | foreach (SchedulerJob *job, jobs) | | |||
1555 | { | | |||
1556 | switch (job->getState()) | | |||
1557 | { | 1729 | { | ||
1558 | case SchedulerJob::JOB_INVALID: | 1730 | // Consolidate a new altitude time from the startup time of the current job | ||
1559 | invalidJobs++; | 1731 | QDateTime const nextAltitudeTime = calculateAltitudeTime(currentJob, currentJob->getMinAltitude(), currentJob->getMinMoonSeparation(), currentJob->getStartupTime()); | ||
1560 | break; | | |||
1561 | | ||||
1562 | case SchedulerJob::JOB_ERROR: | | |||
1563 | case SchedulerJob::JOB_ABORTED: | | |||
1564 | abortedJobs++; | | |||
1565 | break; | | |||
1566 | 1732 | | |||
1567 | case SchedulerJob::JOB_COMPLETE: | 1733 | if (nextAltitudeTime.isValid()) | ||
1568 | completedJobs++; | 1734 | { | ||
1569 | break; | 1735 | if (currentJob->getStartupTime() < nextAltitudeTime) | ||
1736 | { | ||||
1737 | currentJob->setStartupTime(nextAltitudeTime); | ||||
1570 | 1738 | | |||
1571 | case SchedulerJob::JOB_SCHEDULED: | 1739 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with altitude and Moon separation requirements.") | ||
1572 | case SchedulerJob::JOB_BUSY: | 1740 | .arg(currentJob->getName()) | ||
1573 | upcomingJobs++; | 1741 | .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); | ||
1574 | break; | | |||
1575 | 1742 | | |||
1576 | default: | 1743 | continue; | ||
1577 | break; | | |||
1578 | } | 1744 | } | ||
1579 | } | 1745 | } | ||
1580 | 1746 | else | |||
1581 | /* And render some statistics */ | | |||
1582 | if (upcomingJobs == 0 && jobEvaluationOnly == false) | | |||
1583 | { | 1747 | { | ||
1584 | if (invalidJobs > 0) | 1748 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||
1585 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("%L1 job(s) invalid.").arg(invalidJobs); | | |||
1586 | 1749 | | |||
1587 | if (abortedJobs > 0) | 1750 | appendLogText(i18n("Warning: job '%1' requires minimum altitude %2 and Moon separation %3, not achievable, marking invalid.", | ||
1588 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("%L1 job(s) aborted.").arg(abortedJobs); | 1751 | currentJob->getName(), | ||
1752 | QString("%L1").arg(static_cast<double>(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals()), | ||||
1753 | 0.0 < currentJob->getMinMoonSeparation() ? | ||||
1754 | QString("%L1").arg(static_cast<double>(currentJob->getMinMoonSeparation()), 0, 'f', minMoonSeparation->decimals()) : | ||||
1755 | QString("-"))); | ||||
1589 | 1756 | | |||
1590 | if (completedJobs > 0) | 1757 | break; | ||
1591 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("%L1 job(s) completed.").arg(completedJobs); | | |||
1592 | } | 1758 | } | ||
1593 | 1759 | | |||
1594 | /* | 1760 | Q_ASSERT_X(0 <= getAltitudeScore(currentJob, currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); | ||
1595 | * At this step, we still have jobs to run. | 1761 | } | ||
1596 | * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time. | | |||
1597 | */ | | |||
1598 | 1762 | | |||
1599 | updatePreDawn(); | | |||
1600 | 1763 | | |||
1601 | /* Remove complete and invalid jobs that could have appeared during the last evaluation */ | 1764 | // ----- #6 Should we reject the current job because it overlaps the next job and that next job is not movable? | ||
1602 | sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(),[](SchedulerJob* job) | 1765 | // | ||
1603 | { return SchedulerJob::JOB_ABORTED < job->getState(); }), sortedJobs.end()); | 1766 | // If we have a blocker next to the current job, we compare the completion time of the current job and the startup time of this next job, taking lead time into account. | ||
1767 | // This verification obviously relies on the imaging time to be reliable, but there's not much we can do at this stage of the implementation. | ||||
1604 | 1768 | | |||
1605 | /* If there are no jobs left to run in the filtered list, stop evaluation */ | 1769 | if (nullptr != nextJob && SchedulerJob::START_AT == nextJob->getFileStartupCondition()) | ||
1606 | if (sortedJobs.isEmpty()) | | |||
1607 | { | 1770 | { | ||
1608 | appendLogText(i18n("No jobs left in the scheduler queue.")); | 1771 | // In the current implementation, it is not possible to abort a running job when the next job is supposed to start. | ||
1609 | setCurrentJob(nullptr); | 1772 | // Movable jobs after this one will be delayed, but non-movable jobs are considered blockers. | ||
1610 | jobEvaluationOnly = false; | | |||
1611 | return; | | |||
1612 | } | | |||
1613 | 1773 | | |||
1614 | /* Now that jobs are scheduled, possibly at the same time, reorder by altitude and priority again */ | 1774 | // Calculate time we have between the end of the current job and the next job | ||
1615 | if (Options::sortSchedulerJobs()) | 1775 | double const timeToNext = static_cast<double> (currentJob->getCompletionTime().secsTo(nextJob->getStartupTime())); | ||
1776 | | ||||
1777 | // If that time is overlapping the next job, abort the current job | ||||
1778 | if (timeToNext < Options::leadTime()*60) | ||||
1616 | { | 1779 | { | ||
1617 | qStableSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::decreasingAltitudeOrder); | 1780 | currentJob->setState(SchedulerJob::JOB_ABORTED); | ||
1618 | qStableSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); | 1781 | | ||
1782 | appendLogText(i18n("Warning: job '%1' is constrained by the start time of the next job, and cannot finish in time, marking aborted.", | ||||
1783 | currentJob->getName())); | ||||
1784 | | ||||
1785 | break; | ||||
1619 | } | 1786 | } | ||
1620 | 1787 | | |||
1621 | /* Reorder jobs by schedule time */ | 1788 | Q_ASSERT_X(currentJob->getCompletionTime().addSecs(Options::leadTime()*60) < nextJob->getStartupTime(), __FUNCTION__, "No overlap "); | ||
1622 | qStableSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingStartupTimeOrder); | 1789 | } | ||
1623 | 1790 | | |||
1624 | // Our first job now takes priority over ALL others. | | |||
1625 | // So if any other jobs conflicts with ours, we re-schedule that job to another time. | | |||
1626 | SchedulerJob *firstJob = sortedJobs.first(); | | |||
1627 | QDateTime firstStartTime = firstJob->getStartupTime(); | | |||
1628 | QDateTime lastStartTime = firstJob->getStartupTime(); | | |||
1629 | double lastJobEstimatedTime = firstJob->getEstimatedTime(); | | |||
1630 | int daysCount = 0; | | |||
1631 | 1791 | | |||
1632 | qCInfo(KSTARS_EKOS_SCHEDULER) << "Option to sort jobs based on priority and altitude is" << Options::sortSchedulerJobs(); | 1792 | // ----- #7 Should we reject the current job because it exceeded its fixed completion time? | ||
1633 | qCDebug(KSTARS_EKOS_SCHEDULER) << "First job after sort is" << firstJob->getName() << "starting at" << firstJob->getStartupTime().toString(firstJob->getDateTimeDisplayFormat()); | 1793 | // | ||
1794 | // This verification simply checks that because of previous jobs, the startup time of the current job doesn't exceed its fixed completion time. | ||||
1795 | // Its main objective is to catch wrong dates in the FINISH_AT configuration. | ||||
1634 | 1796 | | |||
1635 | // Make sure no two jobs have the same scheduled time or overlap with other jobs | 1797 | if (SchedulerJob::FINISH_AT == currentJob->getCompletionCondition()) | ||
1636 | // FIXME: the rescheduling algorithm is incorrect when mixing asap and fixed startup times. | | |||
1637 | foreach (SchedulerJob *job, sortedJobs) | | |||
1638 | { | 1798 | { | ||
1639 | // First job is our time origin | 1799 | if (currentJob->getCompletionTime() < currentJob->getStartupTime()) | ||
1640 | if (job == firstJob) | 1800 | { | ||
1641 | continue; | 1801 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||
1642 | 1802 | | |||
1643 | // Bypass non-scheduled jobs | 1803 | appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3)", | ||
1644 | if (SchedulerJob::JOB_SCHEDULED != job->getState() || SchedulerJob::START_AT != job->getStartupCondition()) | 1804 | currentJob->getName(), | ||
1645 | continue; | 1805 | currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()), | ||
1806 | currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); | ||||
1807 | | ||||
yurchor: Typo: alitude->altitude | |||||
1808 | break; | ||||
1809 | } | ||||
1810 | } | ||||
1646 | 1811 | | |||
1647 | qCDebug(KSTARS_EKOS_SCHEDULER) << "Examining job" << job->getName() << "starting at" << job->getStartupTime().toString(job->getDateTimeDisplayFormat()); | | |||
1648 | 1812 | | |||
1649 | // At this point, a job with no valid start date is a problem | 1813 | // ----- #8 Should we reject the current job because of weather? | ||
1650 | Q_ASSERT_X(job->getStartupTime().isValid(), __FUNCTION__, "Jobs in the schedule list have a valid startup time"); | 1814 | // | ||
1815 | // That verification is left for runtime | ||||
1816 | // | ||||
1817 | // if (false == isWeatherOK(currentJob)) | ||||
1818 | //{ | ||||
1819 | // currentJob->setState(SchedulerJob::JOB_ABORTED); | ||||
1820 | // | ||||
1821 | // appendLogText(i18n("Job '%1' cannot run now because of bad weather, marking aborted.", currentJob->getName())); | ||||
1822 | //} | ||||
1651 | 1823 | | |||
1652 | double timeBetweenJobs = static_cast<double>(std::abs(firstStartTime.secsTo(job->getStartupTime()))); | | |||
1653 | 1824 | | |||
1654 | qCDebug(KSTARS_EKOS_SCHEDULER) << "Job starts in" << timeBetweenJobs << "seconds (lead time" << Options::leadTime()*60 << ")"; | 1825 | // ----- #9 Update score for current time and mark evaluating jobs as scheduled | ||
1655 | 1826 | | |||
1656 | // If there are within 5 minutes of each other, delay scheduling time of the lower altitude one | 1827 | currentJob->setScore(calculateJobScore(currentJob, now)); | ||
1657 | if (timeBetweenJobs < (Options::leadTime()) * 60) | 1828 | currentJob->setState(SchedulerJob::JOB_SCHEDULED); | ||
1658 | { | | |||
1659 | double delayJob = timeBetweenJobs + lastJobEstimatedTime; | | |||
1660 | 1829 | | |||
1661 | if (delayJob < (Options::leadTime() * 60)) | 1830 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' on row #%2 passed all checks after %3 attempts, will proceed at %4 for approximately %5 seconds, marking scheduled") | ||
1662 | delayJob = Options::leadTime() * 60; | 1831 | .arg(currentJob->getName()) | ||
1832 | .arg(index + 1) | ||||
1833 | .arg(attempt) | ||||
1834 | .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) | ||||
1835 | .arg(currentJob->getEstimatedTime()); | ||||
1663 | 1836 | | |||
1664 | QDateTime otherjob_time = lastStartTime.addSecs(delayJob); | 1837 | break; | ||
1665 | QDateTime nextPreDawnTime = preDawnDateTime.addDays(daysCount); | 1838 | } | ||
1666 | // If other jobs starts after pre-dawn limit, then we schedule it to the next day. | 1839 | | ||
1667 | // But we only take this action IF the job we are checking against starts _before_ dawn and our | 1840 | // Check if job was successfully scheduled, else reject it | ||
1668 | // job therefore carry us after down, then there is an actual need to schedule it next day. | 1841 | if (SchedulerJob::JOB_EVALUATION == currentJob->getState()) | ||
1669 | // FIXME: After changing time we are not evaluating job again when we should. | | |||
1670 | if (job->getEnforceTwilight() && lastStartTime < nextPreDawnTime && otherjob_time >= nextPreDawnTime) | | |||
1671 | { | 1842 | { | ||
1672 | QDateTime date; | 1843 | currentJob->setState(SchedulerJob::JOB_INVALID); | ||
1673 | 1844 | | |||
1674 | daysCount++; | 1845 | //appendLogText(i18n("Warning: job '%1' on row #%2 could not be scheduled during evaluation and is marked invalid, please review your plan.", | ||
1846 | // currentJob->getName(), | ||||
1847 | // index + 1)); | ||||
1675 | 1848 | | |||
1676 | lastStartTime = job->getStartupTime().addDays(daysCount); | 1849 | #if 0 | ||
1677 | job->setStartupTime(lastStartTime); | 1850 | // Advices | ||
1678 | date = lastStartTime.addSecs(delayJob); | 1851 | if (-90 < currentJob->getMinAltitude()) | ||
1852 | appendLogText(i18n("Job '%1' may require relaxing the current altitude requirement of %2 degrees.", | ||||
1853 | currentJob->getName(), | ||||
1854 | QString("%L1").arg(static_cast<double>(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals))); | ||||
1855 | | ||||
1856 | if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition() && Options::leadTime() < 5) | ||||
1857 | appendLogText(i18n("Job '%1' may require increasing the current lead time of %2 minutes to make transit time calculation stable.", | ||||
1858 | currentJob->getName(), | ||||
1859 | Options::leadTime())); | ||||
1860 | #endif | ||||
1679 | } | 1861 | } | ||
1680 | else | | |||
1681 | { | | |||
1682 | lastStartTime = lastStartTime.addSecs(delayJob); | | |||
1683 | job->setStartupTime(lastStartTime); | | |||
1684 | } | 1862 | } | ||
1685 | 1863 | | |||
1686 | /* Kept the informative log now that aborted jobs are rescheduled */ | 1864 | /* Remove unscheduled jobs that may have appeared during the last step - safeguard */ | ||
1687 | appendLogText(i18n("Jobs '%1' and '%2' have close start up times, job '%2' is rescheduled to %3.", | 1865 | sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob* job) | ||
1688 | firstJob->getName(), job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); | 1866 | { return SchedulerJob::JOB_SCHEDULED != job->getState(); }), sortedJobs.end()); | ||
1689 | } | | |||
1690 | 1867 | | |||
1691 | lastJobEstimatedTime = job->getEstimatedTime(); | 1868 | /* Apply sorting to queue table, and mark it for saving if it changes */ | ||
1692 | } | 1869 | mDirty = reorderJobs(sortedJobs); | ||
1693 | 1870 | | |||
1694 | if (jobEvaluationOnly || state != SCHEDULER_RUNNIG) | 1871 | if (jobEvaluationOnly || state != SCHEDULER_RUNNIG) | ||
1695 | { | 1872 | { | ||
1696 | qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required."; | 1873 | qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required."; | ||
1697 | jobEvaluationOnly = false; | 1874 | jobEvaluationOnly = false; | ||
1698 | return; | 1875 | return; | ||
1699 | } | 1876 | } | ||
1700 | 1877 | | |||
1701 | /* | 1878 | /* | ||
1702 | * At this step, we finished evaluating jobs. | 1879 | * At this step, we finished evaluating jobs. | ||
1703 | * We select the first job that has to be run, per schedule. | 1880 | * We select the first job that has to be run, per schedule. | ||
1704 | */ | 1881 | */ | ||
1705 | 1882 | | |||
1706 | /* Remove unscheduled jobs that may have appeared during the last step - safeguard */ | | |||
1707 | sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob* job) | | |||
1708 | { return SchedulerJob::JOB_SCHEDULED != job->getState(); }), sortedJobs.end()); | | |||
1709 | | ||||
1710 | // Sort again by schedule, sooner first, as some jobs may have shifted during the last step | | |||
1711 | qStableSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingStartupTimeOrder); | | |||
1712 | | ||||
1713 | /* If there are no jobs left to run in the filtered list, stop evaluation */ | 1883 | /* If there are no jobs left to run in the filtered list, stop evaluation */ | ||
1714 | if (sortedJobs.isEmpty()) | 1884 | if (sortedJobs.isEmpty()) | ||
1715 | { | 1885 | { | ||
1716 | appendLogText(i18n("No jobs left in the scheduler queue.")); | 1886 | appendLogText(i18n("No jobs left in the scheduler queue.")); | ||
1717 | setCurrentJob(nullptr); | 1887 | setCurrentJob(nullptr); | ||
1718 | jobEvaluationOnly = false; | 1888 | jobEvaluationOnly = false; | ||
1719 | return; | 1889 | return; | ||
1720 | } | 1890 | } | ||
1721 | 1891 | | |||
1722 | SchedulerJob * const job_to_execute = sortedJobs.first(); | 1892 | SchedulerJob * const job_to_execute = sortedJobs.first(); | ||
1723 | 1893 | | |||
1724 | /* Check if job can be processed right now */ | 1894 | /* Check if job can be processed right now */ | ||
1725 | if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP) | 1895 | if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP) | ||
1726 | if( 0 < calculateJobScore(job_to_execute, now)) | 1896 | if( 0 <= calculateJobScore(job_to_execute, now)) | ||
1727 | job_to_execute->setStartupTime(now); | 1897 | job_to_execute->setStartupTime(now); | ||
1728 | 1898 | | |||
1729 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is selected for next observation with priority #%2 and score %3.") | 1899 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is selected for next observation with priority #%2 and score %3.") | ||
1730 | .arg(job_to_execute->getName()) | 1900 | .arg(job_to_execute->getName()) | ||
1731 | .arg(job_to_execute->getPriority()) | 1901 | .arg(job_to_execute->getPriority()) | ||
1732 | .arg(job_to_execute->getScore()); | 1902 | .arg(job_to_execute->getScore()); | ||
1733 | 1903 | | |||
1734 | // Set the current job, and let the status timer execute it when ready | 1904 | // Set the current job, and let the status timer execute it when ready | ||
Show All 17 Lines | 1921 | if (state == SCHEDULER_RUNNIG) | |||
1752 | appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready...")); | 1922 | appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready...")); | ||
1753 | else | 1923 | else | ||
1754 | appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed.")); | 1924 | appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed.")); | ||
1755 | 1925 | | |||
1756 | schedulerTimer.start(); | 1926 | schedulerTimer.start(); | ||
1757 | } | 1927 | } | ||
1758 | } | 1928 | } | ||
1759 | 1929 | | |||
1760 | double Scheduler::findAltitude(const SkyPoint &target, const QDateTime &when) | 1930 | double Scheduler::findAltitude(const SkyPoint &target, const QDateTime &when, bool * is_setting, bool debug) | ||
1761 | { | 1931 | { | ||
1762 | // Make a copy | 1932 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||
1763 | /*SkyPoint p = target; | | |||
1764 | QDateTime lt(when.date(), QTime()); | | |||
1765 | KStarsDateTime ut = KStarsData::Instance()->geo()->LTtoUT(KStarsDateTime(lt)); | | |||
1766 | | ||||
1767 | KStarsDateTime myUT = ut.addSecs(when.time().msecsSinceStartOfDay() / 1000); | | |||
1768 | 1933 | | |||
1769 | CachingDms LST = KStarsData::Instance()->geo()->GSTtoLST(myUT.gst()); | 1934 | GeoLocation * const geo = KStarsData::Instance()->geo(); | ||
1770 | p.EquatorialToHorizontal(&LST, KStarsData::Instance()->geo()->lat()); | | |||
1771 | 1935 | | |||
1772 | return p.alt().Degrees();*/ | 1936 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||
1773 | 1937 | KStarsDateTime ltWhen(when.isValid() ? | |||
1774 | SkyPoint p = target; | 1938 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||
1775 | KStarsDateTime lt(when); | 1939 | KStarsData::Instance()->lt()); | ||
1776 | CachingDms LST = KStarsData::Instance()->geo()->GSTtoLST(lt.gst()); | | |||
1777 | p.EquatorialToHorizontal(&LST, KStarsData::Instance()->geo()->lat()); | | |||
1778 | | ||||
1779 | return p.alt().Degrees(); | | |||
1780 | } | | |||
1781 | 1940 | | |||
1782 | bool Scheduler::calculateAltitudeTime(SchedulerJob *job, double minAltitude, double minMoonAngle) | 1941 | // Create a sky object with the target catalog coordinates | ||
1783 | { | 1942 | SkyObject o; | ||
1784 | // We wouldn't stat observation 30 mins (default) before dawn. | 1943 | o.setRA0(target.ra0()); | ||
1785 | double const earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); | 1944 | o.setDec0(target.dec0()); | ||
1786 | 1945 | | |||
1787 | /* Compute UTC for beginning of today */ | 1946 | // Update RA/DEC of the target for the current fraction of the day | ||
1788 | QDateTime const lt(KStarsData::Instance()->lt().date(), QTime()); | 1947 | KSNumbers numbers(ltWhen.djd()); | ||
1789 | KStarsDateTime const ut = geo->LTtoUT(KStarsDateTime(lt)); | 1948 | o.updateCoordsNow(&numbers); | ||
1949 | | ||||
1950 | // Calculate alt/az coordinates using KStars instance's geolocation | ||||
1951 | CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); | ||||
1952 | o.EquatorialToHorizontal(&LST, geo->lat()); | ||||
1953 | | ||||
1954 | // Hours are reduced to [0,24[, meridian being at 0 | ||||
1955 | double offset = LST.Hours() - o.ra().Hours(); | ||||
1956 | if (24.0 <= offset) | ||||
1957 | offset -= 24.0; | ||||
1958 | else if (offset < 0.0) | ||||
1959 | offset += 24.0; | ||||
1960 | bool const passed_meridian = 0.0 <= offset && offset < 12.0; | ||||
1961 | | ||||
1962 | if (debug) | ||||
1963 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7") | ||||
1964 | .arg(o.ra().toHMSString()) | ||||
1965 | .arg(o.ra0().toHMSString()) | ||||
1966 | .arg(o.dec().toHMSString()) | ||||
1967 | .arg(o.dec0().toHMSString()) | ||||
1968 | .arg(o.alt().Degrees()) | ||||
1969 | .arg(passed_meridian ? "yes":"no") | ||||
1970 | .arg(o.ra().Hours()) | ||||
1971 | .arg(LST.toHMSString()) | ||||
1972 | .arg(ltWhen.toString("HH:mm:ss")); | ||||
1973 | | ||||
1974 | if (is_setting) | ||||
1975 | *is_setting = passed_meridian; | ||||
1976 | | ||||
1977 | return o.alt().Degrees(); | ||||
1978 | } | ||||
1979 | | ||||
1980 | QDateTime Scheduler::calculateAltitudeTime(SchedulerJob const *job, double minAltitude, double minMoonAngle, QDateTime const &when) const | ||||
1981 | { | ||||
1982 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||||
1983 | | ||||
1984 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||||
1985 | KStarsDateTime ltWhen(when.isValid() ? | ||||
1986 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||||
1987 | KStarsData::Instance()->lt()); | ||||
1790 | 1988 | | |||
1791 | /* Retrieve target coordinates to be converted to horizontal to determine altitude */ | 1989 | // Create a sky object with the target catalog coordinates | ||
1792 | SkyPoint target = job->getTargetCoords(); | 1990 | SkyPoint const target = job->getTargetCoords(); | ||
1991 | SkyObject o; | ||||
1992 | o.setRA0(target.ra0()); | ||||
1993 | o.setDec0(target.dec0()); | ||||
1793 | 1994 | | |||
1794 | /* Retrieve the current fraction of the day */ | 1995 | // Calculate the UT at the argument time | ||
1795 | QTime const now = KStarsData::Instance()->lt().time(); | 1996 | KStarsDateTime const ut = geo->LTtoUT(ltWhen); | ||
1796 | double const fraction = now.hour() + now.minute() / 60.0 + now.second() / 3600; | | |||
1797 | 1997 | | |||
1798 | /* This attempts to locate the first minute of the next 24 hours when the job target matches the altitude and moon constraints */ | 1998 | // Within the next 24 hours, search when the job target matches the altitude and moon constraints | ||
1799 | for (double hour = fraction; hour < (fraction + 24); hour += 1.0 / 60.0) | 1999 | for (unsigned int minute = 0; minute < 24*60; minute++) | ||
1800 | { | 2000 | { | ||
1801 | double const rawFrac = (hour > 24 ? (hour - 24) : hour) / 24.0; | 2001 | KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60)); | ||
1802 | 2002 | | |||
1803 | /* Test twilight enforcement, and if enforced, bail out if start time is during day */ | 2003 | // Update RA/DEC of the target for the current fraction of the day | ||
1804 | /* FIXME: rework day fraction loop to shift to dusk directly */ | 2004 | KSNumbers numbers(ltOffset.djd()); | ||
1805 | if (job->getEnforceTwilight() && Dawn <= rawFrac && rawFrac <= Dusk) | 2005 | o.updateCoordsNow(&numbers); | ||
1806 | continue; | | |||
1807 | 2006 | | |||
1808 | /* Compute altitude of target for the current fraction of the day */ | 2007 | // Compute local sidereal time for the current fraction of the day, calculate altitude | ||
1809 | KStarsDateTime const myUT = ut.addSecs(hour * 3600.0); | 2008 | CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltOffset).gst()); | ||
1810 | CachingDms const LST = geo->GSTtoLST(myUT.gst()); | 2009 | o.EquatorialToHorizontal(&LST, geo->lat()); | ||
1811 | target.EquatorialToHorizontal(&LST, geo->lat()); | 2010 | double const altitude = o.alt().Degrees(); | ||
1812 | double const altitude = target.alt().Degrees(); | | |||
1813 | 2011 | | |||
1814 | if (altitude > minAltitude) | 2012 | if (minAltitude <= altitude) | ||
1815 | { | 2013 | { | ||
1816 | QDateTime const startTime = geo->UTtoLT(myUT); | 2014 | // Don't test proximity to dawn in this situation, we only cater for altitude here | ||
1817 | 2015 | | |||
1818 | /* Test twilight enforcement, and if enforced, bail out if start time is too close to dawn */ | 2016 | // Continue searching if Moon separation is not good enough | ||
1819 | if (job->getEnforceTwilight() && earlyDawn < rawFrac && rawFrac < Dawn) | 2017 | if (0 < minMoonAngle && getMoonSeparationScore(job, ltOffset) < 0) | ||
1820 | { | 2018 | continue; | ||
1821 | appendLogText(i18n("Warning: job '%1' reaches an altitude of %2 degrees at %3 but will not be scheduled due to " | | |||
1822 | "close proximity to astronomical twilight rise.", | | |||
1823 | job->getName(), QString("%L1").arg(minAltitude, 0, 'f', 3), startTime.toString(job->getDateTimeDisplayFormat()))); | | |||
1824 | return false; | | |||
1825 | } | | |||
1826 | 2019 | | |||
1827 | /* Continue searching if Moon separation is not good enough */ | 2020 | // Continue searching if target is setting and under the cutoff | ||
1828 | if (minMoonAngle > 0 && getMoonSeparationScore(job, startTime) < 0) | 2021 | double offset = LST.Hours() - o.ra().Hours(); | ||
2022 | if (24.0 <= offset) | ||||
2023 | offset -= 24.0; | ||||
2024 | else if (offset < 0.0) | ||||
2025 | offset += 24.0; | ||||
2026 | if (0.0 <= offset && offset < 12.0) | ||||
2027 | if (altitude - SETTING_ALTITUDE_CUTOFF < minAltitude) | ||||
1829 | continue; | 2028 | continue; | ||
1830 | 2029 | | |||
1831 | /* FIXME: the name of the function doesn't suggest the job can be modified */ | 2030 | return ltOffset; | ||
1832 | job->setStartupTime(startTime); | | |||
1833 | /* Kept the informative log because of the reschedule of aborted jobs */ | | |||
1834 | appendLogText(i18n("Job '%1' is scheduled to start at %2 where its altitude is %3 degrees.", job->getName(), | | |||
1835 | startTime.toString(job->getDateTimeDisplayFormat()), QString("%L1").arg(altitude, 0, 'f', 3))); | | |||
1836 | return true; | | |||
1837 | } | 2031 | } | ||
1838 | } | 2032 | } | ||
1839 | 2033 | | |||
1840 | /* FIXME: move this to the caller too to comment the decision to reject the job */ | 2034 | return QDateTime(); | ||
1841 | if (minMoonAngle == -1) | | |||
1842 | { | | |||
1843 | if (job->getEnforceTwilight()) | | |||
1844 | { | | |||
1845 | appendLogText(i18n("Warning: job '%1' has no night time with an altitude above %2 degrees during the next 24 hours, marking invalid.", | | |||
1846 | job->getName(), QString("%L1").arg(minAltitude, 0, 'f', 3))); | | |||
1847 | } | | |||
1848 | else appendLogText(i18n("Warning: job '%1' cannot rise to an altitude above %2 degrees in the next 24 hours, marking invalid.", | | |||
1849 | job->getName(), QString("%L1").arg(minAltitude, 0, 'f', 3))); | | |||
1850 | } | | |||
1851 | else appendLogText(i18n("Warning: job '%1' cannot be scheduled with an altitude above %2 degrees with minimum moon " | | |||
1852 | "separation of %3 degrees in the next 24 hours, marking invalid.", | | |||
1853 | job->getName(), QString("%L1").arg(minAltitude, 0, 'f', 3), | | |||
1854 | QString("%L1").arg(minMoonAngle, 0, 'f', 3))); | | |||
1855 | return false; | | |||
1856 | } | 2035 | } | ||
1857 | 2036 | | |||
1858 | bool Scheduler::calculateCulmination(SchedulerJob *job) | 2037 | QDateTime Scheduler::calculateCulmination(SchedulerJob const *job, int offset_minutes, QDateTime const &when) const | ||
1859 | { | 2038 | { | ||
1860 | SkyPoint target = job->getTargetCoords(); | 2039 | // FIXME: culmination calculation is a min altitude requirement, should be an interval altitude requirement | ||
2040 | // FIXME: block calculating target coordinates at a particular time is duplicated in calculateCulmination | ||||
1861 | 2041 | | |||
1862 | SkyObject o; | 2042 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||
2043 | KStarsDateTime ltWhen(when.isValid() ? | ||||
2044 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||||
2045 | KStarsData::Instance()->lt()); | ||||
1863 | 2046 | | |||
2047 | // Create a sky object with the target catalog coordinates | ||||
2048 | SkyPoint const target = job->getTargetCoords(); | ||||
2049 | SkyObject o; | ||||
1864 | o.setRA0(target.ra0()); | 2050 | o.setRA0(target.ra0()); | ||
1865 | o.setDec0(target.dec0()); | 2051 | o.setDec0(target.dec0()); | ||
1866 | 2052 | | |||
1867 | o.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); | 2053 | // Update RA/DEC for the argument date/time | ||
2054 | KSNumbers numbers(ltWhen.djd()); | ||||
2055 | o.updateCoordsNow(&numbers); | ||||
1868 | 2056 | | |||
1869 | QDateTime midnight(KStarsData::Instance()->lt().date(), QTime()); | 2057 | // Calculate transit date/time at the argument date - transitTime requires UT and returns LocalTime | ||
1870 | KStarsDateTime dt = geo->LTtoUT(KStarsDateTime(midnight)); | 2058 | KStarsDateTime transitDateTime(ltWhen.date(), o.transitTime(geo->LTtoUT(ltWhen), geo), Qt::LocalTime); | ||
1871 | 2059 | | |||
1872 | QTime transitTime = o.transitTime(dt, geo); | 2060 | // Shift transit date/time by the argument offset | ||
2061 | KStarsDateTime observationDateTime = transitDateTime.addSecs(offset_minutes * 60); | ||||
1873 | 2062 | | |||
1874 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' transit time is %2") | 2063 | // Relax observation time, culmination calculation is stable at minute only | ||
1875 | .arg(job->getName()) | 2064 | KStarsDateTime relaxedDateTime = observationDateTime.addSecs(Options::leadTime() * 60); | ||
1876 | .arg(transitTime.toString("hh:mm:ss")); | | |||
1877 | | ||||
1878 | int dayOffset = 0; | | |||
1879 | if (KStarsData::Instance()->lt().time() > transitTime) | | |||
1880 | dayOffset = 1; | | |||
1881 | 2065 | | |||
1882 | QDateTime observationDateTime(QDate::currentDate().addDays(dayOffset), | 2066 | // Verify resulting observation time is under lead time vs. argument time | ||
1883 | transitTime.addSecs(job->getCulminationOffset() * 60)); | 2067 | // If sooner, delay by 8 hours to get to the next transit - perhaps in a third call | ||
1884 | 2068 | if (relaxedDateTime < ltWhen) | |||
1885 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' observation time is %2 adjusted for %L3 min.") | 2069 | { | ||
2070 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' startup %2 is posterior to transit %3, shifting by 8 hours.") | ||||
1886 | .arg(job->getName()) | 2071 | .arg(job->getName()) | ||
1887 | .arg(observationDateTime.toString(job->getDateTimeDisplayFormat())) | 2072 | .arg(ltWhen.toString(job->getDateTimeDisplayFormat())) | ||
1888 | .arg(static_cast<double>(job->getCulminationOffset()), 0, 'f', 3); | 2073 | .arg(relaxedDateTime.toString(job->getDateTimeDisplayFormat())); | ||
1889 | 2074 | | |||
1890 | if (job->getEnforceTwilight() && getDarkSkyScore(observationDateTime) < 0) | 2075 | return calculateCulmination(job, offset_minutes, when.addSecs(8*60*60)); | ||
1891 | { | | |||
1892 | appendLogText(i18n("Job '%1' target culminates during the day and cannot be scheduled for observation.", job->getName())); | | |||
1893 | return false; | | |||
1894 | } | | |||
1895 | | ||||
1896 | if (observationDateTime < (static_cast<QDateTime>(KStarsData::Instance()->lt()))) | | |||
1897 | { | | |||
1898 | appendLogText(i18n("Job '%1' observation time %2 is passed for today.", | | |||
1899 | job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); | | |||
1900 | return false; | | |||
1901 | } | 2076 | } | ||
1902 | 2077 | | |||
2078 | // Guarantees - culmination calculation is stable at minute level, so relax by lead time | ||||
1903 | Q_ASSERT_X(observationDateTime.isValid(), __FUNCTION__, "Observation time for target culmination is valid."); | 2079 | Q_ASSERT_X(observationDateTime.isValid(), __FUNCTION__, "Observation time for target culmination is valid."); | ||
2080 | Q_ASSERT_X(ltWhen <= relaxedDateTime, __FUNCTION__, "Observation time for target culmination is at or after than argument time"); | ||||
1904 | 2081 | | |||
1905 | job->setStartupTime(observationDateTime); | 2082 | // Return consolidated culmination time | ||
1906 | return true; | 2083 | return Qt::UTC == observationDateTime.timeSpec() ? geo->UTtoLT(observationDateTime) : observationDateTime; | ||
1907 | } | 2084 | } | ||
1908 | 2085 | | |||
1909 | int16_t Scheduler::getWeatherScore() | 2086 | int16_t Scheduler::getWeatherScore() const | ||
1910 | { | 2087 | { | ||
1911 | if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) | 2088 | if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) | ||
1912 | return 0; | 2089 | return 0; | ||
1913 | 2090 | | |||
1914 | if (weatherStatus == IPS_BUSY) | 2091 | if (weatherStatus == IPS_BUSY) | ||
1915 | return BAD_SCORE / 2; | 2092 | return BAD_SCORE / 2; | ||
1916 | else if (weatherStatus == IPS_ALERT) | 2093 | else if (weatherStatus == IPS_ALERT) | ||
1917 | return BAD_SCORE; | 2094 | return BAD_SCORE; | ||
1918 | 2095 | | |||
1919 | return 0; | 2096 | return 0; | ||
1920 | } | 2097 | } | ||
1921 | 2098 | | |||
1922 | int16_t Scheduler::getDarkSkyScore(const QDateTime &observationDateTime) | 2099 | int16_t Scheduler::getDarkSkyScore(QDateTime const &when) const | ||
1923 | { | 2100 | { | ||
1924 | // if (job->getStartingCondition() == SchedulerJob::START_CULMINATION) | 2101 | double const secsPerDay = 24.0 * 3600.0; | ||
1925 | // return -1000; | 2102 | double const minsPerDay = 24.0 * 60.0; | ||
2103 | | ||||
2104 | // Dark sky score is calculated based on distance to today's dawn and next dusk. | ||||
2105 | // Option "Pre-dawn Time" avoids executing a job when dawn is approaching, and is a value in minutes. | ||||
2106 | // - If observation is between option "Pre-dawn Time" and dawn, score is BAD_SCORE/50. | ||||
2107 | // - If observation is before dawn today, score is fraction of the day from beginning of observation to dawn time, as percentage. | ||||
2108 | // - If observation is after dusk, score is fraction of the day from dusk to beginning of observation, as percentage. | ||||
2109 | // - If observation is between dawn and dusk, score is BAD_SCORE. | ||||
2110 | // | ||||
2111 | // If observation time is invalid, the score is calculated for the current day time. | ||||
2112 | // Note exact dusk time is considered valid in terms of night time, and will return a positive, albeit null, score. | ||||
2113 | | ||||
2114 | // FIXME: Dark sky score should consider the middle of the local night as best value. | ||||
yurchor: Typo: best->the best | |||||
TallFurryMan: Arguable but OK. Will address the FIXME soon anyway. | |||||
2115 | // FIXME: Current algorithm uses the dawn and dusk of today, instead of the day of the observation. | ||||
2116 | | ||||
2117 | int const earlyDawnSecs = static_cast <int> ((Dawn - static_cast <double> (Options::preDawnTime()) / minsPerDay) * secsPerDay); | ||||
2118 | int const dawnSecs = static_cast <int> (Dawn * secsPerDay); | ||||
2119 | int const duskSecs = static_cast <int> (Dusk * secsPerDay); | ||||
2120 | int const obsSecs = (when.isValid() ? when : KStarsData::Instance()->lt()).time().msecsSinceStartOfDay()/1000; | ||||
1926 | 2121 | | |||
1927 | int16_t score = 0; | 2122 | int16_t score = 0; | ||
1928 | double dayFraction = 0; | | |||
1929 | 2123 | | |||
1930 | // Anything half an hour before dawn shouldn't be a good candidate | 2124 | if (earlyDawnSecs <= obsSecs && obsSecs < dawnSecs) | ||
1931 | double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); | 2125 | { | ||
2126 | score = BAD_SCORE / 50; | ||||
1932 | 2127 | | |||
1933 | dayFraction = observationDateTime.time().msecsSinceStartOfDay() / (24.0 * 60.0 * 60.0 * 1000.0); | 2128 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (between pre-dawn and dawn).") | ||
2129 | // .arg(observationDateTime.toString()) | ||||
2130 | // .arg(QString::asprintf("%+d", score)); | ||||
2131 | } | ||||
2132 | else if (obsSecs < dawnSecs) | ||||
2133 | { | ||||
2134 | score = static_cast <int16_t> ((dawnSecs - obsSecs) / secsPerDay) * 100; | ||||
1934 | 2135 | | |||
1935 | // The farther the target from dawn, the better. | 2136 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (before dawn).") | ||
1936 | if (dayFraction > earlyDawn && dayFraction < Dawn) | 2137 | // .arg(observationDateTime.toString()) | ||
1937 | score = BAD_SCORE / 50; | 2138 | // .arg(QString::asprintf("%+d", score)); | ||
1938 | else if (dayFraction < Dawn) | 2139 | } | ||
1939 | score = (Dawn - dayFraction) * 100; | 2140 | else if (duskSecs <= obsSecs) | ||
1940 | else if (dayFraction > Dusk) | | |||
1941 | { | 2141 | { | ||
1942 | score = (dayFraction - Dusk) * 100; | 2142 | score = static_cast <int16_t> ((obsSecs - duskSecs) / secsPerDay) * 100; | ||
2143 | | ||||
2144 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (after dusk).") | ||||
2145 | // .arg(observationDateTime.toString()) | ||||
2146 | // .arg(QString::asprintf("%+d", score)); | ||||
1943 | } | 2147 | } | ||
1944 | else | 2148 | else | ||
2149 | { | ||||
1945 | score = BAD_SCORE; | 2150 | score = BAD_SCORE; | ||
1946 | 2151 | | |||
1947 | qCDebug(KSTARS_EKOS_SCHEDULER) << "Dark sky score is" << score << "for time" << observationDateTime.toString(); | 2152 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (during daylight).") | ||
2153 | // .arg(observationDateTime.toString()) | ||||
2154 | // .arg(QString::asprintf("%+d", score)); | ||||
2155 | } | ||||
1948 | 2156 | | |||
1949 | return score; | 2157 | return score; | ||
1950 | } | 2158 | } | ||
1951 | 2159 | | |||
1952 | int16_t Scheduler::calculateJobScore(SchedulerJob *job, QDateTime when) | 2160 | int16_t Scheduler::calculateJobScore(SchedulerJob const *job, QDateTime const &when) const | ||
1953 | { | 2161 | { | ||
2162 | if (nullptr == job) | ||||
2163 | return BAD_SCORE; | ||||
2164 | | ||||
1954 | /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */ | 2165 | /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */ | ||
1955 | if (!job->getLightFramesRequired()) | 2166 | if (!job->getLightFramesRequired()) | ||
1956 | return 1000; | 2167 | return 1000; | ||
1957 | 2168 | | |||
1958 | int16_t total = 0; | 2169 | int16_t total = 0; | ||
1959 | 2170 | | |||
1960 | /* As soon as one score is negative, it's a no-go and other scores are unneeded */ | 2171 | /* As soon as one score is negative, it's a no-go and other scores are unneeded */ | ||
1961 | 2172 | | |||
1962 | if (job->getEnforceTwilight()) | 2173 | if (job->getEnforceTwilight()) | ||
1963 | total += getDarkSkyScore(when); | 2174 | { | ||
2175 | int16_t const darkSkyScore = getDarkSkyScore(when); | ||||
2176 | | ||||
2177 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' dark sky score is %2 at %3") | ||||
2178 | .arg(job->getName()) | ||||
2179 | .arg(QString::asprintf("%+d", darkSkyScore)) | ||||
2180 | .arg(when.toString(job->getDateTimeDisplayFormat())); | ||||
2181 | | ||||
2182 | total += darkSkyScore; | ||||
2183 | } | ||||
1964 | 2184 | | |||
1965 | /* We still enforce altitude if the job is neither required to track nor guide, because this is too confusing for the end-user. | 2185 | /* We still enforce altitude if the job is neither required to track nor guide, because this is too confusing for the end-user. | ||
1966 | * If we bypass calculation here, it must also be bypassed when checking job constraints in checkJobStage. | 2186 | * If we bypass calculation here, it must also be bypassed when checking job constraints in checkJobStage. | ||
1967 | */ | 2187 | */ | ||
1968 | if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/) | 2188 | if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/) | ||
1969 | total += getAltitudeScore(job, when); | 2189 | { | ||
2190 | int16_t const altitudeScore = getAltitudeScore(job, when); | ||||
2191 | | ||||
2192 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' altitude score is %2 at %3") | ||||
2193 | .arg(job->getName()) | ||||
2194 | .arg(QString::asprintf("%+d", altitudeScore)) | ||||
2195 | .arg(when.toString(job->getDateTimeDisplayFormat())); | ||||
2196 | | ||||
2197 | total += altitudeScore; | ||||
2198 | } | ||||
1970 | 2199 | | |||
1971 | if (0 <= total) | 2200 | if (0 <= total) | ||
1972 | total += getMoonSeparationScore(job, when); | 2201 | { | ||
2202 | int16_t const moonSeparationScore = getMoonSeparationScore(job, when); | ||||
2203 | | ||||
2204 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' Moon separation score is %2 at %3") | ||||
2205 | .arg(job->getName()) | ||||
2206 | .arg(QString::asprintf("%+d", moonSeparationScore)) | ||||
2207 | .arg(when.toString(job->getDateTimeDisplayFormat())); | ||||
2208 | | ||||
2209 | total += moonSeparationScore; | ||||
2210 | } | ||||
2211 | | ||||
2212 | qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a total score of %2 at %3.") | ||||
2213 | .arg(job->getName()) | ||||
2214 | .arg(QString::asprintf("%+d", total)) | ||||
2215 | .arg(when.toString(job->getDateTimeDisplayFormat())); | ||||
1973 | 2216 | | |||
1974 | qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a total score of %2").arg(job->getName()).arg(total); | | |||
1975 | return total; | 2217 | return total; | ||
1976 | } | 2218 | } | ||
1977 | 2219 | | |||
1978 | int16_t Scheduler::getAltitudeScore(SchedulerJob *job, QDateTime when) | 2220 | int16_t Scheduler::getAltitudeScore(SchedulerJob const *job, QDateTime const &when) const | ||
1979 | { | 2221 | { | ||
1980 | int16_t score = 0; | 2222 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||
1981 | double currentAlt = findAltitude(job->getTargetCoords(), when); | 2223 | | ||
2224 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||||
2225 | KStarsDateTime ltWhen(when.isValid() ? | ||||
2226 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||||
2227 | KStarsData::Instance()->lt()); | ||||
2228 | | ||||
2229 | // Create a sky object with the target catalog coordinates | ||||
2230 | SkyPoint const target = job->getTargetCoords(); | ||||
2231 | SkyObject o; | ||||
2232 | o.setRA0(target.ra0()); | ||||
2233 | o.setDec0(target.dec0()); | ||||
1982 | 2234 | | |||
1983 | if (currentAlt < 0) | 2235 | // Update RA/DEC of the target for the current fraction of the day | ||
2236 | KSNumbers numbers(ltWhen.djd()); | ||||
2237 | o.updateCoordsNow(&numbers); | ||||
2238 | | ||||
2239 | // Compute local sidereal time for the current fraction of the day, calculate altitude | ||||
2240 | CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); | ||||
2241 | o.EquatorialToHorizontal(&LST, geo->lat()); | ||||
2242 | double const altitude = o.alt().Degrees(); | ||||
2243 | | ||||
2244 | int16_t score = BAD_SCORE - 1; | ||||
2245 | | ||||
2246 | // If altitude is negative, bad score | ||||
2247 | // FIXME: some locations may allow negative altitudes | ||||
2248 | if (altitude < 0) | ||||
1984 | { | 2249 | { | ||
1985 | score = BAD_SCORE; | 2250 | score = BAD_SCORE; | ||
1986 | } | 2251 | } | ||
1987 | // If minimum altitude is specified | 2252 | else if (-90 < job->getMinAltitude()) | ||
1988 | else if (job->getMinAltitude() > 0) | | |||
1989 | { | 2253 | { | ||
1990 | // if current altitude is lower that's not good | 2254 | // If under altitude constraint, bad score | ||
1991 | if (currentAlt < job->getMinAltitude()) | 2255 | if (altitude < job->getMinAltitude()) | ||
1992 | score = BAD_SCORE; | 2256 | score = BAD_SCORE; | ||
2257 | // Else if setting and under altitude cutoff, job would end soon after starting, bad score | ||||
2258 | // FIXME: half bad score when under altitude cutoff risk getting positive again | ||||
1993 | else | 2259 | else | ||
1994 | { | 2260 | { | ||
1995 | // Get HA of actual object, and not of the mount as was done below | 2261 | double offset = LST.Hours() - o.ra().Hours(); | ||
1996 | double HA = KStars::Instance()->data()->lst()->Hours() - job->getTargetCoords().ra().Hours(); | 2262 | if (24.0 <= offset) | ||
1997 | 2263 | offset -= 24.0; | |||
1998 | #if 0 | 2264 | else if (offset < 0.0) | ||
1999 | if (indiState == INDI_READY) | 2265 | offset += 24.0; | ||
2000 | { | 2266 | if (0.0 <= offset && offset < 12.0) | ||
2001 | QDBusReply<double> haReply = mountInterface->call(QDBus::AutoDetect, "getHourAngle"); | 2267 | if (altitude - SETTING_ALTITUDE_CUTOFF < job->getMinAltitude()) | ||
2002 | if (haReply.error().type() == QDBusError::NoError) | 2268 | score = BAD_SCORE / 2; | ||
2003 | HA = haReply.value(); | | |||
2004 | } | 2269 | } | ||
2005 | #endif | | |||
2006 | | ||||
2007 | // If already passed the meridian and setting we check if it is within setting altitude cut off value (3 degrees default) | | |||
2008 | // If it is within that value then it is useless to start the job which will end very soon so we better look for a better job. | | |||
2009 | /* FIXME: don't use BAD_SCORE/2, a negative result implies the job has to be aborted - we'd be annoyed if that score became positive again */ | | |||
2010 | /* FIXME: bug here, raising target will get a negative score if under cutoff, issue mitigated by aborted jobs getting rescheduled */ | | |||
2011 | if (HA > 0 && (currentAlt - SETTING_ALTITUDE_CUTOFF) < job->getMinAltitude()) | | |||
2012 | score = BAD_SCORE / 2.0; | | |||
2013 | else | | |||
2014 | // Otherwise, adjust score and add current altitude to score weight | | |||
2015 | score = (1.5 * pow(1.06, currentAlt)) - (minAltitude->minimum() / 10.0); | | |||
2016 | } | 2270 | } | ||
2017 | } | 2271 | // If not constrained but below minimum hard altitude, set score to 10% of altitude value | ||
2018 | // If it's below minimum hard altitude (15 degrees now), set score to 10% of altitude value | 2272 | else if (altitude < minAltitude->minimum()) | ||
2019 | else if (currentAlt < minAltitude->minimum()) | | |||
2020 | { | | |||
2021 | score = currentAlt / 10.0; | | |||
2022 | } | | |||
2023 | // If no minimum altitude, then adjust altitude score to account for current target altitude | | |||
2024 | else | | |||
2025 | { | 2273 | { | ||
2026 | score = (1.5 * pow(1.06, currentAlt)) - (minAltitude->minimum() / 10.0); | 2274 | score = static_cast <int16_t> (altitude / 10.0); | ||
2027 | } | 2275 | } | ||
2028 | 2276 | | |||
2029 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target altitude is %L3 degrees at %2 (score %4).") | 2277 | // Else default score calculation without altitude constraint | ||
2030 | .arg(job->getName()) | 2278 | if (score < BAD_SCORE) | ||
2031 | .arg(currentAlt, 0, 'f', 3) | 2279 | score = static_cast <int16_t> ((1.5 * pow(1.06, altitude)) - (minAltitude->minimum() / 10.0)); | ||
2032 | .arg(when.toString(job->getDateTimeDisplayFormat())) | 2280 | | ||
2033 | .arg(QString::asprintf("%+d", score)); | 2281 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target altitude is %3 degrees at %2 (score %4).") | ||
2282 | // .arg(job->getName()) | ||||
2283 | // .arg(when.toString(job->getDateTimeDisplayFormat())) | ||||
2284 | // .arg(currentAlt, 0, 'f', minAltitude->decimals()) | ||||
2285 | // .arg(QString::asprintf("%+d", score)); | ||||
2034 | 2286 | | |||
2035 | return score; | 2287 | return score; | ||
2036 | } | 2288 | } | ||
2037 | 2289 | | |||
2038 | double Scheduler::getCurrentMoonSeparation(SchedulerJob *job) | 2290 | double Scheduler::getCurrentMoonSeparation(SchedulerJob const *job) const | ||
2039 | { | 2291 | { | ||
2040 | // Get target altitude given the time | 2292 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||
2041 | SkyPoint p = job->getTargetCoords(); | 2293 | | ||
2042 | QDateTime midnight(KStarsData::Instance()->lt().date(), QTime()); | 2294 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||
2043 | KStarsDateTime ut = geo->LTtoUT(KStarsDateTime(midnight)); | 2295 | KStarsDateTime ltWhen(KStarsData::Instance()->lt()); | ||
2044 | KStarsDateTime myUT = ut.addSecs(KStarsData::Instance()->lt().time().msecsSinceStartOfDay() / 1000); | 2296 | | ||
2045 | CachingDms LST = geo->GSTtoLST(myUT.gst()); | 2297 | // Create a sky object with the target catalog coordinates | ||
2046 | p.EquatorialToHorizontal(&LST, geo->lat()); | 2298 | SkyPoint const target = job->getTargetCoords(); | ||
2299 | SkyObject o; | ||||
2300 | o.setRA0(target.ra0()); | ||||
2301 | o.setDec0(target.dec0()); | ||||
2302 | | ||||
2303 | // Update RA/DEC of the target for the current fraction of the day | ||||
2304 | KSNumbers numbers(ltWhen.djd()); | ||||
2305 | o.updateCoordsNow(&numbers); | ||||
2047 | 2306 | | |||
2048 | // Update moon | 2307 | // Update moon | ||
2049 | ut = geo->LTtoUT(KStarsData::Instance()->lt()); | 2308 | //ut = geo->LTtoUT(ltWhen); | ||
2050 | KSNumbers ksnum(ut.djd()); | 2309 | //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation | ||
2051 | LST = geo->GSTtoLST(ut.gst()); | 2310 | //LST = geo->GSTtoLST(ut.gst()); | ||
2052 | moon->updateCoords(&ksnum, true, geo->lat(), &LST, true); | 2311 | CachingDms LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); | ||
2312 | moon->updateCoords(&numbers, true, geo->lat(), &LST, true); | ||||
2053 | 2313 | | |||
2054 | // Moon/Sky separation p | 2314 | // Moon/Sky separation p | ||
2055 | return moon->angularDistanceTo(&p).Degrees(); | 2315 | return moon->angularDistanceTo(&o).Degrees(); | ||
2056 | } | 2316 | } | ||
2057 | 2317 | | |||
2058 | int16_t Scheduler::getMoonSeparationScore(SchedulerJob *job, QDateTime when) | 2318 | int16_t Scheduler::getMoonSeparationScore(SchedulerJob const *job, QDateTime const &when) const | ||
2059 | { | 2319 | { | ||
2060 | int16_t score = 0; | 2320 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||
2061 | 2321 | | |||
2062 | // Get target altitude given the time | 2322 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||
2063 | SkyPoint p = job->getTargetCoords(); | 2323 | KStarsDateTime ltWhen(when.isValid() ? | ||
2064 | QDateTime midnight(when.date(), QTime()); | 2324 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||
2065 | KStarsDateTime ut = geo->LTtoUT(KStarsDateTime(midnight)); | 2325 | KStarsData::Instance()->lt()); | ||
2066 | KStarsDateTime myUT = ut.addSecs(when.time().msecsSinceStartOfDay() / 1000); | 2326 | | ||
2067 | CachingDms LST = geo->GSTtoLST(myUT.gst()); | 2327 | // Create a sky object with the target catalog coordinates | ||
2068 | p.EquatorialToHorizontal(&LST, geo->lat()); | 2328 | SkyPoint const target = job->getTargetCoords(); | ||
2069 | double currentAlt = p.alt().Degrees(); | 2329 | SkyObject o; | ||
2330 | o.setRA0(target.ra0()); | ||||
2331 | o.setDec0(target.dec0()); | ||||
2332 | | ||||
2333 | // Update RA/DEC of the target for the current fraction of the day | ||||
2334 | KSNumbers numbers(ltWhen.djd()); | ||||
2335 | o.updateCoordsNow(&numbers); | ||||
2070 | 2336 | | |||
2071 | // Update moon | 2337 | // Update moon | ||
2072 | ut = geo->LTtoUT(KStarsDateTime(when)); | 2338 | //ut = geo->LTtoUT(ltWhen); | ||
2073 | KSNumbers ksnum(ut.djd()); | 2339 | //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation | ||
This will require deeper investigation. KStarsDateTime possibly has a bug with TimeSpec. TallFurryMan: This will require deeper investigation. KStarsDateTime possibly has a bug with TimeSpec.
Julian… | |||||
2074 | LST = geo->GSTtoLST(ut.gst()); | 2340 | //LST = geo->GSTtoLST(ut.gst()); | ||
2075 | moon->updateCoords(&ksnum, true, geo->lat(), &LST, true); | 2341 | CachingDms LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); | ||
2342 | moon->updateCoords(&numbers, true, geo->lat(), &LST, true); | ||||
2076 | 2343 | | |||
2077 | double moonAltitude = moon->alt().Degrees(); | 2344 | double const moonAltitude = moon->alt().Degrees(); | ||
2078 | 2345 | | |||
2079 | // Lunar illumination % | 2346 | // Lunar illumination % | ||
2080 | double illum = moon->illum() * 100.0; | 2347 | double const illum = moon->illum() * 100.0; | ||
2081 | 2348 | | |||
2082 | // Moon/Sky separation p | 2349 | // Moon/Sky separation p | ||
2083 | double separation = moon->angularDistanceTo(&p).Degrees(); | 2350 | double const separation = moon->angularDistanceTo(&o).Degrees(); | ||
2084 | 2351 | | |||
2085 | // Zenith distance of the moon | 2352 | // Zenith distance of the moon | ||
2086 | double zMoon = (90 - moonAltitude); | 2353 | double const zMoon = (90 - moonAltitude); | ||
2087 | // Zenith distance of target | 2354 | // Zenith distance of target | ||
2088 | double zTarget = (90 - currentAlt); | 2355 | double const zTarget = (90 - o.alt().Degrees()); | ||
2356 | | ||||
2357 | int16_t score = 0; | ||||
2089 | 2358 | | |||
2090 | // If target = Moon, or no illuminiation, or moon below horizon, return static score. | 2359 | // If target = Moon, or no illuminiation, or moon below horizon, return static score. | ||
2091 | if (zMoon == zTarget || illum == 0 || zMoon >= 90) | 2360 | if (zMoon == zTarget || illum == 0 || zMoon >= 90) | ||
2092 | score = 100; | 2361 | score = 100; | ||
2093 | else | 2362 | else | ||
2094 | { | 2363 | { | ||
2095 | // JM: Some magic voodoo formula I came up with! | 2364 | // JM: Some magic voodoo formula I came up with! | ||
2096 | double moonEffect = (pow(separation, 1.7) * pow(zMoon, 0.5)) / (pow(zTarget, 1.1) * pow(illum, 0.5)); | 2365 | double moonEffect = (pow(separation, 1.7) * pow(zMoon, 0.5)) / (pow(zTarget, 1.1) * pow(illum, 0.5)); | ||
Show All 10 Lines | |||||
2107 | } | 2376 | } | ||
2108 | else | 2377 | else | ||
2109 | score = moonEffect; | 2378 | score = moonEffect; | ||
2110 | } | 2379 | } | ||
2111 | 2380 | | |||
2112 | // Limit to 0 to 20 | 2381 | // Limit to 0 to 20 | ||
2113 | score /= 5.0; | 2382 | score /= 5.0; | ||
2114 | 2383 | | |||
2115 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target is %L3 degrees from Moon (score %2).") | 2384 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target is %L3 degrees from Moon (score %2).") | ||
2116 | .arg(job->getName()) | 2385 | // .arg(job->getName()) | ||
2117 | .arg(separation, 0, 'f', 3) | 2386 | // .arg(separation, 0, 'f', 3) | ||
2118 | .arg(QString::asprintf("%+d", score)); | 2387 | // .arg(QString::asprintf("%+d", score)); | ||
2119 | 2388 | | |||
2120 | return score; | 2389 | return score; | ||
2121 | } | 2390 | } | ||
2122 | 2391 | | |||
2123 | void Scheduler::calculateDawnDusk() | 2392 | void Scheduler::calculateDawnDusk() | ||
2124 | { | 2393 | { | ||
2125 | KSAlmanac ksal; | 2394 | KSAlmanac ksal; | ||
2126 | Dawn = ksal.getDawnAstronomicalTwilight(); | 2395 | Dawn = ksal.getDawnAstronomicalTwilight(); | ||
2127 | Dusk = ksal.getDuskAstronomicalTwilight(); | 2396 | Dusk = ksal.getDuskAstronomicalTwilight(); | ||
2128 | 2397 | | |||
2129 | QTime now = KStarsData::Instance()->lt().time(); | 2398 | QTime now = KStarsData::Instance()->lt().time(); | ||
2130 | QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); | 2399 | QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); | ||
2131 | QTime dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); | 2400 | QTime dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); | ||
2132 | 2401 | | |||
2133 | duskDateTime.setDate(KStars::Instance()->data()->lt().date()); | 2402 | duskDateTime.setDate(KStars::Instance()->data()->lt().date()); | ||
2134 | duskDateTime.setTime(dusk); | 2403 | duskDateTime.setTime(dusk); | ||
2135 | 2404 | | |||
2136 | // FIXME: reduce spam by moving twilight time to a text label | 2405 | // FIXME: reduce spam by moving twilight time to a text label | ||
2137 | appendLogText(i18n("Astronomical twilight: dusk at %1, dawn at %2, and current time is %3", | 2406 | //appendLogText(i18n("Astronomical twilight: dusk at %1, dawn at %2, and current time is %3", | ||
2138 | dusk.toString(), dawn.toString(), now.toString())); | 2407 | // dusk.toString(), dawn.toString(), now.toString())); | ||
2139 | } | 2408 | } | ||
2140 | 2409 | | |||
2141 | void Scheduler::executeJob(SchedulerJob *job) | 2410 | void Scheduler::executeJob(SchedulerJob *job) | ||
2142 | { | 2411 | { | ||
2143 | // Some states have executeJob called after current job is cancelled - checkStatus does this | 2412 | // Some states have executeJob called after current job is cancelled - checkStatus does this | ||
2144 | if (job == nullptr) | 2413 | if (job == nullptr) | ||
2145 | return; | 2414 | return; | ||
2146 | 2415 | | |||
▲ Show 20 Lines • Show All 881 Lines • ▼ Show 20 Line(s) | 3293 | { | |||
3028 | stopCurrentJobAction(); | 3297 | stopCurrentJobAction(); | ||
3029 | stopGuiding(); | 3298 | stopGuiding(); | ||
3030 | findNextJob(); | 3299 | findNextJob(); | ||
3031 | return; | 3300 | return; | ||
3032 | } | 3301 | } | ||
3033 | } | 3302 | } | ||
3034 | 3303 | | |||
3035 | // #2 Check if altitude restriction still holds true | 3304 | // #2 Check if altitude restriction still holds true | ||
3036 | if (currentJob->getMinAltitude() > 0) | 3305 | if (-90 < currentJob->getMinAltitude()) | ||
3037 | { | 3306 | { | ||
3038 | SkyPoint p = currentJob->getTargetCoords(); | 3307 | SkyPoint p = currentJob->getTargetCoords(); | ||
3039 | 3308 | | |||
3040 | p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); | 3309 | p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); | ||
3041 | 3310 | | |||
3042 | /* FIXME: find a way to use altitude cutoff here, because the job can be scheduled when evaluating, then aborted when running */ | 3311 | /* FIXME: find a way to use altitude cutoff here, because the job can be scheduled when evaluating, then aborted when running */ | ||
3043 | if (p.alt().Degrees() < currentJob->getMinAltitude()) | 3312 | if (p.alt().Degrees() < currentJob->getMinAltitude()) | ||
3044 | { | 3313 | { | ||
3045 | // Only terminate job due to altitude limitation if mount is NOT parked. | 3314 | // Only terminate job due to altitude limitation if mount is NOT parked. | ||
3046 | if (isMountParked() == false) | 3315 | if (isMountParked() == false) | ||
3047 | { | 3316 | { | ||
3048 | appendLogText(i18n("Job '%1' current altitude (%2 degrees) crossed minimum constraint altitude (%3 degrees), " | 3317 | appendLogText(i18n("Job '%1' current altitude (%2 degrees) crossed minimum constraint altitude (%3 degrees), " | ||
3049 | "marking aborted.", | 3318 | "marking aborted.", currentJob->getName(), | ||
3050 | currentJob->getName(), p.alt().Degrees(), currentJob->getMinAltitude())); | 3319 | QString("%L1").arg(0, p.alt().Degrees(), minAltitude->decimals()), | ||
3320 | QString("%L1").arg(0, currentJob->getMinAltitude(), minAltitude->decimals()))); | ||||
3051 | 3321 | | |||
3052 | currentJob->setState(SchedulerJob::JOB_ABORTED); | 3322 | currentJob->setState(SchedulerJob::JOB_ABORTED); | ||
3053 | stopCurrentJobAction(); | 3323 | stopCurrentJobAction(); | ||
3054 | stopGuiding(); | 3324 | stopGuiding(); | ||
3055 | findNextJob(); | 3325 | findNextJob(); | ||
3056 | return; | 3326 | return; | ||
3057 | } | 3327 | } | ||
3058 | } | 3328 | } | ||
▲ Show 20 Lines • Show All 1004 Lines • ▼ Show 20 Line(s) | 4315 | { | |||
4063 | else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION) | 4333 | else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION) | ||
4064 | outstream << "<Condition value='" << cLocale.toString(job->getCulminationOffset()) << "'>Culmination</Condition>" << endl; | 4334 | outstream << "<Condition value='" << cLocale.toString(job->getCulminationOffset()) << "'>Culmination</Condition>" << endl; | ||
4065 | else if (job->getFileStartupCondition() == SchedulerJob::START_AT) | 4335 | else if (job->getFileStartupCondition() == SchedulerJob::START_AT) | ||
4066 | outstream << "<Condition value='" << job->getFileStartupTime().toString(Qt::ISODate) << "'>At</Condition>" | 4336 | outstream << "<Condition value='" << job->getFileStartupTime().toString(Qt::ISODate) << "'>At</Condition>" | ||
4067 | << endl; | 4337 | << endl; | ||
4068 | outstream << "</StartupCondition>" << endl; | 4338 | outstream << "</StartupCondition>" << endl; | ||
4069 | 4339 | | |||
4070 | outstream << "<Constraints>" << endl; | 4340 | outstream << "<Constraints>" << endl; | ||
4071 | if (job->getMinAltitude() > 0) | 4341 | if (-90 < job->getMinAltitude()) | ||
4072 | outstream << "<Constraint value='" << cLocale.toString(job->getMinAltitude()) << "'>MinimumAltitude</Constraint>" << endl; | 4342 | outstream << "<Constraint value='" << cLocale.toString(job->getMinAltitude()) << "'>MinimumAltitude</Constraint>" << endl; | ||
4073 | if (job->getMinMoonSeparation() > 0) | 4343 | if (job->getMinMoonSeparation() > 0) | ||
4074 | outstream << "<Constraint value='" << cLocale.toString(job->getMinMoonSeparation()) << "'>MoonSeparation</Constraint>" | 4344 | outstream << "<Constraint value='" << cLocale.toString(job->getMinMoonSeparation()) << "'>MoonSeparation</Constraint>" | ||
4075 | << endl; | 4345 | << endl; | ||
4076 | if (job->getEnforceWeather()) | 4346 | if (job->getEnforceWeather()) | ||
4077 | outstream << "<Constraint>EnforceWeather</Constraint>" << endl; | 4347 | outstream << "<Constraint>EnforceWeather</Constraint>" << endl; | ||
4078 | if (job->getEnforceTwilight()) | 4348 | if (job->getEnforceTwilight()) | ||
4079 | outstream << "<Constraint>EnforceTwilight</Constraint>" << endl; | 4349 | outstream << "<Constraint>EnforceTwilight</Constraint>" << endl; | ||
▲ Show 20 Lines • Show All 233 Lines • ▼ Show 20 Line(s) | 4572 | { | |||
4313 | stopGuiding(); | 4583 | stopGuiding(); | ||
4314 | 4584 | | |||
4315 | appendLogText(i18n("Job '%1' is complete.", currentJob->getName())); | 4585 | appendLogText(i18n("Job '%1' is complete.", currentJob->getName())); | ||
4316 | setCurrentJob(nullptr); | 4586 | setCurrentJob(nullptr); | ||
4317 | schedulerTimer.start(); | 4587 | schedulerTimer.start(); | ||
4318 | } | 4588 | } | ||
4319 | else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) | 4589 | else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) | ||
4320 | { | 4590 | { | ||
4591 | /* If the job is about to repeat, decrease its repeat count and reset its start time */ | ||||
4321 | if (0 < currentJob->getRepeatsRemaining()) | 4592 | if (0 < currentJob->getRepeatsRemaining()) | ||
4593 | { | ||||
4322 | currentJob->setRepeatsRemaining(currentJob->getRepeatsRemaining() - 1); | 4594 | currentJob->setRepeatsRemaining(currentJob->getRepeatsRemaining() - 1); | ||
4595 | currentJob->setStartupTime(QDateTime()); | ||||
4596 | } | ||||
4323 | 4597 | | |||
4324 | /* Mark the job idle as well as all its duplicates for re-evaluation */ | 4598 | /* Mark the job idle as well as all its duplicates for re-evaluation */ | ||
4325 | foreach(SchedulerJob *a_job, jobs) | 4599 | foreach(SchedulerJob *a_job, jobs) | ||
4326 | if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) | 4600 | if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) | ||
4327 | a_job->setState(SchedulerJob::JOB_IDLE); | 4601 | a_job->setState(SchedulerJob::JOB_IDLE); | ||
4328 | 4602 | | |||
4329 | /* Re-evaluate all jobs, without selecting a new job */ | 4603 | /* Re-evaluate all jobs, without selecting a new job */ | ||
4330 | jobEvaluationOnly = true; | 4604 | jobEvaluationOnly = true; | ||
▲ Show 20 Lines • Show All 290 Lines • ▼ Show 20 Line(s) | |||||
4621 | 4895 | | |||
4622 | void Scheduler::setDirty() | 4896 | void Scheduler::setDirty() | ||
4623 | { | 4897 | { | ||
4624 | mDirty = true; | 4898 | mDirty = true; | ||
4625 | 4899 | | |||
4626 | if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup) | 4900 | if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup) | ||
4627 | return; | 4901 | return; | ||
4628 | 4902 | | |||
4629 | if (0 <= jobUnderEdit && state != SCHEDULER_RUNNIG && !queueTable->selectedItems().isEmpty()) | 4903 | if (0 <= jobUnderEdit && state != SCHEDULER_RUNNIG && 0 <= queueTable->currentRow()) | ||
4904 | { | ||||
4905 | // Now that jobs are sorted, reset jobs that are later than the edited one for re-evaluation | ||||
4906 | for (int row = jobUnderEdit; row < jobs.size(); row++) | ||||
4907 | jobs.at(row)->reset(); | ||||
4908 | | ||||
4630 | saveJob(); | 4909 | saveJob(); | ||
4910 | } | ||||
4631 | 4911 | | |||
4632 | // For object selection, all fields must be filled | 4912 | // For object selection, all fields must be filled | ||
4633 | bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty(); | 4913 | bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty(); | ||
4634 | 4914 | | |||
4635 | // For FITS selection, only the name and fits URL should be filled. | 4915 | // For FITS selection, only the name and fits URL should be filled. | ||
4636 | bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty(); | 4916 | bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty(); | ||
4637 | 4917 | | |||
4638 | // Sequence selection is required | 4918 | // Sequence selection is required | ||
▲ Show 20 Lines • Show All 266 Lines • ▼ Show 20 Line(s) | 4999 | { | |||
4905 | } | 5185 | } | ||
4906 | 5186 | | |||
4907 | schedJob->setCapturedFramesMap(capture_map); | 5187 | schedJob->setCapturedFramesMap(capture_map); | ||
4908 | schedJob->setSequenceCount(totalSequenceCount); | 5188 | schedJob->setSequenceCount(totalSequenceCount); | ||
4909 | schedJob->setCompletedCount(totalCompletedCount); | 5189 | schedJob->setCompletedCount(totalCompletedCount); | ||
4910 | 5190 | | |||
4911 | qDeleteAll(seqJobs); | 5191 | qDeleteAll(seqJobs); | ||
4912 | 5192 | | |||
5193 | // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations! | ||||
5194 | | ||||
4913 | // We can't estimate times that do not finish when sequence is done | 5195 | // We can't estimate times that do not finish when sequence is done | ||
4914 | if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) | 5196 | if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) | ||
4915 | { | 5197 | { | ||
4916 | // We can't know estimated time if it is looping indefinitely | 5198 | // We can't know estimated time if it is looping indefinitely | ||
4917 | appendLogText(i18n("Warning: job '%1' will be looping until Scheduler is stopped manually.", schedJob->getName())); | | |||
4918 | schedJob->setEstimatedTime(-2); | 5199 | schedJob->setEstimatedTime(-2); | ||
5200 | | ||||
5201 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.") | ||||
5202 | .arg(schedJob->getName()); | ||||
4919 | } | 5203 | } | ||
4920 | // If we know startup and finish times, we can estimate time right away | 5204 | // If we know startup and finish times, we can estimate time right away | ||
4921 | else if (schedJob->getStartupCondition() == SchedulerJob::START_AT && | 5205 | else if (schedJob->getStartupCondition() == SchedulerJob::START_AT && | ||
4922 | schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) | 5206 | schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) | ||
4923 | { | 5207 | { | ||
5208 | // FIXME: SchedulerJob is probably doing this already | ||||
4924 | qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); | 5209 | qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); | ||
4925 | appendLogText(i18n("Job '%1' will run for %2.", schedJob->getName(), dms(diff * 15.0 / 3600.0f).toHMSString())); | | |||
4926 | schedJob->setEstimatedTime(diff); | 5210 | schedJob->setEstimatedTime(diff); | ||
5211 | | ||||
5212 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.") | ||||
5213 | .arg(schedJob->getName()) | ||||
5214 | .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); | ||||
4927 | } | 5215 | } | ||
4928 | // If we know finish time only, we can roughly estimate the time considering the job starts now | 5216 | // If we know finish time only, we can roughly estimate the time considering the job starts now | ||
4929 | else if (schedJob->getStartupCondition() != SchedulerJob::START_AT && | 5217 | else if (schedJob->getStartupCondition() != SchedulerJob::START_AT && | ||
4930 | schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) | 5218 | schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) | ||
4931 | { | 5219 | { | ||
4932 | qint64 const diff = KStarsData::Instance()->lt().secsTo(schedJob->getCompletionTime()); | 5220 | qint64 const diff = KStarsData::Instance()->lt().secsTo(schedJob->getCompletionTime()); | ||
4933 | appendLogText(i18n("Job '%1' will run for %2 if started now.", schedJob->getName(), dms(diff * 15.0 / 3600.0f).toHMSString())); | | |||
4934 | schedJob->setEstimatedTime(diff); | 5221 | schedJob->setEstimatedTime(diff); | ||
5222 | | ||||
5223 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.") | ||||
5224 | .arg(schedJob->getName()) | ||||
5225 | .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); | ||||
4935 | } | 5226 | } | ||
4936 | // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null | 5227 | // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null | ||
4937 | else if (totalImagingTime <= 0) | 5228 | else if (totalImagingTime <= 0) | ||
4938 | { | 5229 | { | ||
5230 | schedJob->setEstimatedTime(0); | ||||
5231 | | ||||
4939 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.") | 5232 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.") | ||
4940 | .arg(schedJob->getName()).arg(totalCompletedCount).arg(totalSequenceCount); | 5233 | .arg(schedJob->getName()).arg(totalCompletedCount).arg(totalSequenceCount); | ||
4941 | schedJob->setEstimatedTime(0); | | |||
4942 | } | 5234 | } | ||
5235 | // Else consolidate with step durations | ||||
4943 | else | 5236 | else | ||
4944 | { | 5237 | { | ||
4945 | if (schedJob->getLightFramesRequired()) | 5238 | if (schedJob->getLightFramesRequired()) | ||
4946 | { | 5239 | { | ||
4947 | /* FIXME: estimation doesn't need to consider repeats, those will be optimized away by findNextJob (this is a regression) */ | 5240 | /* FIXME: estimation doesn't need to consider repeats, those will be optimized away by findNextJob (this is a regression) */ | ||
4948 | /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */ | 5241 | /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */ | ||
4949 | // Are we doing tracking? It takes about 30 seconds | 5242 | // Are we doing tracking? It takes about 30 seconds | ||
4950 | if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK) | 5243 | if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK) | ||
4951 | totalImagingTime += 30*schedJob->getRepeatsRequired(); | 5244 | totalImagingTime += 30*schedJob->getRepeatsRequired(); | ||
4952 | // Are we doing initial focusing? That can take about 2 minutes | 5245 | // Are we doing initial focusing? That can take about 2 minutes | ||
4953 | if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS) | 5246 | if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS) | ||
4954 | totalImagingTime += 120*schedJob->getRepeatsRequired(); | 5247 | totalImagingTime += 120*schedJob->getRepeatsRequired(); | ||
4955 | // Are we doing astrometry? That can take about 30 seconds | 5248 | // Are we doing astrometry? That can take about 30 seconds | ||
4956 | if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN) | 5249 | if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN) | ||
4957 | totalImagingTime += 30*schedJob->getRepeatsRequired(); | 5250 | totalImagingTime += 30*schedJob->getRepeatsRequired(); | ||
4958 | // Are we doing guiding? Calibration process can take about 2 mins | 5251 | // Are we doing guiding? Calibration process can take about 2 mins | ||
4959 | if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE) | 5252 | if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE) | ||
4960 | totalImagingTime += 120*schedJob->getRepeatsRequired(); | 5253 | totalImagingTime += 120*schedJob->getRepeatsRequired(); | ||
4961 | } | 5254 | } | ||
4962 | 5255 | | |||
4963 | dms const estimatedTime(totalImagingTime * 15.0 / 3600.0); | 5256 | dms const estimatedTime(totalImagingTime * 15.0 / 3600.0); | ||
4964 | qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), estimatedTime.toHMSString()); | | |||
4965 | | ||||
4966 | schedJob->setEstimatedTime(totalImagingTime); | 5257 | schedJob->setEstimatedTime(totalImagingTime); | ||
5258 | | ||||
5259 | qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), estimatedTime.toHMSString()); | ||||
4967 | } | 5260 | } | ||
4968 | 5261 | | |||
4969 | return true; | 5262 | return true; | ||
4970 | } | 5263 | } | ||
4971 | 5264 | | |||
4972 | void Scheduler::parkMount() | 5265 | void Scheduler::parkMount() | ||
4973 | { | 5266 | { | ||
4974 | QVariant parkingStatus = mountInterface->property("parkStatus"); | 5267 | QVariant parkingStatus = mountInterface->property("parkStatus"); | ||
▲ Show 20 Lines • Show All 600 Lines • ▼ Show 20 Line(s) | 5797 | { | |||
5575 | } | 5868 | } | ||
5576 | } | 5869 | } | ||
5577 | 5870 | | |||
5578 | void Scheduler::startJobEvaluation() | 5871 | void Scheduler::startJobEvaluation() | ||
5579 | { | 5872 | { | ||
5580 | // Reset current job | 5873 | // Reset current job | ||
5581 | setCurrentJob(nullptr); | 5874 | setCurrentJob(nullptr); | ||
5582 | 5875 | | |||
5876 | // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept | ||||
5877 | for (SchedulerJob * job: jobs) | ||||
5878 | { | ||||
5879 | job->reset(); | ||||
5880 | job->setCompletedCount(0); | ||||
5881 | } | ||||
5882 | | ||||
5583 | // Unconditionally update the capture storage | 5883 | // Unconditionally update the capture storage | ||
5584 | updateCompletedJobsCount(true); | 5884 | updateCompletedJobsCount(true); | ||
5585 | 5885 | | |||
5586 | // Reset ALL scheduler jobs to IDLE and re-evaluate them all again | 5886 | // And evaluate all pending jobs per the conditions set in each | ||
5887 | jobEvaluationOnly = true; | ||||
5888 | evaluateJobs(); | ||||
5889 | } | ||||
5890 | | ||||
5891 | void Scheduler::sortJobsPerAltitude() | ||||
5892 | { | ||||
5893 | // We require a first job to sort, so bail out if list is empty | ||||
5894 | if (jobs.isEmpty()) | ||||
5895 | return; | ||||
5896 | | ||||
5897 | // Don't reset current job | ||||
5898 | // setCurrentJob(nullptr); | ||||
5899 | | ||||
5900 | // Don't reset scheduler jobs startup times before sorting - we need the first job startup time | ||||
5901 | | ||||
5902 | // Sort by startup time, using the first job time as reference for altitude calculations | ||||
5903 | using namespace std::placeholders; | ||||
5904 | QList<SchedulerJob*> sortedJobs = jobs; | ||||
5905 | std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(), | ||||
5906 | std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, jobs.first()->getStartupTime())); | ||||
5907 | | ||||
5908 | // If order changed, reset and re-evaluate | ||||
5909 | if (reorderJobs(sortedJobs)) | ||||
5910 | { | ||||
5587 | for (SchedulerJob * job: jobs) | 5911 | for (SchedulerJob * job: jobs) | ||
5588 | job->reset(); | 5912 | job->reset(); | ||
5589 | 5913 | | |||
5590 | // And evaluate all pending jobs per the conditions set in each | | |||
5591 | jobEvaluationOnly = true; | 5914 | jobEvaluationOnly = true; | ||
5592 | evaluateJobs(); | 5915 | evaluateJobs(); | ||
5593 | } | 5916 | } | ||
5917 | } | ||||
5594 | 5918 | | |||
5595 | void Scheduler::updatePreDawn() | 5919 | void Scheduler::updatePreDawn() | ||
5596 | { | 5920 | { | ||
5597 | double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); | 5921 | double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); | ||
5598 | int dayOffset = 0; | 5922 | int dayOffset = 0; | ||
5599 | QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); | 5923 | QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); | ||
5600 | if (KStarsData::Instance()->lt().time() >= dawn) | 5924 | if (KStarsData::Instance()->lt().time() >= dawn) | ||
5601 | dayOffset = 1; | 5925 | dayOffset = 1; | ||
▲ Show 20 Lines • Show All 225 Lines • ▼ Show 20 Line(s) | 6098 | { | |||
5827 | return true; | 6151 | return true; | ||
5828 | } | 6152 | } | ||
5829 | 6153 | | |||
5830 | void Scheduler::resetAllJobs() | 6154 | void Scheduler::resetAllJobs() | ||
5831 | { | 6155 | { | ||
5832 | if (state == SCHEDULER_RUNNIG) | 6156 | if (state == SCHEDULER_RUNNIG) | ||
5833 | return; | 6157 | return; | ||
5834 | 6158 | | |||
6159 | // Reset capture count of all jobs before re-evaluating | ||||
6160 | foreach (SchedulerJob *job, jobs) | ||||
6161 | job->setCompletedCount(0); | ||||
6162 | | ||||
5835 | // Evaluate all jobs, this refreshes storage and resets job states | 6163 | // Evaluate all jobs, this refreshes storage and resets job states | ||
5836 | startJobEvaluation(); | 6164 | startJobEvaluation(); | ||
5837 | } | 6165 | } | ||
5838 | 6166 | | |||
5839 | void Scheduler::checkTwilightWarning(bool enabled) | 6167 | void Scheduler::checkTwilightWarning(bool enabled) | ||
5840 | { | 6168 | { | ||
5841 | if (enabled) | 6169 | if (enabled) | ||
5842 | return; | 6170 | return; | ||
▲ Show 20 Lines • Show All 922 Lines • Show Last 20 Lines |
Would this lead to memory leak or is it reparented when setItem(..) is used?