org.libreplan.web.planner.chart.ChartFiller.java Source code

Java tutorial

Introduction

Here is the source code for org.libreplan.web.planner.chart.ChartFiller.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.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;
    }

}