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 <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
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:      * <code>WKTReader</code> 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 <code>Result</code>
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 <code>Point</code> where the scan for gaps should begin.
ingo@767:      * @param endValue <code>Point</code> where the scan should end.
ingo@767:      * @param startPos Start position of this series in <code>results</code>.
ingo@767:      * @param endPos End position of a series in <code>results</code>
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 <code>PERCENTAGE</code> percent
sascha@778:      * of delta of start and end.
ingo@767:      * <br>
ingo@767:      * (smallDelta &gt; 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:<br>
ingo@767:      * smallDelta &gt; (3.0 / (count - 1) * delta)<br>
ingo@767:      * smallDelta = distance between <code>current</code> and <code>last</code>
ingo@767:      * <br>
ingo@767:      * delta = distance between <code>start</code> and <code>end</code>
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:      * <code>result</code>.
ingo@767:      *
ingo@767:      * @param result <code>Result</code> 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 :