Mercurial > dive4elements > river
view flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java @ 3115:5482a8a48a3f
I18N of SQ relation charts.
flys-artifacts/trunk@4715 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author | Ingo Weinzierl <ingo.weinzierl@intevation.de> |
---|---|
date | Wed, 20 Jun 2012 07:49:36 +0000 |
parents | 6c91e05a5f51 |
children | a08538e21b55 |
line wrap: on
line source
package de.intevation.flys.exports; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Paint; import java.awt.Stroke; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.LinkedHashMap; import java.util.Map; import org.w3c.dom.Document; 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.XYBoxAnnotation; import org.jfree.chart.annotations.XYLineAnnotation; import org.jfree.chart.annotations.XYTextAnnotation; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.Marker; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.data.Range; import org.jfree.data.general.Series; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; import org.jfree.data.xy.XYDataset; import org.jfree.ui.TextAnchor; import de.intevation.artifactdatabase.state.ArtifactAndFacet; import de.intevation.artifactdatabase.state.Facet; import de.intevation.flys.jfree.Bounds; import de.intevation.flys.jfree.DoubleBounds; import de.intevation.flys.jfree.FLYSAnnotation; import de.intevation.flys.jfree.StickyAxisAnnotation; import de.intevation.flys.jfree.CollisionFreeXYTextAnnotation; import de.intevation.flys.jfree.StyledAreaSeriesCollection; import de.intevation.flys.jfree.StyledXYSeries; import de.intevation.flys.utils.ThemeAccess; import de.intevation.flys.utils.ThemeUtil; import de.intevation.flys.artifacts.model.HYKFactory; import org.json.JSONArray; import org.json.JSONException; /** * An abstract base class for creating XY charts. * * With respect to datasets, ranges and axis, there are following requirements: * <ul> * <li> First in, first drawn: "Early" datasets should be of lower Z-Oder * than later ones (only works per-axis). </li> * <li> Visible axis should initially show the range of all datasets that * show data for this axis (even invisible ones). Motivation: Once * a dataset (theme) has been activated, it should be on screen. </li> * <li> There should always be a Y-Axis on the "left". </li> * </ul> * * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a> */ public abstract class XYChartGenerator extends ChartGenerator { // TODO Consider storing the renderer here. private class XYAxisDataset implements AxisDataset { /** Symbolic integer, but also coding the priority (0 goes first). */ protected int axisSymbol; /** List of assigned datasets (in order). */ protected List<XYDataset> datasets; /** Range to use to include all given datasets. */ protected Range range; /** Index of axis in plot. */ protected int plotAxisIndex; /** Create AxisDataset. */ public XYAxisDataset(int symb) { this.axisSymbol = symb; datasets = new ArrayList<XYDataset>(); } /** Merge (or create given range with range so far (if any). */ private void mergeRanges(Range subRange) { // 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); } /** Add a dataset to internal list for this axis. */ @Override public void addDataset(XYDataset dataset) { datasets.add(dataset); includeYRange(((XYSeriesCollection) dataset).getSeries(0)); } /** Add a dataset, include its range. */ public void addDataset(XYSeries series) { addDataset(new XYSeriesCollection(series)); } /** Set Range for this axis. */ @Override public void setRange(Range range) { this.range = range; } /** Get Range for this axis. */ @Override public Range getRange() { return range; } /** Get Array of Datasets. */ @Override public XYDataset[] getDatasets() { return (XYDataset[]) datasets.toArray(new XYDataset[datasets.size()]); } /** Add a Dataset that describes an area. */ public void addArea(StyledAreaSeriesCollection series) { this.datasets.add(series); } // TODO obsolete? /** True if to be rendered as area. */ @Override public boolean isArea(XYDataset series) { return (series instanceof StyledAreaSeriesCollection); } /** Adjust range to include given dataset. */ public void includeYRange(XYSeries dataset) { mergeRanges(new Range(dataset.getMinY(), dataset.getMaxY())); } /** True if no datasets given. */ @Override public boolean isEmpty() { return this.datasets.isEmpty(); } /** Set the 'real' axis index that this axis is mapped to. */ @Override public void setPlotAxisIndex(int axisIndex) { this.plotAxisIndex = axisIndex; } /** Get the 'real' axis index that this axis is mapped to. */ @Override public int getPlotAxisIndex() { return this.plotAxisIndex; } } // class AxisDataset /** Enumerator over existing axes. */ protected abstract YAxisWalker getYAxisWalker(); protected static float ANNOTATIONS_AXIS_OFFSET = 0.02f; public static final int AXIS_SPACE = 5; /** The logger that is used in this generator. */ private static Logger logger = Logger.getLogger(XYChartGenerator.class); /** List of annotations to insert in plot. */ protected List<FLYSAnnotation> annotations; protected List<Marker> domainMarkers = new ArrayList<Marker>(); /** The max X range to include all X values of all series for each axis. */ protected Map<Integer, Bounds> xBounds; /** The max Y range to include all Y values of all series for each axis. */ protected Map<Integer, Bounds> yBounds; public XYChartGenerator() { super(); xBounds = new HashMap<Integer, Bounds>(); yBounds = new HashMap<Integer, Bounds>(); } /** * Generate the chart anew (including localized axis and all). */ public JFreeChart generateChart() { logger.debug("XYChartGenerator.generateChart"); JFreeChart chart = ChartFactory.createXYLineChart( getChartTitle(), getXAxisLabel(), getYAxisLabel(0), null, PlotOrientation.VERTICAL, isLegendVisible(), false, false); XYPlot plot = (XYPlot) chart.getPlot(); plot.setDomainAxis(createXAxis(getXAxisLabel())); chart.setBackgroundPaint(Color.WHITE); plot.setBackgroundPaint(Color.WHITE); addSubtitles(chart); adjustPlot(plot); //debugAxis(plot); addDatasets(plot); //debugDatasets(plot); addMarkers(plot); recoverEmptyPlot(plot); preparePointRanges(plot); //debugAxis(plot); localizeAxes(plot); adjustAxes(plot); autoZoom(plot); // These have to go after the autozoom. addAnnotationsToRenderer(plot); //aggregateLegendEntries(plot); return chart; } protected NumberAxis createXAxis(String label) { return new NumberAxis(label); } @Override protected Series getSeriesOf(XYDataset dataset, int idx) { return ((XYSeriesCollection) dataset).getSeries(idx); } @Override protected AxisDataset createAxisDataset(int idx) { logger.debug("Create new XYAxisDataset for index: " + idx); return new XYAxisDataset(idx); } /** * Put debug output about datasets. */ public void debugDatasets(XYPlot plot) { logger.debug("Number of datasets: " + plot.getDatasetCount()); for (int i = 0, P = plot.getDatasetCount(); i < P; i++) { if (plot.getDataset(i) == null) { logger.debug("Dataset #" + i + " is null"); continue; } logger.debug("Dataset #" + i + ":" + plot.getDataset(i)); XYSeriesCollection series = (XYSeriesCollection) plot.getDataset(i); logger.debug("X-Extend of Dataset: " + series.getSeries(0).getMinX() + " " + series.getSeries(0).getMaxX()); logger.debug("Y-Extend of Dataset: " + series.getSeries(0).getMinY() + " " + series.getSeries(0).getMaxY()); } } /** * Put debug output about axes. */ public void debugAxis(XYPlot plot) { logger.debug("..............."); for (int i = 0, P = plot.getRangeAxisCount(); i < P; i++) { if (plot.getRangeAxis(i) == null) logger.debug("Range-Axis #" + i + " == null"); else { logger.debug("Range-Axis " + i + " != null [" + plot.getRangeAxis(i).getRange().getLowerBound() + " " + plot.getRangeAxis(i).getRange().getUpperBound() + "]"); } } logger.debug("..............."); } /** * Registers an area to be drawn. * @param area Area to be drawn. * @param index 'axis index' * @param visible Whether or not to be visible (important for range calculations). */ public void addAreaSeries(StyledAreaSeriesCollection area, int index, boolean visible) { if (area == null) { logger.warn("Cannot yet render above/under curve."); return; } XYAxisDataset axisDataset = (XYAxisDataset) getAxisDataset(index); if (visible) { axisDataset.addArea(area); } else { // TODO only range merging. } //TODO range merging. } /** * Add given series if visible, if not visible adjust ranges (such that * all points in data would be plotted once visible). * @param series the dataseries to include in plot. * @param index ('symbolic') index of the series and of its axis. * @param visible whether or not the data should be plotted. */ public void addAxisSeries(XYSeries series, int index, boolean visible) { if (series == null) { return; } logger.debug("Y Range of XYSeries: " + series.getMinY() + " | " + series.getMaxY()); addAxisDataset(new XYSeriesCollection(series), index, visible); XYAxisDataset axisDataset = (XYAxisDataset) getAxisDataset(index); if (!visible) { // Do this also when not visible to have axis scaled by default such // that every data-point could be seen (except for annotations). axisDataset.includeYRange(series); } } /** * Add the given vertical marker to the chart. */ public void addDomainMarker(Marker marker) { if (marker == null) { return; } domainMarkers.add(marker); } protected void addMarkers(XYPlot plot) { for(Marker marker : domainMarkers) { plot.addDomainMarker(marker); } } /** * Effect: extend range of x axis to include given limits. * * @param range the given ("minimal") range. * @param index index of axis to be merged. */ @Override protected void combineXBounds(Bounds bounds, int index) { if (!(bounds instanceof DoubleBounds)) { logger.warn("Unsupported Bounds type: " + bounds.getClass()); return; } DoubleBounds dBounds = (DoubleBounds) bounds; if (dBounds == null || Double.isNaN((Double) dBounds.getLower()) || Double.isNaN((Double) dBounds.getUpper())) { return; } Bounds old = getXBounds(index); if (old != null) { dBounds = (DoubleBounds) dBounds.combine(old); } setXBounds(index, dBounds); } @Override protected void combineYBounds(Bounds bounds, int index) { if (!(bounds instanceof DoubleBounds)) { logger.warn("Unsupported Bounds type: " + bounds.getClass()); return; } DoubleBounds dBounds = (DoubleBounds) bounds; if (dBounds == null || Double.isNaN((Double) dBounds.getLower()) || Double.isNaN((Double) dBounds.getUpper())) { return; } Bounds old = getYBounds(index); if (old != null) { dBounds = (DoubleBounds) dBounds.combine(old); } setYBounds(index, dBounds); } /** * Adds annotations to list (if visible is true). */ public void addVisibleAnnotations(FLYSAnnotation annotation) { if (annotations == null) { annotations = new ArrayList<FLYSAnnotation>(); } annotations.add(annotation); } /** * If no data is visible, draw at least empty axis. */ private void recoverEmptyPlot(XYPlot plot) { if (plot.getRangeAxis() == null) { logger.debug("debug: No range axis"); plot.setRangeAxis(createYAxis(0)); } } /** * Expands X axes if only a point is shown. */ private void preparePointRanges(XYPlot plot) { for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) { logger.debug("Check whether to expand a x axis."); Integer key = Integer.valueOf(i); Bounds b = getXBounds(key); if (b != null && b.getLower() == b.getUpper()) { double lo = (Double) b.getLower(); double hi = (Double) b.getUpper(); double add = (hi - lo) / 100 * 5; setXBounds(key, new DoubleBounds(lo-add, hi+add)); } } } /** * This method zooms the plot to the specified ranges in the attribute * document or to the ranges specified by the min/max values in the * datasets. <b>Note:</b> We determine the range manually if no zoom ranges * are given, because JFreeCharts auto-zoom adds a margin to the left and * right of the data area. * * @param plot The XYPlot. */ protected void autoZoom(XYPlot plot) { logger.debug("Zoom to specified ranges."); Range xrange = getDomainAxisRange(); Range yrange = getValueAxisRange(); ValueAxis xAxis = plot.getDomainAxis(); Range fixedXRange = getRangeForAxisFromSettings("X"); if (fixedXRange != null) { xAxis.setRange(fixedXRange); } else { zoomX(plot, xAxis, getXBounds(0), xrange); } for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) { ValueAxis yaxis = plot.getRangeAxis(i); if (yaxis instanceof IdentifiableNumberAxis) { IdentifiableNumberAxis idAxis = (IdentifiableNumberAxis) yaxis; Range fixedRange = getRangeForAxisFromSettings(idAxis.getId()); if (fixedRange != null) { yaxis.setRange(fixedRange); continue; } } if (yaxis == null) { logger.debug("Zoom problem: no Y Axis for index: " + i); continue; } logger.debug("Prepare zoom settings for y axis at index: " + i); zoomY(plot, yaxis, getYBounds(Integer.valueOf(i)), yrange); } } protected Range getDomainAxisRange() { String[] ranges = getDomainAxisRangeFromRequest(); if (ranges == null || ranges.length < 2) { logger.debug("No zoom range for domain axis specified."); return null; } if (ranges[0].length() > 0 && ranges[1].length() > 0) { try { double from = Double.parseDouble(ranges[0]); double to = Double.parseDouble(ranges[1]); if (from == 0 && to == 0) { logger.debug("No range specified. Lower and upper X == 0"); return null; } if (from > to) { double tmp = to; to = from; from = tmp; } return new Range(from, to); } catch (NumberFormatException nfe) { logger.warn("Wrong values for domain axis range."); } } return null; } protected Range getValueAxisRange() { String[] ranges = getValueAxisRangeFromRequest(); if (ranges == null || ranges.length < 2) { logger.debug("No range specified. Lower and upper Y == 0"); return null; } if (ranges[0].length() > 0 && ranges[1].length() > 0) { try { double from = Double.parseDouble(ranges[0]); double to = Double.parseDouble(ranges[1]); if (from == 0 && to == 0) { logger.debug("No range specified. Lower and upper Y == 0"); return null; } return from > to ? new Range(to, from) : new Range(from, to); } catch (NumberFormatException nfe) { logger.warn("Wrong values for value axis range."); } } return null; } protected boolean zoomX(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) { return zoom(plot, axis, bounds, x); } protected boolean zoomY(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) { return zoom(plot, axis, bounds, x); } /** * Zooms the x axis to the range specified in the attribute document. * * @param plot The XYPlot. * @param axis The axis the shoud be modified. * @param range The whole range specified by a dataset. * @param x A user defined range (null permitted). * * @return true, if a zoom range was specified, otherwise false. */ protected boolean zoom(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) { if (bounds == null) { return false; } if (x != null) { double min = bounds.getLower().doubleValue(); double max = bounds.getUpper().doubleValue(); if (logger.isDebugEnabled()) { logger.debug("Minimum is: " + min); logger.debug("Maximum is: " + max); logger.debug("Lower zoom is: " + x.getLowerBound()); logger.debug("Upper zoom is: " + x.getUpperBound()); } double diff = max > min ? max - min : min - max; DoubleBounds computed = new DoubleBounds( min + x.getLowerBound() * diff, min + x.getUpperBound() * diff); computed.applyBounds(axis, AXIS_SPACE); logger.debug("Zoom axis to: " + computed); return true; } bounds.applyBounds(axis, AXIS_SPACE); return false; } /** * Extract the minimum and maximum values for x and y axes * which are stored in <i>xRanges</i> and <i>yRanges</i>. * * @param index The index of the y-Axis. * * @return a Range[] as follows: [x-Range, y-Range]. */ @Override public Range[] getRangesForAxis(int index) { logger.debug("getRangesForAxis " + index); Bounds rx = getXBounds(Integer.valueOf(0)); Bounds ry = getYBounds(Integer.valueOf(index)); if (rx == null) { logger.warn("Range for x axis not set." + " Using default values: 0 - 1."); rx = new DoubleBounds(0, 1); } if (ry == null) { logger.warn("Range for y" + index + " axis not set. Using default values: 0 - 1."); ry = new DoubleBounds(0, 1); } return new Range[] { new Range(rx.getLower().doubleValue(), rx.getUpper().doubleValue()), new Range(ry.getLower().doubleValue(), ry.getUpper().doubleValue()) }; } /** Get X (usually horizontal) extent for given axis. */ @Override public Bounds getXBounds(int axis) { return xBounds.get(axis); } /** Set X (usually horizontal) extent for given axis. */ @Override protected void setXBounds(int axis, Bounds bounds) { xBounds.put(axis, bounds); } /** Get Y (usually vertical) extent for given axis. */ @Override public Bounds getYBounds(int axis) { return yBounds.get(axis); } /** Set Y (usually vertical) extent for given axis. */ @Override protected void setYBounds(int axis, Bounds bounds) { yBounds.put(axis, bounds); } /** Get color for hyk zones by their type (which is the name). */ public Paint colorForHYKZone(String zoneName) { if (zoneName.startsWith("R")) { // Brownish. return new Color(153, 60, 0); } else if (zoneName.startsWith("V")) { // Greenish. return new Color(0, 255, 0); } else if (zoneName.startsWith("B")) { // Grayish. return new Color(128, 128, 128); } else if (zoneName.startsWith("H")) { // Blueish. return new Color(0, 0, 255); } else { // Default. logger.debug("Unknown zone type found."); return new Color(255, 0, 0); } } /** * Create annotation that sticks to "ground" (X) axis. * @param area helper to calculate coordinates * @param pos one-dimensional position (distance from axis) * @param lineStyle the line style to use for the line. */ protected static XYLineAnnotation createGroundStickAnnotation( Area area, float pos, ThemeAccess.LineStyle lineStyle ) { // Style the line. if (lineStyle != null) { return new XYLineAnnotation( pos, area.atGround(), pos, area.ofGround(ANNOTATIONS_AXIS_OFFSET), new BasicStroke(lineStyle.getWidth()),lineStyle.getColor()); } else { return new XYLineAnnotation( pos, area.atGround(), pos, area.ofGround(ANNOTATIONS_AXIS_OFFSET)); } } /** * Create annotation that sticks to the second Y axis ("right"). * @param area helper to calculate coordinates * @param pos one-dimensional position (distance from axis) * @param lineStyle the line style to use for the line. */ protected static XYLineAnnotation createRightStickAnnotation( Area area, float pos, ThemeAccess.LineStyle lineStyle ) { // Style the line. if (lineStyle != null) { return new XYLineAnnotation( area.ofRight(ANNOTATIONS_AXIS_OFFSET), pos, area.atRight(), pos, new BasicStroke(lineStyle.getWidth()), lineStyle.getColor()); } else { return new XYLineAnnotation( area.atRight(), pos, area.ofRight(ANNOTATIONS_AXIS_OFFSET), pos); } } /** * Create annotation that sticks to the second Y axis ("right"). * @param area helper to calculate coordinates * @param pos one-dimensional position (distance from axis) * @param lineStyle the line style to use for the line. */ protected static XYLineAnnotation createLeftStickAnnotation( Area area, float pos, ThemeAccess.LineStyle lineStyle ) { // Style the line. if (lineStyle != null) { return new XYLineAnnotation( area.atLeft(), pos, area.ofLeft(ANNOTATIONS_AXIS_OFFSET), pos, new BasicStroke(lineStyle.getWidth()), lineStyle.getColor()); } else { return new XYLineAnnotation( area.atLeft(), pos, area.ofLeft(ANNOTATIONS_AXIS_OFFSET), pos); } } /** * Create a line from a axis to a given point. * @param axis The "simple" axis. * @param fromD1 from-location in first dimension. * @param toD2 to-location in second dimension. * @param area helper to calculate offsets. * @param lineStyle optional line style. */ protected static XYLineAnnotation createStickyLineAnnotation( StickyAxisAnnotation.SimpleAxis axis, float fromD1, float toD2, Area area, ThemeAccess.LineStyle lineStyle ) { double anchorX1 = 0d, anchorX2 = 0d, anchorY1 = 0d, anchorY2 = 0d; switch(axis) { case X_AXIS: anchorX1 = fromD1; anchorX2 = fromD1; anchorY1 = area.atGround(); anchorY2 = toD2; break; case Y_AXIS: anchorX1 = area.atLeft(); anchorX2 = toD2; anchorY1 = fromD1; anchorY2 = fromD1; break; case Y_AXIS2: anchorX1 = area.atRight(); anchorX2 = toD2; anchorY1 = fromD1; anchorY2 = fromD1; break; } // Style the line. if (lineStyle != null) { return new XYLineAnnotation( anchorX1, anchorY1, anchorX2, anchorY2, new BasicStroke(lineStyle.getWidth()), lineStyle.getColor()); } else { return new XYLineAnnotation( anchorX1, anchorY1, anchorX2, anchorY2); } } /** * Add a text and a line annotation. * @param area convenience to determine positions in plot. * @param theme (optional) theme document */ public void addStickyAnnotation( StickyAxisAnnotation annotation, XYPlot plot, Area area, ThemeAccess.LineStyle lineStyle, ThemeAccess.TextStyle textStyle, Document theme ) { // OPTIMIZE pre-calculate area-related values final float TEXT_OFF = 0.03f; XYLineAnnotation lineAnnotation = null; XYTextAnnotation textAnnotation = null; int rendererIndex = 0; if (annotation.atX()) { textAnnotation = new CollisionFreeXYTextAnnotation( annotation.getText(), annotation.getPos(), area.ofGround(TEXT_OFF)); // OPTIMIZE externalize the calculation involving PI. textAnnotation.setRotationAngle(270f*Math.PI/180f); lineAnnotation = createGroundStickAnnotation( area, annotation.getPos(), lineStyle); textAnnotation.setRotationAnchor(TextAnchor.CENTER_LEFT); textAnnotation.setTextAnchor(TextAnchor.CENTER_LEFT); } else { // Do the more complicated case where we stick to the Y-Axis. // There is one nasty case (duration curves, where annotations // might stick to the second y-axis). XYAxisDataset dataset = (XYAxisDataset) getAxisDataset( new Integer(annotation.getAxisSymbol())); if (dataset == null) { logger.warn("Annotation should stick to unfindable y-axis: " + annotation.getAxisSymbol()); rendererIndex = 0; } else { rendererIndex = dataset.getPlotAxisIndex(); } // Stick to the "right" (opposed to left) Y-Axis. if (rendererIndex != 0) { // OPTIMIZE: Pass a different area to this function, // do the adding to renderer outside (let this // function return the annotations). // Note that this path is travelled rarely. Area area2 = new Area(plot.getDomainAxis(), plot.getRangeAxis(rendererIndex)); textAnnotation = new CollisionFreeXYTextAnnotation( annotation.getText(), area2.ofRight(TEXT_OFF), annotation.getPos()); textAnnotation.setRotationAnchor(TextAnchor.CENTER_RIGHT); textAnnotation.setTextAnchor(TextAnchor.CENTER_RIGHT); lineAnnotation = createRightStickAnnotation( area2, annotation.getPos(), lineStyle); if (!Float.isNaN(annotation.getHitPoint()) && theme != null) { // New line annotation to hit curve. if (ThemeUtil.parseShowVerticalLine(theme)) { XYLineAnnotation hitLineAnnotation = createStickyLineAnnotation( StickyAxisAnnotation.SimpleAxis.X_AXIS, annotation.getHitPoint(), annotation.getPos(),// annotation.getHitPoint(), area2, lineStyle); plot.getRenderer(rendererIndex).addAnnotation(hitLineAnnotation, org.jfree.ui.Layer.BACKGROUND); } if (ThemeUtil.parseShowHorizontalLine(theme)) { XYLineAnnotation lineBackAnnotation = createStickyLineAnnotation( StickyAxisAnnotation.SimpleAxis.Y_AXIS2, annotation.getPos(), annotation.getHitPoint(), area2, lineStyle); plot.getRenderer(rendererIndex).addAnnotation(lineBackAnnotation, org.jfree.ui.Layer.BACKGROUND); } } } else { // Stick to the left y-axis. textAnnotation = new CollisionFreeXYTextAnnotation( annotation.getText(), area.ofLeft(TEXT_OFF), annotation.getPos()); textAnnotation.setRotationAnchor(TextAnchor.CENTER_LEFT); textAnnotation.setTextAnchor(TextAnchor.CENTER_LEFT); lineAnnotation = createLeftStickAnnotation(area, annotation.getPos(), lineStyle); if (!Float.isNaN(annotation.getHitPoint()) && theme != null) { // New line annotation to hit curve. if (ThemeUtil.parseShowHorizontalLine(theme)) { XYLineAnnotation hitLineAnnotation = createStickyLineAnnotation( StickyAxisAnnotation.SimpleAxis.Y_AXIS, annotation.getPos(), annotation.getHitPoint(), area, lineStyle); plot.getRenderer(rendererIndex).addAnnotation(hitLineAnnotation, org.jfree.ui.Layer.BACKGROUND); } if (ThemeUtil.parseShowVerticalLine(theme)) { XYLineAnnotation lineBackAnnotation = createStickyLineAnnotation( StickyAxisAnnotation.SimpleAxis.X_AXIS, annotation.getHitPoint(), annotation.getPos(), area, lineStyle); plot.getRenderer(rendererIndex).addAnnotation(lineBackAnnotation, org.jfree.ui.Layer.BACKGROUND); } } } } // Style the text. if (textStyle != null) { textStyle.apply(textAnnotation); } // Add the Annotations to renderer. plot.getRenderer(rendererIndex).addAnnotation(textAnnotation, org.jfree.ui.Layer.FOREGROUND); plot.getRenderer(rendererIndex).addAnnotation(lineAnnotation, org.jfree.ui.Layer.FOREGROUND); } /** * Add the annotations (Sticky, Text and hyk zones) stored * in the annotations field. */ public void addAnnotationsToRenderer(XYPlot plot) { logger.debug("XYChartGenerator.addAnnotationsToRenderer"); if (annotations == null) { logger.debug("XYChartGenerator.addAnnotationsToRenderer: no annotations."); return; } // Paints for the boxes/lines. Stroke basicStroke = new BasicStroke(1.0f); Paint linePaint = new Color(255, 0,0,60); Paint fillPaint = new Color(0, 255,0,60); Paint tranPaint = new Color(0, 0,0, 0); // OPTMIMIZE: Pre-calculate positions Area area = new Area( plot.getDomainAxis(0).getRange(), plot.getRangeAxis().getRange()); // Walk over all Annotation sets. for (FLYSAnnotation fa: annotations) { // Access text styling, if any. Document theme = fa.getTheme(); ThemeAccess.TextStyle textStyle = null; ThemeAccess.LineStyle lineStyle = null; // Get Themeing information and add legend item. if (theme != null) { ThemeAccess themeAccess = new ThemeAccess(theme); textStyle = themeAccess.parseTextStyle(); lineStyle = themeAccess.parseLineStyle(); if (fa.getLabel() != null) { LegendItemCollection lic = new LegendItemCollection(); LegendItemCollection old = plot.getFixedLegendItems(); lic.add(createLegendItem(theme, fa.getLabel())); // (Re-)Add prior legend entries. if (old != null) { old.addAll(lic); } else { old = lic; } plot.setFixedLegendItems(old); } } // The 'Sticky' Annotations (at axis, with line and text). for (StickyAxisAnnotation sta: fa.getAxisTextAnnotations()) { addStickyAnnotation( sta, plot, area, lineStyle, textStyle, theme); } // Other Text Annotations (e.g. labels of manual points). for (XYTextAnnotation ta: fa.getTextAnnotations()) { // Style the text. if (textStyle != null) { textStyle.apply(ta); } ta.setY(area.above(0.05d, ta.getY())); plot.getRenderer().addAnnotation(ta, org.jfree.ui.Layer.FOREGROUND); } // Hyks. for (HYKFactory.Zone zone: fa.getBoxes()) { // For each zone, create a box to fill with color, a box to draw // the lines and a text to display the type. fillPaint = colorForHYKZone(zone.getName()); XYBoxAnnotation boxA = new XYBoxAnnotation(zone.getFrom(), area.atGround(), zone.getTo(), area.ofGround(0.03f), basicStroke, tranPaint, fillPaint); XYBoxAnnotation boxB = new XYBoxAnnotation(zone.getFrom(), area.atGround(), zone.getTo(), area.atTop(), basicStroke, fillPaint, tranPaint); XYTextAnnotation tex = new XYTextAnnotation(zone.getName(), zone.getFrom() + (zone.getTo() - zone.getFrom()) / 2.0d, area.ofGround(0.015f)); if (textStyle != null) { textStyle.apply(tex); } plot.getRenderer().addAnnotation(boxA, org.jfree.ui.Layer.BACKGROUND); plot.getRenderer().addAnnotation(boxB, org.jfree.ui.Layer.BACKGROUND); plot.getRenderer().addAnnotation(tex, org.jfree.ui.Layer.BACKGROUND); } } } /** * Adjusts the axes of a plot. This method sets the <i>labelFont</i> of the * X axis. * * @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); } /** * This method walks over all axes (domain and range) of <i>plot</i> and * calls localizeDomainAxis() for domain axes or localizeRangeAxis() for * range axes. * * @param plot The XYPlot. */ private void localizeAxes(XYPlot plot) { for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) { ValueAxis axis = plot.getDomainAxis(i); if (axis != null) { localizeDomainAxis(axis); } else { logger.warn("Domain axis at " + i + " is null."); } } for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) { ValueAxis axis = plot.getRangeAxis(i); if (axis != null) { localizeRangeAxis(axis); } else { logger.warn("Range axis at " + i + " is null."); } } } /** * Overrides the NumberFormat with the NumberFormat for the current locale * that is provided by getLocale(). * * @param domainAxis The domain axis that needs localization. */ protected void localizeDomainAxis(ValueAxis domainAxis) { NumberFormat nf = NumberFormat.getInstance(getLocale()); ((NumberAxis) domainAxis).setNumberFormatOverride(nf); } /** * Overrides the NumberFormat with the NumberFormat for the current locale * that is provided by getLocale(). * * @param domainAxis The domain axis that needs localization. */ protected void localizeRangeAxis(ValueAxis rangeAxis) { NumberFormat nf = NumberFormat.getInstance(getLocale()); ((NumberAxis) rangeAxis).setNumberFormatOverride(nf); } /** * Register annotations like MainValues for later plotting * * @param annotations list of annotations (data of facet). * @param aandf Artifact and the facet. * @param theme Theme document for given annotations. * @param visible The visibility of the annotations. */ protected void doAnnotations( FLYSAnnotation annotations, ArtifactAndFacet aandf, Document theme, boolean visible ){ // Running into trouble here. logger.debug("doAnnotations"); // Add all annotations to our annotation pool. annotations.setTheme(theme); if (aandf != null) { Facet facet = aandf.getFacet(); annotations.setLabel(aandf.getFacetDescription()); } else { logger.debug( "Art/Facet for Annotations is null. " + "This should never happen!"); } if (visible) { addVisibleAnnotations(annotations); } } /** * Do Points out. */ protected void doPoints( Object o, ArtifactAndFacet aandf, Document theme, boolean visible, int axisIndex ) { String seriesName = aandf.getFacetDescription(); XYSeries series = new StyledXYSeries(seriesName, theme); // Add text annotations for single points. List<XYTextAnnotation> xy = new ArrayList<XYTextAnnotation>(); try { JSONArray points = new JSONArray((String) o); for (int i = 0, P = points.length(); i < P; i++) { JSONArray array = points.getJSONArray(i); double x = array.getDouble(0); double y = array.getDouble(1); String name = array.getString(2); boolean act = array.getBoolean(3); if (!act) { continue; } //logger.debug(" x " + x + " y " + y ); series.add(x, y, false); xy.add(new CollisionFreeXYTextAnnotation(name, x, y)); } } catch(JSONException e){ logger.error("Could not decode json."); } 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); addAxisSeries(series, axisIndex, visible); } /** * Create a hash from a legenditem. * This hash can then be used to merge legend items labels. * @return hash for given legenditem to identify mergeables. */ public static String legendItemHash(LegendItem li) { // TODO Do proper implementation. Ensure that only mergable sets are created. // getFillPaint() // getFillPaintTransformer() // getLabel() // getLine() // getLinePaint() // getLineStroke() // getOutlinePaint() // getOutlineStroke() // Shape getShape() // String getToolTipText() // String getURLText() // boolean isLineVisible() // boolean isShapeFilled() // boolean isShapeOutlineVisible() // boolean isShapeVisible() String hash = li.getLinePaint().toString(); String label = li.getLabel(); if (label.startsWith("W (") || label.startsWith("W(")) { hash += "-W-"; } else if (label.startsWith("Q(") || label.startsWith("Q (")) { hash += "-Q-"; } // WQ.java holds example of using regex Matcher/Pattern. return hash; } /** * Create a label for multiple items. * * For example from "W(Q=100)" and "W(Q=220)" * "W(Q= {100, 22})" would result. * * @param items list of legenditems. * @return the merged label. */ public String mergeLegendNames(List<LegendItem> items) { if (items.size() == 0) { return null; } if (items.size() == 1) { return items.get(0).getLabel(); } // TODO consider using regionMathches for implementation. int lastMatchedChar = 0; boolean first = true; String startPattern = ""; String endPattern = ""; String name = ""; // First, figure out beginning and end patterns, then merge. for (LegendItem item : items) { if (first) { startPattern = item.getLabel(); endPattern = item.getLabel(); first = false; continue; } while (startPattern.length() > 0 && !item.getLabel().startsWith(startPattern)) { startPattern = startPattern.substring(0, startPattern.length() -1); } while (endPattern.length() > 0 && !item.getLabel().endsWith(endPattern)) { endPattern = endPattern.substring(1); } } // Then, merge. name = startPattern + " {"; first = true; for (LegendItem item : items) { if (!first) { name += ", "; } else { first = false; } // Possible case in differences if (startPattern.length() + endPattern.length() < item.getLabel().length()) { // Note that we do not really want this. logger.error("Legend aggregation does not cover case: " + startPattern + " -- " + item.getLabel() + " -- " + endPattern); name += item.getLabel(); } else { name += item.getLabel().substring(startPattern.length(), item.getLabel().length() - endPattern.length()); } } name += "} "; name += endPattern; return name; } /** * Create new legend entries, dependant on settings. * @param plot The plot for which to modify the legend. */ public void aggregateLegendEntries(XYPlot plot) { LegendItemCollection old = plot.getLegendItems(); // Find "similar" entries if aggregation is enabled. int maxListSize = 0; int AGGR_THRESHOLD = 2; HashMap<String, List<LegendItem>> entries = new LinkedHashMap<String, List<LegendItem>>(); for (Iterator i = old.iterator(); i.hasNext();) { LegendItem item = (LegendItem) i.next(); String hash = legendItemHash(item); List<LegendItem> itemList = entries.get(hash); if (itemList == null) { itemList = new ArrayList<LegendItem>(); } itemList.add(item); if (itemList.size() > maxListSize) { maxListSize = itemList.size(); } entries.put(legendItemHash(item), itemList); } if (maxListSize < AGGR_THRESHOLD) { // No need to do anything. return; } // Run over collected entries, merge their names and create new // entry if needed. LegendItemCollection newLegend = new LegendItemCollection(); for (Map.Entry<String, List<LegendItem>> cursor: entries.entrySet()) { List<LegendItem> itemList = cursor.getValue(); if (itemList.size() >= AGGR_THRESHOLD) { // TODO now do merging } else { // TODO create singular new entry } LegendItem item = (LegendItem) itemList.get(0); // Unfortunately we cannot clone and just setDescription, as this // method was added in JFreeChart 1.0.14 (we are at .13). LegendItem merged = new LegendItem( mergeLegendNames(itemList), item.getDescription(), item.getToolTipText(), item.getURLText(), item.isShapeVisible(), item.getShape(), item.isShapeFilled(), item.getFillPaint(), item.isShapeOutlineVisible(), item.getOutlinePaint(), item.getOutlineStroke(), item.isLineVisible(), item.getLine(), item.getLineStroke(), item.getLinePaint()); newLegend.add(merged); } plot.setFixedLegendItems (newLegend); } /** 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 :