Mercurial > dive4elements > gnv-client
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 > 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 > (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 :