/* The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.projity.com/license . The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 have been added to cover use of software over a computer network and provide for limited attribution for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is OpenProj. The Original Developer is the Initial Developer and is Projity, Inc. All portions of the code written by Projity are Copyright (c) 2006, 2007. All Rights Reserved. Contributors Projity, Inc. Alternatively, the contents of this file may be used under the terms of the Projity End-User License Agreeement (the Projity License), in which case the provisions of the Projity License are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the Projity License and not to allow others to use your version of this file under the CPAL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the Projity License. If you do not delete the provisions above, a recipient may use your version of this file under either the CPAL or the Projity License. [NOTE: The text of this license may differ slightly from the text of the notices in Exhibits A and B of the license at http://www.projity.com/license. You should use the latest text at http://www.projity.com/license for your modifications. You may not remove this license text from the source files.] Attribution Information: Attribution Copyright Notice: Copyright � 2006, 2007 Projity, Inc. Attribution Phrase (not exceeding 10 words): Powered by OpenProj, an open source solution from Projity. Attribution URL: http://www.projity.com Graphic Image as provided in the Covered Code as file: openproj_logo.png with alternatives listed on http://www.projity.com/logo Display of Attribution Information is required in Larger Works which are defined in the CPAL as a work which combines Covered Code or portions thereof with code not governed by the terms of the CPAL. However, in addition to the other notice obligations, all copies of the Covered Code in Executable and Source Code form distributed must, as a form of attribution of the original author, include on each user interface screen the "OpenProj" logo visible to all users. The OpenProj logo should be located horizontally aligned with the menu bar and left justified on the top left of the screen adjacent to the File menu. The logo must be at least 100 x 25 pixels. When users click on the "OpenProj" logo it must direct them back to http://www.projity.com. */ package com.projity.pm.criticalpath; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Collection; import java.util.Date; import java.util.Iterator; import org.apache.commons.lang.builder.ToStringBuilder; import com.projity.datatype.Duration; import com.projity.grouping.core.Node; import com.projity.pm.dependency.Dependency; import com.projity.pm.task.NormalTask; import com.projity.pm.task.SubProj; import com.projity.pm.task.Task; public final class TaskSchedule implements Cloneable { public static final int CURRENT = 0; public static final int EARLY = -1; public static final int LATE = 1; //Persisted fields private double percentComplete = 0D; // these are calculated, but are persisted anyway for reporting private long rawDuration; private long start; private long finish; // Calculated fields that are transient //TODO don't bother serializing these. When I make them transient, the program hangs private Task task; private int type; private boolean forward = true; private long dependencyDate = Dependency.NEEDS_CALCULATION; private long remainingDependencyDate = 0; public TaskSchedule() { } public TaskSchedule(Task task, int type) { init(task,type); start = 0; finish = 0; } public void init(Task task, int type) { this.task = task; this.type = type; if (type == EARLY) forward = true; else if (type == LATE) forward = false; dependencyDate = Dependency.NEEDS_CALCULATION; invalidate(); } public void initSerialized(Task task, int type) { this.task = task; this.type = type; if (type == EARLY) forward = true; else if (type == LATE) forward = false; } public void setTask(Task task) { this.task = task; } public void serialize(ObjectOutputStream s) throws IOException { s.writeDouble(percentComplete); s.writeLong(rawDuration); s.writeLong(start); s.writeLong(finish); } //call init to complete initialization public static TaskSchedule deserialize(ObjectInputStream s) throws IOException, ClassNotFoundException { TaskSchedule t=new TaskSchedule(); t.setPercentComplete(s.readDouble()); t.setRawDuration(s.readLong()); t.setStart(s.readLong()); t.setFinish(s.readLong()); return t; } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(); } } public Object cloneWithTask(Task task) { TaskSchedule ts=(TaskSchedule)clone(); ts.setTask(task); ts.invalidate(); return ts; } // // void copyDatesFrom(TaskSchedule from) { // start = from.start; // finish = from.finish; // if (task.isWbsParent()) // rawDuration = from.rawDuration; // dependencyDate = from.dependencyDate; // remainingDependencyDate = from.remainingDependencyDate; // } public long getBegin() { return forward ? start : -finish; } public long getEnd() { return forward ? finish : -start; } public long getWindowBegin() { return forward ? task.getWindowEarlyStart() : -task.getWindowLateFinish(); } public long getWindowEnd() { return forward ? task.getWindowEarlyFinish() : -task.getWindowLateStart(); } public void setBegin(long begin) { if (forward) start = begin; else finish = -begin; } public void setEnd(long end) { if (forward) finish = end; else start = -end; } public long getBeginDependency() { if (dependencyDate == Dependency.NEEDS_CALCULATION) dependencyDate = calcDependencyDate(); return dependencyDate; } public final void invalidate() { if (task != null && task.isSubproject() && !((SubProj)task).isValidAndOpen()) // until they are open, don't touch subprojects return; start = 0; finish = 0; dependencyDate = Dependency.NEEDS_CALCULATION; } public void copyDatesAfterClone(TaskSchedule from) { start = from.start; finish = from.finish; dependencyDate = from.dependencyDate; } final void invalidateDependencyDate() { dependencyDate = Dependency.NEEDS_CALCULATION; } /** * @return Returns the percentComplete. */ public final double getPercentComplete() { return percentComplete; } /** * @param percentComplete The percentComplete to set. */ public final void setPercentComplete(double percentComplete) { this.percentComplete = percentComplete; } /** * @return Returns the rawDuration. */ public final long getRawDuration() { return rawDuration; } /** * @param rawDuration The rawDuration to set. */ public final void setRawDuration(long rawDuration) { this.rawDuration = rawDuration; } private final boolean isLate() { return type == LATE; } public final long getDependencyDate() { return dependencyDate; } public final void setDependencyDate(long dependencyDate) { this.dependencyDate = dependencyDate; } public final long getFinish() { return finish; } public final void setFinish(long finish) { this.finish = finish; } public final long getStart() { return start; } public final void setStart(long start) { this.start = start; } public TaskSchedule getOppositeSchedule() { return task.getSchedule(forward ? LATE : EARLY); } /** * @return Returns the forward. */ public final boolean isForward() { return forward; } public void setForward(boolean forward) { if (this.forward != forward) { this.forward = forward; long s = start; start = -finish; finish = -s; dependencyDate = -dependencyDate; remainingDependencyDate = -remainingDependencyDate; } } /** * @param begin */ public final void setRemainingDependencyDate(long remainingDependencyDate) { this.remainingDependencyDate = remainingDependencyDate; } /** * @return Returns the remainingDependencyDate. */ public final long getRemainingDependencyDate() { return remainingDependencyDate; } /** * Calculate a task's dates and see if the critical path changes. This means either: The task is currently critical, or the task becomes critical. * This function is useful in seeing whether a backward pass is necessary. The backward pass is only necessary when the CP is modified. * @param honorRequiredDates * @param boundary * @return */ final boolean affectsCriticalPath(CalculationContext context) { if (task.isOrWasCritical()) return true; calcStartAndFinish(context); // for parents, it will examine all children long newEnd = getEnd(); long oppositeEnd = -getOppositeSchedule().getBegin(); // System.out.println("Affects" + (oppositeEnd < newEnd) + " opposite " + new Date(oppositeEnd) + " new" + new Date(newEnd)); return (oppositeEnd < newEnd); } final void calcDates(CalculationContext context) { long oldBegin = getBegin(); long oldEnd = getEnd(); long newBegin = 0L; long newEnd=0L; final long needsCalculation =Dependency.NEEDS_CALCULATION; boolean unopenedSubproject = task.isSubproject() && !((SubProj)task).isValidAndOpen(); boolean external = task.isExternal(); if (!external && !unopenedSubproject) { if (context.taskReferenceType == PredecessorTaskList.TaskReference.PARENT_END) { assignDatesFromChildren(context); } else { calcStartAndFinish(context); // for parents, it will examine all children } newBegin = getBegin(); newEnd = getEnd(); boolean reverseScheduled = task.isReverseScheduled(); // if not just calculating early dates, check if reverse scheduled if (!context.earlyOnly && reverseScheduled) { TaskSchedule oppositeSchedule = getOppositeSchedule(); newBegin = -oppositeSchedule.getEnd(); newEnd = -oppositeSchedule.getBegin(); } if (context.assign && !unopenedSubproject) { TaskSchedule currentSchedule = task.getCurrentSchedule(); if (newBegin < 0) { currentSchedule.setStart(-newEnd); currentSchedule.setFinish(-newBegin); currentSchedule.setRemainingDependencyDate(-remainingDependencyDate); } else { currentSchedule.setStart(newBegin); currentSchedule.setFinish(newEnd); currentSchedule.setRemainingDependencyDate(remainingDependencyDate); } currentSchedule.setDependencyDate(dependencyDate); // for parents, set current schedule's duration if (context.taskReferenceType == PredecessorTaskList.TaskReference.PARENT_END) { //TODO this only needs to be done if advancement changed on a task currentSchedule.updateDurationFromDates(); // calculate duration based on parent start/end ((NormalTask)(currentSchedule.task)).assignActualDatesFromChildren(); } // System.out.println(task.getName() + " Set current " + new Date(currentSchedule.getStart()) + " " + new Date(currentSchedule.getFinish())); } } else if (external) { // external TaskSchedule currentSchedule = task.getCurrentSchedule(); newBegin = currentSchedule.getBegin(); newEnd = currentSchedule.getEnd(); oldBegin = 0; } if (oldBegin == newBegin && oldEnd == newEnd) { // System.out.println("no change"); if (!unopenedSubproject) return; } if (unopenedSubproject) { // need to put back old dates because we want the reverse pass to work right newBegin = oldBegin; newEnd = oldEnd; } Collection list = task.getDependencyList(!forward); Task parent = task.getWbsParentTask(); TaskSchedule parentSchedule = null; long parentEnd = 0; if (parent != null) { parentSchedule = parent.getSchedule(type); parentEnd = parentSchedule.getEnd(); } if (context.taskReferenceType == PredecessorTaskList.TaskReference.PARENT_BEGIN) { if (oldBegin != newBegin) { // if parent start (finish) changed, then all of its children need to me marked flagChildren(); setDependencyDate(newBegin); //This fixes a problem in incorrect propagation of constraints to children hk 16/8/05 // make sure that in second pass over this, the schedule will change so it will be marked in backward pass. However, we don't want to lose // information - specificially whether this task affects its parent's task. In case it does, it is marked with a special value (Dependency.NEEDS_CALCULATION) // This is a bit of a hack, but it's for optimization purposes. if (parentEnd == oldEnd) setEnd(needsCalculation); else setEnd(0); } return; } Dependency dependency; if (list.isEmpty()) { if (!task.isExternal() && task != context.sentinel) { // When the task is the sentinel, do nothing, otherwise find dependency and update it dependency = (Dependency) context.sentinel.getDependencyList(forward).find(forward,task); // find sentinel's dependency concerning this task if (dependency != null) { // tasks in a subproject won't have a sentinel dependency dependency.calcDependencyDate(forward,newBegin,newEnd,false); // calculate it to store off value context.sentinel.setCalculationStateCount(context.stateCount); // need to process successor(predecessor) later on in pass context.sentinel.getSchedule(type).setDependencyDate(needsCalculation); //sentinel needs dependencies calculated - I assume more than one } } } else { // Go Thru Successors (Predecessors) and calculate a dependency date for them and mark them for further treatment. There is an optimization here: // If the successor(pred) task only has one predecessor(succ), then just set its dependency date instead of calculating it. This avoids reprocessing // the predecessor(successor) list of that task later on. Since in most cases, a task has only one predecessor, this saves time. for (Iterator d = list.iterator(); d.hasNext();) { Task dependencyTask; TaskSchedule dependencyTaskSchedule; dependency = (Dependency) d.next(); if (dependency.isDisabled()) continue; dependencyTask = (Task) dependency.getTask(!forward); // get the successor(pred) task dependencyTaskSchedule = dependencyTask.getSchedule(type); // if this task is the only predecessor(successor) for the successor(predecessor) task, avoid long calculation and just calculate the date, otherwise // flag the task for later calculation long dependencyCount = dependencyTask.getDependencyList(forward).size(); long dep = newBegin; // by default (if no preds for example) if (dependencyCount > 0) { boolean useSooner = !dependencyTask.isWbsParent() && dependencyTask.hasDuration(); dep = dependency.calcDependencyDate(forward,newBegin,newEnd,useSooner); // calculate it and store off value if (dependencyCount > 1) // can't just set date directly because more than one dep = needsCalculation; // it will need to be calculated later } dependencyTaskSchedule.setDependencyDate(dep); dependencyTask.setCalculationStateCount(context.stateCount); // need to process successor(predecessor) later on in pass } } // mark parent also if it is affected and isn't marked already if (parent != null && parent.getCalculationStateCount() != context.stateCount) { long parentBegin = parentSchedule.getBegin(); if (oldEnd == 0 || oldEnd ==Dependency.NEEDS_CALCULATION) // in case a parent itself modifies its parent or this task has been invalidated (if it is the task modified) parent.setCalculationStateCount(context.stateCount); // mark its parent else if ( (oldEnd != newEnd && (newEnd > parentEnd || oldEnd == parentEnd)) // if this task previously determined the parent end date, the parent will need to be recalculated || (oldBegin != newBegin && ( newBegin < parentBegin || oldBegin == parentBegin))) { // if this task previously determined parent start date parent.setCalculationStateCount(context.stateCount); // mark parent } } if (context.pass == 1) // only mark during first pass. task.setCalculationStateCount(context.stateCount+1); // signal that backward pass needs to be done on this } private void flagChildren() { int stateCount = task.getCalculationStateCount(); Collection children = task.getWbsChildrenNodes(); if (children == null) return; Object current; Iterator i = children.iterator(); while (i.hasNext()) { current = ((Node) i.next()).getImpl(); if (! (current instanceof Task)) continue; ((Task) current).setCalculationStateCount(stateCount); // mark parent } } private void updateDurationFromDates() { setRawDuration(task.getEffectiveWorkCalendar().compare(getFinish(), getStart(),false)); } public void assignDatesFromChildren(CalculationContext context) { Collection children = task.getWbsChildrenNodes(); if (children == null) return; long begin = Long.MAX_VALUE; long end = Long.MIN_VALUE; Iterator i = children.iterator(); NormalTask child; Object current; TaskSchedule childSchedule; boolean estimated = false; int t = type; if (context != null && context.pass == 3) t = CURRENT; //System.out.println("assign from children top ass" + assign + " " + this); while (i.hasNext()) { current = ((Node) i.next()).getImpl(); if (! (current instanceof NormalTask)) continue; child = (NormalTask) current; estimated |= child.isEstimated(); // if (context != null && context.pass == 3 && child.isReverseScheduled()) { // // childSchedule = child.getSchedule(-type); // System.out.println("reverse " + child + " " + childSchedule); // } else childSchedule = child.getSchedule(t); // if (assign && child.isReverseScheduled()) // childSchedule = childSchedule.getOppositeSchedule(); long childBegin = childSchedule.getBegin(); if (childBegin != 0) begin = Math.min(begin,childBegin); long childEnd = childSchedule.getEnd(); if (childEnd != 0) end = Math.max(end,childEnd); } if (begin != Long.MAX_VALUE && begin != 0) // in case of invalid subproject having no children setBegin(begin); else { return; } if (end != Long.MIN_VALUE && end != 0) setEnd(end); else { return;// System.out.println("begin is " + new Date(begin) + " end " + new Date(end)); } long duration = task.getEffectiveWorkCalendar().compare(end,begin,false); duration = Duration.setAsEstimated(duration,estimated); ((NormalTask)task).setEstimated(estimated); setRawDuration(duration); } /** * Calculate the date which predecessors(successors) push this task to start by looping thru all of its predecesors(succ) and choosing the max value * @return max date */ long calcDependencyDate() { long result = 0; Dependency dependency; long current; Collection list = task.getDependencyList(forward); for (Iterator i = list.iterator(); i.hasNext();) { dependency = (Dependency) i.next(); if (dependency.isDisabled()) continue; current = dependency.getDate(forward); if (result == 0) result = current; else result = Math.max(result,current); } setDependencyDate(result); return result; } /** * During the forward pass, begin is early dates, during backward pass, it is late dates. * When reverse scheduling, the backward pass is executed first, then the forward. * The late schedule uses a trick: dates are returned as negative values. This lets me to use the same min/max code. Also * the calendar code knows to reverse durations when the date is negative. * @param boundary * @param honorRequiredDates * @param early * @return */ private void calcStartAndFinish(CalculationContext context) { long begin = getBeginDependency(); Task parent = task.getWbsParentTask(); //boolean useSooner = (forward != task.hasDuration()); // shortcut: if forward and is milestone, use sooner, otherwise later. And conversely, if reverse and isn't milestone, use sooner, othewise later boolean useSooner = !task.hasDuration(); if (parent != null) { TaskSchedule parentSchedule = parent.getSchedule(type); long parentDependency = parentSchedule.getBeginDependency(); long parentWindow = parentSchedule.getWindowBegin(); if (parentDependency == 0 || (parentWindow != 0 && parentWindow > parentDependency)) parentDependency = parentWindow; // in case where parent determines start time, make sure that this // task starts either at day end if milesetone, or at next working // day otherwise if parent is at day end if (parentDependency != 0 && (begin == 0 || parentDependency > begin)) { begin = task.getEffectiveWorkCalendar().add(parentDependency, 0, useSooner); } } if (task.isInSubproject()) begin = Math.max(begin,context.forward ? task.getOwningProject().getStartConstraint() : -task.getOwningProject().getEnd()); // Soft constraints long windowBegin = getWindowBegin(); // Make sure the task starts after its early start window date. This // is a soft constraint during forward pass. // For SNET if (windowBegin != 0) { if (begin == 0) begin = windowBegin; else if (windowBegin < begin) { if (task.startsBeforeProject()) // case of task starting before project start but has SNET constraint begin = windowBegin; } else begin = windowBegin; } // For FNET long windowEnd = getWindowEnd(); if (windowEnd != 0) { if (begin == 0) begin = Long.MIN_VALUE; begin = Math.max(begin, task.calcOffsetFrom(windowEnd,windowEnd,false, false, useSooner)); // System.out.println("Applying FNET " + task + " " + d(windowEnd) + " begin is now " + d(begin)); } // Hard constraints if (context.honorRequiredDates) { // If honoring required dates, check the hard constraint that the // task is finished by its late finish date. // Note that currently late finish has priority over early start. long oppositeEnd = -getOppositeSchedule().getWindowBegin(); // For FNLT if (oppositeEnd != 0) { if (begin == 0) begin = Long.MAX_VALUE; begin = Math.min(begin, task.calcOffsetFrom(oppositeEnd,dependencyDate,false, false, useSooner)); // System.out.println("Applying FNLT " + task + " " + d(oppositeEnd) + " begin is now " + d(begin)); } // For SNLT long oppositeBegin = -getOppositeSchedule().getWindowEnd(); if (oppositeBegin != 0) { if (begin == 0) begin = Long.MAX_VALUE; begin = Math.min(begin, oppositeBegin); // System.out.println("Applying SNLT " + task + " " + d(oppositeBegin) + " begin is now " + d(begin)); } } if (begin == 0) { if (!task.isWbsParent()) // if no constraints at all begin = context.boundary; } if (task.isSubproject()) {// subprojects can't start before their project start SubProj subProj = (SubProj)task; if (!subProj.isValidAndOpen()) return; if (task.getPredecessorList().size() == 0 && task.getConstraintDate() == 0) return; begin = Math.max(begin, context.forward ? subProj.getSubproject().getStartConstraint() : -subProj.getSubproject().getEnd()); } long levelingDelay = task.getLevelingDelay(); if (Duration.millis(levelingDelay) != 0) begin = task.getEffectiveWorkCalendar().add(begin,levelingDelay,useSooner); long remainingBegin = begin; if (forward == context.forward) setRemainingDependencyDate(remainingBegin); // the date which predecessors push the task to start at. Actuals can override this if (context.forward) { long actualStart = task.getActualStart(); if (actualStart != 0) begin = actualStart; } setBegin(begin); long end = ((Task)task).calcOffsetFrom(begin,remainingBegin,true, true, true); setEnd(end); } public void dump() { System.out.println("Task " + task + " schedule " + type + " start " + new Date(start) + " finish " + new Date(finish)); } public String toString() { return "Task " + task + " schedule " + type + " begin " + new Date(getBegin()) + " end " +new Date(getEnd()) + " start " + new Date(start) + " finish " + new Date(finish); } /** * Function used for tracing dates when debugging * @param l date either postive or negative * @return a date using the absolute value of the input */ public static Date d(long l) { return new Date(Math.abs(l)); } /** * Structure used to store variables related to the pass */ static class CalculationContext { int stateCount; boolean forward; boolean honorRequiredDates; Task sentinel; int taskReferenceType; long boundary; boolean earlyOnly; boolean assign; int scheduleType; int pass; public String toString() { return ToStringBuilder.reflectionToString(this); } } }