/*
* 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);
}
}