org.libreplan.business.planner.entities.EffortDistributor.java Source code

Java tutorial

Introduction

Here is the source code for org.libreplan.business.planner.entities.EffortDistributor.java

Source

/*
 * This file is part of LibrePlan
 *
 * Copyright (C) 2009-2010 Fundacin para o Fomento da Calidade Industrial e
 *                         Desenvolvemento Tecnolxico 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 Gonzlez Fernndez <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;
        }

    }

}