/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 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.business.planner.limiting.entities; import static org.libreplan.business.workingday.EffortDuration.hours; import static org.libreplan.business.workingday.EffortDuration.min; import static org.libreplan.business.workingday.EffortDuration.zero; import java.math.BigDecimal; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import org.joda.time.LocalDate; import org.libreplan.business.calendars.entities.ResourceCalendar; import org.libreplan.business.planner.entities.DayAssignment; import org.libreplan.business.planner.entities.GenericDayAssignment; import org.libreplan.business.planner.entities.GenericResourceAllocation; import org.libreplan.business.planner.entities.ResourceAllocation; import org.libreplan.business.planner.entities.SpecificDayAssignment; import org.libreplan.business.planner.entities.SpecificResourceAllocation; import org.libreplan.business.resources.entities.LimitingResourceQueue; import org.libreplan.business.resources.entities.Resource; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.business.workingday.IntraDayDate; import org.libreplan.business.workingday.IntraDayDate.PartialDay; import org.libreplan.business.workingday.IntraDayDate.UntilEnd; import org.libreplan.business.workingday.ResourcesPerDay; /** * Handles all the logic related to allocation of * {@link LimitingResourceQueueElement} into {@link LimitingResourceQueue}. * * The class does not do the allocation itself but provides methods * needed to do the allocation of {@link LimitingResourceQueueElement}: * <ul> * <li><em>getFirstValidGap</em></li> * <li><em>calculateStartAndEndTime</em></li> * <li><em>generateDayAssignments</em></li> * </ul> * * * @author Diego Pino Garcia <dpino@igalia.com> */ public class LimitingResourceAllocator { private static final class UntilEndAndEffort extends UntilEnd { private final EffortDuration goal; private EffortDuration currentDuration = zero(); private UntilEndAndEffort(IntraDayDate endExclusive, EffortDuration goal) { super(endExclusive); this.goal = goal; } @Override protected boolean hasNext(boolean currentDateIsLessThanEnd) { return currentDateIsLessThanEnd || currentDuration.compareTo(goal) <= 0; } public void setCurrent(EffortDuration currentDuration) { this.currentDuration = currentDuration; } @Override public IntraDayDate limitNext(IntraDayDate nextDay) { return nextDay; } } private final static ResourcesPerDay ONE_RESOURCE_PER_DAY = ResourcesPerDay.amount(new BigDecimal(1)); /** * Returns first valid gap in queue for element. * * Returns null if there is not a valid gap. * This case can only happen on trying to allocate an element related to a generic resource allocation. * It is possible that queue.resource does not hold element.criteria at any interval of time. * * @param queue search gap inside queue * @param element element to fit into queue * @return {@link Gap} */ public static Gap getFirstValidGap(LimitingResourceQueue queue, LimitingResourceQueueElement element) { final Resource resource = queue.getResource(); final List<LimitingResourceQueueElement> elements = new LinkedList<>(queue.getLimitingResourceQueueElements()); final int size = elements.size(); final DateAndHour startTime = getStartTimeBecauseOfGantt(element); int pos = 0; // Iterate through queue elements while (pos <= size) { Gap gap = getGapInQueueAtPosition(resource, elements, startTime, pos++); if ( gap != null ) { List<Gap> subgaps = getFittingSubgaps(element, gap, resource); if ( !subgaps.isEmpty() ) { return subgaps.get(0); } } } // The queue cannot hold this element (queue.resource doesn't meet element.criteria) return null; } private static List<Gap> getFittingSubgaps(LimitingResourceQueueElement element, final Gap gap, final Resource resource) { List<Gap> result = new ArrayList<>(); if ( isSpecific(element) && gap.canFit(element) ) { result.add(gap); } else if ( isGeneric(element) ) { final List<Gap> gaps = gap.splitIntoGapsSatisfyingCriteria(resource, element.getCriteria()); for (Gap subgap : gaps) { if ( subgap.canFit(element) ) { result.add(subgap); } } } return result; } public static Gap getFirstValidGapSince(LimitingResourceQueueElement element, LimitingResourceQueue queue, DateAndHour since) { List<Gap> gaps = getValidGapsForElementSince(element, queue, since); return (!gaps.isEmpty()) ? gaps.get(0) : null; } public static List<Gap> getValidGapsForElementSince(LimitingResourceQueueElement element, LimitingResourceQueue queue, DateAndHour since) { List<Gap> result = new ArrayList<>(); final Resource resource = queue.getResource(); final List<LimitingResourceQueueElement> elements = new LinkedList<>(queue.getLimitingResourceQueueElements()); final int size = elements.size(); int pos = moveUntil(elements, since); // Iterate through queue elements while (pos <= size) { Gap gap = getGapInQueueAtPosition( resource, elements, since, pos++); // The queue cannot hold this element (queue.resource doesn't meet element.criteria) if ( gap != null ) { List<Gap> subgaps = getFittingSubgaps(element, gap, resource); result.addAll(subgaps); } } return result; } private static int moveUntil(List<LimitingResourceQueueElement> elements, DateAndHour until) { int pos = 0; if ( elements.size() > 0 ) { // Space between until and first element start time LimitingResourceQueueElement first = elements.get(0); if (until.isBefore(first.getStartTime())) { return 0; } for (pos = 1; pos < elements.size(); pos++) { final LimitingResourceQueueElement each = elements.get(pos); final DateAndHour startTime = each.getStartTime(); if ( until.isBefore(startTime) || until.isEquals(startTime) ) { return pos; } } } return pos; } private static boolean isGeneric(LimitingResourceQueueElement element) { return element.getResourceAllocation() instanceof GenericResourceAllocation; } private static boolean isSpecific(LimitingResourceQueueElement element) { return element.getResourceAllocation() instanceof SpecificResourceAllocation; } public static DateAndHour getFirstElementTime(List<DayAssignment> dayAssignments) { final DayAssignment start = dayAssignments.get(0); return new DateAndHour(start.getDay(), start.getDuration().getHours()); } public static DateAndHour getLastElementTime(List<DayAssignment> dayAssignments) { final DayAssignment end = dayAssignments.get(dayAssignments.size() - 1); return new DateAndHour(end.getDay(), end.getDuration().getHours()); } private static Gap getGapInQueueAtPosition(Resource resource, List<LimitingResourceQueueElement> elements, DateAndHour startTimeBecauseOfGantt, int pos) { final int size = elements.size(); // No elements in queue if ( size == 0 ) { return createLastGap(startTimeBecauseOfGantt, null, resource); } // Last element if ( pos == size ) { return createLastGap(startTimeBecauseOfGantt, elements.get(size - 1), resource); } LimitingResourceQueueElement next = elements.get(pos); // First element if ( pos == 0 && startTimeBecauseOfGantt.getDate().isBefore(next.getStartDate()) ) { return Gap.create(resource, startTimeBecauseOfGantt, next.getStartTime()); } // In the middle of two elements if (pos > 0) { LimitingResourceQueueElement previous = elements.get(pos - 1); return Gap.create(resource, DateAndHour.max(previous.getEndTime(), startTimeBecauseOfGantt), next.getStartTime()); } return null; } private static DateAndHour getStartTimeBecauseOfGantt(LimitingResourceQueueElement element) { return new DateAndHour(new LocalDate(element.getEarliestStartDateBecauseOfGantt()), 0); } private static Gap createLastGap(DateAndHour _startTime, LimitingResourceQueueElement lastElement, Resource resource) { final DateAndHour queueEndTime = (lastElement != null) ? lastElement.getEndTime() : null; DateAndHour startTime = DateAndHour.max(_startTime, queueEndTime); return Gap.create(resource, startTime, null); } /** * Generates a list of {@link DayAssignment} for {@link Resource} starting from startTime. * * The returned list is not associated to resourceAllocation. * * resourceAllocation is passed to know if the list of day assignments * should be {@link GenericDayAssignment} or {@link SpecificDayAssignment}. * * @param resourceAllocation * @param resource * @param startTime * @return {@link List<DayAssignment>} */ public static List<DayAssignment> generateDayAssignments(ResourceAllocation<?> resourceAllocation, Resource resource, DateAndHour startTime, DateAndHour endsAfter) { List<DayAssignment> assignments = new LinkedList<>(); ResourceCalendar calendar = resource.getCalendar(); final EffortDuration totalEffort = hours(resourceAllocation.getIntendedTotalHours()); EffortDuration effortAssigned = zero(); UntilEndAndEffort condition = new UntilEndAndEffort(endsAfter.toIntraDayDate(), totalEffort); for (PartialDay each : startTime.toIntraDayDate().daysUntil(condition)) { EffortDuration effortForDay = EffortDuration.min(calendar.asDurationOn(each, ONE_RESOURCE_PER_DAY), totalEffort); DayAssignment dayAssignment = createDayAssignment(resourceAllocation, resource, each.getDate(), effortForDay); effortAssigned = effortAssigned.plus(addDayAssignment(assignments, dayAssignment)); condition.setCurrent(effortAssigned); } if ( effortAssigned.compareTo(totalEffort) > 0 ) { stripStartAssignments(assignments, effortAssigned.minus(totalEffort)); } return new ArrayList<>(assignments); } private static void stripStartAssignments(List<DayAssignment> assignments, EffortDuration durationSurplus) { ListIterator<DayAssignment> listIterator = assignments.listIterator(); while (listIterator.hasNext() && durationSurplus.compareTo(zero()) > 0) { DayAssignment current = listIterator.next(); EffortDuration durationTaken = min(durationSurplus, current.getDuration()); durationSurplus = durationSurplus.minus(durationTaken); if ( durationTaken.compareTo(current.getDuration()) == 0 ) { listIterator.remove(); } else { listIterator.set(current.withDuration(durationTaken)); } } } private static List<DayAssignment> generateDayAssignmentsStartingFromEnd(ResourceAllocation<?> resourceAllocation, Resource resource, DateAndHour endTime) { ResourceCalendar calendar = resource.getCalendar(); List<DayAssignment> result = new ArrayList<>(); LocalDate date = endTime.getDate(); EffortDuration totalIntended = hours(resourceAllocation.getIntendedTotalHours()); // Generate last day assignment PartialDay firstDay = new PartialDay(IntraDayDate.startOfDay(date), IntraDayDate.create(date, hours(endTime.getHour()))); EffortDuration effortCanAllocate = min(totalIntended, calendar.asDurationOn(firstDay, ONE_RESOURCE_PER_DAY)); if (effortCanAllocate.compareTo(zero()) > 0) { DayAssignment dayAssignment = createDayAssignment(resourceAllocation, resource, date, effortCanAllocate); totalIntended = totalIntended.minus(addDayAssignment(result, dayAssignment)); } // Generate rest of day assignments for (date = date.minusDays(1); totalIntended.compareTo(zero()) > 0; date = date.minusDays(1)) { EffortDuration duration = min(totalIntended, calendar.asDurationOn(PartialDay.wholeDay(date), ONE_RESOURCE_PER_DAY)); DayAssignment dayAssignment = createDayAssignment(resourceAllocation, resource, date, duration); totalIntended = totalIntended.minus(addDayAssignment(result, dayAssignment)); } return result; } private static DayAssignment createDayAssignment(ResourceAllocation<?> resourceAllocation, Resource resource, LocalDate date, EffortDuration toAllocate) { if ( resourceAllocation instanceof SpecificResourceAllocation ) { return SpecificDayAssignment.create(date, toAllocate, resource); } else if ( resourceAllocation instanceof GenericResourceAllocation ) { return GenericDayAssignment.create(date, toAllocate, resource); } return null; } private static EffortDuration addDayAssignment(List<DayAssignment> list, DayAssignment dayAssignment) { if ( dayAssignment != null ) { list.add(dayAssignment); return dayAssignment.getDuration(); } return zero(); } public static DateAndHour startTimeToAllocateStartingFromEnd(ResourceAllocation<?> resourceAllocation, Resource resource, Gap gap) { // Last element, time is end of last element (gap.starttime) if ( gap.getEndTime() == null ) { return gap.getStartTime(); } final List<DayAssignment> dayAssignments = LimitingResourceAllocator .generateDayAssignmentsStartingFromEnd(resourceAllocation, resource, gap.getEndTime()); return LimitingResourceAllocator.getLastElementTime(dayAssignments); } }