net.sourceforge.processdash.ui.web.psp.TimeLogPhaseWaterfallChart.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.processdash.ui.web.psp.TimeLogPhaseWaterfallChart.java

Source

// Copyright (C) 2009 Tuma Solutions, LLC
// Process Dashboard - Data Automation Tool for high-maturity processes
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 3
// of the License, or (at your option) any later version.
//
// Additional permissions also apply; see the README-license.txt
// file in the project root directory for more information.
//
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <http://www.gnu.org/licenses/>.
//
// The author(s) may be contacted at:
//     processdash@tuma-solutions.com
//     processdash-devel@lists.sourceforge.net

package net.sourceforge.processdash.ui.web.psp;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Stroke;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import net.sourceforge.processdash.i18n.Resources;
import net.sourceforge.processdash.i18n.Translator;
import net.sourceforge.processdash.log.time.TimeLog;
import net.sourceforge.processdash.log.time.TimeLogEntry;
import net.sourceforge.processdash.process.ProcessUtil;
import net.sourceforge.processdash.ui.lib.chart.DrawingSupplierFactory;
import net.sourceforge.processdash.ui.lib.chart.PhaseChartColorer;
import net.sourceforge.processdash.ui.web.CGIChartBase;
import net.sourceforge.processdash.util.ComparableValue;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYBoxAnnotation;
import org.jfree.chart.annotations.XYLineAnnotation;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.SegmentedTimeline;
import org.jfree.chart.axis.SymbolAxis;
import org.jfree.chart.labels.XYToolTipGenerator;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.data.xy.IntervalXYDataset;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYIntervalSeries;
import org.jfree.data.xy.XYIntervalSeriesCollection;

public class TimeLogPhaseWaterfallChart extends CGIChartBase {

    private static final Resources resources = Resources.getDashBundle("PspForEng.TimeLogPhaseChart");

    @Override
    protected void writeContents() throws IOException {
        if (isHtmlMode()) {
            out.write("<p>");
            super.writeContents();
            out.write("</p>\n");
        } else {
            super.writeContents();
        }
    }

    @Override
    protected void buildData() {
        // this method in the superclass builds a ResultSet object.  We
        // do not use ResultSets as the basis for our dataset, so we override
        // this method to do nothing.
    }

    @Override
    public JFreeChart createChart() {
        ProcessUtil process = new ProcessUtil(getDataContext());
        List<String> phases = getPhaseList(process);

        List timeLogEntries = getTimeLogEntries();
        GapSkipTracker gaps = createGapTracker();
        IntervalXYDataset dataset = createDataset(process, phases, timeLogEntries, gaps);

        return createChart(dataset, process, phases, gaps);
    }

    private List<String> getPhaseList(ProcessUtil process) {
        List phases = process.getProcessListPlain("Phase_List");
        phases = process.filterPhaseList(phases);
        Collections.reverse(phases);
        return phases;
    }

    private List getTimeLogEntries() {
        String path = getPrefix();
        TimeLog tl = getDashboardContext().getTimeLog();
        List result;
        try {
            result = Collections.list(tl.filter(path, null, null));
            Collections.sort(result);
        } catch (IOException e) {
            e.printStackTrace();
            result = Collections.EMPTY_LIST;
        }
        return result;
    }

    private GapSkipTracker createGapTracker() {
        int gapWidth;
        try {
            double gapSize = Double.parseDouble(getParameter("GapSize"));
            String units = getParameter("GapUnits");
            if (units != null && units.startsWith("M"))
                gapWidth = (int) (gapSize * MINUTE_MILLIS);
            else
                gapWidth = (int) (gapSize * HOUR_MILLIS);
        } catch (Exception e) {
            gapWidth = HOUR_MILLIS;
        }
        return new GapSkipTracker(gapWidth);
    }

    private static final int MINUTE_MILLIS = 60 * 1000;
    private static final int HOUR_MILLIS = 60 * MINUTE_MILLIS;

    private IntervalXYDataset createDataset(ProcessUtil process, List<String> phases, List timeLogEntries,
            GapSkipTracker gaps) {
        XYIntervalSeriesCollection result = new XYIntervalSeriesCollection();

        Map<String, PhaseSeries> series = makeSeries(phases);
        ExceptionSeries exceptions = new ExceptionSeries();

        addTimeLogEntries(process, series, timeLogEntries, gaps, exceptions);

        for (PhaseSeries ps : series.values())
            result.addSeries(ps);
        result.addSeries(exceptions);

        return result;
    }

    private Map<String, PhaseSeries> makeSeries(List<String> phases) {
        Map<String, PhaseSeries> result = new LinkedHashMap();
        for (int i = 0; i < phases.size(); i++) {
            String phase = phases.get(i);
            PhaseSeries series = new PhaseSeries(phase, i);
            result.put(phase, series);
        }
        return result;
    }

    private void addTimeLogEntries(ProcessUtil process, Map<String, PhaseSeries> phases, List timeLogEntries,
            GapSkipTracker gaps, ExceptionSeries exceptions) {
        for (Iterator i = timeLogEntries.iterator(); i.hasNext();) {
            TimeLogEntry tle = (TimeLogEntry) i.next();
            addTimeLogEntry(process, phases, tle, gaps, exceptions);
        }
    }

    private void addTimeLogEntry(ProcessUtil process, Map<String, PhaseSeries> phases, TimeLogEntry tle,
            GapSkipTracker gaps, ExceptionSeries exceptions) {
        String entryPath = tle.getPath();
        String phase = process.getEffectivePhase(entryPath, true);
        PhaseSeries series = phases.get(phase);
        if (series != null) {
            long start = tle.getStartTime().getTime();
            long deltaMins = tle.getElapsedTime() + tle.getInterruptTime();
            long end = start + deltaMins * MINUTE_MILLIS;
            series.add(start, end);
            gaps.addTimeLogEntry(start, end);
            exceptions.maybeAddException(start, end, phase);
        }
    }

    private JFreeChart createChart(IntervalXYDataset dataset, ProcessUtil process, List<String> phases,
            GapSkipTracker gaps) {
        JFreeChart result = ChartFactory.createXYBarChart(null, null, true, null, dataset, PlotOrientation.VERTICAL,
                false, true, false);

        XYPlot xyplot = (XYPlot) result.getPlot();
        DateAxis dateAxis = new DateAxis(null);
        dateAxis.setTickMarksVisible(false);
        dateAxis.setTickLabelsVisible(false);
        dateAxis.setLowerMargin(0.01);
        dateAxis.setUpperMargin(0.01);
        setupGaps(gaps, dateAxis, xyplot);
        xyplot.setDomainAxis(dateAxis);

        String[] phaseNameList = new String[phases.size()];
        for (int i = 0; i < phaseNameList.length; i++)
            phaseNameList[i] = Translator.translate(phases.get(i));
        SymbolAxis symbolaxis = new SymbolAxis(null, phaseNameList);
        symbolaxis.setGridBandsVisible(false);
        xyplot.setRangeAxis(symbolaxis);

        final XYBarRenderer renderer = (XYBarRenderer) xyplot.getRenderer();
        renderer.setUseYInterval(true);
        renderer.setDrawBarOutline(true);
        renderer.setBaseOutlinePaint(Color.black);

        new PhaseChartColorer(process, phases) {
            public void setItemColor(Object key, int pos, Color c) {
                renderer.setSeriesPaint(pos, c);
            }
        }.run();

        int exceptionSeries = dataset.getSeriesCount() - 1;
        renderer.setSeriesPaint(exceptionSeries, EXCEPTION_PAINT);
        renderer.setSeriesFillPaint(exceptionSeries, EXCEPTION_PAINT);
        renderer.setSeriesOutlinePaint(exceptionSeries, EXCEPTION_PAINT);

        renderer.setBaseToolTipGenerator(new TooltipGenerator());

        xyplot.setBackgroundPaint(Color.white);
        xyplot.setDomainGridlinesVisible(false);
        xyplot.setRangeGridlinePaint(Color.lightGray);
        xyplot.setDrawingSupplier(DRAWING_SUPPLIER_FACTORY.newDrawingSupplier());

        result.setAntiAlias(false);
        result.setTextAntiAlias(true);

        parameters.put("title", resources.getString("Title"));

        return result;
    }

    private void setupGaps(GapSkipTracker gapTracker, DateAxis dateAxis, XYPlot plot) {
        if (gapTracker.gaps.isEmpty())
            return;

        SegmentedTimeline timeline = new SegmentedTimeline(1000, 100, 0);
        timeline.setStartTime(gapTracker.leftEnd);

        for (Span gap : gapTracker.gaps) {
            timeline.addException(gap.start + GAP_SPACING, gap.end - 1000);

            long annotationX = gap.start + GAP_SPACING / 2;
            plot.addAnnotation(new XYLineAnnotation(annotationX, VERT_LINE_MIN_Y, annotationX, VERT_LINE_MAX_Y,
                    GAP_STROKE, Color.darkGray));

            double boxW = GAP_SPACING * 0.4;
            XYBoxAnnotation box = new XYBoxAnnotation(annotationX - boxW, VERT_LINE_MIN_Y, annotationX + boxW,
                    VERT_LINE_MAX_Y, null, null, null);
            String toolTip = resources.format("No_Activity_FMT", gap.getStart(), gap.getEnd());
            box.setToolTipText(toolTip);
            plot.addAnnotation(box);
        }

        dateAxis.setTimeline(timeline);
    }

    private static final long GAP_SPACING = 2 * MINUTE_MILLIS;

    private class PhaseSeries extends XYIntervalSeries {

        private int index;

        public PhaseSeries(String phaseName, int index) {
            super(new ComparableValue(phaseName, index));
            this.index = index;
        }

        public void add(long start, long end) {
            add(start, start, end, index, (double) index - 0.4D, (double) index + 0.4D);
        }
    }

    private class ExceptionSeries extends XYIntervalSeries {

        private List<Span> activeEntries;

        public ExceptionSeries() {
            super(new ComparableValue("Problems", EXCEPTION_SERIES_ORDINAL));
            this.activeEntries = new LinkedList<Span>();
        }

        public void maybeAddException(long start, long end, String phaseName) {
            for (Iterator i = activeEntries.iterator(); i.hasNext();) {
                Span e = (Span) i.next();
                if (e.end <= start + ALLOWED_OVERLAP) {
                    i.remove();
                } else {
                    long exceptionEnd = Math.min(end, e.end);
                    super.add(start, start, exceptionEnd, 0, VERT_LINE_MIN_Y, VERT_LINE_MAX_Y);
                }
            }
            activeEntries.add(new Span(start, end));
        }

    }

    private static final int EXCEPTION_SERIES_ORDINAL = -1;
    private static final long ALLOWED_OVERLAP = MINUTE_MILLIS;

    private class GapSkipTracker {

        private int collapseWidth;

        private long leftEnd, rightEnd;

        private List<Span> gaps;

        public GapSkipTracker(int collapseWidth) {
            if (collapseWidth < GAP_SPACING)
                this.collapseWidth = Integer.MAX_VALUE;
            else
                this.collapseWidth = collapseWidth;
            this.leftEnd = this.rightEnd = 0;
            this.gaps = new ArrayList<Span>();
        }

        public void addTimeLogEntry(long start, long end) {
            if (leftEnd == 0) {
                leftEnd = start;
                rightEnd = end;
                return;
            }

            long thisGap = start - rightEnd;
            if (thisGap > collapseWidth) {
                gaps.add(new Span(rightEnd, start));
            }

            rightEnd = Math.max(rightEnd, end);
        }

    }

    private class Span {
        long start, end;

        public Span(long start, long end) {
            this.start = start;
            this.end = end;
        }

        public Date getStart() {
            return new Date(start);
        }

        public Date getEnd() {
            return new Date(end);
        }
    }

    private class TooltipGenerator implements XYToolTipGenerator {
        public String generateToolTip(XYDataset xy, int series, int item) {
            ComparableValue key = (ComparableValue) xy.getSeriesKey(series);
            if (key.getOrdinal() == EXCEPTION_SERIES_ORDINAL)
                return resources.getString("Overlap_Message");

            IntervalXYDataset dataset = (IntervalXYDataset) xy;
            String phaseName = Translator.translate(key.toString());
            long start = dataset.getStartX(series, item).longValue();
            long end = dataset.getEndX(series, item).longValue();
            return resources.format("Time_Log_Entry_FMT", phaseName, new Date(start), new Date(end));
        }
    }

    private static final DrawingSupplierFactory DRAWING_SUPPLIER_FACTORY = new DrawingSupplierFactory()
            .setPaintSequence(DrawingSupplierFactory.PALETTE1)
            .setFillPaintSequence(DrawingSupplierFactory.PALETTE1);

    private static final Color EXCEPTION_PAINT = new Color(1f, 0.5f, 0.5f);

    private static final Stroke GAP_STROKE = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1f,
            new float[] { 10f, 5f }, 0f);

    private static final double VERT_LINE_MIN_Y = -1;
    private static final double VERT_LINE_MAX_Y = 99;

}