view gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalProfileChart.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 8b6ef091d38c
children ec512e7992c6
line wrap: on
line source
package de.intevation.gnv.chart;

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

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

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;

import org.apache.log4j.Logger;

import org.jfree.chart.ChartTheme;

import org.jfree.chart.axis.Axis;
import org.jfree.chart.axis.NumberAxis;

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

import org.jfree.data.Range;

import org.jfree.data.general.Series;

import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

/**
 * This class is used to create xy charts of vertical profiles.
 *
 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
 */
public class VerticalProfileChart
extends      AbstractXYLineChart
{
    /**
     * Default axis identifier which is used if @see #getDependendAxisName does
     * not return a value. The value of this field is {@value}.
     */
    public static final String DEFAULT_AXIS = "KPOSITION";

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

    /**
     * Constant used for gap detection. Its value is {@value}.
     */
    protected static int PERCENTAGE     = 5;

    /**
     * Constnat used for gap detection in @see #gridDetection.
     */
    protected final double GAP_MAX_LEVEL  = Math.sqrt(2.0);

    /**
     * Constant used for gap detection in @see #addGaps. Its value is {@value}.
     */
    protected final int    GAP_MAX_VALUES = 60;

    /**
     * Map to store max ranges of each parameter
     * (org.jfree.chart.axis.Axis.setAutoRange(true) doesn't seem to work
     * properly.
     */
    protected Map values;

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


    /**
     * Constructor used to create xy-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 VerticalProfileChart(
        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.HORIZONTAL;
        this.linesVisible     = linesVisible;
        this.shapesVisible    = shapesVisible;
        this.datasets         = new HashMap();
        this.ranges           = new HashMap();
        this.values           = new HashMap();
    }


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

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

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

        int idx           = 0;
        int startPos      = 0;
        int endPos        = 0;
        double startValue = 0;
        double endValue   = 0;

        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) {
                    gapDetection(results, series, 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 XYSeries(seriesName);
            }

            addValue(row, series);
            Object x = getValue(row);
            Double y = row.getDouble("YORDINATE");
            if (x != null && y != null) {
                storeMaxRange(ranges, y, parameter);
                storeMaxValue(values, x, parameter);
            }
            endPos++;
        }

        if (results.length == 0)
            return;

        gapDetection(results, series, startPos, endPos);
        addSeries(series, parameter, idx);

        addDatasets();
    }


    /**
     * Extract the important value from <code>Result</code> object.
     *
     * @param row <code>Result</code> object which contains a required value.
     *
     * @return X-ordinate
     */
    protected Object getValue(Result row) {
        return row.getDouble("XORDINATE");
    }


    /**
     * General method to start a gap detection. The switch between standard gap
     * detection method <code>addGaps</code> and a specialized method
     * <code>addGapsOnGrid</code> is done by a parameter <code>DATEID</code>
     * which is stored a each <code>Result</code> object. Specialized method is
     * used if <code>DATEID</code> equals 2, otherwise the standard method is
     * used.
     *
     * @param results Array of <code>Result</code> objects storing data of
     * this chart.
     * @param series Series used to add gaps.
     * @param startPos Index of first element of series in results.
     * @param endPos Index of last element of series in results.
     */
    protected void gapDetection(
        Result[] results,
        Series   series,
        int      startPos,
        int      endPos
    ) {
        double startValue = results[startPos].getDouble("XORDINATE");
        double endValue   = results[endPos-1].getDouble("XORDINATE");
        if (results[0].getInteger("DATAID") == 2)
            addGapsOnGrid(results, series, startPos, endPos);
        else
            addGaps(results, series, startValue, endValue, startPos, endPos);
    }

    @Override
    protected void prepareAxis(String seriesKey, int idx) {
        super.prepareAxis(seriesKey, idx);

        XYPlot plot = chart.getXYPlot();
        NumberAxis domainAxis = (NumberAxis) plot.getRangeAxis();
        NumberAxis rangeAxis  = (NumberAxis) plot.getDomainAxis();

        Range domainRange = domainAxis.getRange();
        Range rangeRange  = rangeAxis.getRange();
        log.debug("Domain axis range before: " + domainRange.toString());
        log.debug("Range axis range before: " + rangeRange.toString());

        domainRange = Range.expand(domainRange, LOWER_MARGIN, UPPER_MARGIN);
        rangeRange = Range.expand(rangeRange, LOWER_MARGIN, UPPER_MARGIN);

        double lower = domainRange.getLowerBound();
        double upper = domainRange.getUpperBound();

        if (lower == upper) {
            domainRange = new Range(
                lower - (lower * 0.05d),
                upper + (upper * 0.05d));
        }

        log.debug("Domain axis range after: " + domainRange.toString());
        log.debug("Range axis range after: " + rangeRange.toString());
        domainAxis.setRange(domainRange);
        rangeAxis.setRange(rangeRange);

        plot.setRangeAxis(domainAxis);
    }


    /**
     * Method to expand max range of a range axis identified by seriesKey.
     * <code>LOWER_MARGIN</code> and <code>UPPER_MARGIN</code> are used to
     * expand the range.
     *
     * @param seriesKey Key to identify the series stored at the current
     * Dataset.
     * @param idx Currently not used.
     */
    protected void prepareRangeAxis(String seriesKey, int idx) {
        XYPlot plot      = chart.getXYPlot();
        NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();

        Range xRange     = (Range) values.get(seriesKey);
        xAxis.setRange(Range.expand(xRange, LOWER_MARGIN, UPPER_MARGIN));
        log.debug("Max X-Range of dataset is: " + xRange.toString());
    }


    /**
     * @see de.intevation.gnv.chart.AbstractXYLineChart#addValue(Result, Series)
     */
    @Override
    protected void addValue(Result row, Series series) {
        ((XYSeries) series).add(
            row.getDouble("XORDINATE"),
            row.getDouble("YORDINATE")
        );
    }


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

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

        XYSeriesCollection xysc = null;

        if (datasets.containsKey(parameter))
            xysc = (XYSeriesCollection) datasets.get(parameter);
        else
            xysc = new XYSeriesCollection();

        xysc.addSeries((XYSeries) series);
        datasets.put(parameter, xysc);
    }


    /**
     * 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;

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

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


    /**
     * Method used to store the max y-range of each parameter in this chart.
     *
     * @param values Map to store max values for each parameter.
     * @param val Value used to be a Double.
     * @param parameter Title used to identify a range object stored in values.
     */
    protected void storeMaxValue(Map values, Object val, String parameter) {
        double value = ((Double) val).doubleValue();
        Range  range = null;

        range = values.containsKey(parameter)
            ? (Range) values.get(parameter)
            : new Range(value, value);

        double lower = range.getLowerBound();
        double upper = range.getUpperBound();

        lower = value < lower ? value : lower;
        upper = value > upper ? value : upper;

        values.put(parameter, new Range(lower, upper));
    }


    /**
     * @param locale
     * @see de.intevation.gnv.chart.AbstractXYLineChart#localizeDomainAxis(Axis,
     * Locale)
     */
    @Override
    protected void localizeDomainAxis(Axis axis, Locale locale) {
        // call localizeRangeAxis from superclass which formats NumberAxis
        super.localizeRangeAxis(axis, locale);
    }


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


    /**
     * Method used to add gaps between data points on grids. The real detection
     * is done in <code>gridDetection</code>.
     *
     * @param results Array of <code>Result</code> objects storing the relevant
     * values.
     * @param series Series object where the gaps are added to.
     * @param startPos Index of first element which should be used in gap
     * detection. Other series are stored in results as well.
     * @param endPos Index of last element which should be used in gap
     * detection.
     */
    protected void addGapsOnGrid(
        Result[] results,
        Series   series,
        int      startPos,
        int      endPos
    ) {
        String axis = null;

        if (results.length > (startPos+1)) {
            axis = getDependendAxisName(
                results[startPos],
                results[startPos+1]
            );
        }
        else {
            axis = DEFAULT_AXIS;
        }

        double range        = 0;
        int    last         = 0;
        int    current      = 0;

        for (int i = startPos+1; i < endPos; i++) {
            last    = results[i-1].getInteger(axis);
            current = results[i].getInteger(axis);

            boolean detected = gridDetection(last, current);

            if (detected) {
                double xOld = results[i-1].getDouble("XORDINATE");
                double xNow = results[i].getDouble("XORDINATE");
                log.debug("Gap detected on grid between "+ xOld +" and "+ xNow);
                ((XYSeries) series).add(xOld+0.0001, null);
            }
        }
    }


    /**
     * Standarad method to add gaps. There are two different methods to detect
     * gaps. <code>simpleDetection</code> is used if the number of data points
     * in this chart is lower than <code>GAP_MAX_VALUES</code>. Otherwise
     * <code>specialDetection</code> is used. A data point with
     * <code>null</code> value is added where a gap should be. This lets
     * JFreeChart break the current graph.
     *
     * @param results Array of <code>Result</code> objects storing the relevant
     * values.
     * @param series Series object where the gaps are added to.
     * @param startValue First data point value in series.
     * @param endValue Last data point value in series.
     * @param startPos Index of first data point in results which contains all
     * data points of all series.
     * @param endPos Index of last data point in results which contains all data
     * points of all series.
     */
    protected void addGaps(
        Result[] results,
        Series   series,
        double   startValue,
        double   endValue,
        int      startPos,
        int      endPos
    ) {

        double last    = 0;
        double current = 0;
        int    num     = results.length;

        for (int i = startPos+1; i < endPos; i++) {
            boolean detected = false;

            last    = results[i-1].getDouble("YORDINATE");
            current = results[i].getDouble("YORDINATE");

            // gap detection for more than GAP_MAX_VALUES values
            if (num > GAP_MAX_VALUES)
                detected = simpleDetection(startValue, endValue, last, current);
            // gap detection for less than GAP_MAX_VALUES values
            else
                detected = specialDetection(
                    startValue,
                    endValue,
                    last,
                    current,
                    num
                );

            if (detected) {
                log.info("Gap between " + last + " and " + current);
                ((XYSeries) series).add((last+current)/2, null);
            }
        }
    }


    /**
     * Simple method to detect gaps. A gap is detected if the delta between two
     * data points (current, last) is bigger than <code>PERCENTAGE</code> percent
     * of delta of start and end.
     * <br>
     * (smallDelta &gt; delta / 100 * PERCENTAGE)
     *
     * @param start First data point value in a series.
     * @param end Last data point value in a series.
     * @param last Left value
     * @param current Right value
     *
     * @return true, if a gap is detected between last and current - otherwise
     * false.
     */
    protected boolean simpleDetection(
        double start,
        double end,
        double last,
        double current
    ) {
        double delta      = Math.abs(end - start);
        double smallDelta = Math.abs(current - last);

        return (smallDelta > delta / 100 * PERCENTAGE);
    }


    /**
     * Method to detect gaps between two data points. Following formula is used
     * for detection:<br>
     * smallDelta &gt; (3.0 / (count - 1) * delta)<br>
     * smallDelta = current - last<br>
     * delta      = end - start
     *
     * @param start First data point value in a series.
     * @param end Last data point value in a series.
     * @param last Left value
     * @param current Right value
     *
     * @param count
     * @return true, if a gap is detected between last and current - otherwise
     * false.
     */
    protected boolean specialDetection(
        double start,
        double end,
        double last,
        double current,
        int    count
    ) {
        double delta      = Math.abs(end - start);
        double smallDelta = Math.abs(current - last);

        return (smallDelta > (3.0 / (count - 1) * delta));
    }


    /**
     * Method used to detect gaps between two data points grids. If the delta
     * between current and last is bigger than <code>GAP_MAX_LEVEL</code>, a gap
     * is detected.
     *
     * @param last Left value
     * @param current Right value
     *
     * @return True, if a gap was detected - otherwise false.
     */
    protected boolean gridDetection(double last, double current) {
        if (log.isDebugEnabled()) {
            log.debug("######################################################");
            log.debug("Parameters for gap detection");
            log.debug("Defined gap size for grids: " + GAP_MAX_LEVEL);
            log.debug("1st value to compare: " + last);
            log.debug("2nd value to compare: " + current);
            log.debug("Difference: " + Math.abs(current - last));
        }
        return (Math.abs(current - last) > GAP_MAX_LEVEL);
    }


    /**
     * This method returns the key which is used to retrieve the y-value served
     * by a <code>Result</code> object.
     *
     * @param first <code>Result</code> object - not used in this class.
     * @param second <code>Result</code> object - not used in this class.
     *
     * @return the string "KPOSITION"
     */
    protected String getDependendAxisName(Result first, Result second) {
        return "KPOSITION";
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :

http://dive4elements.wald.intevation.org