ingo@1115: /* ingo@1115: * Copyright (c) 2010 by Intevation GmbH ingo@1115: * ingo@1115: * This program is free software under the LGPL (>=v2.1) ingo@1115: * Read the file LGPL.txt coming with the software for details ingo@1115: * or visit http://www.gnu.org/licenses/ if it does not exist. ingo@1115: */ ingo@1115: ingo@298: package de.intevation.gnv.chart; ingo@298: sascha@779: import de.intevation.gnv.geobackend.base.Result; sascha@779: sascha@779: import de.intevation.gnv.state.describedata.KeyValueDescibeData; sascha@779: ingo@298: import java.util.Collection; ingo@334: import java.util.HashMap; ingo@298: import java.util.Iterator; ingo@298: import java.util.Locale; sascha@779: import java.util.Map; ingo@298: ingo@298: import org.apache.log4j.Logger; ingo@298: ingo@298: import org.jfree.chart.ChartTheme; sascha@779: ingo@315: import org.jfree.chart.axis.Axis; ingo@656: import org.jfree.chart.axis.NumberAxis; sascha@779: ingo@315: import org.jfree.chart.plot.PlotOrientation; sascha@779: import org.jfree.chart.plot.XYPlot; sascha@779: ingo@656: import org.jfree.data.Range; sascha@779: ingo@656: import org.jfree.data.general.Series; sascha@779: ingo@298: import org.jfree.data.xy.XYSeries; ingo@298: import org.jfree.data.xy.XYSeriesCollection; ingo@298: ingo@298: /** ingo@767: * This class is used to create xy charts of vertical profiles. ingo@767: * ingo@767: * @author Ingo Weinzierl ingo@298: */ ingo@298: public class VerticalProfileChart ingo@298: extends AbstractXYLineChart ingo@298: { ingo@767: /** ingo@767: * Default axis identifier which is used if @see #getDependendAxisName does ingo@767: * not return a value. The value of this field is {@value}. ingo@767: */ ingo@530: public static final String DEFAULT_AXIS = "KPOSITION"; ingo@530: ingo@767: /** ingo@767: * Logger used for logging with log4j. ingo@767: */ ingo@298: private static Logger log = Logger.getLogger(VerticalProfileChart.class); ingo@298: ingo@767: /** ingo@767: * Constant used for gap detection. Its value is {@value}. ingo@767: */ ingo@831: protected static int PERCENTAGE = 5; ingo@767: ingo@767: /** ingo@815: * Constnat used for gap detection in @see #gridDetection. ingo@767: */ ingo@340: protected final double GAP_MAX_LEVEL = Math.sqrt(2.0); ingo@767: ingo@767: /** ingo@767: * Constant used for gap detection in @see #addGaps. Its value is {@value}. ingo@767: */ ingo@340: protected final int GAP_MAX_VALUES = 60; ingo@340: ingo@767: /** sascha@778: * Map to store max ranges of each parameter sascha@778: * (org.jfree.chart.axis.Axis.setAutoRange(true) doesn't seem to work ingo@767: * properly. ingo@767: */ ingo@656: protected Map values; ingo@656: ingo@831: static { sascha@835: /* The percentage defining the width of a gap should be configured in ingo@831: * conf.xml instead of being configured in a system property */ ingo@831: PERCENTAGE = Integer.getInteger("chart.gap.percentage", PERCENTAGE); ingo@831: } ingo@831: ingo@298: ingo@767: /** ingo@767: * Constructor used to create xy-charts. ingo@767: * ingo@767: * @param labels Labels used to be displayed in title, subtitle and so on. ingo@767: * @param theme ChartTheme used to adjust the rendering of this chart. ingo@767: * @param parameters Collection containing a bunch of parameters. ingo@767: * @param measurements Collection containing a bunch of measurements. ingo@767: * @param dates Collection containing a bunch of date objects. ingo@767: * @param result Collection containing a bunch of Result ingo@767: * objects which contain the actual data items to be displayed. ingo@767: * @param timeGaps Collection with timegap definitions. ingo@767: * @param locale Locale used to specify the format of labels, numbers, ... ingo@767: * @param linesVisible Render lines between data points if true, otherwise ingo@767: * not. ingo@767: * @param shapesVisible Render vertices as points if true, otherwise not. ingo@767: */ ingo@298: public VerticalProfileChart( ingo@298: ChartLabels labels, ingo@298: ChartTheme theme, ingo@298: Collection parameters, ingo@298: Collection measurements, ingo@310: Collection dates, ingo@298: Collection result, ingo@310: Collection timeGaps, ingo@327: Locale locale, ingo@327: boolean linesVisible, ingo@327: boolean shapesVisible ingo@298: ) { ingo@298: this.labels = labels; ingo@298: this.theme = theme; ingo@298: this.parameters = parameters; ingo@298: this.measurements = measurements; ingo@310: this.dates = dates; ingo@298: this.resultSet = result; ingo@310: this.timeGaps = timeGaps; ingo@298: this.locale = locale; ingo@298: this.PLOT_ORIENTATION = PlotOrientation.HORIZONTAL; ingo@327: this.linesVisible = linesVisible; ingo@327: this.shapesVisible = shapesVisible; ingo@334: this.datasets = new HashMap(); ingo@364: this.ranges = new HashMap(); ingo@656: this.values = new HashMap(); ingo@298: } ingo@298: ingo@298: ingo@767: /** ingo@767: * @see de.intevation.gnv.chart.AbstractXYLineChart#initData() ingo@767: */ ingo@767: @Override ingo@298: protected void initData() { ingo@298: log.debug("init data for VerticalProfileChart"); ingo@298: ingo@1085: int items = resultSet.size(); ingo@1085: log.debug("Found " + items + " items for this chart."); ingo@1085: ingo@298: String breakPoint1 = null; ingo@298: String breakPoint2 = null; ingo@298: String breakPoint3 = null; ingo@298: ingo@298: Iterator iter = resultSet.iterator(); ingo@298: Result row = null; ingo@298: String seriesName = null; ingo@364: String parameter = null; ingo@298: XYSeries series = null; ingo@298: ingo@340: int idx = 0; ingo@340: int startPos = 0; ingo@340: int endPos = 0; ingo@340: double startValue = 0; ingo@340: double endValue = 0; ingo@340: ingo@340: Result[] results = ingo@340: (Result[]) resultSet.toArray(new Result[resultSet.size()]); ingo@298: ingo@298: while (iter.hasNext()) { ingo@298: row = (Result) iter.next(); ingo@298: ingo@298: // add current data to plot and prepare for next one ingo@298: if (!row.getString("GROUP1").equals(breakPoint1) || ingo@298: !row.getString("GROUP2").equals(breakPoint2) || ingo@298: !row.getString("GROUP3").equals(breakPoint3) ingo@298: ) { ingo@298: log.debug("prepare data/plot for next dataset"); ingo@298: ingo@298: if(series != null) { ingo@1085: if (startPos >= 0 && endPos < items) { ingo@1085: gapDetection(results, series, startPos, endPos); ingo@1085: } ingo@364: addSeries(series, parameter, idx); ingo@340: ingo@340: startPos = endPos +1; ingo@298: } ingo@298: ingo@298: // prepare variables for next plot ingo@298: breakPoint1 = row.getString("GROUP1"); ingo@298: breakPoint2 = row.getString("GROUP2"); ingo@298: breakPoint3 = row.getString("GROUP3"); ingo@298: ingo@298: seriesName = createSeriesName( ingo@298: breakPoint1, ingo@298: breakPoint2, ingo@298: breakPoint3 ingo@298: ); ingo@364: parameter = findParameter(seriesName); ingo@298: ingo@298: log.debug("next dataset is '" + seriesName + "'"); ingo@298: series = new XYSeries(seriesName); ingo@298: } ingo@298: ingo@298: addValue(row, series); ingo@656: Object x = getValue(row); ingo@656: Double y = row.getDouble("YORDINATE"); ingo@656: if (x != null && y != null) { ingo@656: storeMaxRange(ranges, y, parameter); ingo@656: storeMaxValue(values, x, parameter); ingo@656: } ingo@340: endPos++; ingo@298: } ingo@298: ingo@1085: if (items == 0) ingo@340: return; ingo@340: ingo@1085: if (startPos >= 0 && endPos < items) { ingo@1085: gapDetection(results, series, startPos, endPos); ingo@1085: } ingo@364: addSeries(series, parameter, idx); ingo@310: ingo@334: addDatasets(); ingo@298: } ingo@298: ingo@298: ingo@767: /** ingo@767: * Extract the important value from Result object. ingo@767: * ingo@767: * @param row Result object which contains a required value. ingo@767: * ingo@767: * @return X-ordinate ingo@767: */ ingo@656: protected Object getValue(Result row) { ingo@656: return row.getDouble("XORDINATE"); ingo@656: } ingo@656: ingo@656: ingo@767: /** ingo@767: * General method to start a gap detection. The switch between standard gap ingo@767: * detection method addGaps and a specialized method ingo@767: * addGapsOnGrid is done by a parameter DATEID ingo@767: * which is stored a each Result object. Specialized method is ingo@767: * used if DATEID equals 2, otherwise the standard method is ingo@767: * used. ingo@767: * ingo@767: * @param results Array of Result objects storing data of ingo@767: * this chart. ingo@767: * @param series Series used to add gaps. ingo@767: * @param startPos Index of first element of series in results. ingo@767: * @param endPos Index of last element of series in results. ingo@767: */ ingo@340: protected void gapDetection( ingo@340: Result[] results, ingo@340: Series series, ingo@340: int startPos, ingo@340: int endPos ingo@340: ) { ingo@340: double startValue = results[startPos].getDouble("XORDINATE"); ingo@340: double endValue = results[endPos-1].getDouble("XORDINATE"); ingo@340: if (results[0].getInteger("DATAID") == 2) ingo@340: addGapsOnGrid(results, series, startPos, endPos); ingo@340: else ingo@340: addGaps(results, series, startValue, endValue, startPos, endPos); ingo@340: } ingo@340: ingo@846: @Override ingo@846: protected void prepareAxis(String seriesKey, int idx) { ingo@846: super.prepareAxis(seriesKey, idx); ingo@846: ingo@846: XYPlot plot = chart.getXYPlot(); ingo@1085: NumberAxis domainAxis = (NumberAxis) plot.getRangeAxis(idx); ingo@846: ingo@846: Range domainRange = domainAxis.getRange(); ingo@846: log.debug("Domain axis range before: " + domainRange.toString()); ingo@846: ingo@846: domainRange = Range.expand(domainRange, LOWER_MARGIN, UPPER_MARGIN); ingo@846: ingo@846: double lower = domainRange.getLowerBound(); ingo@846: double upper = domainRange.getUpperBound(); ingo@846: ingo@846: if (lower == upper) { ingo@1085: double lo = lower > 0 ? lower - lower*0.05d : lower + lower*0.05d; ingo@1085: double up = upper > 0 ? upper + upper*0.05d : upper - upper*0.05d; ingo@1085: ingo@1085: domainRange = new Range(lo, up); ingo@846: } ingo@846: ingo@1085: log.debug("Range axis range after: " + domainRange.toString()); ingo@846: domainAxis.setRange(domainRange); ingo@1085: plot.setRangeAxis(idx, domainAxis); ingo@846: } ingo@846: ingo@340: ingo@767: /** ingo@1085: * Method to expand max range of a range axis. ingo@767: * LOWER_MARGIN and UPPER_MARGIN are used to ingo@767: * expand the range. ingo@767: */ ingo@656: protected void prepareRangeAxis(String seriesKey, int idx) { ingo@1085: log.debug("Adjust domain range now..."); ingo@656: XYPlot plot = chart.getXYPlot(); ingo@1085: NumberAxis yAxis = (NumberAxis) plot.getDomainAxis(); ingo@656: ingo@1085: Range yRange = yAxis.getRange(); ingo@1085: double lo = yRange.getLowerBound(); ingo@1085: double hi = yRange.getUpperBound(); ingo@1085: ingo@1085: Iterator iter = values.values().iterator(); ingo@1085: while (iter.hasNext()) { ingo@1085: Range tmp = (Range) iter.next(); ingo@1085: log.debug("Series range: " + tmp.toString()); ingo@1085: ingo@1085: lo = lo < tmp.getLowerBound() ? lo : tmp.getLowerBound(); ingo@1085: hi = hi > tmp.getUpperBound() ? hi : tmp.getUpperBound(); ingo@1085: } ingo@1085: ingo@1085: Range merged = Range.expand( ingo@1085: new Range(lo, hi), ingo@1085: LOWER_MARGIN, UPPER_MARGIN); ingo@1085: log.debug("Calculated range for all series = " + merged.toString()); ingo@1085: ingo@1085: yAxis.setRange(merged); ingo@1085: plot.setDomainAxis(yAxis); ingo@656: } ingo@656: ingo@656: ingo@767: /** ingo@767: * @see de.intevation.gnv.chart.AbstractXYLineChart#addValue(Result, Series) ingo@767: */ ingo@767: @Override ingo@298: protected void addValue(Result row, Series series) { ingo@298: ((XYSeries) series).add( ingo@298: row.getDouble("XORDINATE"), ingo@298: row.getDouble("YORDINATE") ingo@298: ); ingo@298: } ingo@298: ingo@298: ingo@767: /** sascha@803: * @param parameter ingo@767: * @see de.intevation.gnv.chart.AbstractXYLineChart#addSeries(Series, String, ingo@767: * int) ingo@767: */ ingo@767: @Override ingo@364: protected void addSeries(Series series, String parameter, int idx) { ingo@364: log.debug("add series (" + parameter + ")to chart"); ingo@298: ingo@298: if (series == null) { ingo@298: log.warn("no data to add"); ingo@298: return; ingo@298: } ingo@298: ingo@334: XYSeriesCollection xysc = null; ingo@334: ingo@334: if (datasets.containsKey(parameter)) ingo@334: xysc = (XYSeriesCollection) datasets.get(parameter); ingo@334: else ingo@334: xysc = new XYSeriesCollection(); ingo@334: ingo@334: xysc.addSeries((XYSeries) series); ingo@334: datasets.put(parameter, xysc); ingo@334: } ingo@334: ingo@334: ingo@767: /** ingo@767: * Method to add processed datasets to plot. Each dataset is adjusted using ingo@767: * prepareAxis and adjustRenderer methods. ingo@767: */ ingo@334: protected void addDatasets() { ingo@334: Iterator iter = parameters.iterator(); ingo@334: XYPlot plot = chart.getXYPlot(); ingo@334: int idx = 0; ingo@334: ingo@334: XYSeriesCollection xysc = null; ingo@334: KeyValueDescibeData data = null; ingo@334: String key = null; ingo@334: while (iter.hasNext()) { ingo@334: data = (KeyValueDescibeData) iter.next(); ingo@334: key = data.getValue(); ingo@334: ingo@334: if (datasets.containsKey(key)) { ingo@334: xysc = (XYSeriesCollection)datasets.get(key); ingo@334: plot.setDataset(idx, xysc ); ingo@334: log.debug("Added " + key + " parameter to plot."); ingo@334: prepareAxis(key, idx); ingo@334: adjustRenderer( ingo@334: idx++, ingo@334: xysc.getSeriesCount(), ingo@334: linesVisible, ingo@334: shapesVisible ingo@334: ); ingo@334: } ingo@334: } ingo@1085: ingo@1085: prepareRangeAxis(null, -1); ingo@298: } ingo@298: ingo@298: ingo@767: /** ingo@767: * Method used to store the max y-range of each parameter in this chart. ingo@767: * ingo@767: * @param values Map to store max values for each parameter. ingo@767: * @param val Value used to be a Double. ingo@767: * @param parameter Title used to identify a range object stored in values. ingo@767: */ ingo@656: protected void storeMaxValue(Map values, Object val, String parameter) { ingo@656: double value = ((Double) val).doubleValue(); ingo@656: Range range = null; ingo@656: ingo@656: range = values.containsKey(parameter) ingo@656: ? (Range) values.get(parameter) ingo@656: : new Range(value, value); ingo@656: ingo@656: double lower = range.getLowerBound(); ingo@656: double upper = range.getUpperBound(); ingo@656: ingo@656: lower = value < lower ? value : lower; ingo@656: upper = value > upper ? value : upper; ingo@656: ingo@656: values.put(parameter, new Range(lower, upper)); ingo@656: } ingo@656: ingo@656: ingo@767: /** ingo@788: * @param locale ingo@767: * @see de.intevation.gnv.chart.AbstractXYLineChart#localizeDomainAxis(Axis, ingo@767: * Locale) ingo@767: */ ingo@767: @Override ingo@315: protected void localizeDomainAxis(Axis axis, Locale locale) { ingo@315: // call localizeRangeAxis from superclass which formats NumberAxis ingo@315: super.localizeRangeAxis(axis, locale); ingo@315: } ingo@315: ingo@315: ingo@767: /** ingo@767: * @see de.intevation.gnv.chart.AbstractXYLineChart#createSeriesName(String, ingo@767: * String, String) ingo@767: */ ingo@767: @Override ingo@298: protected String createSeriesName( ingo@298: String breakPoint1, ingo@298: String breakPoint2, ingo@298: String breakPoint3 ingo@298: ) { ingo@298: log.debug("create seriesname of verticalprofile chart"); ingo@298: return findValueTitle(parameters, breakPoint1) + ingo@298: " " + ingo@298: findValueTitle(measurements, breakPoint2) + ingo@298: "m"; ingo@298: } ingo@340: ingo@340: ingo@767: /** ingo@767: * Method used to add gaps between data points on grids. The real detection ingo@767: * is done in gridDetection. ingo@767: * ingo@767: * @param results Array of Result objects storing the relevant ingo@767: * values. ingo@767: * @param series Series object where the gaps are added to. ingo@767: * @param startPos Index of first element which should be used in gap ingo@767: * detection. Other series are stored in results as well. ingo@767: * @param endPos Index of last element which should be used in gap ingo@767: * detection. ingo@767: */ ingo@340: protected void addGapsOnGrid( ingo@340: Result[] results, ingo@340: Series series, ingo@340: int startPos, ingo@340: int endPos ingo@340: ) { ingo@530: String axis = null; sascha@778: ingo@530: if (results.length > (startPos+1)) { ingo@530: axis = getDependendAxisName( ingo@530: results[startPos], ingo@530: results[startPos+1] ingo@530: ); ingo@530: } ingo@530: else { ingo@530: axis = DEFAULT_AXIS; ingo@530: } ingo@530: ingo@340: double range = 0; ingo@340: int last = 0; ingo@340: int current = 0; ingo@340: ingo@340: for (int i = startPos+1; i < endPos; i++) { ingo@340: last = results[i-1].getInteger(axis); ingo@340: current = results[i].getInteger(axis); ingo@340: ingo@340: boolean detected = gridDetection(last, current); ingo@340: ingo@340: if (detected) { ingo@340: double xOld = results[i-1].getDouble("XORDINATE"); ingo@340: double xNow = results[i].getDouble("XORDINATE"); ingo@340: log.debug("Gap detected on grid between "+ xOld +" and "+ xNow); ingo@340: ((XYSeries) series).add(xOld+0.0001, null); ingo@340: } ingo@340: } ingo@340: } ingo@340: ingo@340: ingo@767: /** ingo@767: * Standarad method to add gaps. There are two different methods to detect ingo@767: * gaps. simpleDetection is used if the number of data points ingo@767: * in this chart is lower than GAP_MAX_VALUES. Otherwise ingo@767: * specialDetection is used. A data point with ingo@767: * null value is added where a gap should be. This lets ingo@767: * JFreeChart break the current graph. ingo@767: * ingo@767: * @param results Array of Result objects storing the relevant ingo@767: * values. ingo@767: * @param series Series object where the gaps are added to. ingo@767: * @param startValue First data point value in series. ingo@767: * @param endValue Last data point value in series. ingo@767: * @param startPos Index of first data point in results which contains all ingo@767: * data points of all series. ingo@767: * @param endPos Index of last data point in results which contains all data ingo@767: * points of all series. ingo@767: */ ingo@340: protected void addGaps( ingo@340: Result[] results, ingo@340: Series series, ingo@340: double startValue, ingo@340: double endValue, ingo@340: int startPos, ingo@340: int endPos ingo@340: ) { ingo@340: ingo@340: double last = 0; ingo@340: double current = 0; ingo@340: int num = results.length; ingo@340: ingo@340: for (int i = startPos+1; i < endPos; i++) { ingo@340: boolean detected = false; ingo@340: ingo@340: last = results[i-1].getDouble("YORDINATE"); ingo@340: current = results[i].getDouble("YORDINATE"); ingo@340: ingo@340: // gap detection for more than GAP_MAX_VALUES values ingo@340: if (num > GAP_MAX_VALUES) ingo@340: detected = simpleDetection(startValue, endValue, last, current); ingo@340: // gap detection for less than GAP_MAX_VALUES values ingo@340: else ingo@340: detected = specialDetection( ingo@340: startValue, ingo@340: endValue, ingo@340: last, ingo@340: current, ingo@340: num ingo@340: ); ingo@340: ingo@340: if (detected) { ingo@340: log.info("Gap between " + last + " and " + current); ingo@340: ((XYSeries) series).add((last+current)/2, null); ingo@340: } ingo@340: } ingo@340: } ingo@340: ingo@340: ingo@767: /** sascha@778: * Simple method to detect gaps. A gap is detected if the delta between two sascha@778: * data points (current, last) is bigger than PERCENTAGE percent sascha@778: * of delta of start and end. ingo@767: *
ingo@767: * (smallDelta > delta / 100 * PERCENTAGE) ingo@767: * ingo@767: * @param start First data point value in a series. ingo@767: * @param end Last data point value in a series. ingo@767: * @param last Left value ingo@767: * @param current Right value ingo@767: * ingo@767: * @return true, if a gap is detected between last and current - otherwise ingo@767: * false. ingo@767: */ ingo@340: protected boolean simpleDetection( ingo@340: double start, ingo@340: double end, ingo@340: double last, ingo@340: double current ingo@340: ) { ingo@340: double delta = Math.abs(end - start); ingo@340: double smallDelta = Math.abs(current - last); ingo@340: ingo@340: return (smallDelta > delta / 100 * PERCENTAGE); ingo@340: } ingo@340: ingo@340: ingo@767: /** ingo@767: * Method to detect gaps between two data points. Following formula is used ingo@767: * for detection:
ingo@767: * smallDelta > (3.0 / (count - 1) * delta)
ingo@767: * smallDelta = current - last
ingo@767: * delta = end - start ingo@767: * ingo@767: * @param start First data point value in a series. ingo@767: * @param end Last data point value in a series. ingo@767: * @param last Left value ingo@767: * @param current Right value ingo@767: * ingo@788: * @param count ingo@767: * @return true, if a gap is detected between last and current - otherwise ingo@767: * false. ingo@767: */ ingo@340: protected boolean specialDetection( ingo@340: double start, ingo@340: double end, ingo@340: double last, ingo@340: double current, ingo@340: int count ingo@340: ) { ingo@340: double delta = Math.abs(end - start); ingo@340: double smallDelta = Math.abs(current - last); ingo@340: ingo@340: return (smallDelta > (3.0 / (count - 1) * delta)); ingo@340: } ingo@340: sascha@778: ingo@767: /** ingo@767: * Method used to detect gaps between two data points grids. If the delta ingo@767: * between current and last is bigger than GAP_MAX_LEVEL, a gap ingo@767: * is detected. ingo@767: * ingo@767: * @param last Left value ingo@767: * @param current Right value ingo@767: * ingo@767: * @return True, if a gap was detected - otherwise false. ingo@767: */ ingo@340: protected boolean gridDetection(double last, double current) { ingo@643: if (log.isDebugEnabled()) { ingo@643: log.debug("######################################################"); ingo@643: log.debug("Parameters for gap detection"); ingo@643: log.debug("Defined gap size for grids: " + GAP_MAX_LEVEL); ingo@643: log.debug("1st value to compare: " + last); ingo@643: log.debug("2nd value to compare: " + current); ingo@643: log.debug("Difference: " + Math.abs(current - last)); ingo@643: } ingo@340: return (Math.abs(current - last) > GAP_MAX_LEVEL); ingo@340: } ingo@340: ingo@340: ingo@767: /** ingo@767: * This method returns the key which is used to retrieve the y-value served sascha@778: * by a Result object. ingo@767: * ingo@767: * @param first Result object - not used in this class. ingo@767: * @param second Result object - not used in this class. ingo@767: * ingo@767: * @return the string "KPOSITION" ingo@767: */ ingo@340: protected String getDependendAxisName(Result first, Result second) { ingo@370: return "KPOSITION"; ingo@340: } ingo@298: } ingo@315: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :