view gnv-artifacts/src/main/java/de/intevation/gnv/chart/TimeSeriesChart.java @ 1038:9981452c7e75

First step: Added a new state handling the selection between vector or scalar and a new transition in timeseries to provide vector values (issue27). gnv-artifacts/trunk@1110 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Ingo Weinzierl <ingo.weinzierl@intevation.de>
date Tue, 18 May 2010 16:28:05 +0000
parents 2423cefe7d39
children f953c9a559d8
line wrap: on
line source
package de.intevation.gnv.chart;

import de.intevation.gnv.artifacts.ressource.RessourceFactory;

import de.intevation.gnv.geobackend.base.Result;

import de.intevation.gnv.state.describedata.KeyValueDescibeData;

import de.intevation.gnv.timeseries.gap.TimeGap;

import java.text.DateFormat;
import java.text.SimpleDateFormat;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.TimeZone;

import org.apache.log4j.Logger;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartTheme;

import org.jfree.chart.axis.Axis;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickUnit;
import org.jfree.chart.axis.DateTickUnitType;
import org.jfree.chart.axis.TickUnitSource;
import org.jfree.chart.axis.TickUnits;
import org.jfree.chart.axis.ValueAxis;

import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;

import org.jfree.data.general.Series;

import org.jfree.data.time.Minute;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;

/**
 * This class is used to create timeseries charts. The domain axis contains
 * multiple date/time objects.
 *
 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
 */
public class TimeSeriesChart
extends      AbstractXYLineChart
{

    /**
     * Constant format which can be useful to format date items. Value is
     * {@value}.
     */
    public static final String DEFAULT_DATE_FORMAT = "dd-MMM-yyyy";

    /**
     * Constant field used if no gap detection should be done here. This field
     * is used in @see #getTimeGapValue. Value is {@value}.
     */
    public static final long   NO_TIME_GAP = Long.MAX_VALUE - 1000;

    /**
     * Percentage used for gap detection. Its value is {@value}.
     */
    public static int    GAP_SIZE    = 5; // in percent

    /**
     * Logger used for logging with log4j.
     */
    private static Logger log = Logger.getLogger(TimeSeriesChart.class);

    static {
        /* The percentage defining the width of a gap should be configured in
         * conf.xml instead of being configured in a system property */
        GAP_SIZE = Integer.getInteger("chart.gap.percentage", GAP_SIZE);
    }


    /**
     * Constructor used to create <code>TimeSeries</code> charts.
     *
     * @param labels Labels used to be displayed in title, subtitle and so on.
     * @param theme ChartTheme used to adjust the rendering of this chart.
     * @param parameters Collection containing a bunch of parameters.
     * @param measurements Collection containing a bunch of measurements.
     * @param dates Collection containing a bunch of date objects.
     * @param result Collection containing a bunch of <code>Result</code>
     * objects which contain the actual data items to be displayed.
     * @param timeGaps Collection with timegap definitions.
     * @param locale Locale used to specify the format of labels, numbers, ...
     * @param linesVisible Render lines between data points if true, otherwise
     * not.
     * @param shapesVisible Render vertices as points if true, otherwise not.
     */
    public TimeSeriesChart(
        ChartLabels labels,
        ChartTheme  theme,
        Collection  parameters,
        Collection  measurements,
        Collection  dates,
        Collection  result,
        Collection  timeGaps,
        Locale      locale,
        boolean     linesVisible,
        boolean     shapesVisible
    ) {
        this.labels           = labels;
        this.theme            = theme;
        this.parameters       = parameters;
        this.measurements     = measurements;
        this.dates            = dates;
        this.resultSet        = result;
        this.timeGaps         = timeGaps;
        this.locale           = locale;
        this.PLOT_ORIENTATION = PlotOrientation.VERTICAL;
        this.linesVisible     = linesVisible;
        this.shapesVisible    = shapesVisible;
        this.datasets         = new HashMap();
        this.ranges           = new HashMap();
    }


    /**
     * see de.intevation.gnv.chart.AbstractXYLineChart#initChart()
     */
    @Override
    protected void initChart() {
        chart = ChartFactory.createTimeSeriesChart(
            labels.getTitle(),
            labels.getDomainAxisLabel(),
            null,
            null,
            true,
            false,
            false
        );

        XYPlot plot = (XYPlot) chart.getPlot();
        plot.setDomainAxis(0, new DateAxis(
            labels.getDomainAxisLabel(), TimeZone.getDefault(), locale));
    }


    /**
     * @see de.intevation.gnv.chart.AbstractXYLineChart#initData()
     */
    protected void initData() {
        log.debug("init data for timeseries chart");

        String  breakPoint1       = null;
        String  breakPoint2       = null;
        String  breakPoint3       = null;

        Iterator   iter       = resultSet.iterator();
        Result     row        = null;
        String     seriesName = null;
        String     parameter  = null;
        TimeSeries series     = null;

        int  idx       = 0;
        int  startPos  = 0;
        int  endPos    = 0;
        Date startDate = null;
        Date endDate   = null;

        Result[] results =
            (Result[]) resultSet.toArray(new Result[resultSet.size()]);

        while (iter.hasNext()) {
            row = (Result) iter.next();

            // add current data to plot and prepare for next one
            if (!row.getString("GROUP1").equals(breakPoint1) ||
                !row.getString("GROUP2").equals(breakPoint2) ||
                !row.getString("GROUP3").equals(breakPoint3)
            ) {
                log.debug("prepare data/plot for next dataset");

                if(series != null) {
                    // add gaps before adding series to chart
                    startDate = results[startPos].getDate("XORDINATE");
                    endDate   = results[endPos-1].getDate("XORDINATE");
                    addGaps(results,series,startDate,endDate,startPos,endPos);
                    addSeries(series, parameter, idx);

                    startPos  = endPos + 1;
                }

                // prepare variables for next plot
                breakPoint1 = row.getString("GROUP1");
                breakPoint2 = row.getString("GROUP2");
                breakPoint3 = row.getString("GROUP3");

                seriesName = createSeriesName(
                    breakPoint1,
                    breakPoint2,
                    breakPoint3
                );
                parameter = findParameter(seriesName);

                log.debug("next dataset is '" + seriesName + "'");
                series = new TimeSeries(seriesName, Minute.class);
            }

            addValue(row, series);
            storeMaxRange(ranges, row.getDouble("YORDINATE"), parameter);
            endPos++;
        }

        if (startPos < results.length && endPos-1 < results.length) {
            // add the last dataset if existing to plot and prepare its axis
            startDate = results[startPos].getDate("XORDINATE");
            endDate = results[endPos-1].getDate("XORDINATE");
            addGaps(results, series, startDate, endDate, startPos, endPos);
            addSeries(series, parameter, idx);
        }

        addDatasets();
    }


    /**
     * @see de.intevation.gnv.chart.AbstractXYLineChart#addValue(Result, Series)
     */
    protected void addValue(Result row, Series series) {
        ((TimeSeries) series).addOrUpdate(
            new Minute(row.getDate("XORDINATE")),
            row.getDouble("YORDINATE")
        );
    }


    /**
     * @param parameter
     * @see de.intevation.gnv.chart.AbstractXYLineChart#addSeries(Series,
     * String, int)
     */
    protected void addSeries(Series series, String parameter, int idx) {
        log.debug("add series (" + parameter + ")to timeseries chart");

        if (series == null) {
            log.warn("no data to add");
            return;
        }

        TimeSeriesCollection tsc = null;

        if (datasets.containsKey(parameter))
            tsc = (TimeSeriesCollection) datasets.get(parameter);
        else
            tsc = new TimeSeriesCollection();

        tsc.addSeries((TimeSeries) series);
        datasets.put(parameter, tsc);
    }


    /**
     * Method to add processed datasets to plot. Each dataset is adjusted using
     * <code>prepareAxis</code> and <code>adjustRenderer</code> methods.
     */
    protected void addDatasets() {
        Iterator   iter = parameters.iterator();
        XYPlot     plot = chart.getXYPlot();
        int        idx  = 0;

        TimeSeriesCollection tsc  = null;
        KeyValueDescibeData  data = null;
        String               key  = null;
        while (iter.hasNext()) {
            data = (KeyValueDescibeData) iter.next();
            key  = data.getValue();

            if (datasets.containsKey(key)) {
                tsc  = (TimeSeriesCollection)datasets.get(key);
                plot.setDataset(idx, tsc );
                log.debug("Added " + key + " parameter to plot.");
                prepareAxis(key, idx);
                adjustRenderer(
                    idx++,
                    tsc.getSeriesCount(),
                    linesVisible,
                    shapesVisible
                );
            }
        }
    }


    /**
     * @param locale
     * @see de.intevation.gnv.chart.AbstractXYLineChart#localizeDomainAxis(Axis,
     * Locale)
     */
    protected void localizeDomainAxis(Axis axis, Locale locale) {
        ((ValueAxis)axis).setStandardTickUnits(createStandardDateTickUnits(
            TimeZone.getDefault(),
            locale));
    }


    /**
     * @param zone
     * @param locale
     * @return TickUnitSource
     * @see org.jfree.chart.axis.DateAxis#createStandardDateTickUnits(TimeZone,
     * Locale)
     */
    public static TickUnitSource createStandardDateTickUnits(
        TimeZone zone,
        Locale locale)
    {
        /*
         * This method have been copied from JFreeChart's DateAxis class.
         * DateFormat objects are hard coded in DateAxis and cannot be adjusted.
         */
        if (zone == null) {
            throw new IllegalArgumentException("Null 'zone' argument.");
        }
        if (locale == null) {
            throw new IllegalArgumentException("Null 'locale' argument.");
        }
        TickUnits units = new TickUnits();

        // date formatters
        DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale);
        DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale);
        DateFormat f3 = new SimpleDateFormat("HH:mm", locale);
        DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm", locale);
        DateFormat f5 = new SimpleDateFormat("d-MMM yyyy", locale);
        DateFormat f6 = new SimpleDateFormat("MMM-yyyy", locale);
        DateFormat f7 = new SimpleDateFormat("yyyy", locale);

        f1.setTimeZone(zone);
        f2.setTimeZone(zone);
        f3.setTimeZone(zone);
        f4.setTimeZone(zone);
        f5.setTimeZone(zone);
        f6.setTimeZone(zone);
        f7.setTimeZone(zone);

        // milliseconds
        units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 1, f1));
        units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 5,
                DateTickUnitType.MILLISECOND, 1, f1));
        units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 10,
                DateTickUnitType.MILLISECOND, 1, f1));
        units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 25,
                DateTickUnitType.MILLISECOND, 5, f1));
        units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 50,
                DateTickUnitType.MILLISECOND, 10, f1));
        units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 100,
                DateTickUnitType.MILLISECOND, 10, f1));
        units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 250,
                DateTickUnitType.MILLISECOND, 10, f1));
        units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 500,
                DateTickUnitType.MILLISECOND, 50, f1));

        // seconds
        units.add(new DateTickUnit(DateTickUnitType.SECOND, 1,
                DateTickUnitType.MILLISECOND, 50, f2));
        units.add(new DateTickUnit(DateTickUnitType.SECOND, 5,
                DateTickUnitType.SECOND, 1, f2));
        units.add(new DateTickUnit(DateTickUnitType.SECOND, 10,
                DateTickUnitType.SECOND, 1, f2));
        units.add(new DateTickUnit(DateTickUnitType.SECOND, 30,
                DateTickUnitType.SECOND, 5, f2));

        // minutes
        units.add(new DateTickUnit(DateTickUnitType.MINUTE, 1,
                DateTickUnitType.SECOND, 5, f3));
        units.add(new DateTickUnit(DateTickUnitType.MINUTE, 2,
                DateTickUnitType.SECOND, 10, f3));
        units.add(new DateTickUnit(DateTickUnitType.MINUTE, 5,
                DateTickUnitType.MINUTE, 1, f3));
        units.add(new DateTickUnit(DateTickUnitType.MINUTE, 10,
                DateTickUnitType.MINUTE, 1, f3));
        units.add(new DateTickUnit(DateTickUnitType.MINUTE, 15,
                DateTickUnitType.MINUTE, 5, f3));
        units.add(new DateTickUnit(DateTickUnitType.MINUTE, 20,
                DateTickUnitType.MINUTE, 5, f3));
        units.add(new DateTickUnit(DateTickUnitType.MINUTE, 30,
                DateTickUnitType.MINUTE, 5, f3));

        // hours
        units.add(new DateTickUnit(DateTickUnitType.HOUR, 1,
                DateTickUnitType.MINUTE, 5, f3));
        units.add(new DateTickUnit(DateTickUnitType.HOUR, 2,
                DateTickUnitType.MINUTE, 10, f3));
        units.add(new DateTickUnit(DateTickUnitType.HOUR, 4,
                DateTickUnitType.MINUTE, 30, f3));
        units.add(new DateTickUnit(DateTickUnitType.HOUR, 6,
                DateTickUnitType.HOUR, 1, f3));
        units.add(new DateTickUnit(DateTickUnitType.HOUR, 12,
                DateTickUnitType.HOUR, 1, f4));

        // days
        units.add(new DateTickUnit(DateTickUnitType.DAY, 1,
                DateTickUnitType.HOUR, 1, f5));
        units.add(new DateTickUnit(DateTickUnitType.DAY, 2,
                DateTickUnitType.HOUR, 1, f5));
        units.add(new DateTickUnit(DateTickUnitType.DAY, 7,
                DateTickUnitType.DAY, 1, f5));
        units.add(new DateTickUnit(DateTickUnitType.DAY, 15,
                DateTickUnitType.DAY, 1, f5));

        // months
        units.add(new DateTickUnit(DateTickUnitType.MONTH, 1,
                DateTickUnitType.DAY, 1, f6));
        units.add(new DateTickUnit(DateTickUnitType.MONTH, 2,
                DateTickUnitType.DAY, 1, f6));
        units.add(new DateTickUnit(DateTickUnitType.MONTH, 3,
                DateTickUnitType.MONTH, 1, f6));
        units.add(new DateTickUnit(DateTickUnitType.MONTH, 4,
                DateTickUnitType.MONTH, 1, f6));
        units.add(new DateTickUnit(DateTickUnitType.MONTH, 6,
                DateTickUnitType.MONTH, 1, f6));

        // years
        units.add(new DateTickUnit(DateTickUnitType.YEAR, 1,
                DateTickUnitType.MONTH, 1, f7));
        units.add(new DateTickUnit(DateTickUnitType.YEAR, 2,
                DateTickUnitType.MONTH, 3, f7));
        units.add(new DateTickUnit(DateTickUnitType.YEAR, 5,
                DateTickUnitType.YEAR, 1, f7));
        units.add(new DateTickUnit(DateTickUnitType.YEAR, 10,
                DateTickUnitType.YEAR, 1, f7));
        units.add(new DateTickUnit(DateTickUnitType.YEAR, 25,
                DateTickUnitType.YEAR, 5, f7));
        units.add(new DateTickUnit(DateTickUnitType.YEAR, 50,
                DateTickUnitType.YEAR, 10, f7));
        units.add(new DateTickUnit(DateTickUnitType.YEAR, 100,
                DateTickUnitType.YEAR, 20, f7));

        return units;
    }


    /**
     * Method to get a message from resource bundle.
     *
     * @param locale Locale used to specify the resource bundle.
     * @param key Key to specify the required message.
     * @param def Default string if resource is not existing.
     *
     * @return Message
     */
    protected String getMessage(Locale locale, String key, String def) {
        return RessourceFactory.getInstance().getRessource(locale, key, def);
    }


    /**
     * @see de.intevation.gnv.chart.AbstractXYLineChart#createSeriesName(String,
     * String, String)
     */
    protected String createSeriesName(
        String breakPoint1,
        String breakPoint2,
        String breakPoint3
    ) {
        log.debug("create seriesname of timeseries chart");
        return findValueTitle(parameters, breakPoint1) +
            " " +
            findValueTitle(measurements, breakPoint2) +
            "m";
    }


    /**
     * Method to add gaps between two data points. The max valid space between
     * two data points is calculated by <code>calculateGapSize</code>.
     *
     * @param results All data points in this dataset.
     * @param series Series to be processed.
     * @param startDate Date item where the scan for gaps should begin.
     * @param endDate Date item where the scan should end.
     * @param startPos Start position of this series in <code>results</code>.
     * @param endPos End position of a series in <code>results</code>
     */
    protected void addGaps(
        Result[] results,
        Series   series,
        Date     startDate,
        Date     endDate,
        int      startPos,
        int      endPos
    ) {
        int  gapID   = results[startPos].getInteger("GAPID");
        long maxDiff = calculateGapSize(
            startDate, endDate, startPos, endPos, gapID
        );

        if (log.isDebugEnabled()) {
            log.debug("*****************************************************");
            log.debug("Values of gap detection.");
            log.debug("Start date: " + startDate.toString());
            log.debug("End date: " + endDate.toString());
            long diff = endDate.getTime() - startDate.getTime();
            log.debug("Time difference (in ms): " + diff);
            log.debug("Time difference (in h): " + (diff/(1000*60*60)));
            log.debug("Configured gap size (in %): " + GAP_SIZE);
            log.debug("Calculated gap size (in ms): " + maxDiff);
            log.debug("Calculated gap size (in h): " + (maxDiff/(1000*60*60)));
            log.debug("*****************************************************");
        }

        Date last = startDate;
        for (int i = startPos+1; i < endPos; i++) {
            Result res = results[i];
            Date   now = res.getDate("XORDINATE");

            if ((now.getTime() - last.getTime()) > maxDiff) {
                // add gap, add 1 minute to last date and add null value
                log.info(
                    "Gap between " +
                    last.toString() + " and " + now.toString()
                );
                last.setTime(last.getTime() + 60000);
                ((TimeSeries) series).addOrUpdate(new Minute(last), null);
            }

            last = now;
        }
    }


    /**
     * Method to calculate the max space between two data points.
     *
     * @param start First date
     * @param end Last date
     * @param startPos Start position of the current series in the collection
     * containing the bunch of series.
     * @param endPos End position of the current series in the collection
     * containing the bunch of series.
     * @param gapID Gap id used to specify the time intervals.
     *
     * @return Min size of a gap.
     */
    protected long calculateGapSize(
        Date start,
        Date end,
        int  startPos,
        int  endPos,
        int  gapID
    ){
        long maxGap   = (end.getTime() - start.getTime()) / 100 * GAP_SIZE;
        long interval = getTimeGapValue(start, end, startPos, endPos, gapID);

        if (maxGap < interval)
            maxGap = interval + 10;

        return maxGap;
    }


    /**
     * Determine the interval size between two data points.
     *
     * @param dStart Start date
     * @param dEnd End date
     * @param pStart Index of start point in series used to specify the total
     * amount of date items.
     * @param pEnd Index of end point in series used to specify the total amount
     * of date items.
     * @param gapID Gap id used to determine gaps configured in a xml document.
     *
     * @return Interval size between two data points.
     */
    protected long getTimeGapValue(
        Date dStart,
        Date dEnd,
        int  pStart,
        int  pEnd,
        int  gapID
    ){
        long gap = 0;

        if (gapID < 0 || gapID >= 99) {

            if (gapID == -1) {
                // no gaps in meshes
                gap = NO_TIME_GAP;
            }
            else if (pEnd-pStart < 60) {
                gap = (3/(pEnd-pStart)) * (dEnd.getTime() - dStart.getTime());
            }
        }
        else{
            Iterator it = timeGaps.iterator();

            while (it.hasNext()) {
                TimeGap tempTimeGap = (TimeGap) it.next();

                if (tempTimeGap.getKey() == gapID){
                    String unit     = tempTimeGap.getUnit();
                    int    gapValue = tempTimeGap.getValue();

                    if (unit.equals(TimeGap.TIME_UNIT_MINUTE)) {
                        gap = gapValue * TimeGap.MINUTE_IN_MILLIS;
                    }
                    else if (unit.equals(TimeGap.TIME_UNIT_HOUR)) {
                        gap = gapValue * TimeGap.HOUR_IN_MILLIS;
                    }
                    else if (unit.equals(TimeGap.TIME_UNIT_DAY)) {
                        gap = gapValue * TimeGap.DAY_IN_MILLIS;
                    }
                    else if (unit.equals(TimeGap.TIME_UNIT_WEEK)) {
                        gap = gapValue * TimeGap.WEEK_IN_MILLIS;
                    }
                    else if (unit.equals(TimeGap.TIME_UNIT_MONTH)) {
                        gap = gapValue * (TimeGap.DAY_IN_MILLIS *30);
                    }
                    else if (unit.equals(TimeGap.TIME_UNIT_YEAR)) {
                        gap = gapValue * (TimeGap.DAY_IN_MILLIS *365);
                    }
                    break;
                }
            }
        }

        return gap;
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :

http://dive4elements.wald.intevation.org