/*
* 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.web.planner.chart;
import static org.libreplan.business.workingday.EffortDuration.zero;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.libreplan.business.planner.entities.DayAssignment;
import org.libreplan.business.resources.entities.Resource;
import org.libreplan.business.workingday.EffortDuration;
import org.libreplan.business.workingday.IntraDayDate.PartialDay;
import org.zkforge.timeplot.Plotinfo;
import org.zkforge.timeplot.Timeplot;
import org.zkforge.timeplot.data.PlotDataSource;
import org.zkforge.timeplot.geometry.DefaultTimeGeometry;
import org.zkforge.timeplot.geometry.DefaultValueGeometry;
import org.zkforge.timeplot.geometry.TimeGeometry;
import org.zkforge.timeplot.geometry.ValueGeometry;
import org.zkoss.ganttz.servlets.CallbackServlet;
import org.zkoss.ganttz.servlets.CallbackServlet.DisposalMode;
import org.zkoss.ganttz.servlets.CallbackServlet.IServletRequestHandler;
import org.zkoss.ganttz.timetracker.zoom.ZoomLevel;
import org.zkoss.ganttz.util.Interval;
import org.zkoss.zk.ui.Executions;
/**
* Abstract class with the basic functionality to fill the chart.
* @author Manuel Rego Casasnovas <mrego@igalia.com>
*/
public abstract class ChartFiller implements IChartFiller {
protected abstract class EffortByDayCalculator<T> {
public SortedMap<LocalDate, EffortDuration> calculate(
Collection<? extends T> elements) {
SortedMap<LocalDate, EffortDuration> result = new TreeMap<LocalDate, EffortDuration>();
if (elements.isEmpty()) {
return result;
}
for (T element : elements) {
if (included(element)) {
EffortDuration duration = getDurationFor(element);
LocalDate day = getDayFor(element);
EffortDuration previous = result.get(day);
previous = previous == null ? zero() : previous;
result.put(day, previous.plus(duration));
}
}
return groupAsNeededByZoom(result);
}
protected abstract LocalDate getDayFor(T element);
protected abstract EffortDuration getDurationFor(T element);
protected boolean included(T each) {
return true;
}
}
protected static EffortDuration sumCalendarCapacitiesForDay(
Collection<? extends Resource> resources, LocalDate day) {
PartialDay wholeDay = PartialDay.wholeDay(day);
EffortDuration sum = zero();
for (Resource resource : resources) {
sum = sum.plus(calendarCapacityFor(resource,
wholeDay));
}
return sum;
}
protected static EffortDuration calendarCapacityFor(Resource resource,
PartialDay day) {
return resource.getCalendarOrDefault().getCapacityOn(day);
}
protected abstract class GraphicSpecificationCreator implements
IServletRequestHandler {
private final LocalDate finish;
private final SortedMap<LocalDate, BigDecimal> map;
private final LocalDate start;
protected GraphicSpecificationCreator(LocalDate finish,
SortedMap<LocalDate, BigDecimal> map, LocalDate start) {
this.finish = new LocalDate(finish);
this.map = map;
this.start = new LocalDate(start);
}
protected Set<LocalDate> getDays() {
return map.keySet();
}
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
PrintWriter writer = response.getWriter();
fillValues(writer);
writer.close();
}
private void fillValues(PrintWriter writer) {
fillZeroValueFromStart(writer);
fillInnerValues(writer, firstDay(), lastDay());
fillZeroValueToFinish(writer);
}
protected abstract void fillInnerValues(PrintWriter writer,
LocalDate firstDay, LocalDate lastDay);
protected LocalDate nextDay(LocalDate date) {
if (isZoomByDayOrWeek()) {
return date.plusDays(1);
} else {
return date.plusWeeks(1);
}
}
private LocalDate firstDay() {
LocalDate date = map.firstKey();
return convertAsNeededByZoom(date);
}
private LocalDate lastDay() {
LocalDate date = map.lastKey();
return convertAsNeededByZoom(date);
}
private LocalDate convertAsNeededByZoom(LocalDate date) {
if (isZoomByDayOrWeek()) {
return date;
} else {
return getThursdayOfThisWeek(date);
}
}
protected BigDecimal getHoursForDay(LocalDate day) {
return map.get(day) != null ? map.get(day) : BigDecimal.ZERO;
}
protected void printLine(PrintWriter writer, DateTime day,
BigDecimal hours) {
// using ISO 8601 format [YYYY][MM][DD]T[hh][mm][ss]Z.
String position = day.toString("yyyyMMdd") + "T"
+ day.toString("HHmmss") + "Z";
writer.println(position + " " + hours);
}
protected void printIntervalLine(PrintWriter writer, LocalDate day,
BigDecimal hours, boolean isZoomByDay) {
// using ISO 8601 format [YYYY][MM][DD]T[hh][mm][ss]Z.
DateTime initOfInterval = getInitOfInterval(day, isZoomByDay);
DateTime finishOfInterval = getFinishOfInterval(day, isZoomByDay);
printLine(writer, initOfInterval, hours);
printLine(writer, finishOfInterval, hours);
}
protected DateTime getInitOfInterval(LocalDate day,
boolean isZoomByDayOrWeek) {
if (isZoomByDayOrWeek) {
return day.toDateTimeAtStartOfDay();
} else {
return day.minusDays(day.getDayOfWeek() - 1)
.toDateTimeAtStartOfDay();
}
}
protected DateTime getFinishOfInterval(LocalDate day,
boolean isZoomByDayOrWeek) {
if (isZoomByDayOrWeek) {
return day.plusDays(1).toDateTimeAtStartOfDay().minusSeconds(1);
} else {
return day.plusDays(8 - day.getDayOfWeek())
.toDateTimeAtStartOfDay().minusSeconds(1);
}
}
private void fillZeroValueFromStart(PrintWriter writer) {
if (!startIsDayOfFirstAssignment()) {
printLine(writer, start.toDateTimeAtStartOfDay(),
BigDecimal.ZERO);
if (startIsPreviousToPreviousDayToFirstAssignment()) {
printLine(writer, previousDayToFirstAssignment(),
BigDecimal.ZERO);
}
}
}
private boolean startIsDayOfFirstAssignment() {
return !map.isEmpty() && start.compareTo(map.firstKey()) == 0;
}
private boolean startIsPreviousToPreviousDayToFirstAssignment() {
return !map.isEmpty()
&& start.compareTo(previousDayToFirstAssignment()
.toLocalDate()) < 0;
}
private DateTime previousDayToFirstAssignment() {
return getInitOfInterval(map.firstKey(), isZoomByDayOrWeek())
.minusSeconds(1);
}
private void fillZeroValueToFinish(PrintWriter writer) {
if (!finishIsDayOfLastAssignment()) {
if (finishIsPosteriorToNextDayToLastAssignment()) {
printLine(writer, nextDayToLastAssignment(),
BigDecimal.ZERO);
}
DateTime finishMidNight = finish.plusDays(1)
.toDateTimeAtStartOfDay().minusSeconds(1);
printLine(writer, finishMidNight, BigDecimal.ZERO);
}
}
private boolean finishIsDayOfLastAssignment() {
return !map.isEmpty() && start.compareTo(map.lastKey()) == 0;
}
private boolean finishIsPosteriorToNextDayToLastAssignment() {
return !map.isEmpty()
&& finish
.compareTo(nextDayToLastAssignment().toLocalDate()) > 0;
}
private DateTime nextDayToLastAssignment() {
return this.getFinishOfInterval(map.lastKey(), isZoomByDayOrWeek())
.plusSeconds(1);
}
}
protected class DefaultGraphicSpecificationCreator extends
GraphicSpecificationCreator {
private DefaultGraphicSpecificationCreator(LocalDate finish,
SortedMap<LocalDate, BigDecimal> map, LocalDate start) {
super(finish, map, start);
}
@Override
protected void fillInnerValues(PrintWriter writer, LocalDate firstDay,
LocalDate lastDay) {
for (LocalDate day = firstDay; day.compareTo(lastDay) <= 0; day = nextDay(day)) {
BigDecimal hours = getHoursForDay(day);
printIntervalLine(writer, day, hours, isZoomByDayOrWeek());
}
}
}
protected class JustDaysWithInformationGraphicSpecificationCreator extends
GraphicSpecificationCreator {
public JustDaysWithInformationGraphicSpecificationCreator(
LocalDate finish, SortedMap<LocalDate, BigDecimal> map,
LocalDate start) {
super(finish, map, start);
}
@Override
protected void fillInnerValues(PrintWriter writer, LocalDate firstDay,
LocalDate lastDay) {
for (LocalDate day : getDays()) {
BigDecimal hours = getHoursForDay(day);
printLine(writer, day.toDateTimeAtStartOfDay(), hours);
}
}
}
/**
* Number of days to Thursday since the beginning of the week. In order to
* calculate the middle of a week.
*/
private final static int DAYS_TO_THURSDAY = 3;
private ZoomLevel zoomLevel = ZoomLevel.DETAIL_ONE;
private BigDecimal minimumValueForChart = BigDecimal.ZERO;
private BigDecimal maximumValueForChart = BigDecimal.ZERO;
@Override
public abstract void fillChart(Timeplot chart, Interval interval,
Integer size);
private void setMinimumValueForChartIfLess(BigDecimal min) {
if (minimumValueForChart.compareTo(min) > 0) {
minimumValueForChart = min;
}
}
private void setMaximumValueForChartIfGreater(BigDecimal max) {
if (maximumValueForChart.compareTo(max) < 0) {
maximumValueForChart = max;
}
}
private static LocalDate getThursdayOfThisWeek(LocalDate date) {
return date.dayOfWeek().withMinimumValue().plusDays(DAYS_TO_THURSDAY);
}
private boolean isZoomByDayOrWeek() {
return (zoomLevel.equals(ZoomLevel.DETAIL_FIVE) || zoomLevel
.equals(ZoomLevel.DETAIL_FOUR));
}
protected void resetMinimumAndMaximumValueForChart() {
this.minimumValueForChart = BigDecimal.ZERO;
this.maximumValueForChart = BigDecimal.ZERO;
}
protected BigDecimal getMinimumValueForChart() {
return minimumValueForChart;
}
protected BigDecimal getMaximumValueForChart() {
return maximumValueForChart;
}
protected SortedMap<LocalDate, BigDecimal> groupByWeek(
SortedMap<LocalDate, BigDecimal> map) {
SortedMap<LocalDate, BigDecimal> result = new TreeMap<LocalDate, BigDecimal>();
for (Entry<LocalDate, BigDecimal> entry : map.entrySet()) {
LocalDate day = entry.getKey();
LocalDate key = getThursdayOfThisWeek(day);
BigDecimal hours = entry.getValue() == null ? BigDecimal.ZERO
: entry.getValue();
if (result.get(key) == null) {
result.put(key, hours);
} else {
result.put(key, result.get(key).add(hours));
}
}
for (Entry<LocalDate, BigDecimal> entry : result.entrySet()) {
LocalDate day = entry.getKey();
result.put(entry.getKey(), result.get(day).setScale(2).divide(
new BigDecimal(7), RoundingMode.DOWN));
}
return result;
}
protected SortedMap<LocalDate, EffortDuration> groupAsNeededByZoom(
SortedMap<LocalDate, EffortDuration> map) {
if (isZoomByDayOrWeek()) {
return map;
}
return groupByWeekDurations(map);
}
protected SortedMap<LocalDate, EffortDuration> groupByWeekDurations(
SortedMap<LocalDate, EffortDuration> map) {
return average(accumulatePerWeek(map));
}
private static SortedMap<LocalDate, EffortDuration> accumulatePerWeek(
SortedMap<LocalDate, EffortDuration> map) {
SortedMap<LocalDate, EffortDuration> result = new TreeMap<LocalDate, EffortDuration>();
for (Entry<LocalDate, EffortDuration> each : map.entrySet()) {
LocalDate centerOfWeek = getThursdayOfThisWeek(each.getKey());
EffortDuration accumulated = result.get(centerOfWeek);
accumulated = accumulated == null ? zero() : accumulated;
result.put(centerOfWeek, accumulated.plus(each.getValue()));
}
return result;
}
private static SortedMap<LocalDate, EffortDuration> average(
SortedMap<LocalDate, EffortDuration> accumulatedPerWeek) {
SortedMap<LocalDate, EffortDuration> result = new TreeMap<LocalDate, EffortDuration>();
for (Entry<LocalDate, EffortDuration> each : accumulatedPerWeek
.entrySet()) {
result.put(each.getKey(), each.getValue().divideBy(7));
}
return result;
}
protected TimeGeometry getTimeGeometry(Interval interval) {
LocalDate start = new LocalDate(interval.getStart());
LocalDate finish = new LocalDate(interval.getFinish());
TimeGeometry timeGeometry = new DefaultTimeGeometry();
if (!isZoomByDayOrWeek()) {
start = getThursdayOfThisWeek(start);
finish = getThursdayOfThisWeek(finish);
}
timeGeometry.setMin(start.toDateTimeAtStartOfDay().toDate());
timeGeometry.setMax(finish.toDateTimeAtStartOfDay().toDate());
timeGeometry.setAxisLabelsPlacement("bottom");
// Remove year separators
timeGeometry.setGridColor("#FFFFFF");
return timeGeometry;
}
protected ValueGeometry getValueGeometry() {
DefaultValueGeometry valueGeometry = new DefaultValueGeometry();
valueGeometry.setMin(getMinimumValueForChart().intValue());
valueGeometry.setMax(getMaximumValueForChart().intValue());
valueGeometry.setGridColor("#000000");
valueGeometry.setAxisLabelsPlacement("left");
return valueGeometry;
}
protected SortedMap<LocalDate, Map<Resource, EffortDuration>> groupDurationsByDayAndResource(
List<DayAssignment> dayAssignments) {
SortedMap<LocalDate, Map<Resource, EffortDuration>> map = new TreeMap<LocalDate, Map<Resource, EffortDuration>>();
for (DayAssignment dayAssignment : dayAssignments) {
final LocalDate day = dayAssignment.getDay();
final EffortDuration dayAssignmentDuration = dayAssignment
.getDuration();
Resource resource = dayAssignment.getResource();
if (map.get(day) == null) {
map.put(day, new HashMap<Resource, EffortDuration>());
}
Map<Resource, EffortDuration> forDay = map.get(day);
EffortDuration previousDuration = forDay.get(resource);
previousDuration = previousDuration != null ? previousDuration
: EffortDuration.zero();
forDay.put(dayAssignment.getResource(),
previousDuration.plus(dayAssignmentDuration));
}
return map;
}
protected void addCost(SortedMap<LocalDate, BigDecimal> currentCost,
SortedMap<LocalDate, BigDecimal> additionalCost) {
for (LocalDate day : additionalCost.keySet()) {
if (!currentCost.containsKey(day)) {
currentCost.put(day, BigDecimal.ZERO);
}
currentCost.put(day, currentCost.get(day).add(
additionalCost.get(day)));
}
}
protected SortedMap<LocalDate, BigDecimal> accumulateResult(
SortedMap<LocalDate, BigDecimal> map) {
SortedMap<LocalDate, BigDecimal> result = new TreeMap<LocalDate, BigDecimal>();
if (map.isEmpty()) {
return result;
}
BigDecimal accumulatedResult = BigDecimal.ZERO;
for (LocalDate day : map.keySet()) {
BigDecimal value = map.get(day);
accumulatedResult = accumulatedResult.add(value);
result.put(day, accumulatedResult);
}
return result;
}
protected SortedMap<LocalDate, BigDecimal> convertToBigDecimal(
SortedMap<LocalDate, Integer> map) {
SortedMap<LocalDate, BigDecimal> result = new TreeMap<LocalDate, BigDecimal>();
for (LocalDate day : map.keySet()) {
BigDecimal value = new BigDecimal(map.get(day));
result.put(day, value);
}
return result;
}
protected SortedMap<LocalDate, BigDecimal> calculatedValueForEveryDay(
SortedMap<LocalDate, BigDecimal> values, Interval interval) {
return calculatedValueForEveryDay(values, interval.getStart(),
interval.getFinish());
}
protected SortedMap<LocalDate, BigDecimal> calculatedValueForEveryDay(
SortedMap<LocalDate, BigDecimal> map, Date start, Date finish) {
return calculatedValueForEveryDay(map, new LocalDate(start),
new LocalDate(finish));
}
protected SortedMap<LocalDate, BigDecimal> calculatedValueForEveryDay(
SortedMap<LocalDate, BigDecimal> map, LocalDate start,
LocalDate finish) {
SortedMap<LocalDate, BigDecimal> result = new TreeMap<LocalDate, BigDecimal>();
LocalDate previousDay = start;
BigDecimal previousValue = BigDecimal.ZERO;
for (LocalDate day : map.keySet()) {
BigDecimal value = map.get(day);
fillValues(result, previousDay, day, previousValue, value);
previousDay = day;
previousValue = value;
}
if (previousDay.compareTo(finish) < 0) {
fillValues(result, previousDay, finish, previousValue,
previousValue);
}
return result;
}
private void fillValues(SortedMap<LocalDate, BigDecimal> map,
LocalDate firstDay, LocalDate lastDay, BigDecimal firstValue,
BigDecimal lastValue) {
Integer days = Days.daysBetween(firstDay, lastDay).getDays();
if (days > 0) {
BigDecimal ammount = lastValue.subtract(firstValue);
BigDecimal ammountPerDay = ammount.setScale(2, RoundingMode.DOWN).divide(
new BigDecimal(days), RoundingMode.DOWN);
BigDecimal value = firstValue.setScale(2, RoundingMode.DOWN);
for (LocalDate day = firstDay; day.compareTo(lastDay) <= 0; day = day
.plusDays(1)) {
map.put(day, value);
value = value.add(ammountPerDay);
}
}
}
protected Plotinfo createPlotinfoFromDurations(SortedMap<LocalDate, EffortDuration> map,
Interval interval) {
return createPlotinfo(toHoursDecimal(map), interval);
}
public static <K> SortedMap<K, BigDecimal> toHoursDecimal(
Map<K, EffortDuration> map) {
SortedMap<K, BigDecimal> result = new TreeMap<K, BigDecimal>();
for (Entry<K, EffortDuration> each : map.entrySet()) {
result.put(each.getKey(), each.getValue()
.toHoursAsDecimalWithScale(2));
}
return result;
}
protected Plotinfo createPlotinfo(SortedMap<LocalDate, BigDecimal> map,
Interval interval) {
return createPlotinfo(map, interval, false);
}
protected Plotinfo createPlotinfo(SortedMap<LocalDate, BigDecimal> map,
Interval interval, boolean justDaysWithInformation) {
if (!map.isEmpty()) {
setMinimumValueForChartIfLess(Collections.min(map.values()));
setMaximumValueForChartIfGreater(Collections.max(map.values()));
}
return createPlotInfoFrom(createGraphicSpecification(map,
interval, justDaysWithInformation));
}
private GraphicSpecificationCreator createGraphicSpecification(
SortedMap<LocalDate, BigDecimal> map, Interval interval,
boolean justDaysWithInformation) {
if (map.isEmpty()) {
return null;
}
if (justDaysWithInformation) {
return new JustDaysWithInformationGraphicSpecificationCreator(
interval.getFinish(), map, interval.getStart());
} else {
return new DefaultGraphicSpecificationCreator(interval.getFinish(),
map, interval.getStart());
}
}
private String getServletUri(
final GraphicSpecificationCreator graphicSpecificationCreator) {
if (graphicSpecificationCreator == null) {
return "";
}
HttpServletRequest request = (HttpServletRequest) Executions
.getCurrent().getNativeRequest();
return CallbackServlet.registerAndCreateURLFor(request,
graphicSpecificationCreator, false,
DisposalMode.WHEN_NO_LONGER_REFERENCED);
}
private Plotinfo createPlotInfoFrom(
GraphicSpecificationCreator graphicSpecificationCreator) {
PlotDataSource pds = new PlotDataSource();
pds.setDataSourceUri(getServletUri(graphicSpecificationCreator));
pds.setSeparator(" ");
Plotinfo plotinfo = new Plotinfo();
plotinfo.setAttribute("keep-chart-specification-creator-referenced",
graphicSpecificationCreator);
plotinfo.setPlotDataSource(pds);
return plotinfo;
}
protected void appendPlotinfo(Timeplot chart, Plotinfo plotinfo,
ValueGeometry valueGeometry, TimeGeometry timeGeometry) {
plotinfo.setValueGeometry(valueGeometry);
plotinfo.setTimeGeometry(timeGeometry);
plotinfo.setShowValues(true);
chart.appendChild(plotinfo);
}
@Override
public void setZoomLevel(ZoomLevel zoomLevel) {
this.zoomLevel = zoomLevel;
}
}