/* * This file is part of LibrePlan * * Copyright (C) 2010-2012 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.libreplan.web.dashboard; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.joda.time.Days; import org.joda.time.LocalDate; import org.libreplan.business.orders.entities.Order; import org.libreplan.business.planner.chart.ContiguousDaysLine; import org.libreplan.business.planner.chart.ContiguousDaysLine.OnDay; import org.libreplan.business.planner.entities.IOrderResourceLoadCalculator; import org.libreplan.business.planner.entities.TaskDeadlineViolationStatusEnum; import org.libreplan.business.planner.entities.TaskElement; import org.libreplan.business.planner.entities.TaskGroup; import org.libreplan.business.planner.entities.TaskStatusEnum; import org.libreplan.business.planner.entities.visitors.AccumulateTasksDeadlineStatusVisitor; import org.libreplan.business.planner.entities.visitors.AccumulateTasksStatusVisitor; import org.libreplan.business.planner.entities.visitors.CalculateFinishedTasksEstimationDeviationVisitor; import org.libreplan.business.planner.entities.visitors.CalculateFinishedTasksLagInCompletionVisitor; import org.libreplan.business.planner.entities.visitors.ResetTasksStatusVisitor; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.web.planner.order.PlanningStateCreator.PlanningState; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; /** * @author Nacho Barrientos <nacho@igalia.com> * @author Lorenzo Tilve Álvaro <ltilve@igalia.com> * @author Diego Pino García <dpino@igalia.com> * @author Manuel Rego Casasnovas <rego@igalia.com> * * Model for UI operations related to Order Dashboard View * */ @Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class DashboardModel implements IDashboardModel { @Autowired private IOrderResourceLoadCalculator resourceLoadCalculator; private Order currentOrder; private List<TaskElement> criticalPath; private Integer taskCount = null; private final Map<TaskStatusEnum, BigDecimal> taskStatusStats; private final Map<TaskDeadlineViolationStatusEnum, BigDecimal> taskDeadlineViolationStatusStats; private BigDecimal marginWithDeadLine; private Integer absoluteMarginWithDeadLine; public DashboardModel() { taskStatusStats = new EnumMap<>(TaskStatusEnum.class); taskDeadlineViolationStatusStats = new EnumMap<>(TaskDeadlineViolationStatusEnum.class); } @Override public void setCurrentOrder(PlanningState planningState, List<TaskElement> criticalPath) { final Order order = planningState.getOrder(); resourceLoadCalculator.setOrder(order, planningState.getAssignmentsCalculator()); this.currentOrder = order; this.criticalPath = criticalPath; this.taskCount = null; if ( tasksAvailable() ) { this.calculateGlobalProgress(); this.calculateTaskStatusStatistics(); this.calculateTaskViolationStatusStatistics(); this.calculateAbsoluteMarginWithDeadLine(); this.calculateMarginWithDeadLine(); } } /* Progress KPI: "Number of tasks by status" */ @Override public BigDecimal getPercentageOfFinishedTasks() { return taskStatusStats.get(TaskStatusEnum.FINISHED); } @Override public BigDecimal getPercentageOfInProgressTasks() { return taskStatusStats.get(TaskStatusEnum.IN_PROGRESS); } @Override public BigDecimal getPercentageOfReadyToStartTasks() { return taskStatusStats.get(TaskStatusEnum.READY_TO_START); } @Override public BigDecimal getPercentageOfBlockedTasks() { return taskStatusStats.get(TaskStatusEnum.BLOCKED); } /* Progress KPI: "Deadline violation" */ @Override public BigDecimal getPercentageOfOnScheduleTasks() { return taskDeadlineViolationStatusStats.get(TaskDeadlineViolationStatusEnum.ON_SCHEDULE); } @Override public BigDecimal getPercentageOfTasksWithViolatedDeadline() { return taskDeadlineViolationStatusStats.get(TaskDeadlineViolationStatusEnum.DEADLINE_VIOLATED); } @Override public BigDecimal getPercentageOfTasksWithNoDeadline() { return taskDeadlineViolationStatusStats.get(TaskDeadlineViolationStatusEnum.NO_DEADLINE); } /* Progress KPI: "Global Progress of the Project" */ private void calculateGlobalProgress() { TaskGroup rootTask = getRootTask(); if ( rootTask == null ) { throw new RuntimeException("Root task is null"); } rootTask.updateCriticalPathProgress(criticalPath); } @Override public BigDecimal getSpreadProgress() { return asPercentage(getRootTask().getAdvancePercentage()); } private BigDecimal asPercentage(BigDecimal value) { return value != null ? value.multiply(BigDecimal.valueOf(100)) : BigDecimal.ZERO; } @Override public BigDecimal getAdvancePercentageByHours() { return asPercentage(getRootTask().getProgressAllByNumHours()); } @Override public BigDecimal getExpectedAdvancePercentageByHours() { return asPercentage(getRootTask().getTheoreticalProgressByNumHoursForAllTasksUntilNow()); } @Override public BigDecimal getCriticalPathProgressByNumHours() { return asPercentage(getRootTask().getCriticalPathProgressByNumHours()); } @Override public BigDecimal getExpectedCriticalPathProgressByNumHours() { return asPercentage(getRootTask().getTheoreticalProgressByNumHoursForCriticalPathUntilNow()); } @Override public BigDecimal getCriticalPathProgressByDuration() { return asPercentage(getRootTask().getCriticalPathProgressByDuration()); } @Override public BigDecimal getExpectedCriticalPathProgressByDuration() { return asPercentage(getRootTask().getTheoreticalProgressByDurationForCriticalPathUntilNow()); } /* Time KPI: Margin with deadline */ @Override public BigDecimal getMarginWithDeadLine() { return this.marginWithDeadLine; } private void calculateMarginWithDeadLine() { if ( this.getRootTask() == null ) { throw new RuntimeException("Root task is null"); } if ( this.currentOrder.getDeadline() == null ) { this.marginWithDeadLine = null; return; } TaskGroup rootTask = getRootTask(); LocalDate endDate = TaskElement.maxDate(rootTask.getChildren()).asExclusiveEnd(); Days orderDuration = Days.daysBetween(TaskElement.minDate(rootTask.getChildren()).getDate(), endDate); LocalDate deadLineAsLocalDate = LocalDate.fromDateFields(currentOrder.getDeadline()); Days deadlineOffset = Days.daysBetween(endDate, deadLineAsLocalDate.plusDays(1)); BigDecimal outcome = new BigDecimal(deadlineOffset.getDays(), MathContext.DECIMAL32); this.marginWithDeadLine = outcome.divide(new BigDecimal(orderDuration.getDays()), 8, BigDecimal.ROUND_HALF_EVEN); } @Override public Integer getAbsoluteMarginWithDeadLine() { return absoluteMarginWithDeadLine; } private void calculateAbsoluteMarginWithDeadLine() { TaskElement rootTask = getRootTask(); Date deadline = currentOrder.getDeadline(); if ( rootTask == null ) { throw new RuntimeException("Root task is null"); } if ( deadline == null ) { this.absoluteMarginWithDeadLine = null; return; } absoluteMarginWithDeadLine = daysBetween( TaskElement.maxDate(rootTask.getChildren()).asExclusiveEnd(), LocalDate.fromDateFields(deadline).plusDays(1)); } private int daysBetween(LocalDate start, LocalDate end) { return Days.daysBetween(start, end).getDays(); } /** * Calculates the task completion deviations for the current order * * All the deviations are groups in 6 intervals of equal size. If the order * contains just one single task then, the upper limit will be the deviation * of the task +3, and the lower limit will be deviation of the task -3 * * Each {@link Interval} contains the number of tasks that fit in that * interval */ @Override public Map<Interval, Integer> calculateTaskCompletion() { List<Double> deviations = getTaskLagDeviations(); return calculateHistogramIntervals(deviations, 6, 1); } private List<Double> getTaskLagDeviations() { if ( this.getRootTask() == null ) { throw new RuntimeException("Root task is null"); } CalculateFinishedTasksLagInCompletionVisitor visitor = new CalculateFinishedTasksLagInCompletionVisitor(); TaskElement rootTask = getRootTask(); rootTask.acceptVisitor(visitor); return visitor.getDeviations(); } /** * Calculates the estimation accuracy deviations for the current order. * * All the deviations are groups in 6 intervals of equal size (not less than * 10). There're some restrictions: * <ul> * <li>If the order contains just one single task then, the upper limit will * be the deviation of the task +30, and the lower limit will be deviation * of the task -30.</li> * <li>If the difference between values is bigger than 60, then the * intervals will be bigger than 10 but it'll keep generating 6 intervals. * For example with min -45 and max +45, we'll have 6 intervals of size 15.</li> * <li>In the case that we have enough distance for, it doesn't need to set * the min to -30. For example, with min 0 and max 60, it'll keep intervals * of size 10.</li> * <li>If the min was 10 and the max 40, it'll have to decrease the min and * increase the max to get a difference of 60. For example setting min to * -10 and max to 50. (In order to calculate this it subtracts 10 to the min * and check if the difference is 60 again, if not it adds 10 to the max and * check it again, repeating this till it has a difference of 60).</li> * </ul> * * Each {@link Interval} contains the number of tasks that fit in that * interval. */ @Override public Map<Interval, Integer> calculateEstimationAccuracy() { List<Double> deviations = getEstimationAccuracyDeviations(); return calculateHistogramIntervals(deviations, 6, 10); } private Map<Interval, Integer> calculateHistogramIntervals(List<Double> values, int intervalsNumber, int intervalMinimumSize) { Map<Interval, Integer> result = new LinkedHashMap<>(); int totalMinimumSize = intervalsNumber * intervalMinimumSize; int halfSize = totalMinimumSize / 2; double maxDouble, minDouble; if ( values.isEmpty() ) { minDouble = -halfSize; maxDouble = halfSize; } else { minDouble = Collections.min(values); maxDouble = Collections.max(values); } // If min and max are between -halfSize and +halfSize, set -halfSize as // min and +halfSize as max if ( minDouble >= -halfSize && maxDouble <= halfSize ) { minDouble = -halfSize; maxDouble = halfSize; } // If the difference between min and max is less than totalMinimumSize, // decrease min while (maxDouble - minDouble < totalMinimumSize) { minDouble -= intervalMinimumSize; } // Round min and max properly depending on decimal part or not int min; double minDecimalPart = minDouble - (int) minDouble; if ( minDouble >= 0 ) { min = (int) (minDouble - minDecimalPart); } else { min = (int) (minDouble - minDecimalPart); if ( minDecimalPart != 0 ) { min--; } } int max; double maxDecimalPart = maxDouble - (int) maxDouble; if ( maxDouble >= 0 ) { max = (int) (maxDouble - maxDecimalPart); if ( maxDecimalPart != 0 ) { max++; } } else { max = (int) (maxDouble - maxDecimalPart); } // Calculate intervals size double delta = (double) (max - min) / intervalsNumber; double deltaDecimalPart = delta - (int) delta; // Generate intervals int from = min; for (int i = 0; i < intervalsNumber; i++) { int to = from + (int) delta; // Fix to depending on decimal part if it's not the last interval if ( deltaDecimalPart == 0 && i != (intervalsNumber - 1) ) { to--; } result.put(new Interval(from, to), 0); from = to + 1; } // Construct map with number of tasks for each interval final Set<Interval> intervals = result.keySet(); for (Double each : values) { Interval interval = Interval.containingValue(intervals, each); if ( interval != null ) { Integer value = result.get(interval); result.put(interval, value + 1); } } return result; } private List<Double> getEstimationAccuracyDeviations() { if ( this.getRootTask() == null ) { throw new RuntimeException("Root task is null"); } CalculateFinishedTasksEstimationDeviationVisitor visitor = new CalculateFinishedTasksEstimationDeviationVisitor(); TaskElement rootTask = getRootTask(); rootTask.acceptVisitor(visitor); return visitor.getDeviations(); } static class Interval { private int min; private int max; public Interval(int min, int max) { this.min = min; this.max = max; } public static Interval containingValue(Collection<Interval> intervals, double value) { for (Interval each : intervals) { if ( each.includes(value) ) { return each; } } return null; } private boolean includes(double value) { return (value >= min) && (value <= max); } @Override public String toString() { return "[" + min + ", " + max + "]"; } } @Override public Map<TaskStatusEnum, Integer> calculateTaskStatus() { AccumulateTasksStatusVisitor visitor = new AccumulateTasksStatusVisitor(); TaskElement rootTask = getRootTask(); if ( this.getRootTask() == null ) { throw new RuntimeException("Root task is null"); } resetTasksStatusInGraph(); rootTask.acceptVisitor(visitor); return visitor.getTaskStatusData(); } private void calculateTaskStatusStatistics() { AccumulateTasksStatusVisitor visitor = new AccumulateTasksStatusVisitor(); TaskElement rootTask = getRootTask(); if ( this.getRootTask() == null ) { throw new RuntimeException("Root task is null"); } resetTasksStatusInGraph(); rootTask.acceptVisitor(visitor); Map<TaskStatusEnum, Integer> count = visitor.getTaskStatusData(); mapAbsoluteValuesToPercentages(count, taskStatusStats); } private void calculateTaskViolationStatusStatistics() { AccumulateTasksDeadlineStatusVisitor visitor = new AccumulateTasksDeadlineStatusVisitor(); TaskElement rootTask = getRootTask(); if ( this.getRootTask() == null ) { throw new RuntimeException("Root task is null"); } rootTask.acceptVisitor(visitor); Map<TaskDeadlineViolationStatusEnum, Integer> count = visitor.getTaskDeadlineViolationStatusData(); mapAbsoluteValuesToPercentages(count, taskDeadlineViolationStatusStats); } private <T> void mapAbsoluteValuesToPercentages(Map<T, Integer> source, Map<T, BigDecimal> dest) { int totalTasks = countTasksInAResultMap(source); for (Map.Entry<T, Integer> entry : source.entrySet()) { BigDecimal percentage; if ( totalTasks == 0 ) { percentage = BigDecimal.ZERO; } else { percentage = new BigDecimal( 100 * (entry.getValue() / (1.0 * totalTasks)), MathContext.DECIMAL32); } dest.put(entry.getKey(), percentage); } } private TaskGroup getRootTask() { return currentOrder.getAssociatedTaskElement(); } private void resetTasksStatusInGraph() { ResetTasksStatusVisitor visitor = new ResetTasksStatusVisitor(); getRootTask().acceptVisitor(visitor); } private int countTasksInAResultMap(Map<? extends Object, Integer> map) { // It's only needed to count the number of tasks once each time setOrder is called. if ( this.taskCount != null ) { return this.taskCount.intValue(); } int sum = 0; for (Object count : map.values()) { sum += (Integer) count; } this.taskCount = sum; return sum; } @Override public boolean tasksAvailable() { return getRootTask() != null; } @Override public BigDecimal getOvertimeRatio() { EffortDuration totalLoad = sumAll(resourceLoadCalculator.getAllLoad()); EffortDuration overload = sumAll(resourceLoadCalculator.getAllOverload()); return overload.dividedByAndResultAsBigDecimal(totalLoad).setScale(2, RoundingMode.HALF_UP); } private EffortDuration sumAll(ContiguousDaysLine<EffortDuration> contiguousDays) { EffortDuration result = EffortDuration.zero(); Iterator<OnDay<EffortDuration>> iterator = contiguousDays.iterator(); while (iterator.hasNext()) { OnDay<EffortDuration> value = iterator.next(); EffortDuration effort = value.getValue(); result = EffortDuration.sum(result, effort); } return result; } @Override public BigDecimal getAvailabilityRatio() { EffortDuration totalLoad = sumAll(resourceLoadCalculator.getAllLoad()); EffortDuration overload = sumAll(resourceLoadCalculator.getAllOverload()); EffortDuration load = totalLoad.minus(overload); EffortDuration capacity = sumAll(resourceLoadCalculator.getMaxCapacityOnResources()); return BigDecimal.ONE.setScale(2, RoundingMode.HALF_UP).subtract(load.dividedByAndResultAsBigDecimal(capacity)); } }