Mercurial > dive4elements > gnv-client
view gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalProfileChart.java @ 848:47280aff0eb7
Added an y-axis label in histograms (issue243).
gnv-artifacts/trunk@964 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author | Ingo Weinzierl <ingo.weinzierl@intevation.de> |
---|---|
date | Wed, 21 Apr 2010 09:43:46 +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 > 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 :