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@340: protected final double PERCENTAGE = 5.0;
ingo@767:
ingo@767: /**
ingo@767: * Constnat used for gap detection in @see #gridDetection. Its value is
ingo@767: * {@value}.
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@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@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@340: gapDetection(results, series, startPos, endPos);
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@340: if (results.length == 0)
ingo@340: return;
ingo@340:
ingo@340: gapDetection(results, series, startPos, endPos);
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@340:
ingo@767: /**
ingo@767: * Method to expand max range of a range axis identified by seriesKey.
ingo@767: * LOWER_MARGIN
and UPPER_MARGIN
are used to
ingo@767: * expand the range.
ingo@767: *
ingo@767: * @param seriesKey Key to identify the series stored at the current
ingo@767: * Dataset.
ingo@767: * @param idx Currently not used.
ingo@767: */
ingo@656: protected void prepareRangeAxis(String seriesKey, int idx) {
ingo@656: XYPlot plot = chart.getXYPlot();
ingo@656: NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
ingo@656:
ingo@656: Range xRange = (Range) values.get(seriesKey);
ingo@656: xAxis.setRange(Range.expand(xRange, LOWER_MARGIN, UPPER_MARGIN));
ingo@656: log.debug("Max X-Range of dataset is: " + xRange.toString());
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: /**
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@656: prepareRangeAxis(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@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@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@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 :