Mercurial > dive4elements > river
view flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java @ 2167:e0add4b2d4bc
Issue 461.
Override buildChartSection() to disable the chart subtitle entry in
ChartSettings.
flys-artifacts/trunk@3762 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author | Raimund Renkert <raimund.renkert@intevation.de> |
---|---|
date | Wed, 25 Jan 2012 10:19:30 +0000 |
parents | 105097966111 |
children | a79d5cd26083 |
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.awt.TexturePaint; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.TreeMap; import java.util.List; import java.util.Map; import java.util.SortedMap; 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.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.Range; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; import org.jfree.data.xy.XYDataset; import org.jfree.ui.RectangleInsets; import org.jfree.ui.TextAnchor; import de.intevation.artifacts.CallContext; import de.intevation.artifactdatabase.state.Facet; import de.intevation.artifactdatabase.state.Settings; import de.intevation.flys.exports.ChartExportHelper; import de.intevation.flys.jfree.EnhancedLineAndShapeRenderer; import de.intevation.flys.jfree.FLYSAnnotation; import de.intevation.flys.jfree.StableXYDifferenceRenderer; 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.artifacts.model.HYKFactory; /** * 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 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 AxisDataset(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, include its range. */ public void addDataset(XYSeries dataset) { this.datasets.add(new XYSeriesCollection(dataset)); includeYRange(dataset); } public void addArea(StyledAreaSeriesCollection series) { this.datasets.add(series); } /** True if to be renedered as area. */ public boolean isArea(XYSeriesCollection 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. */ public boolean isEmpty() { return this.datasets.isEmpty(); } /** Set the 'real' axis index that this axis is mapped to. */ public void setPlotAxisIndex(int axisIndex) { this.plotAxisIndex = axisIndex; } /** Get the 'real' axis index that this axis is mapped to. */ public int getPlotAxisIndex() { return this.plotAxisIndex; } } // class AxisDataset /** * A mini interface that allows to walk over the YAXIS enums defined in * subclasses. */ public interface YAxisWalker { int length(); String getId(int idx); } /** Override to make axis information available. */ protected YAxisWalker getYAxisWalker() { return new YAxisWalker() { /** Get number of items. */ @Override public int length() { return 0; } /** Get identifier for this index. */ @Override public String getId(int idx) { return null; } }; } /** The logger that is used in this generator. */ private static Logger logger = Logger.getLogger(XYChartGenerator.class); /** Map of datasets ("index"). */ protected SortedMap<Integer, AxisDataset> datasets; /** List of annotations to insert in plot. */ protected List<FLYSAnnotation> annotations; /** The max X range to include all X values of all series for each axis. */ protected Map<Integer, Range> xRanges; /** The max Y range to include all Y values of all series for each axis. */ protected Map<Integer, Range> yRanges; public static final Color DEFAULT_GRID_COLOR = Color.GRAY; public static final float DEFAULT_GRID_LINE_WIDTH = 0.3f; public static final int DEFAULT_FONT_SIZE = 12; public static final String DEFAULT_FONT_NAME = "Tahoma"; public XYChartGenerator() { xRanges = new HashMap<Integer, Range>(); yRanges = new HashMap<Integer, Range>(); datasets = new TreeMap<Integer, AxisDataset>(); } /** * Returns the title of a chart. The return value depends on the existence * of ChartSettings: if there are ChartSettings set, this method returns the * chart title provided by those settings. Otherwise, this method returns * getDefaultChartTitle(). * * @return the title of a chart. */ protected String getChartTitle() { ChartSettings chartSettings = getChartSettings(); if (chartSettings != null) { return getChartTitle(chartSettings); } return getDefaultChartTitle(); } protected abstract String getDefaultChartTitle(); /** * Returns the subtitle of a chart. The return value depends on the * existence of ChartSettings: if there are ChartSettings set, this method * returns the chart title provided by those settings. Otherwise, this * method returns getDefaultChartSubtitle(). * * @return the subtitle of a chart. */ protected String getChartSubtitle() { ChartSettings chartSettings = getChartSettings(); if (chartSettings != null) { return getChartSubtitle(chartSettings); } return getDefaultChartSubtitle(); } /** * This method always returns null. Override it in subclasses that require * subtitles. * * @return null. */ protected String getDefaultChartSubtitle() { // Override this method in subclasses return null; } /** * This method is used to determine, if the chart's legend is visible or * not. If a <i>settings</i> instance is set, this instance determines the * visibility otherwise, this method returns true as default if no * <i>settings</i> is set. * * @return true, if the legend should be visible, otherwise false. */ protected boolean isLegendVisible() { ChartSettings chartSettings = getChartSettings(); if (chartSettings != null) { return isLegendVisible(chartSettings); } return true; } /** * This method is used to determine the font size of the chart's legend. If * a <i>settings</i> instance is set, this instance determines the font * size, otherwise this method returns 12 as default if no <i>settings</i> * is set or if it doesn't provide a legend font size. * * @return a legend font size. */ protected int getLegendFontSize() { Integer fontSize = null; ChartSettings chartSettings = getChartSettings(); if (chartSettings != null) { fontSize = getLegendFontSize(chartSettings); } return fontSize != null ? fontSize : DEFAULT_FONT_SIZE; } /** * This method is used to determine if the resulting chart should display * grid lines or not. <b>Note: this method always returns true!</b> * * @return true, if the chart should display grid lines, otherwise false. */ protected boolean isGridVisible() { return true; } /** * Returns the X-Axis label of a chart. * * @return the X-Axis label of a chart. */ protected String getXAxisLabel() { ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) { return getDefaultXAxisLabel(); } AxisSection as = chartSettings.getAxisSection("X"); if (as != null) { String label = as.getLabel(); if (label != null) { return label; } } return getDefaultXAxisLabel(); } /** * Returns the default X-Axis label of a chart. * * @return the default X-Axis label of a chart. */ protected abstract String getDefaultXAxisLabel(); /** * Returns the Y-Axis label of a chart at position <i>pos</i>. * * @return the Y-Axis label of a chart at position <i>0</i>. */ protected String getYAxisLabel(int pos) { ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) { return getDefaultYAxisLabel(pos); } YAxisWalker walker = getYAxisWalker(); AxisSection as = chartSettings.getAxisSection(walker.getId(pos)); if (as != null) { String label = as.getLabel(); if (label != null) { return label; } } return getDefaultYAxisLabel(pos); } /** * This method is called to retrieve the default label for an Y axis at * position <i>pos</i>. * * @param pos The position of an Y axis. * * @return the default Y axis label at position <i>pos</i>. */ protected abstract String getDefaultYAxisLabel(int pos); /** * This method returns the font size for the X axis. If the font size is * specified in ChartSettings (if <i>chartSettings</i> is set), this size is * returned. Otherwise the default font size 12 is returned. * * @return the font size for the x axis. */ protected int getXAxisLabelFontSize() { ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) { return DEFAULT_FONT_SIZE; } AxisSection as = chartSettings.getAxisSection("X"); Integer fontSize = as.getFontSize(); return fontSize != null ? fontSize : DEFAULT_FONT_SIZE; } /** * This method returns the font size for an Y axis. If the font size is * specified in ChartSettings (if <i>chartSettings</i> is set), this size is * returned. Otherwise the default font size 12 is returned. * * @return the font size for the x axis. */ protected int getYAxisFontSize(int pos) { ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) { return DEFAULT_FONT_SIZE; } YAxisWalker walker = getYAxisWalker(); AxisSection as = chartSettings.getAxisSection(walker.getId(pos)); Integer fontSize = as.getFontSize(); return fontSize != null ? fontSize : DEFAULT_FONT_SIZE; } /** * This method returns the export dimension specified in ChartSettings as * int array [width,height]. * * @return an int array with [width,height]. */ protected int[] getExportDimension() { ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) { return new int[] { 600, 400 }; } ExportSection export = chartSettings.getExportSection(); Integer width = export.getWidth(); Integer height = export.getHeight(); if (width != null && height != null) { return new int[] { width, height }; } return new int[] { 600, 400 }; } /** * Generate chart. */ public void generate() throws IOException { logger.debug("XYChartGenerator.generate"); JFreeChart chart = generateChart(); String format = getFormat(); int[] size = getSize(); if (size == null) { size = getExportDimension(); } context.putContextValue("chart.width", size[0]); context.putContextValue("chart.height", size[1]); if (format.equals(ChartExportHelper.FORMAT_PNG)) { context.putContextValue("chart.image.format", "png"); ChartExportHelper.exportImage( out, chart, context); } else if (format.equals(ChartExportHelper.FORMAT_PDF)) { preparePDFContext(context); ChartExportHelper.exportPDF( out, chart, context); } else if (format.equals(ChartExportHelper.FORMAT_SVG)) { prepareSVGContext(context); ChartExportHelper.exportSVG( out, chart, context); } } /** * 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(); chart.setBackgroundPaint(Color.WHITE); plot.setBackgroundPaint(Color.WHITE); addSubtitles(chart); adjustPlot(plot); //debugAxis(plot); addDatasets(plot); //debugDatasets(plot); recoverEmptyPlot(plot); preparePointRanges(plot); //debugAxis(plot); localizeAxes(plot); adjustAxes(plot); autoZoom(plot); // These have to go after the autozoom. addAnnotationsToRenderer(plot); return chart; } protected void preparePDFContext(CallContext context) { int[] dimension = getExportDimension(); context.putContextValue("chart.width", dimension[0]); context.putContextValue("chart.height", dimension[1]); context.putContextValue("chart.marginLeft", 5f); context.putContextValue("chart.marginRight", 5f); context.putContextValue("chart.marginTop", 5f); context.putContextValue("chart.marginBottom", 5f); context.putContextValue( "chart.page.format", ChartExportHelper.DEFAULT_PAGE_SIZE); } protected void prepareSVGContext(CallContext context) { int[] dimension = getExportDimension(); context.putContextValue("chart.width", dimension[0]); context.putContextValue("chart.height", dimension[1]); context.putContextValue( "chart.encoding", ChartExportHelper.DEFAULT_ENCODING); } /** * Put debug output about datasets. */ public void debugDatasets(XYPlot plot) { logger.debug("Number of datasets: " + plot.getDatasetCount()); for (int i = 0; i < plot.getDatasetCount(); 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; i < plot.getRangeAxisCount(); 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("..............."); } /** * Add datasets to plot. * @param plot plot to add datasets to. */ protected void addDatasets(XYPlot plot) { // AxisDatasets are sorted, but some might be empty. // Thus, generate numbering on the fly. int axisIndex = 0; int datasetIndex = 0; for (Map.Entry<Integer, AxisDataset> entry: datasets.entrySet()) { if (!entry.getValue().isEmpty()) { // Add axis and range information. AxisDataset axisDataset = entry.getValue(); NumberAxis axis = createYAxis(entry.getKey()); plot.setRangeAxis(axisIndex, axis); if (axis.getAutoRangeIncludesZero()) { axisDataset.range = Range.expandToInclude(axisDataset.range, 0d); } yRanges.put(axisIndex, expandPointRange(axisDataset.range)); // Add contained datasets, mapping to axis. for (XYDataset dataset: axisDataset.datasets) { plot.setDataset(datasetIndex, dataset); plot.mapDatasetToRangeAxis(datasetIndex, axisIndex); applyThemes(plot, (XYSeriesCollection) dataset, datasetIndex, axisDataset.isArea((XYSeriesCollection)dataset)); datasetIndex++; } axisDataset.setPlotAxisIndex(axisIndex); axisIndex++; } } } /** * 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; } AxisDataset axisDataset = datasets.get(index); if (axisDataset == null) { axisDataset = new AxisDataset(index); datasets.put(index, axisDataset); } 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; } AxisDataset axisDataset = datasets.get(index); if (axisDataset == null) { axisDataset = new AxisDataset(index); datasets.put(index, axisDataset); } logger.debug("addAxisSeries: extent X " + series.getMinX() + " : " + series.getMaxX() + " extent y " + series.getMinY() + " : " + series.getMaxY()); if (visible) { axisDataset.addDataset(series); } else { // 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); } combineXRanges(new Range(series.getMinX(), series.getMaxX()), 0); } /** * Effect: extend range of x axis to include given limits. * @param range the given ("minimal") range. * @param index index of axis to be merged. */ private void combineXRanges(Range range, int index) { if (range == null || Double.isNaN(range.getLowerBound()) || Double.isNaN(range.getUpperBound())) { return; } Range old = xRanges.get(index); if (old != null) { range = Range.combine(old, range); } xRanges.put(index, range); } /** * Adds annotations to list (if visible is true). */ public void addAnnotations(FLYSAnnotation annotation, boolean visible) { if (!visible) { return; } if (annotations == null) { annotations = new ArrayList<FLYSAnnotation>(); } annotations.add(annotation); } /** * Create Y (range) axis for given index. * Shall be overriden by subclasses. */ protected NumberAxis createYAxis(int index) { YAxisWalker walker = getYAxisWalker(); Font labelFont = new Font( DEFAULT_FONT_NAME, Font.BOLD, getYAxisFontSize(index)); IdentifiableNumberAxis axis = new IdentifiableNumberAxis( walker.getId(index), getYAxisLabel(index)); axis.setAutoRangeIncludesZero(false); axis.setLabelFont(labelFont); return axis; } /** Creates Font (Family and size) to use when creating Legend Items. */ protected Font createLegendLabelFont() { return new Font( DEFAULT_FONT_NAME, Font.PLAIN, getLegendFontSize() ); } /** * 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 a given range if it collapses into one point. * @param Range to be expanded if upper == lower bound. */ private Range expandPointRange(Range range) { if (range != null && range.getLowerBound() == range.getUpperBound()) { return expandRange(range, 5); } return range; } /** * 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); Range r = xRanges.get(key); if (r != null && r.getLowerBound() == r.getUpperBound()) { xRanges.put(key, expandRange(r, 5)); } } } /** * Expand range by percent. */ public static Range expandRange(Range range, double percent) { if (range == null) { return null; } double value = range.getLowerBound(); double expand = Math.abs(value / 100 * percent); return expand != 0 ? new Range(value-expand, value+expand) : new Range(-0.01 * percent, 0.01 * percent); } /** * 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, xRanges.get(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, yRanges.get(Integer.valueOf(i)), yrange); } } protected boolean zoomX(XYPlot plot, ValueAxis axis, Range range, Range x) { return zoom(plot, axis, range, x); } protected boolean zoomY(XYPlot plot, ValueAxis axis, Range range, Range x) { return zoom(plot, axis, range, 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, Range range, Range x) { if (range == null) { return false; } if (x != null) { double min = range.getLowerBound(); double max = range.getUpperBound(); double diff = max > min ? max - min : min - max; Range computed = new Range( min + x.getLowerBound() * diff, min + x.getUpperBound() * diff); axis.setRangeWithMargins(computed); logger.debug("Zoom axis to: " + computed); return true; } axis.setRangeWithMargins(range); return false; } /** * This method extracts 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]. */ public Range[] getRangesForAxis(int index) { logger.debug("getRangesForAxis " + index); Range rx = xRanges.get(Integer.valueOf(0)); Range ry = yRanges.get(Integer.valueOf(index)); if (rx == null) { logger.warn("Range for x axis not set." + " Using default values: 0 - 1."); rx = new Range(0, 1); } if (ry == null) { logger.warn("Range for y" + index + " axis not set. Using default values: 0 - 1."); ry = new Range(0, 1); } return new Range[] {rx, ry}; } /** * This method searches for a specific axis in the <i>settings</i> if * <i>settings</i> is set. If the axis was found, this method returns the * specified axis range if the axis range is fixed. Otherwise, this method * returns null. * * @param axisId The identifier of an axis. * * @return the specified axis range from <i>settings</i> if the axis is * fixed, otherwise null. */ public Range getRangeForAxisFromSettings(String axisId) { ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) { return null; } AxisSection as = chartSettings.getAxisSection(axisId); Boolean fixed = as.isFixed(); if (fixed != null && fixed) { Double upper = as.getUpperRange(); Double lower = as.getLowerRange(); if (upper != null && lower != null) { return lower < upper ? new Range(lower, upper) : new Range(upper, lower); } } return null; } public LegendItem createLegendItem(Document theme, String name) { // OPTIMIZE Pass font, parsed Theme items. ThemeAccess themeAccess = new ThemeAccess(theme); Color color = themeAccess.parseLineColorField(); LegendItem li = new LegendItem(name, color); li.setLabelFont(createLegendLabelFont()); return li; } /** 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); } } /** * Add a text and a line annotation. */ public void addStickyAnnotation( StickyAxisAnnotation annotation, XYPlot plot, Area area, ThemeAccess.LineStyle lineStyle, ThemeAccess.TextStyle textStyle ) { // OPTIMIZE pre-calculate area-related values final float TEXT_OFF = 0.03f; final float LINE_OFF = 0.02f; 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); // Style the line. if (lineStyle != null) { lineAnnotation = new XYLineAnnotation(annotation.getPos(), area.atGround(), annotation.getPos(), area.ofGround(LINE_OFF), new BasicStroke(lineStyle.getWidth()),lineStyle.getColor()); textAnnotation.setRotationAnchor(TextAnchor.CENTER_LEFT); textAnnotation.setTextAnchor(TextAnchor.CENTER_LEFT); } else { lineAnnotation = new XYLineAnnotation(annotation.getPos(), area.atGround(), annotation.getPos(), area.ofGround(LINE_OFF)); 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). AxisDataset dataset = this.datasets.get( new Integer(annotation.getAxisSymbol())); if (dataset == null) { logger.warn("Annotation should stick to unfindable y-axis: " + annotation.getAxisSymbol()); rendererIndex = 0; } else { rendererIndex = dataset.getPlotAxisIndex(); } 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); // Style the line. if (lineStyle != null) { lineAnnotation = new XYLineAnnotation(area2.ofRight(LINE_OFF), annotation.getPos(), area2.atRight(), annotation.getPos(), new BasicStroke(lineStyle.getWidth()), lineStyle.getColor()); } else { lineAnnotation = new XYLineAnnotation(area2.atRight(), annotation.getPos(), area2.ofRight(LINE_OFF), annotation.getPos()); } } else { textAnnotation = new CollisionFreeXYTextAnnotation( annotation.getText(), area.ofLeft(TEXT_OFF), annotation.getPos()); textAnnotation.setRotationAnchor(TextAnchor.CENTER_LEFT); textAnnotation.setTextAnchor(TextAnchor.CENTER_LEFT); // Style the line. if (lineStyle != null) { lineAnnotation = new XYLineAnnotation(area.atLeft(), annotation.getPos(), area.ofLeft(LINE_OFF), annotation.getPos(), new BasicStroke(lineStyle.getWidth()), lineStyle.getColor()); } else { lineAnnotation = new XYLineAnnotation(area.atLeft(), annotation.getPos(), area.ofLeft(LINE_OFF), annotation.getPos()); } } } // Style the text. if (textStyle != null) { textStyle.apply(textAnnotation); } // Add the Annotations to renderer. plot.getRenderer(rendererIndex).addAnnotation(textAnnotation, org.jfree.ui.Layer.BACKGROUND); plot.getRenderer(rendererIndex).addAnnotation(lineAnnotation, org.jfree.ui.Layer.BACKGROUND); } /** Add annotations (Sticky, Text and hyk zones). */ public void addAnnotationsToRenderer(XYPlot plot) { logger.debug("XYChartGenerator.addAnnotationsToRenderer"); if (annotations == null) { logger.debug("XYChartGenerator.addBoxAnnotations: 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(); 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); } // The not yet implemented other Text Annotations. for (XYTextAnnotation ta: fa.getTextAnnotations()) { // TODO implement, one we have textannotations } // 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); } /** * Set some Stroke/Grid defaults. */ protected void adjustPlot(XYPlot plot) { Stroke gridStroke = new BasicStroke( DEFAULT_GRID_LINE_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 3.0f, new float[] { 3.0f }, 0.0f); ChartSettings cs = getChartSettings(); boolean isGridVisible = cs != null ? isGridVisible(cs) : true; plot.setDomainGridlineStroke(gridStroke); plot.setDomainGridlinePaint(DEFAULT_GRID_COLOR); plot.setDomainGridlinesVisible(isGridVisible); plot.setRangeGridlineStroke(gridStroke); plot.setRangeGridlinePaint(DEFAULT_GRID_COLOR); plot.setRangeGridlinesVisible(isGridVisible); plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d)); } /** Override to handle subtitle adding. */ protected void addSubtitles(JFreeChart chart) { // override this method in subclasses that need subtitles } /** * 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); } /** * @param idx "index" of dataset/series (first dataset to be drawn has * index 0), correlates with renderer index. * @param isArea true if the series describes an area and shall be rendered * as such. * @return idx increased by number of items addded. */ protected int applyThemes( XYPlot plot, XYSeriesCollection series, int idx, boolean isArea ) { LegendItemCollection lic = new LegendItemCollection(); LegendItemCollection anno = plot.getFixedLegendItems(); Font legendFont = createLegendLabelFont(); int retidx = idx; if (isArea) { logger.debug("Registering an 'area'renderer at idx: " + idx); StyledAreaSeriesCollection area = (StyledAreaSeriesCollection) series; StableXYDifferenceRenderer dRenderer = new StableXYDifferenceRenderer(); if (area.getMode() == StyledAreaSeriesCollection.FILL_MODE.UNDER) { dRenderer.setPositivePaint(createTransparentPaint()); } plot.setRenderer(idx, dRenderer); area.applyTheme(dRenderer); LegendItem legendItem = dRenderer.getLegendItem(idx, 0); if (legendItem != null) { legendItem.setLabelFont(legendFont); lic.add(legendItem); } else { logger.warn("Could not get LegentItem for renderer: " + idx + ", series-idx " + 0); } if (anno != null) { lic.addAll(anno); } plot.setFixedLegendItems(lic); return retidx + 1; } XYLineAndShapeRenderer renderer = getRenderer(plot, idx); for (int s = 0, num = series.getSeriesCount(); s < num; s++) { XYSeries serie = series.getSeries(s); if (serie instanceof StyledXYSeries) { ((StyledXYSeries) serie).applyTheme(renderer, s); } // special case: if there is just one single item, we need to enable // points for this series, otherwise we would not see anything in // the chart area. if (serie.getItemCount() == 1) { renderer.setSeriesShapesVisible(s, true); } LegendItem legendItem = renderer.getLegendItem(idx, s); if (legendItem != null) { legendItem.setLabelFont(legendFont); lic.add(legendItem); } else { logger.warn("Could not get LegentItem for renderer: " + idx + ", series-idx " + s); } // TODO: why that? isnt renderer set per dataset not per series? retidx++; } if (anno != null) { lic.addAll(anno); } plot.setFixedLegendItems(lic); plot.setRenderer(idx, renderer); return retidx; } /** Returns a transparently textured paint. */ // TODO why not use a transparent color? protected static Paint createTransparentPaint() { BufferedImage texture = new BufferedImage( 1, 1, BufferedImage.TYPE_4BYTE_ABGR); return new TexturePaint( texture, new Rectangle2D.Double(0d, 0d, 0d, 0d)); } /** * Returns a new instance of EnhancedLineAndShapeRenderer always. */ protected XYLineAndShapeRenderer getRenderer(XYPlot plot, int idx) { logger.debug("getRenderer: " + idx); EnhancedLineAndShapeRenderer r = new EnhancedLineAndShapeRenderer(true, false); r.setPlot(plot); return r; } /** * Register annotations like MainValues for later plotting * * @param o list of annotations (data of facet). * @param facet The facet. This facet does NOT support any data objects. Use * FLYSArtifact.getNativeFacet() instead to retrieve a Facet which supports * data. * @param theme Theme document for given annotations. * @param visible The visibility of the annotations. */ protected void doAnnotations( FLYSAnnotation annotations, Facet facet, Document theme, boolean visible ){ logger.debug("doAnnotations"); // Add all annotations to our annotation pool. annotations.setTheme(theme); annotations.setLabel(facet.getDescription()); addAnnotations(annotations, visible); } /** * Creates a new instance of <i>IdentifiableNumberAxis</i>. * * @param idx The index of the new axis. * @param label The label of the new axis. * * @return an instance of IdentifiableNumberAxis. */ protected NumberAxis createNumberAxis(int idx, String label) { YAxisWalker walker = getYAxisWalker(); return new IdentifiableNumberAxis(walker.getId(idx), label); } /** * Returns an instance of <i>ChartSettings</i> with a chart specific section * but with no axes settings. * * @return an instance of <i>ChartSettings</i>. */ public Settings getSettings() { if (this.settings != null) { return this.settings; } ChartSettings settings = new ChartSettings(); ChartSection chartSection = buildChartSection(); LegendSection legendSection = buildLegendSection(); ExportSection exportSection = buildExportSection(); settings.setChartSection(chartSection); settings.setLegendSection(legendSection); settings.setExportSection(exportSection); List<AxisSection> axisSections = buildAxisSections(); for (AxisSection axisSection: axisSections) { settings.addAxisSection(axisSection); } return settings; } /** * Creates a new <i>ChartSection</i>. * * @return a new <i>ChartSection</i>. */ protected ChartSection buildChartSection() { ChartSection chartSection = new ChartSection(); chartSection.setTitle(getChartTitle()); chartSection.setSubtitle(getChartSubtitle()); chartSection.setDisplayGird(isGridVisible()); return chartSection; } /** * Creates a new <i>LegendSection</i>. * * @return a new <i>LegendSection</i>. */ protected LegendSection buildLegendSection() { LegendSection legendSection = new LegendSection(); legendSection.setVisibility(isLegendVisible()); legendSection.setFontSize(getLegendFontSize()); return legendSection; } /** * Creates a new <i>ExportSection</i> with default values <b>WIDTH=600</b> * and <b>HEIGHT=400</b>. * * @return a new <i>ExportSection</i>. */ protected ExportSection buildExportSection() { ExportSection exportSection = new ExportSection(); exportSection.setWidth(600); exportSection.setHeight(400); return exportSection; } /** * Creates a list of Sections that contains all axes of the chart (including * X and Y axes). * * @return a list of Sections for each axis in this chart. */ protected List<AxisSection> buildAxisSections() { List<AxisSection> axisSections = new ArrayList<AxisSection>(); axisSections.addAll(buildXAxisSections()); axisSections.addAll(buildYAxisSections()); return axisSections; } /** * Creates a new Section for chart's X axis. * * @return a List that contains a Section for the X axis. */ protected List<AxisSection> buildXAxisSections() { List<AxisSection> axisSections = new ArrayList<AxisSection>(); String identifier = "X"; AxisSection axisSection = new AxisSection(); axisSection.setIdentifier(identifier); axisSection.setLabel(getXAxisLabel()); axisSection.setFontSize(14); axisSection.setFixed(false); // XXX We are able to find better default ranges that [0,0], but the Y // axes currently have no better ranges set. axisSection.setUpperRange(0d); axisSection.setLowerRange(0d); axisSections.add(axisSection); return axisSections; } /** * Creates a list of Section for the chart's Y axes. This method makes use * of <i>getYAxisWalker</i> to be able to access all Y axes defined in * subclasses. * * @return a list of Y axis sections. */ protected List<AxisSection> buildYAxisSections() { List<AxisSection> axisSections = new ArrayList<AxisSection>(); YAxisWalker walker = getYAxisWalker(); for (int i = 0, n = walker.length(); i < n; i++) { AxisSection ySection = new AxisSection(); ySection.setIdentifier(walker.getId(i)); ySection.setLabel(getYAxisLabel(i)); ySection.setFontSize(14); ySection.setFixed(false); // XXX We are able to find better default ranges that [0,0], the // only problem is, that we do NOT have a better range than [0,0] // for each axis, because the initial chart will not have a dataset // for each axis set! ySection.setUpperRange(0d); ySection.setLowerRange(0d); axisSections.add(ySection); } return axisSections; } /** 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(); } } } // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :