Mercurial > dive4elements > river
diff flys-artifacts/src/main/java/org/dive4elements/river/exports/TimeseriesChartGenerator.java @ 5831:bd047b71ab37
Repaired internal references
author | Sascha L. Teichmann <teichmann@intevation.de> |
---|---|
date | Thu, 25 Apr 2013 12:06:39 +0200 |
parents | flys-artifacts/src/main/java/de/intevation/flys/exports/TimeseriesChartGenerator.java@7eebd9e58641 |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/flys-artifacts/src/main/java/org/dive4elements/river/exports/TimeseriesChartGenerator.java Thu Apr 25 12:06:39 2013 +0200 @@ -0,0 +1,892 @@ +package org.dive4elements.river.exports; + +import org.dive4elements.artifactdatabase.state.ArtifactAndFacet; +import org.dive4elements.river.artifacts.resources.Resources; +import org.dive4elements.river.jfree.Bounds; +import org.dive4elements.river.jfree.CollisionFreeXYTextAnnotation; +import org.dive4elements.river.jfree.DoubleBounds; +import org.dive4elements.river.jfree.FLYSAnnotation; +import org.dive4elements.river.jfree.StyledTimeSeries; +import org.dive4elements.river.jfree.TimeBounds; + +import java.awt.Color; +import java.awt.Font; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.swing.ImageIcon; + +import org.apache.log4j.Logger; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.LegendItem; +import org.jfree.chart.LegendItemCollection; +import org.jfree.chart.annotations.XYAnnotation; +import org.jfree.chart.annotations.XYImageAnnotation; +import org.jfree.chart.annotations.XYTextAnnotation; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.Marker; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.Range; +import org.jfree.data.general.Series; +import org.jfree.data.time.Day; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.data.xy.XYDataset; +import org.jfree.ui.Layer; +import org.json.JSONArray; +import org.json.JSONException; +import org.w3c.dom.Document; + +/** + * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a> + */ +public abstract class TimeseriesChartGenerator extends ChartGenerator { + + + /** + * Inner class TimeseriesAxisDataset stores TimeSeriesCollection. + */ + public class TimeseriesAxisDataset implements AxisDataset { + + protected int axisSymbol; + + protected List<TimeSeriesCollection> datasets; + + protected Range range; + + protected int plotAxisIndex; + + public TimeseriesAxisDataset(int axisSymbol) { + this.axisSymbol = axisSymbol; + this.datasets = new ArrayList<TimeSeriesCollection>(); + } + + + @Override + public void addDataset(XYDataset dataset) { + if (!(dataset instanceof TimeSeriesCollection)) { + logger.warn("Skip non TimeSeriesCollection dataset."); + return; + } + + TimeSeriesCollection tsc = (TimeSeriesCollection) dataset; + + datasets.add(tsc); + mergeRanges(tsc); + } + + + @Override + public XYDataset[] getDatasets() { + return datasets.toArray(new XYDataset[datasets.size()]); + } + + + @Override + public boolean isEmpty() { + return datasets.isEmpty(); + } + + + @Override + public void setRange(Range range) { + this.range = range; + } + + + @Override + public Range getRange() { + return range; + } + + + @Override + public void setPlotAxisIndex(int plotAxisIndex) { + this.plotAxisIndex = plotAxisIndex; + } + + + @Override + public int getPlotAxisIndex() { + return plotAxisIndex; + } + + + @Override + public boolean isArea(XYDataset dataset) { + logger.warn("This AxisDataset doesn't support Areas yet!"); + return false; + } + + + protected void mergeRanges(TimeSeriesCollection dataset) { + logger.debug("Range before merging: " + range); + Range subRange = null; + + // Determine min/max of range axis. + for (int i = 0; i < dataset.getSeriesCount(); i++) { + if (dataset.getSeries(i).getItemCount() == 0) { + continue; + } + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + TimeSeries series = dataset.getSeries(i); + for (int j = 0; j < series.getItemCount(); j++) { + double tmp = series.getValue(j).doubleValue(); + min = tmp < min ? tmp : min; + max = tmp > max ? tmp : max; + } + if (subRange != null) { + subRange = new Range( + min < subRange.getLowerBound() ? + min : subRange.getLowerBound(), + max > subRange.getUpperBound() ? + max : subRange.getUpperBound()); + } + else { + subRange = new Range(min, max); + } + } + + // Avoid merging NaNs, as they take min/max place forever. + if (subRange == null || + Double.isNaN(subRange.getLowerBound()) || + Double.isNaN(subRange.getUpperBound())) { + return; + } + if (range == null) { + range = subRange; + return; + } + range = Range.combine(range, subRange); + } + + } // end of TimeseriesAxisDataset class + + protected List<Marker> domainMarker; + + protected List<Marker> valueMarker; + + protected Map<String, String> attributes; + + protected boolean domainZeroLineVisible; + + private static final Logger logger = + Logger.getLogger(TimeseriesChartGenerator.class); + + public static final int AXIS_SPACE = 5; + + protected Map<Integer, Bounds> xBounds; + + protected Map<Integer, Bounds> yBounds; + + + /** + * The default constructor that initializes internal datastructures. + */ + public TimeseriesChartGenerator() { + super(); + + xBounds = new HashMap<Integer, Bounds>(); + yBounds = new HashMap<Integer, Bounds>(); + domainMarker = new ArrayList<Marker>(); + valueMarker = new ArrayList<Marker>(); + attributes = new HashMap<String, String>(); + } + + + + @Override + public JFreeChart generateChart() { + logger.info("Generate Timeseries Chart."); + + JFreeChart chart = ChartFactory.createTimeSeriesChart( + getChartTitle(), + getXAxisLabel(), + getYAxisLabel(0), + null, + isLegendVisible(), + false, + false); + + XYPlot plot = (XYPlot) chart.getPlot(); + + chart.setBackgroundPaint(Color.WHITE); + plot.setBackgroundPaint(Color.WHITE); + + addSubtitles(chart); + adjustPlot(plot); + addDatasets(plot); + adjustAxes(plot); + addDomainAxisMarker(plot); + addValueAxisMarker(plot); + adaptZoom(plot); + + applySeriesAttributes(plot); + + addAnnotationsToRenderer(plot); + addLogo(plot); + aggregateLegendEntries(plot); + return chart; + } + + + /** + * Return left most data points x value (on first axis). + * Shortcut, especially to be overridden in (LS) charts where + * axis could be inverted. + */ + protected double getLeftX() { + return (Long)getXBounds(0).getLower(); + } + + + /** + * Return right most data points x value (on first axis). + * Shortcut, especially to be overridden in (LS) charts where + * axis could be inverted. + */ + protected double getRightX() { + return (Long)getXBounds(0).getUpper(); + } + + + /** + * Add a logo as background annotation to plot. + * Copy from XYChartGenerator. + */ + protected void addLogo(XYPlot plot) { + String logo = showLogo(); + if (logo == null) { + logger.debug("No logo to show chosen"); + return; + } + + ImageIcon imageIcon = null; + if (logo.equals("none")) { + return; + } + /* + If you want to add images, remember to change code in these places: + flys-artifacts: + XYChartGenerator.java + Timeseries*Generator.java and + in the flys-client projects Chart*Propert*Editor.java. + Also, these images have to be put in + flys-artifacts/src/main/resources/images/ + flys-client/src/main/webapp/images/ + */ + java.net.URL imageURL; + if (logo.equals("Intevation")) { + imageURL = XYChartGenerator.class.getResource("/images/intevation.png"); + } + else { // TODO else if ... + imageURL = XYChartGenerator.class.getResource("/images/bfg_logo.gif"); + } + imageIcon = new ImageIcon(imageURL); + double xPos = 0d, yPos = 0d; + + String placeh = logoHPlace(); + String placev = logoVPlace(); + + if (placev == null || placev.equals("none")) { + placev = "top"; + } + if (placev.equals("top")) { + yPos = (Double)getYBounds(0).getUpper(); + } + else if (placev.equals("bottom")) { + yPos = (Double)getYBounds(0).getLower(); + } + else if (placev.equals("center")) { + yPos = ((Double)getYBounds(0).getUpper() + (Double)getYBounds(0).getLower())/2d; + } + else { + logger.debug("Unknown place-v value: " + placev); + } + + if (placeh == null || placeh.equals("none")) { + placeh = "center"; + } + if (placeh.equals("left")) { + xPos = getLeftX(); + } + else if (placeh.equals("right")) { + xPos = getRightX(); + } + else if (placeh.equals("center")) { + xPos = ((Long)getXBounds(0).getUpper() + (Long)getXBounds(0).getLower())/2d; + } + else { + logger.debug("Unknown place-h value: " + placeh); + } + + logger.debug("logo position: " + xPos + "/" + yPos); + + org.jfree.ui.RectangleAnchor anchor + = org.jfree.ui.RectangleAnchor.TOP; + if (placev.equals("top")) { + if (placeh.equals("left")) { + anchor = org.jfree.ui.RectangleAnchor.TOP_LEFT; + } + else if (placeh.equals("right")) { + anchor = org.jfree.ui.RectangleAnchor.TOP_RIGHT; + } + else if (placeh.equals("center")) { + anchor = org.jfree.ui.RectangleAnchor.TOP; + } + } + else if (placev.equals("bottom")) { + if (placeh.equals("left")) { + anchor = org.jfree.ui.RectangleAnchor.BOTTOM_LEFT; + } + else if (placeh.equals("right")) { + anchor = org.jfree.ui.RectangleAnchor.BOTTOM_RIGHT; + } + else if (placeh.equals("center")) { + anchor = org.jfree.ui.RectangleAnchor.BOTTOM; + } + } + else if (placev.equals("center")) { + if (placeh.equals("left")) { + anchor = org.jfree.ui.RectangleAnchor.LEFT; + } + else if (placeh.equals("right")) { + anchor = org.jfree.ui.RectangleAnchor.RIGHT; + } + else if (placeh.equals("center")) { + anchor = org.jfree.ui.RectangleAnchor.CENTER; + } + } + + XYAnnotation xyannotation = + new XYImageAnnotation(xPos, yPos, imageIcon.getImage(), anchor); + plot.getRenderer().addAnnotation(xyannotation, org.jfree.ui.Layer.BACKGROUND); + } + + + @Override + protected Series getSeriesOf(XYDataset dataset, int idx) { + return ((TimeSeriesCollection) dataset).getSeries(idx); + } + + + /** + * This method creates new instances of TimeseriesAxisDataset. + * + * @param idx The symbol for the new TimeseriesAxisDataset. + */ + @Override + protected AxisDataset createAxisDataset(int idx) { + logger.debug("Create a new AxisDataset for index: " + idx); + return new TimeseriesAxisDataset(idx); + } + + + @Override + protected void combineXBounds(Bounds bounds, int index) { + if (bounds != null) { + Bounds old = getXBounds(index); + + if (old != null) { + bounds = bounds.combine(old); + } + + setXBounds(index, bounds); + } + } + + + @Override + protected void combineYBounds(Bounds bounds, int index) { + if (bounds != null) { + Bounds old = getYBounds(index); + + if (old != null) { + bounds = bounds.combine(old); + } + + setYBounds(index, bounds); + } + } + + + // TODO REPLACE THIS METHOD WITH getBoundsForAxis(index) + @Override + public Range[] getRangesForAxis(int index) { + // TODO + Bounds[] bounds = getBoundsForAxis(index); + + return new Range[] { + new Range( + bounds[0].getLower().doubleValue(), + bounds[0].getUpper().doubleValue()), + new Range( + bounds[1].getLower().doubleValue(), + bounds[1].getUpper().doubleValue()) + }; + } + + + @Override + public Bounds getXBounds(int axis) { + return xBounds.get(axis); + } + + + @Override + protected void setXBounds(int axis, Bounds bounds) { + xBounds.put(axis, bounds); + } + + + @Override + public Bounds getYBounds(int axis) { + return yBounds.get(axis); + } + + + @Override + protected void setYBounds(int axis, Bounds bounds) { + if (bounds != null) { + yBounds.put(axis, bounds); + } + } + + + public Bounds[] getBoundsForAxis(int index) { + logger.debug("Return x and y bounds for axis at: " + index); + + Bounds rx = getXBounds(Integer.valueOf(index)); + Bounds ry = getYBounds(Integer.valueOf(index)); + + if (rx == null) { + logger.warn("Range for x axis not set." + + " Using default values: 0 - 1."); + rx = new TimeBounds(0l, 1l); + } + + if (ry == null) { + logger.warn("Range for y axis not set." + + " Using default values: 0 - 1."); + ry = new DoubleBounds(0l, 1l); + } + + logger.debug("X Bounds at index " + index + " is: " + rx); + logger.debug("Y Bounds at index " + index + " is: " + ry); + + return new Bounds[] {rx, ry}; + } + + + /** Get (zoom)values from request. */ + public Bounds getDomainAxisRange() { + String[] ranges = getDomainAxisRangeFromRequest(); + + if (ranges == null || ranges.length < 2) { + logger.debug("No zoom range for domain axis specified."); + return null; + } + + if (ranges[0] == null || ranges[1] == null) { + logger.warn("Invalid ranges for domain axis specified!"); + return null; + } + + try { + double lower = Double.parseDouble(ranges[0]); + double upper = Double.parseDouble(ranges[1]); + + return new DoubleBounds(lower, upper); + } + catch (NumberFormatException nfe) { + logger.warn("Invalid ranges for domain axis specified: " + nfe); + } + + return null; + } + + + public Bounds getValueAxisRange() { + String[] ranges = getValueAxisRangeFromRequest(); + + if (ranges == null || ranges.length < 2) { + logger.debug("No zoom range for domain axis specified."); + return null; + } + + if (ranges[0] == null || ranges[1] == null) { + logger.warn("Invalid ranges for domain axis specified!"); + return null; + } + + try { + double lower = Double.parseDouble(ranges[0]); + double upper = Double.parseDouble(ranges[1]); + + return new DoubleBounds(lower, upper); + } + catch (NumberFormatException nfe) { + logger.warn("Invalid ranges for domain axis specified: " + nfe); + } + + return null; + } + + + protected void adaptZoom(XYPlot plot) { + logger.debug("Adapt zoom of Timeseries chart."); + + zoomX(plot, plot.getDomainAxis(), getXBounds(0), getDomainAxisRange()); + + Bounds valueAxisBounds = getValueAxisRange(); + + for (int j = 0, n = plot.getRangeAxisCount(); j < n; j++) { + zoomY( + plot, + plot.getRangeAxis(j), + getYBounds(j), + valueAxisBounds); + } + } + + + /** + * @param plot the plot. + * @param axis the value (x, time) axis of which to set bounds. + * @param total the current bounds (?). + */ + protected void zoomX( + XYPlot plot, + ValueAxis axis, + Bounds total,//we could equally nicely getXBounds(0) + Bounds user + ) { + if (logger.isDebugEnabled()) { + logger.debug("== Zoom X axis =="); + logger.debug(" Total axis range : " + total); + logger.debug(" User defined range: " + user); + } + + if (user != null) { + long min = total.getLower().longValue(); + long max = total.getUpper().longValue(); + long diff = max > min ? max - min : min - max; + + long newMin = Math.round(min + user.getLower().doubleValue() * diff); + long newMax = Math.round(min + user.getUpper().doubleValue() * diff); + + TimeBounds newBounds = new TimeBounds(newMin, newMax); + + logger.debug(" Zoom axis to: " + newBounds); + + newBounds.applyBounds(axis, AXIS_SPACE); + } + else { + logger.debug("No user specified zoom values found!"); + if (total != null && axis != null) { + total.applyBounds(axis, AXIS_SPACE); + } + } + } + + + /** + * @param user zoom values in percent. + */ + protected void zoomY( + XYPlot plot, + ValueAxis axis, + Bounds total, + Bounds user + ) { + if (logger.isDebugEnabled()) { + logger.debug("== Zoom Y axis =="); + logger.debug(" Total axis range : " + total); + logger.debug(" User defined range: " + user); + } + + if (user != null) { + double min = total.getLower().doubleValue(); + double max = total.getUpper().doubleValue(); + double diff = max > min ? max - min : min - max; + + double newMin = min + user.getLower().doubleValue() * diff; + double newMax = min + user.getUpper().doubleValue() * diff; + + DoubleBounds newBounds = new DoubleBounds(newMin, newMax); + + logger.debug(" Zoom axis to: " + newBounds); + + newBounds.applyBounds(axis, AXIS_SPACE); + } + else { + logger.debug("No user specified zoom values found!"); + if (total != null && axis != null) { + total.applyBounds(axis, AXIS_SPACE); + } + } + } + + + /** + * Adjusts the axes of a plot. This method sets the <i>labelFont</i> of the + * X axis. + * + * (Duplicate in XYChartGenerator). + * + * @param plot The XYPlot of the chart. + */ + protected void adjustAxes(XYPlot plot) { + ValueAxis xaxis = plot.getDomainAxis(); + + ChartSettings chartSettings = getChartSettings(); + if (chartSettings == null) { + return; + } + + Font labelFont = new Font( + DEFAULT_FONT_NAME, + Font.BOLD, + getXAxisLabelFontSize()); + + xaxis.setLabelFont(labelFont); + xaxis.setTickLabelFont(labelFont); + } + + + protected Date decodeXAxisValue(JSONArray array) throws JSONException, ParseException { + try { + double x = array.getDouble(0); + long l = (new Double(x)).longValue(); + return new Date(l); + } + catch(JSONException ex) { + String str = array.getString(0); + DateFormat df = DateFormat.getDateInstance( + DateFormat.MEDIUM, Resources.getLocale(context.getMeta())); + return df.parse(str); + } + } + + /** + * Do Points out. + */ + protected void doPoints( + Object o, + ArtifactAndFacet aandf, + Document theme, + boolean visible, + int axisIndex + ) { + String seriesName = aandf.getFacetDescription(); + TimeSeries series = new StyledTimeSeries(seriesName, theme); + + // Add text annotations for single points. + List<XYTextAnnotation> xy = new ArrayList<XYTextAnnotation>(); + HashMap<Day, String> names = new HashMap<Day, String>(); + + try { + JSONArray points = new JSONArray((String) o); + for (int i = 0, P = points.length(); i < P; i++) { + JSONArray array = points.getJSONArray(i); + + double y = array.getDouble(1); + String name = array.getString(2); + boolean act = array.getBoolean(3); + if (!act) { + continue; + } + + Date date = decodeXAxisValue(array); + + Day day = new Day(date); + series.add(day, y, false); + names.put(day, name); + } + } + catch(JSONException ex) { + logger.error("Could not decode json"); + } + catch(ParseException ex) { + logger.error("Could not parse date string"); + } + + TimeSeriesCollection tsc = new TimeSeriesCollection(); + tsc.addSeries(series); + // Add Annotations. + for (int i = 0, S = series.getItemCount(); i < S; i++) { + double x = tsc.getXValue(0, i); + double y = tsc.getYValue(0, i); + xy.add(new CollisionFreeXYTextAnnotation( + names.get(series.getTimePeriod(i)), x, y)); + logger.debug("doPoints(): x=" + x + " y=" + y); + } + FLYSAnnotation annotations = + new FLYSAnnotation(null, null, null, theme); + annotations.setTextAnnotations(xy); + + // Do not generate second legend entry. (null was passed for the aand before). + doAnnotations(annotations, null, theme, visible); + + addAxisDataset(tsc, axisIndex, visible); + } + + public void addDomainAxisMarker(XYPlot plot) { + logger.debug("domainmarkers: " + domainMarker.size()); + for (Marker marker: domainMarker) { + logger.debug("adding domain marker"); + plot.addDomainMarker(marker, Layer.BACKGROUND); + } + domainMarker.clear(); + } + + public void addValueAxisMarker(XYPlot plot) { + for (Marker marker: valueMarker) { + logger.debug("adding value marker.."); + plot.addRangeMarker(marker, Layer.BACKGROUND); + } + valueMarker.clear(); + } + + public void addAttribute(String seriesKey, String name) { + attributes.put(seriesKey, name); + } + + private LegendItem getLegendItemFor(XYPlot plot, String interSeriesKey) { + LegendItemCollection litems = plot.getLegendItems(); + Iterator<LegendItem> iter = litems.iterator(); + while(iter.hasNext()) { + LegendItem item = iter.next(); + if(interSeriesKey.startsWith(item.getSeriesKey().toString())) { + return item; + } + } + return null; + } + + protected void applySeriesAttributes(XYPlot plot) { + int count = plot.getDatasetCount(); + for (int i = 0; i < count; i++) { + XYDataset data = plot.getDataset(i); + if (data == null) { + continue; + } + + int seriesCount = data.getSeriesCount(); + for (int j = 0; j < seriesCount; j++) { + StyledTimeSeries series = + (StyledTimeSeries)getSeriesOf(data, j); + String key = series.getKey().toString(); + + if (attributes.containsKey(key)) { + // Interpolated points are drawn unfilled + if (attributes.get(key).equals("interpolate")) { + XYLineAndShapeRenderer renderer = + series.getStyle().getRenderer(); + renderer.setSeriesPaint( + j, + renderer.getSeriesFillPaint(j)); + renderer.setSeriesShapesFilled(j, false); + + LegendItem legendItem = getLegendItemFor(plot, key); + if(legendItem != null) { + LegendItem interLegend = new LegendItem( + legendItem.getLabel(), + legendItem.getDescription(), + legendItem.getToolTipText(), + legendItem.getURLText(), + legendItem.isShapeVisible(), + legendItem.getShape(), + false, // shapeFilled? + legendItem.getFillPaint(), + true, // shapeOutlineVisible? + renderer.getSeriesFillPaint(j), + legendItem.getOutlineStroke(), + legendItem.isLineVisible(), + legendItem.getLine(), + legendItem.getLineStroke(), + legendItem.getLinePaint() + ); + interLegend.setSeriesKey(series.getKey()); + logger.debug("applySeriesAttributes: draw unfilled legend item"); + plot.getLegendItems().add(interLegend); + } + } + } + + if (attributes.containsKey(key)) { + if(attributes.get(key).equals("outline")) { + XYLineAndShapeRenderer renderer = + series.getStyle().getRenderer(); + renderer.setSeriesPaint( + j, + renderer.getSeriesFillPaint(j)); + renderer.setDrawOutlines(true); + } + } + } + } + } + + /** Two Ranges that span a rectangular area. */ + public static class Area { + protected Range xRange; + protected Range yRange; + + public Area(Range rangeX, Range rangeY) { + this.xRange = rangeX; + this.yRange = rangeY; + } + + public Area(ValueAxis axisX, ValueAxis axisY) { + this.xRange = axisX.getRange(); + this.yRange = axisY.getRange(); + } + + public double ofLeft(double percent) { + return xRange.getLowerBound() + + xRange.getLength() * percent; + } + + public double ofRight(double percent) { + return xRange.getUpperBound() + - xRange.getLength() * percent; + } + + public double ofGround(double percent) { + return yRange.getLowerBound() + + yRange.getLength() * percent; + } + + public double atTop() { + return yRange.getUpperBound(); + } + + public double atGround() { + return yRange.getLowerBound(); + } + + public double atRight() { + return xRange.getUpperBound(); + } + + public double atLeft() { + return xRange.getLowerBound(); + } + + public double above(double percent, double base) { + return base + yRange.getLength() * percent; + } + } + +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :