diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/AllocationOnGap.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/AllocationOnGap.java new file mode 100644 index 000000000..63154a82f --- /dev/null +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/AllocationOnGap.java @@ -0,0 +1,182 @@ +/* + * This file is part of NavalPlan + * + * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e + * Desenvolvemento Tecnolóxico de Galicia + * + * 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 . + */ +package org.navalplanner.business.planner.limiting.entities; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.Validate; +import org.joda.time.LocalDate; +import org.navalplanner.business.planner.entities.DayAssignment; +import org.navalplanner.business.planner.entities.GenericDayAssignment; +import org.navalplanner.business.planner.entities.GenericResourceAllocation; +import org.navalplanner.business.planner.entities.ResourceAllocation; +import org.navalplanner.business.planner.entities.SpecificDayAssignment; +import org.navalplanner.business.planner.entities.SpecificResourceAllocation; +import org.navalplanner.business.resources.entities.Resource; + +public abstract class AllocationOnGap { + + public static AllocationOnGap invalidOn(Gap gap) { + return new InvalidAllocationOnGap(gap); + } + + public static AllocationOnGap validOn(Gap gap, DateAndHour start, + DateAndHour endExclusive, int[] assignableHours) { + return new ValidAllocationOnGap(gap, start, endExclusive, + assignableHours); + } + + private final Gap originalGap; + + protected AllocationOnGap(Gap originalGap) { + Validate.notNull(originalGap); + this.originalGap = originalGap; + } + + public abstract boolean isValid(); + + public abstract List getAssignmentsFor( + ResourceAllocation allocation, Resource resource) + throws IllegalStateException; + + public abstract DateAndHour getStartInclusive() + throws IllegalStateException; + + public abstract DateAndHour getEndExclusive() throws IllegalStateException; + + public Gap getOriginalGap() { + return originalGap; + } +} + +class InvalidAllocationOnGap extends AllocationOnGap { + + private static final String INVALID_ALLOCATION_ON_GAP = "invalid allocation on gap"; + + InvalidAllocationOnGap(Gap originalGap) { + super(originalGap); + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public List getAssignmentsFor( + ResourceAllocation allocation, Resource resource) { + throw new IllegalStateException(INVALID_ALLOCATION_ON_GAP); + } + + @Override + public DateAndHour getEndExclusive() throws IllegalStateException { + throw new IllegalStateException(INVALID_ALLOCATION_ON_GAP); + } + + @Override + public DateAndHour getStartInclusive() throws IllegalStateException { + throw new IllegalStateException(INVALID_ALLOCATION_ON_GAP); + } +} + +class ValidAllocationOnGap extends AllocationOnGap { + + private final DateAndHour start; + private final DateAndHour end; + private final int[] assignableHours; + + public ValidAllocationOnGap(Gap gap, DateAndHour startInclusive, + DateAndHour endExclusive, int[] assignableHours) { + super(gap); + Validate.notNull(startInclusive); + Validate.notNull(endExclusive); + Validate.notNull(assignableHours); + Validate.isTrue(endExclusive.isAfter(startInclusive)); + this.start = startInclusive; + this.end = endExclusive; + Validate.isTrue(assignableHours.length == toFiniteList( + start.daysUntil(end)).size()); + this.assignableHours = assignableHours.clone(); + } + + private List toFiniteList(Iterable daysUntil) { + List result = new ArrayList(); + for (LocalDate each : daysUntil) { + result.add(each); + } + return result; + } + + @Override + public List getAssignmentsFor( + ResourceAllocation allocation, Resource resource) + throws IllegalStateException { + List days = toFiniteList(start.daysUntil(end)); + assert assignableHours.length == days.size(); + if (allocation instanceof SpecificResourceAllocation) { + return createSpecific(days, + (SpecificResourceAllocation) allocation, resource); + } else { + return createGeneric(days, (GenericResourceAllocation) allocation, + resource); + } + } + + private List createSpecific(List days, + SpecificResourceAllocation allocation, Resource resource) { + List result = new ArrayList(); + int i = 0; + for (LocalDate each : days) { + result.add(SpecificDayAssignment.create(each, assignableHours[i], + resource)); + i++; + } + return result; + } + + private List createGeneric(List days, + GenericResourceAllocation allocation, Resource resource) { + List result = new ArrayList(); + int i = 0; + for (LocalDate each : days) { + result.add(GenericDayAssignment.create(each, assignableHours[i], + resource)); + i++; + } + return result; + } + + @Override + public DateAndHour getEndExclusive() throws IllegalStateException { + return end; + } + + @Override + public DateAndHour getStartInclusive() throws IllegalStateException { + return start; + } + + @Override + public boolean isValid() { + return true; + } + +} \ No newline at end of file diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/DateAndHour.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/DateAndHour.java index 663c0ed0f..51c77dbaa 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/DateAndHour.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/DateAndHour.java @@ -20,6 +20,8 @@ package org.navalplanner.business.planner.limiting.entities; +import java.util.Iterator; + import org.apache.commons.lang.Validate; import org.joda.time.LocalDate; @@ -99,4 +101,39 @@ public class DateAndHour implements Comparable { return isAfter(DateAndHour.from(date)); } + /** + * Creates an {@link Iterable} that returns a lazy iterator. If + * end is null it will not stop and will keep on + * producing days forever + */ + public Iterable daysUntil(final DateAndHour end) { + Validate.isTrue(end == null || end.isAfter(this)); + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + + private LocalDate current = getDate(); + + @Override + public boolean hasNext() { + return end == null || end.isAfter(current); + } + + @Override + public LocalDate next() { + LocalDate result = current; + current = current.plusDays(1); + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + } diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/Gap.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/Gap.java index d0b3798e7..5a5fea82a 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/Gap.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/Gap.java @@ -20,7 +20,13 @@ package org.navalplanner.business.planner.limiting.entities; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.lang.Validate; import org.joda.time.LocalDate; +import org.navalplanner.business.calendars.entities.BaseCalendar; import org.navalplanner.business.calendars.entities.ResourceCalendar; import org.navalplanner.business.resources.entities.Resource; @@ -69,6 +75,56 @@ public class Gap implements Comparable { } } + public List getHoursInGapUntilAllocatingAndGoingToTheEnd( + BaseCalendar calendar, + DateAndHour realStart, DateAndHour allocationEnd, int total) { + DateAndHour gapEnd = getEndTime(); + Validate.isTrue(gapEnd == null || allocationEnd.compareTo(gapEnd) <= 0); + Validate.isTrue(startTime == null + || realStart.compareTo(startTime) >= 0); + List result = new ArrayList(); + Iterator daysUntilEnd = realStart.daysUntil(gapEnd) + .iterator(); + boolean isFirst = true; + while (daysUntilEnd.hasNext()) { + LocalDate each = daysUntilEnd.next(); + final boolean isLast = !daysUntilEnd.hasNext(); + int hoursAtDay = getHoursAtDay(each, calendar, realStart, isFirst, + isLast); + final int hours; + if (total > 0) { + hours = Math.min(hoursAtDay, total); + total -= hours; + } else { + hours = hoursAtDay; + } + if (isFirst) { + isFirst = false; + } + result.add(hours); + if (total == 0 + && DateAndHour.from(each).compareTo(allocationEnd) >= 0) { + break; + } + } + return result; + } + + private int getHoursAtDay(LocalDate day, BaseCalendar calendar, + DateAndHour realStart, boolean isFirst, final boolean isLast) { + final int capacity = calendar.getCapacityAt(day); + if (isLast && isFirst) { + return Math.min(endTime.getHour() - realStart.getHour(), + capacity); + } else if (isFirst) { + return capacity - realStart.getHour(); + } else if (isLast) { + return Math.min(endTime.getHour(), capacity); + } else { + return capacity; + } + } + public static Gap create(Resource resource, DateAndHour startTime, DateAndHour endTime) { return new Gap(resource, startTime, endTime); diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/GapRequirements.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/GapRequirements.java new file mode 100644 index 000000000..cb1207795 --- /dev/null +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/limiting/entities/GapRequirements.java @@ -0,0 +1,174 @@ +/* + * This file is part of NavalPlan + * + * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e + * Desenvolvemento Tecnolóxico de Galicia + * + * 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 . + */ +package org.navalplanner.business.planner.limiting.entities; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.ListIterator; + +import org.apache.commons.lang.Validate; +import org.joda.time.LocalDate; + + +/** + * @author Óscar González Fernández + */ +public class GapRequirements { + + private final LimitingResourceQueueElement element; + + private final DateAndHour earliestPossibleStart; + + private final DateAndHour earliestPossibleEnd; + + public static GapRequirements forElement( + LimitingResourceQueueElement element) { + return new GapRequirements(element, asDateAndHour(element + .getEarlierStartDateBecauseOfGantt()), asDateAndHour(element + .getEarliestEndDateBecauseOfGantt())); + } + + private static DateAndHour asDateAndHour(Date date) { + return DateAndHour.from(LocalDate.fromDateFields(date)); + } + + private GapRequirements(LimitingResourceQueueElement element, + DateAndHour earliestPossibleStart, DateAndHour earliestPossibleEnd) { + Validate.notNull(element); + Validate.notNull(earliestPossibleStart); + Validate.notNull(earliestPossibleEnd); + this.element = element; + this.earliestPossibleStart = earliestPossibleStart; + this.earliestPossibleEnd = earliestPossibleEnd; + } + + public AllocationOnGap guessValidity(Gap gap) { + DateAndHour gapEnd = gap.getEndTime(); + if (gapEnd != null + && (earliestPossibleStart.compareTo(gapEnd) >= 0 || earliestPossibleEnd + .isAfter(gapEnd))) { + return AllocationOnGap.invalidOn(gap); + } + DateAndHour realStart = DateAndHour.Max(earliestPossibleStart, gap + .getStartTime()); + List hours = gap.getHoursInGapUntilAllocatingAndGoingToTheEnd( + element.getResource().getCalendar(), realStart, + earliestPossibleEnd, element.getIntentedTotalHours()); + int total = sum(hours); + if (total < element.getIntentedTotalHours()) { + return AllocationOnGap.invalidOn(gap); + } else if (total == element.getIntentedTotalHours()) { + return validAllocation(gap, realStart, hours); + } else { + assert total > element.getIntentedTotalHours(); + int hoursSurplus = total - element.getIntentedTotalHours(); + StartRemoval result = StartRemoval.removeStartSurplus(realStart, + hours, hoursSurplus); + return validAllocation(gap, result.newStart, result.hours); + } + + } + + private AllocationOnGap validAllocation(Gap gap, DateAndHour realStart, + List hours) { + return AllocationOnGap.validOn(gap, realStart, calculateEnd(realStart, + hours), asArray(hours)); + } + + private DateAndHour calculateEnd(DateAndHour realStart, List hours) { + if (hours.size() == 1) { + return new DateAndHour(realStart.getDate(), hours.get(0) + + realStart.getHour()); + } + return new DateAndHour(realStart.getDate().plusDays(hours.size() - 1), + getLast(hours)); + } + + private int getLast(List hours) { + return hours.get(hours.size() - 1); + } + + private static class StartRemoval { + + /** + * removes the initial assignments so the resulting list has + * hoursSurplus less hours + */ + static StartRemoval removeStartSurplus(DateAndHour start, + List hours, int hoursSurplus) { + int previousSize = hours.size(); + int hoursRemovedAtFirstDayOfNewHours = stripStartAssignments(hours, + hoursSurplus); + int currentSize = hours.size(); + int daysRemoved = previousSize - currentSize; + LocalDate newStartDay = start.getDate().plusDays(daysRemoved); + return new StartRemoval(new DateAndHour(newStartDay, + hoursRemovedAtFirstDayOfNewHours), hours); + } + + /** + * @return the hours reduced in the resulting first assignment + */ + private static int stripStartAssignments(List hours, + int hoursSurplus) { + ListIterator listIterator = hours.listIterator(); + while (listIterator.hasNext() && hoursSurplus > 0) { + Integer current = listIterator.next(); + int hoursTaken = Math.min(hoursSurplus, current); + hoursSurplus -= hoursTaken; + if (hoursTaken == current) { + listIterator.remove(); + } else { + listIterator.set(hoursTaken); + return current - hoursTaken; + } + } + return 0; + } + + private final DateAndHour newStart; + + private final List hours; + + private StartRemoval(DateAndHour newStart, List hours) { + this.newStart = newStart; + this.hours = hours; + } + } + + private static int[] asArray(Collection integers) { + int[] result = new int[integers.size()]; + int i = 0; + for (Integer each : integers) { + result[i++] = each; + } + return result; + } + + private static int sum(List hours) { + int result = 0; + for (int each : hours) { + result += each; + } + return result; + } + +}