/* * 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.entities; import static org.libreplan.business.workingday.EffortDuration.min; import static org.libreplan.business.workingday.EffortDuration.seconds; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.collections4.ComparatorUtils; import org.apache.commons.lang3.Validate; import org.joda.time.LocalDate; import org.libreplan.business.calendars.entities.Capacity; import org.libreplan.business.calendars.entities.ICalendar; import org.libreplan.business.calendars.entities.ResourceCalendar; import org.libreplan.business.calendars.entities.SameWorkHoursEveryDay; import org.libreplan.business.planner.entities.AssignedEffortForResource.IAssignedEffortForResource; import org.libreplan.business.resources.entities.Resource; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.business.workingday.IntraDayDate.PartialDay; import org.libreplan.business.workingday.ResourcesPerDay; /** * @author Óscar González Fernández <ogonzalez@igalia.com> */ public class EffortDistributor { public interface IResourceSelector { boolean isSelectable(Resource resource, LocalDate day); } private static class CompoundSelector implements IResourceSelector { private List<IResourceSelector> selectors; public CompoundSelector(IResourceSelector... selectors) { Validate.noNullElements(selectors); this.selectors = Arrays.asList(selectors); } @Override public boolean isSelectable(Resource resource, LocalDate day) { for (IResourceSelector each : selectors) { if (!each.isSelectable(resource, day)) { return false; } } return true; } } private static class OnlyCanWork implements IResourceSelector { @Override public boolean isSelectable(Resource resource, LocalDate day) { ResourceCalendar resourceCalendar = resource.getCalendar(); return resourceCalendar == null || resourceCalendar.canWorkOn(day); } } public static class ResourceWithAssignedDuration { public final EffortDuration duration; public final Resource resource; private ResourceWithAssignedDuration(EffortDuration duration, Resource resource) { Validate.notNull(duration); Validate.notNull(resource); this.duration = duration; this.resource = resource; } public static EffortDuration sumDurations(List<ResourceWithAssignedDuration> withoutOvertime) { EffortDuration result = EffortDuration.zero(); for (ResourceWithAssignedDuration each : withoutOvertime) { result = result.plus(each.duration); } return result; } static List<Resource> resources(Collection<? extends ResourceWithAssignedDuration> collection) { List<Resource> result = new ArrayList<>(); for (ResourceWithAssignedDuration each : collection) { result.add(each.resource); } return result; } static Map<Resource, ResourceWithAssignedDuration> byResource( Collection<? extends ResourceWithAssignedDuration> durations) { Map<Resource, ResourceWithAssignedDuration> result = new HashMap<>(); for (ResourceWithAssignedDuration each : durations) { result.put(each.resource, each); } return result; } public static IAssignedEffortForResource sumAssignedEffort( List<ResourceWithAssignedDuration> durations, final IAssignedEffortForResource assignedEffortForResource) { final Map<Resource, ResourceWithAssignedDuration> byResource = byResource(durations); return new IAssignedEffortForResource() { @Override public EffortDuration getAssignedDurationAt(Resource resource, LocalDate day) { EffortDuration previouslyAssigned = assignedEffortForResource.getAssignedDurationAt(resource, day); ResourceWithAssignedDuration withDuration = byResource.get(resource); if (withDuration != null) { return previouslyAssigned.plus(withDuration.duration); } return previouslyAssigned; } }; } public static List<ResourceWithAssignedDuration> join( Collection<? extends ResourceWithAssignedDuration> a, Collection<ResourceWithAssignedDuration> b) { Map<Resource, ResourceWithAssignedDuration> result = byResource(a); Map<Resource, ResourceWithAssignedDuration> byResource = byResource(b); for (Entry<Resource, ResourceWithAssignedDuration> each : byResource.entrySet()) { Resource key = each.getKey(); ResourceWithAssignedDuration value = each.getValue(); if (result.containsKey(key)) { result.put(key, result.get(key).plus(value)); } else { result.put(key, value); } } return new ArrayList<>(result.values()); } ResourceWithAssignedDuration plus(ResourceWithAssignedDuration value) { return new ResourceWithAssignedDuration(this.duration.plus(value.duration), resource); } } private static final ICalendar generateCalendarFor(Resource resource) { if (resource.getCalendar() != null) { return resource.getCalendar(); } else { return SameWorkHoursEveryDay.getDefaultWorkingDay(); } } private static int getCapacityFor(Resource resource) { if (resource.getCalendar() != null) { return resource.getCalendar().getCapacity(); } else { return 1; } } private static class ResourceWithDerivedData { public static List<ResourceWithDerivedData> from(List<Resource> resources) { List<ResourceWithDerivedData> result = new ArrayList<>(); for (Resource each : resources) { result.add(new ResourceWithDerivedData(each)); } return result; } public static List<Resource> resources(List<ResourceWithDerivedData> resources) { List<Resource> result = new ArrayList<>(); for (ResourceWithDerivedData each : resources) { result.add(each.resource); } return result; } public final Resource resource; public final int capacityUnits; public final ICalendar calendar; public ResourceWithDerivedData(Resource resource) { this.resource = resource; this.capacityUnits = getCapacityFor(resource); this.calendar = generateCalendarFor(resource); } ResourceWithAvailableCapacity withAvailableCapacityOn(PartialDay day, IAssignedEffortForResource assignedEffort) { EffortDuration allCapacityForDay = calendar.getCapacityOn(PartialDay.wholeDay(day.getDate())); EffortDuration capacity = calendar.getCapacityOn(day); EffortDuration capacityForAlreadyAssigned = allCapacityForDay.minus(capacity); EffortDuration assigned = assignedEffort.getAssignedDurationAt(resource, day.getDate()); EffortDuration assignedInterfering = assigned.minus(min(assigned, capacityForAlreadyAssigned)); EffortDuration available = capacity.minus(min(assignedInterfering, capacity)); return new ResourceWithAvailableCapacity(resource, available); } Capacity getAvailableCapacityOn(PartialDay day, IAssignedEffortForResource assignedEffort) { Capacity originalCapacity = day.limitCapacity(calendar.getCapacityWithOvertime(day.getDate())); EffortDuration alreadyAssigned = assignedEffort.getAssignedDurationAt(resource, day.getDate()); return originalCapacity.minus(alreadyAssigned); } } /** * Note: this class has a natural ordering that is inconsistent with equals. */ private static class ResourceWithAvailableCapacity implements Comparable<ResourceWithAvailableCapacity> { private final Resource resource; private final EffortDuration available; public ResourceWithAvailableCapacity(Resource resource, EffortDuration available) { Validate.notNull(resource); Validate.notNull(available); this.resource = resource; this.available = available; } public ResourceWithAssignedDuration doBiggestAssignationPossible(EffortDuration remaining) { return new ResourceWithAssignedDuration(EffortDuration.min(remaining, available), resource); } @Override public int compareTo(ResourceWithAvailableCapacity o) { return available.compareTo(o.available); } @SuppressWarnings("unchecked") static Comparator<ResourceWithAvailableCapacity> getComparatorConsidering(final Set<Resource> lastResourcesUsed) { return ComparatorUtils.chainedComparator( new Comparator<ResourceWithAvailableCapacity>() { @Override public int compare(ResourceWithAvailableCapacity o1, ResourceWithAvailableCapacity o2) { boolean resource1Used = lastResourcesUsed.contains(o1.resource); boolean resource2Used = lastResourcesUsed.contains(o2.resource); return asInt(resource1Used) - asInt(resource2Used); } int asInt(boolean b) { return b ? 1 : 0; } }, ComparatorUtils.naturalComparator()); } } private final List<ResourceWithDerivedData> resources; private final IAssignedEffortForResource assignedEffortForResource; private final IResourceSelector resourceSelector; private Set<Resource> resourcesAlreadyPicked = new HashSet<>(); public EffortDistributor(List<Resource> resources, IAssignedEffortForResource assignedHoursForResource) { this(resources, assignedHoursForResource, null); } public EffortDistributor( List<Resource> resources, IAssignedEffortForResource assignedEffortForResource, IResourceSelector selector) { this.resources = ResourceWithDerivedData.from(resources); this.assignedEffortForResource = assignedEffortForResource; this.resourceSelector = selector != null ? new CompoundSelector(new OnlyCanWork(), selector) : new OnlyCanWork(); } public Capacity getCapacityAt(PartialDay day) { List<Capacity> capacities = new ArrayList<>(); for (ResourceWithDerivedData each : resourcesAssignableAt(day.getDate())) { capacities.add(each.getAvailableCapacityOn(day, assignedEffortForResource)); } return Capacity.sum(capacities); } public List<ResourceWithAssignedDuration> distributeForDay(PartialDay day, EffortDuration totalDuration) { return withCaptureOfResourcesPicked(distributeForDay_(day, totalDuration)); } private List<ResourceWithAssignedDuration> withCaptureOfResourcesPicked(List<ResourceWithAssignedDuration> result) { resourcesAlreadyPicked.addAll(ResourceWithAssignedDuration.resources(result)); return result; } private List<ResourceWithAssignedDuration> distributeForDay_(PartialDay day, EffortDuration totalDuration) { List<ResourceWithDerivedData> resourcesAssignable = resourcesAssignableAt(day.getDate()); List<ResourceWithAssignedDuration> withoutOvertime = assignAllPossibleWithoutOvertime(day, totalDuration, resourcesAssignable); EffortDuration remaining = totalDuration.minus(ResourceWithAssignedDuration.sumDurations(withoutOvertime)); if (remaining.isZero()) { return withoutOvertime; } List<ResourceWithAssignedDuration> withOvertime = distributeInOvertimeForDayRemainingEffort( day.getDate(), remaining, ResourceWithAssignedDuration.sumAssignedEffort(withoutOvertime, assignedEffortForResource), resourcesAssignable); return ResourceWithAssignedDuration.join(withoutOvertime, withOvertime); } private List<ResourceWithDerivedData> resourcesAssignableAt(LocalDate day) { List<ResourceWithDerivedData> result = new ArrayList<>(); for (ResourceWithDerivedData each : resources) { if (resourceSelector.isSelectable(each.resource, day)) { result.add(each); } } return result; } private List<ResourceWithAssignedDuration> assignAllPossibleWithoutOvertime( PartialDay day, EffortDuration totalDuration, List<ResourceWithDerivedData> resourcesAssignable) { List<ResourceWithAvailableCapacity> fromMoreToLessCapacity = resourcesFromMoreDesirableToLess(resourcesAssignable, day); EffortDuration remaining = totalDuration; List<ResourceWithAssignedDuration> result = new ArrayList<>(); for (ResourceWithAvailableCapacity each : fromMoreToLessCapacity) { if (!each.available.isZero()) { ResourceWithAssignedDuration r = each.doBiggestAssignationPossible(remaining); remaining = remaining.minus(r.duration); if (!r.duration.isZero()) { result.add(r); } } } return result; } private List<ResourceWithAvailableCapacity> resourcesFromMoreDesirableToLess( List<ResourceWithDerivedData> resourcesAssignable, PartialDay day) { List<ResourceWithAvailableCapacity> result = new ArrayList<>(); for (ResourceWithDerivedData each : resourcesAssignable) { result.add(each.withAvailableCapacityOn(day, assignedEffortForResource)); } Collections.sort( result, Collections.reverseOrder(ResourceWithAvailableCapacity.getComparatorConsidering(resourcesAlreadyPicked))); return result; } private List<ResourceWithAssignedDuration> distributeInOvertimeForDayRemainingEffort( LocalDate day, EffortDuration remainingDuration, IAssignedEffortForResource assignedEffortForEachResource, List<ResourceWithDerivedData> assignableResources) { List<ResourceWithAssignedDuration> remainingDistribution = suppressOverAssignedBeyondAvailableCapacity( day, assignedEffortForEachResource, distributeRemaining(day, remainingDuration, assignedEffortForEachResource, assignableResources)); EffortDuration durationDistributed = ResourceWithAssignedDuration.sumDurations(remainingDistribution); EffortDuration newRemaining = remainingDuration.minus(durationDistributed); assert newRemaining.compareTo(EffortDuration.zero()) >= 0; if (newRemaining.isZero()) { return remainingDistribution; } IAssignedEffortForResource newEffortForEachResource = ResourceWithAssignedDuration.sumAssignedEffort(remainingDistribution, assignedEffortForEachResource); List<ResourceWithDerivedData> resourcesWithAvailableOvertime = withAvailableCapacity(day, newEffortForEachResource, assignableResources); if (resourcesWithAvailableOvertime.isEmpty()) { return remainingDistribution; } return ResourceWithAssignedDuration.join( remainingDistribution, distributeInOvertimeForDayRemainingEffort( day, newRemaining, newEffortForEachResource, resourcesWithAvailableOvertime)); } private List<ResourceWithAssignedDuration> suppressOverAssignedBeyondAvailableCapacity( LocalDate date, IAssignedEffortForResource assignedEffortForEachResource, List<ResourceWithAssignedDuration> resources) { List<ResourceWithAssignedDuration> result = new ArrayList<>(); for (ResourceWithAssignedDuration each : resources) { Resource resource = each.resource; ICalendar calendar = generateCalendarFor(resource); Capacity capacityWithOvertime = calendar.getCapacityWithOvertime(date); if (capacityWithOvertime.isOverAssignableWithoutLimit()) { result.add(each); } else { EffortDuration durationCanBeAdded = calculateDurationCanBeAdded(assignedEffortForEachResource .getAssignedDurationAt(resource, date), capacityWithOvertime, each.duration); if (!durationCanBeAdded.isZero()) { result.add(new ResourceWithAssignedDuration(durationCanBeAdded, resource)); } } } return result; } private EffortDuration calculateDurationCanBeAdded( EffortDuration alreadyAssigned, Capacity capacityWithOvertime, EffortDuration newAddition) { EffortDuration maximum = capacityWithOvertime.getStandardEffort().plus(capacityWithOvertime.getAllowedExtraEffort()); if (alreadyAssigned.compareTo(maximum) >= 0) { return EffortDuration.zero(); } else { return EffortDuration.min(newAddition, maximum.minus(alreadyAssigned)); } } private List<ResourceWithDerivedData> withAvailableCapacity( LocalDate date, IAssignedEffortForResource assignedEffortForEachResource, List<ResourceWithDerivedData> assignableResources) { List<ResourceWithDerivedData> result = new ArrayList<>(); for (ResourceWithDerivedData each : assignableResources) { Capacity capacity = each.calendar.getCapacityWithOvertime(date); EffortDuration assignedEffort = assignedEffortForEachResource.getAssignedDurationAt(each.resource, date); if (capacity.hasSpareSpaceForMoreAllocations(assignedEffort)) { result.add(each); } } return result; } private List<ResourceWithAssignedDuration> distributeRemaining( LocalDate date, EffortDuration remainingDuration, IAssignedEffortForResource assignedEffortForEachResource, List<ResourceWithDerivedData> resourcesWithAvailableOvertime) { List<ShareSource> shares = divisionAt(resourcesWithAvailableOvertime, assignedEffortForEachResource, date); ShareDivision currentDivision = ShareSource.all(shares); ShareDivision newDivision = currentDivision.plus(remainingDuration.getSeconds()); int[] differences = currentDivision.to(newDivision); return ShareSource.durationsForEachResource( shares, differences, ResourceWithDerivedData.resources(resourcesWithAvailableOvertime)); } private List<ShareSource> divisionAt( List<ResourceWithDerivedData> resources, IAssignedEffortForResource assignedEffortForEachResource, LocalDate date) { List<ShareSource> result = new ArrayList<>(); for (ResourceWithDerivedData resource1 : resources) { List<Share> shares = new ArrayList<>(); Resource resource = resource1.resource; ICalendar calendarForResource = resource1.calendar; EffortDuration alreadyAssigned = assignedEffortForEachResource.getAssignedDurationAt(resource, date); final int alreadyAssignedSeconds = alreadyAssigned.getSeconds(); Integer capacityEachOneSeconds = calendarForResource.asDurationOn(PartialDay.wholeDay(date), ONE).getSeconds(); final int capacityUnits = resource1.capacityUnits; assert capacityUnits >= 1; final int assignedForEach = alreadyAssignedSeconds / capacityUnits; final int remainder = alreadyAssignedSeconds % capacityUnits; for (int j = 0; j < capacityUnits; j++) { int assignedSeconds = assignedForEach + (j < remainder ? 1 : 0); shares.add(new Share(assignedSeconds - capacityEachOneSeconds)); } result.add(new ShareSource(shares)); } return result; } private static final ResourcesPerDay ONE = ResourcesPerDay.amount(1); private static class ShareSource { public static ShareDivision all(Collection<ShareSource> sources) { List<Share> shares = new ArrayList<>(); for (ShareSource shareSource : sources) { shares.addAll(shareSource.shares); } return ShareDivision.create(shares); } public static List<ResourceWithAssignedDuration> durationsForEachResource( List<ShareSource> sources, int[] differencesInSeconds, List<Resource> resources) { List<ResourceWithAssignedDuration> result = new ArrayList<>(); int differencesIndex = 0; for (int i = 0; i < resources.size(); i++) { Resource resource = resources.get(i); ShareSource shareSource = sources.get(i); final int differencesToTake = shareSource.shares.size(); int sum = sumDifferences(differencesInSeconds, differencesIndex, differencesToTake); differencesIndex += differencesToTake; ResourceWithAssignedDuration withAssignedDuration = new ResourceWithAssignedDuration(seconds(sum), resource); if (!withAssignedDuration.duration.isZero()) { result.add(withAssignedDuration); } } return result; } private static int sumDifferences(int[] differences, int start, final int toTake) { int sum = 0; for (int i = 0; i < toTake; i++) { sum += differences[start + i]; } return sum; } private final List<Share> shares; private ShareSource(List<Share> shares) { this.shares = shares; } } }