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 com.vividsolutions.jts.geom.Point; sascha@779: sascha@779: import com.vividsolutions.jts.io.ParseException; sascha@779: import com.vividsolutions.jts.io.WKTReader; sascha@779: sascha@779: import de.intevation.gnv.geobackend.base.Result; sascha@779: sascha@779: import de.intevation.gnv.utils.DistanceCalculator; sascha@779: ingo@298: import java.util.Collection; ingo@298: import java.util.Locale; ingo@656: 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@846: import org.jfree.chart.axis.Axis; ingo@846: import org.jfree.chart.axis.AxisLocation; ingo@846: import org.jfree.chart.axis.NumberAxis; ingo@846: import org.jfree.chart.axis.NumberTickUnit; ingo@846: ingo@298: import org.jfree.chart.plot.PlotOrientation; ingo@846: import org.jfree.chart.plot.XYPlot; ingo@846: ingo@846: import org.jfree.data.Range; sascha@779: ingo@298: import org.jfree.data.general.Series; sascha@779: ingo@298: import org.jfree.data.xy.XYSeries; ingo@298: ingo@298: /** ingo@767: * This class is used to create xy-charts of horizontal profiles. ingo@767: * ingo@767: * @author Ingo Weinzierl ingo@298: */ ingo@298: public class HorizontalProfileChart ingo@298: extends VerticalProfileChart ingo@298: { ingo@767: /** ingo@767: * Logger used for logging with log4j. ingo@767: */ ingo@298: private static Logger log = Logger.getLogger(HorizontalProfileChart.class); ingo@298: ingo@767: /** ingo@767: * WKTReader used to turn wkt strings into geometries. ingo@767: */ ingo@298: private static WKTReader wktReader = new WKTReader(); ingo@767: ingo@767: /** ingo@767: * The first point in a HorizontalProfileChart. It is used to calculate the ingo@767: * distance between the currently processed point an the start. ingo@767: */ ingo@1049: protected Point lastPoint; ingo@1049: ingo@1049: protected double distance = 0d; ingo@298: ingo@767: /** ingo@767: * Constructor used to create horizontal profile 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 HorizontalProfileChart( 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@310: super( ingo@310: labels, ingo@310: theme, ingo@310: parameters, ingo@310: measurements, ingo@310: dates, ingo@310: result, ingo@310: timeGaps, ingo@327: locale, ingo@327: linesVisible, ingo@327: shapesVisible ingo@310: ); ingo@298: this.PLOT_ORIENTATION = PlotOrientation.VERTICAL; ingo@298: } ingo@298: ingo@298: ingo@656: @Override ingo@656: protected Object getValue(Result row) { ingo@656: try { ingo@656: return (Point) wktReader.read(row.getString("SHAPE")); ingo@656: } ingo@656: catch(ParseException pe) { ingo@656: log.warn("No data found while parsing."); ingo@656: return null; ingo@656: } ingo@656: } ingo@656: ingo@656: ingo@767: @Override 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: log.debug("Start gap detection."); ingo@340: try { ingo@340: Point startValue = getPoint(results[startPos]); ingo@340: Point endValue = getPoint(results[endPos-1]); ingo@340: if (results[0].getInteger("DATAID") == 2) ingo@340: addGapsOnGrid(results, series, startPos, endPos); ingo@340: else ingo@340: addGaps( ingo@340: results, ingo@340: series, ingo@340: startValue, ingo@340: endValue, ingo@340: startPos, ingo@340: endPos ingo@340: ); ingo@340: } ingo@340: catch (ParseException pe) { ingo@340: log.warn( ingo@340: "Error while parsing points for gap detection. " + ingo@340: "No gaps for current series will be detected." ingo@340: ); ingo@340: } ingo@340: ingo@340: log.debug("Gap detection finished."); ingo@340: } ingo@340: ingo@340: ingo@767: @Override ingo@298: protected void addValue(Result row, Series series) { ingo@298: try { ingo@298: Point point = (Point) wktReader.read(row.getString("SHAPE")); ingo@1049: if (lastPoint != null) { ingo@1049: distance += DistanceCalculator.calculateDistance( ingo@1049: lastPoint, point ingo@298: ); ingo@644: } ingo@298: ingo@298: ((XYSeries) series).add( ingo@298: distance, ingo@298: row.getDouble("YORDINATE") ingo@298: ); ingo@1049: ingo@1049: lastPoint = point; ingo@298: } ingo@298: catch(ParseException pe) { ingo@298: log.warn("No data found while parsing."); ingo@298: } ingo@298: } ingo@298: ingo@298: ingo@767: @Override ingo@334: protected void addSeries(Series series, String label, int idx) { ingo@334: super.addSeries(series, label, idx); ingo@298: ingo@1049: // reset lastPoint and distance of the last series ingo@1049: lastPoint = null; ingo@1049: distance = 0; ingo@298: } ingo@298: ingo@298: ingo@656: @Override ingo@846: protected void prepareAxis(String seriesKey,int idx) { ingo@846: log.debug("prepare axis of xychart"); ingo@846: ingo@846: XYPlot plot = chart.getXYPlot(); ingo@846: Axis xAxis = plot.getDomainAxis(); ingo@846: NumberAxis yAxis = new NumberAxis(seriesKey); ingo@846: ingo@846: localizeDomainAxis(xAxis, locale); ingo@846: localizeRangeAxis(yAxis, locale); ingo@846: ingo@846: // litte workarround to adjust the max range of axes. ingo@846: // NumberAxis.setAutoRange(true) doesn't seem to work properly. ingo@846: Range yRange = (Range) ranges.get(seriesKey); ingo@849: double lo = yRange.getLowerBound(); ingo@849: double hi = yRange.getUpperBound(); ingo@849: ingo@849: // XXX Special case: only a single value in this chart. ingo@849: // JFreeChart doesn't expand this range, because its length is 0. ingo@849: if (lo == hi) { ingo@849: yRange = new Range( ingo@849: lo - (lo / 100 * LOWER_MARGIN), ingo@849: hi + (hi / 100 * UPPER_MARGIN)); ingo@849: } ingo@849: else { ingo@849: yRange = Range.expand(yRange, LOWER_MARGIN, UPPER_MARGIN); ingo@849: } ingo@849: ingo@849: yAxis.setRange(yRange); ingo@846: log.debug("Max Range of dataset is: " + yRange.toString()); ingo@846: ingo@846: if (seriesKey.contains("richtung")) { ingo@846: yAxis.setTickUnit(new NumberTickUnit(30.0)); ingo@846: yAxis.setUpperBound(360.0); ingo@846: yAxis.setLowerBound(0.0); ingo@846: } ingo@846: else { ingo@846: yAxis.setFixedDimension(10.0); ingo@846: yAxis.setAutoRangeIncludesZero(false); ingo@846: } ingo@846: ingo@846: plot.setRangeAxis(idx, yAxis); ingo@846: yAxis.configure(); ingo@846: ingo@846: if (idx % 2 != 0) ingo@846: plot.setRangeAxisLocation(idx, AxisLocation.BOTTOM_OR_RIGHT); ingo@846: else ingo@846: plot.setRangeAxisLocation(idx, AxisLocation.BOTTOM_OR_LEFT); ingo@846: ingo@846: plot.mapDatasetToRangeAxis(idx, idx); ingo@846: } ingo@846: ingo@846: ingo@846: @Override ingo@656: protected void prepareRangeAxis(String seriesKey, int idx) { ingo@656: return; ingo@656: // do nothing here ingo@656: } ingo@656: ingo@656: ingo@656: @Override ingo@656: protected void storeMaxValue(Map values, Object value, String parameter) { ingo@656: return; ingo@656: // do nothing here ingo@656: } ingo@656: ingo@656: 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 horizontalprofile chart"); ingo@298: return super.createSeriesName( ingo@298: breakPoint1, ingo@298: breakPoint2, ingo@298: breakPoint3) + ingo@298: " " + ingo@298: findValueTitle(dates, breakPoint3); ingo@298: } ingo@340: ingo@340: ingo@767: @Override ingo@340: protected void addGapsOnGrid( ingo@340: Result[] results, ingo@340: Series series, ingo@340: int startPos, ingo@340: int endPos ingo@340: ) { ingo@340: String axis = getDependendAxisName( ingo@340: results[startPos], ingo@340: results[startPos+1] ingo@340: ); ingo@340: ingo@340: int last = 0; ingo@340: int current = 0; ingo@340: Point lastPoint = null; ingo@340: Point currentPoint = null; ingo@340: ingo@1049: double distance = 0; ingo@1049: double distanceOld = 0; ingo@340: for (int i = startPos+1; i < endPos; i++) { ingo@340: try { ingo@340: last = results[i-1].getInteger(axis); ingo@340: lastPoint = getPoint(results[i-1]); ingo@340: current = results[i].getInteger(axis); ingo@340: currentPoint = getPoint(results[i]); ingo@1049: ingo@1049: distanceOld = distance; ingo@1049: distance += DistanceCalculator.calculateDistance( ingo@1049: lastPoint, ingo@644: currentPoint); ingo@340: ingo@340: boolean detected = gridDetection(last, current); ingo@340: ingo@643: if (log.isDebugEnabled()) { ingo@643: log.debug("Last point: " + lastPoint.toString()); ingo@643: log.debug("Current point: " + currentPoint.toString()); ingo@644: log.debug("Current distance from start: " + distance); ingo@643: } ingo@643: ingo@340: if (detected) { ingo@643: log.info( ingo@644: "Gap detected on grid between " + distanceOld + ingo@644: " and " + distance); ingo@340: ingo@644: ((XYSeries) series).add(distance-1d, null); ingo@644: ((XYSeries) series).add(distanceOld+1d, null); ingo@340: } ingo@340: } ingo@340: catch (ParseException pe) { ingo@340: log.warn("Error while parsing point for gap detection.", pe); ingo@340: } ingo@340: } ingo@340: } ingo@340: ingo@340: ingo@767: /** ingo@767: * Method to add gaps between two data points. The real detection is done by ingo@767: * {@link #simpleDetection} and {@link #specialDetection}. ingo@767: * ingo@767: * @param results All data points in this dataset. ingo@767: * @param series Series to be processed. ingo@767: * @param startValue Point where the scan for gaps should begin. ingo@767: * @param endValue Point where the scan should end. ingo@767: * @param startPos Start position of this series in results. ingo@767: * @param endPos End position of a series in results ingo@767: */ ingo@340: protected void addGaps( ingo@340: Result[] results, ingo@340: Series series, ingo@340: Point startValue, ingo@340: Point endValue, ingo@340: int startPos, ingo@340: int endPos ingo@340: ) { ingo@340: double range = 0; ingo@340: Point last = null; ingo@340: Point now = null; ingo@340: ingo@340: for (int i = startPos+1; i < endPos; i++) { ingo@340: boolean detected = false; ingo@340: ingo@340: try { ingo@340: last = (Point) getPoint(results[i-1]); ingo@340: now = (Point) getPoint(results[i]); ingo@340: ingo@340: // gap detection for more than GAP_MAX_VALUES values ingo@340: if (results.length > GAP_MAX_VALUES) ingo@340: detected = simpleDetection(startValue, endValue, last, now); 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: now, ingo@340: results.length ingo@340: ); ingo@340: ingo@340: // gap detected, insert null value to break line ingo@340: if (detected) { ingo@340: log.info("Gap after " + range); ingo@340: double x = range + 0.0001; ingo@340: ingo@340: ((XYSeries)series).add(x, null); ingo@340: } ingo@340: ingo@340: range += DistanceCalculator.calculateDistance(last,now); ingo@340: } ingo@340: catch (ParseException pe) { ingo@340: log.warn("Error while parsing point."); ingo@340: } 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 in a series ingo@767: * @param end Last data point in a series ingo@767: * @param last Left point ingo@767: * @param current Right point 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: Point start, ingo@340: Point end, ingo@340: Point last, ingo@340: Point current ingo@340: ) { ingo@340: double delta = DistanceCalculator.calculateDistance(start, end); ingo@340: double deltaSmall = DistanceCalculator.calculateDistance(last,current); ingo@340: ingo@340: return (deltaSmall > (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 = distance between current and last ingo@767: *
ingo@767: * delta = distance between start and end ingo@767: * ingo@767: * @param start First data point in a series ingo@767: * @param end Last data point in a series ingo@767: * @param last Left point ingo@767: * @param current Right point ingo@788: * @param count Number of datapoints 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: Point start, ingo@340: Point end, ingo@340: Point last, ingo@340: Point current, ingo@340: int count ingo@340: ) { ingo@340: double delta = Math.abs( ingo@340: DistanceCalculator.calculateDistance(end, start) ingo@340: ); ingo@340: double smallDelta = Math.abs( ingo@340: DistanceCalculator.calculateDistance(current, last) ingo@340: ); ingo@340: ingo@340: return (smallDelta > (3.0 / (count - 1) * delta)); ingo@340: } ingo@340: ingo@815: ingo@370: @Override ingo@370: protected String getDependendAxisName(Result first, Result second) { ingo@370: if (first.getInteger("IPOSITION") == second.getInteger("IPOSITION")) ingo@370: return "JPOSITION"; ingo@370: ingo@370: return "IPOSITION"; ingo@370: } ingo@340: ingo@767: /** ingo@767: * This method returns a point from a given wkt string stored in ingo@767: * result. ingo@767: * ingo@767: * @param result Result object which contains the wkt string. ingo@767: * The wkt string needs to be available under the key SHAPE. ingo@767: * ingo@767: * @return Point representation of wkt string. ingo@767: */ ingo@340: private Point getPoint(Result result) ingo@340: throws ParseException ingo@340: { ingo@340: return (Point) wktReader.read(result.getString("SHAPE")); ingo@340: } ingo@298: } ingo@327: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :