diff gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalProfileChart.java @ 1119:7c4f81f74c47

merged gnv-artifacts
author Thomas Arendsen Hein <thomas@intevation.de>
date Fri, 28 Sep 2012 12:14:00 +0200
parents f953c9a559d8
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalProfileChart.java	Fri Sep 28 12:14:00 2012 +0200
@@ -0,0 +1,647 @@
+/*
+ * Copyright (c) 2010 by Intevation GmbH
+ *
+ * This program is free software under the LGPL (>=v2.1)
+ * Read the file LGPL.txt coming with the software for details
+ * or visit http://www.gnu.org/licenses/ if it does not exist.
+ */
+
+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");
+
+        int items = resultSet.size();
+        log.debug("Found " + items + " items for this chart.");
+
+        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) {
+                    if (startPos >= 0 && endPos < items) {
+                        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 (items == 0)
+            return;
+
+        if (startPos >= 0 && endPos < items) {
+            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(idx);
+
+        Range domainRange = domainAxis.getRange();
+        log.debug("Domain axis range before: " + domainRange.toString());
+
+        domainRange = Range.expand(domainRange, LOWER_MARGIN, UPPER_MARGIN);
+
+        double lower = domainRange.getLowerBound();
+        double upper = domainRange.getUpperBound();
+
+        if (lower == upper) {
+            double lo = lower > 0 ? lower - lower*0.05d : lower + lower*0.05d;
+            double up = upper > 0 ? upper + upper*0.05d : upper - upper*0.05d;
+
+            domainRange = new Range(lo, up);
+        }
+
+        log.debug("Range axis range after: " + domainRange.toString());
+        domainAxis.setRange(domainRange);
+        plot.setRangeAxis(idx, domainAxis);
+    }
+
+
+    /**
+     * Method to expand max range of a range axis.
+     * <code>LOWER_MARGIN</code> and <code>UPPER_MARGIN</code> are used to
+     * expand the range.
+     */
+    protected void prepareRangeAxis(String seriesKey, int idx) {
+        log.debug("Adjust domain range now...");
+        XYPlot plot      = chart.getXYPlot();
+        NumberAxis yAxis = (NumberAxis) plot.getDomainAxis();
+
+        Range yRange = yAxis.getRange();
+        double lo    = yRange.getLowerBound();
+        double hi    = yRange.getUpperBound();
+
+        Iterator iter = values.values().iterator();
+        while (iter.hasNext()) {
+            Range tmp = (Range) iter.next();
+            log.debug("Series range: " + tmp.toString());
+
+            lo = lo < tmp.getLowerBound() ? lo : tmp.getLowerBound();
+            hi = hi > tmp.getUpperBound() ? hi : tmp.getUpperBound();
+        }
+
+        Range merged = Range.expand(
+            new Range(lo, hi),
+            LOWER_MARGIN, UPPER_MARGIN);
+        log.debug("Calculated range for all series = " + merged.toString());
+
+        yAxis.setRange(merged);
+        plot.setDomainAxis(yAxis);
+    }
+
+
+    /**
+     * @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
+                );
+            }
+        }
+
+        prepareRangeAxis(null, -1);
+    }
+
+
+    /**
+     * 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