2 * Project.cpp - TaskJuggler
4 * Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006
5 * by Chris Schlaeger <cs@kde.org>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of version 2 of the GNU General Public License as
9 * published by the Free Software Foundation.
18 #include "TjMessageHandler.h"
23 #include "HTMLTaskReport.h"
24 #include "HTMLResourceReport.h"
25 #include "HTMLAccountReport.h"
26 #include "HTMLWeeklyCalendar.h"
27 #include "HTMLStatusReport.h"
28 #include "CSVTaskReport.h"
29 #include "CSVResourceReport.h"
30 #include "CSVAccountReport.h"
31 #include "ExportReport.h"
32 #include "ReportXML.h"
33 #include "UsageLimits.h"
34 #include "CustomAttributeDefinition.h"
36 DebugController DebugCtrl;
43 allowRedefinitions(false),
44 weekStartsMonday(true),
50 timeFormat("%Y-%m-%d %H:%M"),
51 shortTimeFormat("%H:%M"),
54 numberFormat("-", "", ",", ".", 1),
55 currencyFormat("(", ")", ",", ".", 0),
60 dailyWorkingHours(8.0),
61 yearlyWorkingDays(260.714),
63 scheduleGranularity(suggestTimingResolution()),
76 originalResourceList(),
77 originalAccountList(),
86 /* Pick some reasonable initial number since we don't know the
87 * project time frame yet. */
90 vacationList.setAutoDelete(true);
91 accountAttributes.setAutoDelete(true);
92 taskAttributes.setAutoDelete(true);
93 resourceAttributes.setAutoDelete(true);
94 reports.setAutoDelete(true);
96 new Scenario(this, "plan", "Plan", 0);
97 scenarioList.createIndex(true);
98 scenarioList.createIndex(false);
102 /* Initialize working hours with default values that match the Monday -
103 * Friday 9 - 6 (with 1 hour lunch break) pattern used by many western
106 workingHours[0] = new QPtrList<Interval>();
107 workingHours[0]->setAutoDelete(true);
109 for (int i = 1; i < 6; ++i)
111 workingHours[i] = new QPtrList<Interval>();
112 workingHours[i]->setAutoDelete(true);
113 workingHours[i]->append(new Interval(9 * ONEHOUR, 12 * ONEHOUR - 1));
114 workingHours[i]->append(new Interval(13 * ONEHOUR, 18 * ONEHOUR - 1));
118 workingHours[6] = new QPtrList<Interval>();
119 workingHours[6]->setAutoDelete(true);
124 taskList.deleteContents();
125 resourceList.deleteContents();
126 Resource::deleteStaticData();
128 accountList.deleteContents();
129 shiftList.deleteContents();
130 scenarioList.deleteContents();
132 delete resourceLimits;
134 // Remove support for 1.0 XML reports for next major release. */
137 for (int i = 0; i < 7; ++i)
138 delete workingHours[i];
143 Project::addSourceFile(const QString& f)
145 if (sourceFiles.find(f) == sourceFiles.end())
146 sourceFiles.append(f);
150 Project::getSourceFiles() const
156 Project::setProgressInfo(const QString& i)
158 emit updateProgressInfo(i);
162 Project::setProgressBar(int i, int of)
164 emit updateProgressBar(i, of);
168 Project::setTimeZone(const QString& tz)
170 if (!setTimezone(tz))
178 Project::getScenario(int sc) const
181 for (ScenarioListIterator sli(scenarioList); sli; ++sli)
188 Project::getScenarioName(int sc) const
191 for (ScenarioListIterator sli(scenarioList); sli; ++sli)
193 return (*sli)->getName();
195 return QString::null;
199 Project::getScenarioId(int sc) const
202 for (ScenarioListIterator sli(scenarioList); sli; ++sli)
204 return (*sli)->getId();
206 return QString::null;
210 Project::getScenarioIndex(const QString& id) const
212 return scenarioList.getIndex(id);
216 Project::setNow(time_t n)
218 /* Align 'now' time to timing resolution. If the resolution is
219 * changed later, this has to be done again. */
220 now = (n / scheduleGranularity) * scheduleGranularity;
224 Project::setWorkingHours(int day, const QPtrList<Interval>& l)
226 if (day < 0 || day > 6)
227 qFatal("day out of range");
228 delete workingHours[day];
230 // Create a deep copy of the interval list.
231 workingHours[day] = new QPtrList<Interval>;
232 workingHours[day]->setAutoDelete(true);
233 for (QPtrListIterator<Interval> pli(l); pli; ++pli)
234 workingHours[day]->append(new Interval(**pli));
238 Project::addId(const QString& id, bool changeCurrentId)
240 if (projectIDs.findIndex(id) != -1)
243 projectIDs.append(id);
252 Project::getIdIndex(const QString& i) const
255 if ((idx = projectIDs.findIndex(i)) == -1)
260 idxStr = QChar('A' + idx % ('Z' - 'A')) + idxStr;
262 } while (idx > 'Z' - 'A');
268 Project::addScenario(Scenario* s)
270 scenarioList.append(s);
272 /* This is not too efficient, but since there are usually only a few
273 * scenarios in a project, this doesn't hurt too much. */
274 scenarioList.createIndex(true);
275 scenarioList.createIndex(false);
279 Project::deleteScenario(Scenario* s)
281 scenarioList.removeRef(s);
285 Project::setResourceLimits(UsageLimits* l)
288 delete resourceLimits;
293 Project::addTask(Task* t)
299 Project::deleteTask(Task* t)
301 taskList.removeRef(t);
305 Project::addTaskAttribute(const QString& id, CustomAttributeDefinition* cad)
307 if (taskAttributes.find(id))
310 taskAttributes.insert(id, cad);
314 const CustomAttributeDefinition*
315 Project::getTaskAttribute(const QString& id) const
317 return taskAttributes[id];
321 Project::addShift(Shift* s)
327 Project::deleteShift(Shift* s)
329 shiftList.removeRef(s);
333 Project::addResource(Resource* r)
335 resourceList.append(r);
339 Project::deleteResource(Resource* r)
341 resourceList.removeRef(r);
345 Project::addResourceAttribute(const QString& id,
346 CustomAttributeDefinition* cad)
348 if (resourceAttributes.find(id))
351 resourceAttributes.insert(id, cad);
355 const CustomAttributeDefinition*
356 Project::getResourceAttribute(const QString& id) const
358 return resourceAttributes[id];
362 Project::addAccount(Account* a)
364 accountList.append(a);
368 Project::deleteAccount(Account* a)
370 accountList.removeRef(a);
374 Project::addAccountAttribute(const QString& id,
375 CustomAttributeDefinition* cad)
377 if (accountAttributes.find(id))
380 accountAttributes.insert(id, cad);
384 const CustomAttributeDefinition*
385 Project::getAccountAttribute(const QString& id) const
387 return accountAttributes[id];
391 Project::isWorkingDay(time_t wd) const
393 return !(workingHours[dayOfWeek(wd, false)]->isEmpty() ||
398 Project::isWorkingTime(time_t wd) const
403 int dow = dayOfWeek(wd, false);
404 for (QPtrListIterator<Interval> ili(*(workingHours[dow])); *ili != 0; ++ili)
406 if ((*ili)->contains(secondsOfDay(wd)))
413 Project::isWorkingTime(const Interval& iv) const
415 if (isVacation(iv.getStart()))
418 int dow = dayOfWeek(iv.getStart(), false);
419 for (QPtrListIterator<Interval> ili(*(workingHours[dow])); *ili != 0; ++ili)
421 if ((*ili)->contains(Interval(secondsOfDay(iv.getStart()),
422 secondsOfDay(iv.getEnd()))))
429 Project::calcWorkingDays(const Interval& iv) const
433 for (time_t s = midnight(iv.getStart()); s <= iv.getEnd();
434 s = sameTimeNextDay(s))
442 Project::convertToDailyLoad(long secs) const
444 return (((double) secs) / (dailyWorkingHours * ONEHOUR));
448 Project::quantizeLoad(double load) const
451 return ((double) (((long) (load * dailyWorkingHours * (double) ONEHOUR)) /
452 scheduleGranularity)) /
453 ((dailyWorkingHours * (double) ONEHOUR)) *
454 (double) scheduleGranularity;
458 Project::convertToSlots(double effort) const
460 return ((long int) ((effort * dailyWorkingHours * (double) ONEHOUR)) /
461 scheduleGranularity);
465 Project::addJournalEntry(JournalEntry* entry)
467 journal.inSort(entry);
471 Project::getJournalIterator() const
473 return Journal::Iterator(journal);
477 Project::pass2(bool noDepCheck)
479 int oldErrors = TJMH.getErrors();
481 if (taskList.isEmpty())
483 TJMH.errorMessage(i18n("The project does not contain any tasks."));
489 /* The optimum size for the localtime hash is twice the number of time
490 * slots times 2 (because of timeslot and timeslot - 1s). */
491 initUtility(4 * ((end - start) / scheduleGranularity));
493 // Generate sequence numbers for all lists.
494 taskList.createIndex(true);
495 resourceList.createIndex(true);
496 accountList.createIndex(true);
497 shiftList.createIndex(true);
499 // Initialize random generator.
502 // Create hash to map task IDs to pointers.
503 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
505 idHash.insert((*tli)->getId(), *tli);
507 // Create cross links from dependency lists.
508 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
509 (*tli)->xRef(idHash);
511 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
513 // Set dates according to implicit dependencies
514 (*tli)->implicitXRef();
516 // Sort allocations properly
517 (*tli)->sortAllocations();
519 // Save so far booked resources as specified resources
520 (*tli)->saveSpecifiedBookedResources();
523 // Save a copy of all manually booked resources.
524 for (ResourceListIterator rli(resourceList); *rli != 0; ++rli)
525 (*rli)->saveSpecifiedBookings();
527 /* Now we can copy the missing values from the plan scenario to the other
529 if (scenarioList.count() > 1)
531 for (ScenarioListIterator sli(scenarioList[0]->getSubListIterator());
533 overlayScenario(0, (*sli)->getSequenceNo() - 1);
536 // Now check that all tasks have sufficient data to be scheduled.
537 setProgressInfo(i18n("Checking scheduling data..."));
539 for (ScenarioListIterator sci(scenarioList); *sci; ++sci)
540 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
541 if (!(*tli)->preScheduleOk((*sci)->getSequenceNo() - 1))
550 setProgressInfo(i18n("Searching for dependency loops ..."));
552 tjDebug("Searching for dependency loops ...");
553 // Check all tasks for dependency loops.
554 LDIList chkedTaskList;
555 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
556 if ((*tli)->loopDetector(chkedTaskList))
559 setProgressInfo(i18n("Searching for underspecified tasks ..."));
561 tjDebug("Searching for underspecified tasks ...");
562 for (ScenarioListIterator sci(scenarioList); *sci; ++sci)
563 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
564 if (!(*tli)->checkDetermination((*sci)->getSequenceNo() - 1))
571 return TJMH.getErrors() == oldErrors;
575 Project::scheduleScenario(Scenario* sc)
577 int oldErrors = TJMH.getErrors();
579 setProgressInfo(i18n("Scheduling scenario %1...").arg(sc->getId()));
581 int scIdx = sc->getSequenceNo() - 1;
582 prepareScenario(scIdx);
584 if (!schedule(scIdx))
587 tjDebug(i18n("Scheduling errors in scenario '%1'.")
592 finishScenario(scIdx);
594 for (ResourceListIterator rli(resourceList); *rli != 0; ++rli)
596 if (!(*rli)->bookingsOk(scIdx))
600 return TJMH.getErrors() == oldErrors;
604 Project::completeBuffersAndIndices()
606 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
607 (*tli)->computeBuffers();
609 /* Create indices for all lists according to their default sorting
611 taskList.createIndex();
612 resourceList.createIndex();
613 accountList.createIndex();
614 shiftList.createIndex();
618 Project::scheduleAllScenarios()
620 bool schedulingOk = true;
621 for (ScenarioListIterator sci(scenarioList); *sci; ++sci)
622 if ((*sci)->getEnabled())
625 tjDebug(i18n("Scheduling scenario '%1' ...")
626 .arg((*sci)->getId()));
628 if (!scheduleScenario(*sci))
629 schedulingOk = false;
634 completeBuffersAndIndices();
640 Project::overlayScenario(int base, int sc)
642 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
643 (*tli)->overlayScenario(base, sc);
645 for (ScenarioListIterator sli(scenarioList[sc]->getSubListIterator());
647 overlayScenario(sc, (*sli)->getSequenceNo() - 1);
651 Project::prepareScenario(int sc)
653 for (ResourceListIterator rli(resourceList); *rli != 0; ++rli)
654 (*rli)->prepareScenario(sc);
656 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
657 (*tli)->prepareScenario(sc);
659 /* First we compute the criticalness of the individual task without their
660 * dependency context. */
661 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
662 (*tli)->computeCriticalness(sc);
664 /* Then we compute the path criticalness that represents the criticalness
665 * of a task taking their dependency context into account. */
666 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
667 (*tli)->computePathCriticalness(sc);
669 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
670 (*tli)->propagateInitialValues(sc);
674 tjDebug("Allocation probabilities for the resources:");
675 for (ResourceListIterator rli(resourceList); *rli != 0; ++rli)
676 qDebug("Resource %s: %f%%",
677 (*rli)->getId().latin1(),
678 (*rli)->getAllocationProbability(sc));
679 tjDebug("Criticalnesses of the tasks with respect to resource "
681 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
682 qDebug("Task %s: %-5.1f %-5.1f", (*tli)->getId().latin1(),
683 (*tli)->getCriticalness(sc),
684 (*tli)->getPathCriticalness(sc));
689 Project::finishScenario(int sc)
691 for (ResourceListIterator rli(resourceList); *rli != 0; ++rli)
692 (*rli)->finishScenario(sc);
693 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
694 (*tli)->finishScenario(sc);
696 /* We need to have finished the scenario for all tasks before we can
697 * calculate the completion degree. */
698 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
699 (*tli)->calcCompletionDegree(sc);
701 /* If the user has not set the minSlackRate to 0 we look for critical
703 if (getScenario(sc)->getMinSlackRate() > 0.0)
705 setProgressInfo(i18n("Computing critical pathes..."));
706 /* The critical path detector needs to know the end of the last task.
707 * So we have to find this out first. */
709 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
710 if (maxEnd < (*tli)->getEnd(sc))
711 maxEnd = (*tli)->getEnd(sc);
713 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
714 (*tli)->checkAndMarkCriticalPath
715 (sc, getScenario(sc)->getMinSlackRate(), maxEnd);
720 Project::schedule(int sc)
722 int oldErrors = TJMH.getErrors();
724 // The scheduling function only cares about leaf tasks. Container tasks
725 // are scheduled automatically when all their childern are scheduled. So
726 // we create a task list that only contains leaf tasks.
727 TaskList allLeafTasks;
728 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
729 if (!(*tli)->hasSubs())
730 allLeafTasks.append(*tli);
732 allLeafTasks.setSorting(CoreAttributesList::PrioDown, 0);
733 allLeafTasks.setSorting(CoreAttributesList::PathCriticalnessDown, 1);
734 allLeafTasks.setSorting(CoreAttributesList::SequenceUp, 2);
737 /* The workItems list contains all tasks that are ready to be scheduled at
738 * any given iteration. When a tasks has been scheduled completely, this
739 * list needs to be updated again as some tasks may now have become ready
740 * to be scheduled. */
743 for (TaskListIterator tli(allLeafTasks); *tli != 0; ++tli)
745 if ((*tli)->isReadyForScheduling())
746 workItems.append(*tli);
747 if ((*tli)->isSchedulingDone())
752 /* While the scheduling process progresses, the list contains more and
753 * more scheduled tasks. We use the cleanupTimer to remove those in
754 * certain intervals. As we potentially have already completed tasks in
755 * the list when we start, we initialize the timer with a very large
756 * number so the first round of cleanup is done right after the first
757 * scheduling pass. */
759 bool runAwayFound = false;
765 double pathCriticalness = 0.0;
766 Task::SchedulingInfo schedulingInfo = Task::ASAP;
768 /* The task list is sorted by priority. The priority decreases towards
769 * the end of the list. We iterate through the list and look for a
770 * task that can be scheduled. It the determines the time slot that
771 * will be scheduled during this run for all subsequent tasks as well.
773 for (TaskListIterator tli(workItems); *tli != 0; ++tli)
777 /* No time slot has been set yet. Check if this task can be
778 * scheduled and provides a suggestion. */
779 slot = (*tli)->nextSlot(scheduleGranularity);
780 /* If not, try the next task. */
783 priority = (*tli)->getPriority();
784 pathCriticalness = (*tli)->getPathCriticalness(sc);
785 schedulingInfo = (*tli)->getScheduling();
788 qDebug("Task '%s' (Prio %d, Direction: %d) requests "
790 (*tli)->getId().latin1(), (*tli)->getPriority(),
791 (*tli)->getScheduling(),
792 time2ISO(slot).latin1());
793 /* If the task wants a time slot outside of the project time
794 * frame, we flag this task as a runaway and go to the next
797 slot > (end - (time_t) scheduleGranularity + 1))
799 (*tli)->setRunaway();
806 /* Each task has a scheduling direction (forward or backward)
807 * depending on it's constrains. The task with the highest
808 * priority/pathCriticalness determins the time slot and hence the
809 * scheduling direction. Since tasks that have the other direction
810 * cannot the scheduled then, we have to stop this run as soon as
811 * we hit a task that runs in the other direction. If we would not
812 * do this, tasks with lower priority/pathCriticalness would grab
813 * resources form tasks with higher priority. */
814 if ((*tli)->getScheduling() != schedulingInfo &&
815 !(*tli)->isMilestone())
818 qDebug("Changing scheduling direction to %d due to task "
819 "'%s'", (*tli)->getScheduling(),
820 (*tli)->getId().latin1());
823 /* We must avoid that lower priority tasks get resources even
824 * though there are higher priority tasks that are ready to be
825 * scheduled but have a non-adjacent last slot. If two tasks have
826 * the same priority the pathCriticalness is being used. */
827 if ((*tli)->getPriority() < priority ||
828 ((*tli)->getPriority() == priority &&
829 (*tli)->getPathCriticalness(sc) < pathCriticalness))
832 // Schedule this task for the current time slot.
833 if ((*tli)->schedule(sc, slot, scheduleGranularity))
836 int oldSortedTasks = sortedTasks;
838 for (TaskListIterator tli(allLeafTasks); *tli != 0; ++tli)
840 if ((*tli)->isReadyForScheduling())
841 workItems.append(*tli);
842 if ((*tli)->isSchedulingDone())
845 // Update the progress bar after every 10th completed tasks.
846 if (oldSortedTasks / 10 != sortedTasks / 10)
848 setProgressBar(sortedTasks - allLeafTasks.count(),
851 (i18n("Scheduling scenario %1 at %2")
852 .arg(getScenarioId(sc)).arg(time2tjp(slot)));
856 } while (!done && !breakFlag);
861 setProgressBar(0, 0);
862 TJMH.errorMessage(i18n("Scheduling aborted on user request"));
867 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
868 if ((*tli)->isRunaway())
870 if ((*tli)->getScheduling() == Task::ASAP)
872 (i18n("End of task '%1' does not fit into the "
873 "project time frame. Try using a later project "
875 .arg((*tli)->getId()));
878 (i18n("Start of task '%1' does not fit into the "
879 "project time frame. Try using an earlier "
880 "project start date.").arg((*tli)->getId()));
883 if (TJMH.getErrors() == oldErrors)
884 setProgressBar(100, 100);
886 /* Check that the resulting schedule meets all the requirements that the
887 * user has specified. */
888 setProgressInfo(i18n("Checking schedule of scenario %1")
889 .arg(getScenarioId(sc)));
892 return TJMH.getErrors() == oldErrors;
896 Project::breakScheduling()
902 Project::checkSchedule(int sc) const
904 int oldErrors = TJMH.getErrors();
905 for (TaskListIterator tli(taskList); *tli != 0; ++tli)
907 /* Only check top-level tasks, since they recursively check their sub
909 if ((*tli)->getParent() == 0)
910 (*tli)->scheduleOk(sc);
911 if (maxErrors > 0 && TJMH.getErrors() >= maxErrors)
914 (i18n("Too many errors in %1 scenario. Giving up.")
915 .arg(getScenarioId(sc)));
920 return TJMH.getErrors() == oldErrors;
924 Project::getReport(uint idx) const
926 QPtrListIterator<Report> it(reports);
927 for (uint i = 0; *it && i < idx; ++it, ++i)
932 QPtrListIterator<Report>
933 Project::getReportListIterator() const
935 return QPtrListIterator<Report>(reports);
939 Project::generateReports() const
943 for (QPtrListIterator<Report> ri(reports); *ri != 0; ++ri)
945 // We generate all but Qt*Reports. Those are for the GUI version.
946 if (strncmp((*ri)->getType(), "Qt", 2) != 0)
949 tjDebug(i18n("Generating report '%1' ...")
950 .arg((*ri)->getFileName()));
952 if (!(*ri)->generate())
962 bool Project::generateXMLReport() const
965 return xmlreport->generate();