/* * file: TimephasedUtility.java * author: Jon Iles * copyright: (c) Packwood Software 2011 * date: 2011-02-12 */ /* * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation; either version 2.1 of the License, or (at your * option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package net.sf.mpxj.utility; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import net.sf.mpxj.DateRange; import net.sf.mpxj.Duration; import net.sf.mpxj.ProjectCalendar; import net.sf.mpxj.ProjectFile; import net.sf.mpxj.TimeUnit; import net.sf.mpxj.TimephasedCost; import net.sf.mpxj.TimephasedItem; import net.sf.mpxj.TimephasedWork; import net.sf.mpxj.common.DateHelper; import net.sf.mpxj.common.NumberHelper; import net.sf.mpxj.mpp.TimescaleUnits; /** * This class contains methods relating to manipulating timephased data. */ public final class TimephasedUtility { /** * This is the main entry point used to convert the internal representation * of timephased work into an external form which can * be displayed to the user. * * @param projectCalendar calendar used by the resource assignment * @param work timephased resource assignment data * @param rangeUnits timescale units * @param dateList timescale date ranges * @return list of durations, one per timescale date range */ public ArrayList<Duration> segmentWork(ProjectCalendar projectCalendar, List<TimephasedWork> work, TimescaleUnits rangeUnits, ArrayList<DateRange> dateList) { ArrayList<Duration> result = new ArrayList<Duration>(dateList.size()); int lastStartIndex = 0; // // Iterate through the list of dates range we are interested in. // Each date range in this list corresponds to a column // shown on the "timescale" view by MS Project // for (DateRange range : dateList) { // // If the current date range does not intersect with any of the // assignment date ranges in the list, then we show a zero // duration for this date range. // int startIndex = lastStartIndex == -1 ? -1 : getStartIndex(range, work, lastStartIndex); if (startIndex == -1) { result.add(Duration.getInstance(0, TimeUnit.HOURS)); } else { // // We have found an assignment which intersects with the current // date range, call the method below to determine how // much time from this resource assignment can be allocated // to the current date range. // result.add(getRangeDuration(projectCalendar, rangeUnits, range, work, startIndex)); lastStartIndex = startIndex; } } return result; } /** * This is the main entry point used to convert the internal representation * of timephased baseline work into an external form which can * be displayed to the user. * * @param file parent project file * @param work timephased resource assignment data * @param rangeUnits timescale units * @param dateList timescale date ranges * @return list of durations, one per timescale date range */ public ArrayList<Duration> segmentBaselineWork(ProjectFile file, List<TimephasedWork> work, TimescaleUnits rangeUnits, ArrayList<DateRange> dateList) { return segmentWork(file.getBaselineCalendar(), work, rangeUnits, dateList); } /** * This is the main entry point used to convert the internal representation * of timephased cost into an external form which can * be displayed to the user. * * @param projectCalendar calendar used by the resource assignment * @param cost timephased resource assignment data * @param rangeUnits timescale units * @param dateList timescale date ranges * @return list of durations, one per timescale date range */ public ArrayList<Double> segmentCost(ProjectCalendar projectCalendar, List<TimephasedCost> cost, TimescaleUnits rangeUnits, ArrayList<DateRange> dateList) { ArrayList<Double> result = new ArrayList<Double>(dateList.size()); int lastStartIndex = 0; // // Iterate through the list of dates range we are interested in. // Each date range in this list corresponds to a column // shown on the "timescale" view by MS Project // for (DateRange range : dateList) { // // If the current date range does not intersect with any of the // assignment date ranges in the list, then we show a zero // duration for this date range. // int startIndex = lastStartIndex == -1 ? -1 : getStartIndex(range, cost, lastStartIndex); if (startIndex == -1) { result.add(NumberHelper.DOUBLE_ZERO); } else { // // We have found an assignment which intersects with the current // date range, call the method below to determine how // much time from this resource assignment can be allocated // to the current date range. // result.add(getRangeCost(projectCalendar, rangeUnits, range, cost, startIndex)); lastStartIndex = startIndex; } } return result; } /** * This is the main entry point used to convert the internal representation * of timephased baseline cost into an external form which can * be displayed to the user. * * @param file parent project file * @param cost timephased resource assignment data * @param rangeUnits timescale units * @param dateList timescale date ranges * @return list of durations, one per timescale date range */ public ArrayList<Double> segmentBaselineCost(ProjectFile file, List<TimephasedCost> cost, TimescaleUnits rangeUnits, ArrayList<DateRange> dateList) { return segmentCost(file.getBaselineCalendar(), cost, rangeUnits, dateList); } /** * Used to locate the first timephased resource assignment block which * intersects with the target date range. * * @param <T> payload type * @param range target date range * @param assignments timephased resource assignments * @param startIndex index at which to start the search * @return index of timephased resource assignment which intersects with the target date range */ private <T extends TimephasedItem<?>> int getStartIndex(DateRange range, List<T> assignments, int startIndex) { int result = -1; if (assignments != null) { long rangeStart = range.getStart().getTime(); long rangeEnd = range.getEnd().getTime(); for (int loop = startIndex; loop < assignments.size(); loop++) { T assignment = assignments.get(loop); int compareResult = DateHelper.compare(assignment.getStart(), assignment.getFinish(), rangeStart); // // The start of the target range falls after the assignment end - // move on to test the next assignment. // if (compareResult > 0) { continue; } // // The start of the target range falls within the assignment - // return the index of this assignment to the caller. // if (compareResult == 0) { result = loop; break; } // // At this point, we know that the start of the target range is before // the assignment start. We need to determine if the end of the // target range overlaps the assignment. // compareResult = DateHelper.compare(assignment.getStart(), assignment.getFinish(), rangeEnd); if (compareResult >= 0) { result = loop; break; } } } return result; } /** * For a given date range, determine the duration of work, based on the * timephased resource assignment data. * * @param projectCalendar calendar used for the resource assignment calendar * @param rangeUnits timescale units * @param range target date range * @param assignments timephased resource assignments * @param startIndex index at which to start searching through the timephased resource assignments * @return work duration */ private Duration getRangeDuration(ProjectCalendar projectCalendar, TimescaleUnits rangeUnits, DateRange range, List<TimephasedWork> assignments, int startIndex) { Duration result; switch (rangeUnits) { case MINUTES: case HOURS: { result = getRangeDurationSubDay(projectCalendar, rangeUnits, range, assignments, startIndex); break; } default: { result = getRangeDurationWholeDay(projectCalendar, rangeUnits, range, assignments, startIndex); break; } } return result; } /** * For a given date range, determine the duration of work, based on the * timephased resource assignment data. * * This method deals with timescale units of less than a day. * * @param projectCalendar calendar used for the resource assignment calendar * @param rangeUnits timescale units * @param range target date range * @param assignments timephased resource assignments * @param startIndex index at which to start searching through the timephased resource assignments * @return work duration */ private Duration getRangeDurationSubDay(ProjectCalendar projectCalendar, TimescaleUnits rangeUnits, DateRange range, List<TimephasedWork> assignments, int startIndex) { throw new UnsupportedOperationException("Please request this functionality from the MPXJ maintainer"); } /** * For a given date range, determine the duration of work, based on the * timephased resource assignment data. * * This method deals with timescale units of one day or more. * * @param projectCalendar calendar used for the resource assignment calendar * @param rangeUnits timescale units * @param range target date range * @param assignments timephased resource assignments * @param startIndex index at which to start searching through the timephased resource assignments * @return work duration */ private Duration getRangeDurationWholeDay(ProjectCalendar projectCalendar, TimescaleUnits rangeUnits, DateRange range, List<TimephasedWork> assignments, int startIndex) { // option 1: // Our date range starts before the start of the TRA at the start index. // We can guarantee that we don't need to look at any earlier TRA blocks so just start here // option 2: // Our date range starts at the same point as the first TRA: do nothing... // option 3: // Our date range starts somewhere inside the first TRA... // if it's option 1 just set the start date to the start of the TRA block // for everything else we just use the start date of our date range. // start counting forwards one day at a time until we reach the end of // the date range, or until we reach the end of the block. // if we have not reached the end of the range, move to the next block and // see if the date range overlaps it. if it does not overlap, then we're // done. // if it does overlap, then move to the next block and repeat int totalDays = 0; double totalWork = 0; TimephasedWork assignment = assignments.get(startIndex); boolean done = false; do { // // Select the correct start date // long startDate = range.getStart().getTime(); long assignmentStart = assignment.getStart().getTime(); if (startDate < assignmentStart) { startDate = assignmentStart; } long rangeEndDate = range.getEnd().getTime(); long traEndDate = assignment.getFinish().getTime(); Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(startDate); Date calendarDate = cal.getTime(); // // Start counting forwards // while (startDate < rangeEndDate && startDate < traEndDate) { if (projectCalendar == null || projectCalendar.isWorkingDate(calendarDate)) { ++totalDays; } cal.add(Calendar.DAY_OF_YEAR, 1); startDate = cal.getTimeInMillis(); calendarDate = cal.getTime(); } // // If we still haven't reached the end of our range // check to see if the next TRA can be used. // done = true; totalWork += (assignment.getAmountPerDay().getDuration() * totalDays); if (startDate < rangeEndDate) { ++startIndex; if (startIndex < assignments.size()) { assignment = assignments.get(startIndex); totalDays = 0; done = false; } } } while (!done); return Duration.getInstance(totalWork, assignment.getAmountPerDay().getUnits()); } /** * For a given date range, determine the cost, based on the * timephased resource assignment data. * * @param projectCalendar calendar used for the resource assignment calendar * @param rangeUnits timescale units * @param range target date range * @param assignments timephased resource assignments * @param startIndex index at which to start searching through the timephased resource assignments * @return work duration */ private Double getRangeCost(ProjectCalendar projectCalendar, TimescaleUnits rangeUnits, DateRange range, List<TimephasedCost> assignments, int startIndex) { Double result; switch (rangeUnits) { case MINUTES: case HOURS: { result = getRangeCostSubDay(projectCalendar, rangeUnits, range, assignments, startIndex); break; } default: { result = getRangeCostWholeDay(projectCalendar, rangeUnits, range, assignments, startIndex); break; } } return result; } /** * For a given date range, determine the cost, based on the * timephased resource assignment data. * * This method deals with timescale units of one day or more. * * @param projectCalendar calendar used for the resource assignment calendar * @param rangeUnits timescale units * @param range target date range * @param assignments timephased resource assignments * @param startIndex index at which to start searching through the timephased resource assignments * @return work duration */ private Double getRangeCostWholeDay(ProjectCalendar projectCalendar, TimescaleUnits rangeUnits, DateRange range, List<TimephasedCost> assignments, int startIndex) { int totalDays = 0; double totalCost = 0; TimephasedCost assignment = assignments.get(startIndex); boolean done = false; do { // // Select the correct start date // long startDate = range.getStart().getTime(); long assignmentStart = assignment.getStart().getTime(); if (startDate < assignmentStart) { startDate = assignmentStart; } long rangeEndDate = range.getEnd().getTime(); long traEndDate = assignment.getFinish().getTime(); Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(startDate); Date calendarDate = cal.getTime(); // // Start counting forwards // while (startDate < rangeEndDate && startDate < traEndDate) { if (projectCalendar == null || projectCalendar.isWorkingDate(calendarDate)) { ++totalDays; } cal.add(Calendar.DAY_OF_YEAR, 1); startDate = cal.getTimeInMillis(); calendarDate = cal.getTime(); } // // If we still haven't reached the end of our range // check to see if the next TRA can be used. // done = true; totalCost += (assignment.getAmountPerDay().doubleValue() * totalDays); if (startDate < rangeEndDate) { ++startIndex; if (startIndex < assignments.size()) { assignment = assignments.get(startIndex); totalDays = 0; done = false; } } } while (!done); return Double.valueOf(totalCost); } /** * For a given date range, determine the cost, based on the * timephased resource assignment data. * * This method deals with timescale units of less than a day. * * @param projectCalendar calendar used for the resource assignment calendar * @param rangeUnits timescale units * @param range target date range * @param assignments timephased resource assignments * @param startIndex index at which to start searching through the timephased resource assignments * @return work duration */ private Double getRangeCostSubDay(ProjectCalendar projectCalendar, TimescaleUnits rangeUnits, DateRange range, List<TimephasedCost> assignments, int startIndex) { throw new UnsupportedOperationException("Please request this functionality from the MPXJ maintainer"); } }