Mercurial > dive4elements > river
view artifacts/src/main/java/org/dive4elements/river/exports/AbstractChartGenerator.java @ 9325:094ed9d1f2ad
Fixed: change of point style of interpolated data did not change in WQ chart of fixanalysis
Fixed: change of point style of interpolated data did not change in dWt chart of fixanalysis; also had duplicate legend entries
author | gernotbelger |
---|---|
date | Fri, 27 Jul 2018 14:33:41 +0200 |
parents | 7c7f73e5e01e |
children | d8e753d0fdb9 |
line wrap: on
line source
/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde * Software engineering by * Björnsen Beratende Ingenieure GmbH * Dr. Schumacher Ingenieurbüro für Wasser und Umwelt * * This file is Free Software under the GNU AGPL (>=v3) * and comes with ABSOLUTELY NO WARRANTY! Check out the * documentation coming with Dive4Elements River for details. */ package org.dive4elements.river.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.io.OutputStream; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import javax.xml.xpath.XPathConstants; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.dive4elements.artifactdatabase.CollectionCallContext; import org.dive4elements.artifactdatabase.state.ArtifactAndFacet; import org.dive4elements.artifactdatabase.state.Settings; import org.dive4elements.artifacts.Artifact; import org.dive4elements.artifacts.ArtifactCollection; import org.dive4elements.artifacts.ArtifactNamespaceContext; import org.dive4elements.artifacts.CallContext; import org.dive4elements.artifacts.CallMeta; import org.dive4elements.artifacts.PreferredLocale; import org.dive4elements.artifacts.common.utils.XMLUtils; import org.dive4elements.river.FLYS; import org.dive4elements.river.artifacts.D4EArtifact; import org.dive4elements.river.artifacts.access.RangeAccess; import org.dive4elements.river.artifacts.access.RiverAccess; import org.dive4elements.river.artifacts.resources.Resources; import org.dive4elements.river.artifacts.sinfo.util.CalculationUtils; import org.dive4elements.river.collections.D4EArtifactCollection; import org.dive4elements.river.jfree.AxisDataset; import org.dive4elements.river.jfree.Bounds; import org.dive4elements.river.jfree.DoubleBounds; import org.dive4elements.river.jfree.EnhancedLineAndShapeRenderer; import org.dive4elements.river.jfree.RiverAnnotation; import org.dive4elements.river.jfree.StableXYDifferenceRenderer; import org.dive4elements.river.jfree.Style; import org.dive4elements.river.jfree.StyledAreaSeriesCollection; import org.dive4elements.river.jfree.StyledSeries; import org.dive4elements.river.themes.ThemeDocument; import org.dive4elements.river.utils.Formatter; import org.jfree.chart.JFreeChart; import org.jfree.chart.LegendItem; import org.jfree.chart.LegendItemCollection; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.title.TextTitle; import org.jfree.data.Range; import org.jfree.data.general.Series; import org.jfree.data.xy.XYDataset; import org.jfree.ui.HorizontalAlignment; import org.jfree.ui.RectangleInsets; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * This class re-unites the tremendous copy/paste code from ChartGenerator and ChartGenerator2. Code is still awful and * encapsulation is broken in too many places. * TODO: instead of deep inheritances, delegate to classes that define the various behaviors. * * @author Gernot Belger */ abstract class AbstractChartGenerator implements OutGenerator { protected static final Logger log = Logger.getLogger(AbstractChartGenerator.class); private static final int DEFAULT_CHART_WIDTH = 600; private static final int DEFAULT_CHART_HEIGHT = 400; private static final Color DEFAULT_GRID_COLOR = Color.GRAY; private static final float DEFAULT_GRID_LINE_WIDTH = 0.3f; protected static final String DEFAULT_FONT_NAME = "Tahoma"; protected static final int DEFAULT_FONT_SIZE = 12; private static final String DEFAULT_CHART_FORMAT = "png"; private static final String XPATH_CHART_EXPORT = "/art:action/art:attributes/art:export/@art:value"; private static final String XPATH_CHART_SIZE = "/art:action/art:attributes/art:size"; private static final String XPATH_CHART_FORMAT = "/art:action/art:attributes/art:format/@art:value"; private static final String XPATH_CHART_X_RANGE = "/art:action/art:attributes/art:xrange"; private static final String XPATH_CHART_Y_RANGE = "/art:action/art:attributes/art:yrange"; /** The document of the incoming out() request. */ private Document request; /** The output stream where the data should be written to. */ private OutputStream out; /** Artifact that is used to decorate the chart with meta information. */ private Artifact master; /** Map of datasets ("index"). */ private final SortedMap<Integer, AxisDataset> datasets = new TreeMap<>(); /** List of annotations to insert in plot. */ private final List<RiverAnnotation> annotations = new ArrayList<>(); private String outName; /** The settings that should be used during output creation. */ private Settings settings; /** The CallContext object. */ private CallContext context; @Override public void init(final String outName, final Document request, final OutputStream out, final CallContext context) { log.debug("ChartGenerator.init"); this.outName = outName; this.request = request; this.out = out; this.context = context; } @Override public void setup(final Object config) { } /** Sets the master artifact. */ @Override public void setMasterArtifact(final Artifact master) { this.master = master; } /** * Gets the master artifact. * * @return the master artifact. */ public Artifact getMaster() { return this.master; } protected final Map<Integer, AxisDataset> getDatasets() { return this.datasets; } @Override public void setCollection(final D4EArtifactCollection collection) { /* we do not need it */ } protected final D4EArtifact getArtifact() { // FIXME: should already made sure when this member is set return (D4EArtifact) this.master; } public final CallContext getContext() { return this.context; } /** The document of the incoming out() request. */ protected final Document getRequest() { return this.request; } /** * Adds annotations to list. The given annotation will be visible. */ public final void addAnnotations(final RiverAnnotation annotation) { this.annotations.add(annotation); } /** * This method needs to be implemented by concrete subclasses to create new * instances of JFreeChart. * * @param context2 * * @return a new instance of a JFreeChart. */ protected abstract JFreeChart generateChart(CallContext context2); /** * For every outable (i.e. facets), this function is * called and handles the data accordingly. */ @Override public abstract void doOut(ArtifactAndFacet bundle, ThemeDocument attr, boolean visible); @Override public void generate() throws IOException { doGenerate(this.context, this.out, this.outName); } protected abstract void doGenerate(CallContext context, OutputStream out, String outName) throws IOException; protected abstract Series getSeriesOf(XYDataset dataset, int idx); /** * Returns the default title of a chart. * * @param context2 * * @return the default title of a chart. */ protected abstract String getDefaultChartTitle(CallContext context); /** * This method is used to create new AxisDataset instances which may differ * in concrete subclasses. * * @param idx * The index of an axis. */ protected abstract AxisDataset createAxisDataset(int idx); /** * Combines the ranges of the X axis at index <i>idx</i>. * * @param bounds * A new Bounds. * @param idx * The index of the X axis that should be comined with * <i>range</i>. */ protected abstract void combineXBounds(Bounds bounds, int idx); /** * Combines the ranges of the Y axis at index <i>idx</i>. * * @param bounds * A new Bounds. * @param index * The index of the Y axis that should be comined with. * <i>range</i>. */ protected abstract void combineYBounds(Bounds bounds, int index); /** * This method is used to determine the ranges for axes at a given index. * * @param index * The index of the axes at the plot. * * @return a Range[] with [xrange, yrange]; */ protected abstract Range[] getRangesForAxis(int index); protected abstract Bounds getXBounds(int axis); protected abstract void setXBounds(int axis, Bounds bounds); protected abstract Bounds getYBounds(int axis); protected abstract void setYBounds(int axis, Bounds bounds); // /** // * Retuns the call context. May be null if init hasn't been called yet. // * // * @return the CallContext instance // */ // protected final CallContext getCallContext() { // return this.context; // } // @Override public final void setSettings(final Settings settings) { this.settings = settings; } /** * Returns the <i>settings</i> as <i>ChartSettings</i>. * * @return the <i>settings</i> as <i>ChartSettings</i> or null, if * <i>settings</i> is not an instance of <i>ChartSettings</i>. */ protected final ChartSettings getChartSettings() { if (this.settings instanceof ChartSettings) { return (ChartSettings) this.settings; } return null; } /** * Return instance of <i>ChartSettings</i> with a chart specific section * but with no axes settings. * * @return an instance of <i>ChartSettings</i>. */ @Override public final Settings getSettings() { if (this.settings != null) return this.settings; final ChartSettings settings = new ChartSettings(); final ChartSection chartSection = buildChartSection(this.context); final LegendSection legendSection = buildLegendSection(); final ExportSection exportSection = buildExportSection(); settings.setChartSection(chartSection); settings.setLegendSection(legendSection); settings.setExportSection(exportSection); final List<AxisSection> axisSections = buildAxisSections(); for (final AxisSection axisSection : axisSections) settings.addAxisSection(axisSection); return settings; } protected abstract ChartSection buildChartSection(CallContext context); /** * Creates a new <i>LegendSection</i>. * * @return a new <i>LegendSection</i>. */ private LegendSection buildLegendSection() { final LegendSection legendSection = new LegendSection(); legendSection.setVisibility(isLegendVisible()); legendSection.setFontSize(getLegendFontSize()); legendSection.setAggregationThreshold(10); 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>. */ private ExportSection buildExportSection() { final ExportSection exportSection = new ExportSection(); exportSection.setWidth(DEFAULT_CHART_WIDTH); exportSection.setHeight(DEFAULT_CHART_HEIGHT); exportSection.setMetadata(true); 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. */ private List<AxisSection> buildAxisSections() { final List<AxisSection> axisSections = new ArrayList<>(); 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() { final List<AxisSection> axisSections = new ArrayList<>(); final String identifier = "X"; final 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; } /** * Returns the X-Axis label of a chart. * * @return the X-Axis label of a chart. */ protected final String getXAxisLabel() { final ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) { return getDefaultXAxisLabel(this.context); } final AxisSection as = chartSettings.getAxisSection("X"); if (as != null) { final String label = as.getLabel(); if (label != null) { return label; } } return getDefaultXAxisLabel(this.context); } protected abstract List<AxisSection> buildYAxisSections(); /** * Returns the default X-Axis label of a chart. * * @param context2 * * @return the default X-Axis label of a chart. */ protected abstract String getDefaultXAxisLabel(final CallContext context2); /** Generate the diagram as an image. */ protected final void generateImage(final CallContext context) throws IOException { log.debug("ChartGenerator2.generateImage"); final JFreeChart chart = generateChart(context); final String format = getFormat(); int[] size = getSize(); if (size == null) size = getExportDimension(); this.context.putContextValue("chart.width", size[0]); this.context.putContextValue("chart.height", size[1]); if (format.equals(ChartExportHelper.FORMAT_PNG)) { this.context.putContextValue("chart.image.format", "png"); ChartExportHelper.exportImage(this.out, chart, this.context); } else if (format.equals(ChartExportHelper.FORMAT_PDF)) { preparePDFContext(this.context); ChartExportHelper.exportPDF(this.out, chart, this.context); } else if (format.equals(ChartExportHelper.FORMAT_SVG)) { prepareSVGContext(this.context); ChartExportHelper.exportSVG(this.out, chart, this.context); } else if (format.equals(ChartExportHelper.FORMAT_CSV)) { this.context.putContextValue("chart.image.format", "csv"); ChartExportHelper.exportCSV(this.out, chart, this.context); } } protected final void generateTitles(final JFreeChart chart) { /* add metadata title before real title */ addMetadataSubtitle(chart); /* add the real chart title, but as subtitle after the metadata */ final String chartTitle = getChartTitle(this.context); if (chartTitle != null) { final TextTitle title = new TextTitle(chartTitle, JFreeChart.DEFAULT_TITLE_FONT); chart.addSubtitle(title); } addSubtitles(this.context, chart); } /** * Adds a metadata sub-title to the chart if it gets exported */ protected final void addMetadataSubtitle(final JFreeChart chart) { if ((!isExport() || !isExportMetadata())) return; final Collection<String> metadata = buildMetadata(); final String text = StringUtils.join(metadata, " - "); /** The default font. */ final Font titleFont = new Font("SansSerif", Font.ITALIC, 10); final TextTitle subtitle = new TextTitle(text, titleFont); subtitle.setHorizontalAlignment(HorizontalAlignment.LEFT); subtitle.setMargin(new RectangleInsets(10,10,10,10)); chart.addSubtitle(subtitle); } private Collection<String> buildMetadata() { final CallMeta meta = this.context.getMeta(); if (!(this.context instanceof CollectionCallContext)) { /* should never happen */ return Collections.emptyList(); } final CollectionCallContext ccc = (CollectionCallContext) this.context; final ArtifactCollection collection = ccc.getCollection(); final List<String> subtitles = new ArrayList<>(); /* version */ final String version = FLYS.VERSION; subtitles.add(Resources.getMsg(meta, "chart.subtitle.metadata.version", "default", version)); /* user */ // REMARK: the use inside the collection is a fake user and hence we cant use it here. // final User user = collection.getUser(); final String userName = CalculationUtils.findArtifactUser(this.context, getArtifact()); subtitles.add(Resources.getMsg(meta, "chart.subtitle.metadata.user", "default", userName)); /* creation date */ final Locale locale = Resources.getLocale(meta); final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale); final String dateText = df.format(new Date()); subtitles.add(Resources.getMsg(meta, "chart.subtitle.metadata.creationdate", "default", dateText)); return subtitles; } private boolean isExportMetadata() { final ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) return true; final ExportSection exportSection = chartSettings.getExportSection(); if (exportSection == null) return true; return exportSection.getMetadata(); } /** * This method returns the export flag specified in the <i>request</i> document * or <i>false</i> if no export is specified in <i>request</i>. */ private boolean isExport() { final Boolean export = (Boolean) XMLUtils.xpath(getRequest(), XPATH_CHART_EXPORT, XPathConstants.BOOLEAN, ArtifactNamespaceContext.INSTANCE); return export == null ? false : export; } protected final String getRiverName() { return new RiverAccess(getArtifact()).getRiver().getName(); } protected final String getRiverUnit() { return new RiverAccess(getArtifact()).getRiver().getWstUnit().getName(); } protected final double[] getRange() { final D4EArtifact flys = getArtifact(); final RangeAccess rangeAccess = new RangeAccess(flys); return rangeAccess.getKmRange(); } /** * Returns a boolean object that determines if the chart grid should be * visible or not. This information needs to be provided by <i>settings</i>, * otherwise the default is true. * * @param settings * A ChartSettings object. * * @return true, if the chart grid should be visible otherwise false. * * @throws NullPointerException * if <i>settings</i> is null. */ private boolean isGridVisible(final ChartSettings settings) { final ChartSection cs = settings.getChartSection(); return cs.getDisplayGrid(); } /** * 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 final boolean isLegendVisible() { final ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) return true; final LegendSection ls = chartSettings.getLegendSection(); return ls.getVisibility(); } /** * 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 final int getXAxisLabelFontSize() { final ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) { return DEFAULT_FONT_SIZE; } final AxisSection as = chartSettings.getAxisSection("X"); final Integer fontSize = as.getFontSize(); return fontSize != null ? fontSize : DEFAULT_FONT_SIZE; } /** * 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. */ private int getLegendFontSize() { final ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) return DEFAULT_FONT_SIZE; final LegendSection ls = chartSettings.getLegendSection(); if (ls == null) return DEFAULT_FONT_SIZE; final Integer fontSize = ls.getFontSize(); if (fontSize == null) return DEFAULT_FONT_SIZE; return fontSize; } /** * Creates a new LegendItem with <i>name</i> and font provided by * <i>createLegendLabelFont()</i>. * * @param theme * The theme of the chart line. * @param name * The displayed name of the item. * * @return a new LegendItem instance. */ protected final LegendItem createLegendItem(final ThemeDocument theme, final String name) { // OPTIMIZE Pass font, parsed Theme items. Color color = theme.parseLineColorField(); if (color == null) color = Color.BLACK; final LegendItem legendItem = new LegendItem(name, color); legendItem.setLabelFont(createLegendLabelFont()); return legendItem; } /** * Create new legend entries, dependent on settings. * * @param plot * The plot for which to modify the legend. */ protected final void aggregateLegendEntries(final XYPlot plot) { final ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) return; final Integer threshold = chartSettings.getLegendSection().getAggregationThreshold(); final int aggrThreshold = threshold != null ? threshold.intValue() : 0; LegendProcessor.aggregateLegendEntries(plot, aggrThreshold); } /** * Creates Font (Family and size) to use when creating Legend Items. The * font size depends in the return value of <i>getLegendFontSize()</i>. * * @return a new Font instance with <i>DEFAULT_FONT_NAME</i>. */ private final Font createLegendLabelFont() { return new Font(DEFAULT_FONT_NAME, Font.PLAIN, getLegendFontSize()); } /** * Adjust some Stroke/Grid parameters for <i>plot</i>. The chart * <i>Settings</i> are applied in this method. * * @param plot * The XYPlot which is adapted. */ protected void adjustPlot(final XYPlot plot) { final Stroke gridStroke = new BasicStroke(DEFAULT_GRID_LINE_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 3.0f, new float[] { 3.0f }, 0.0f); final ChartSettings cs = getChartSettings(); final 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)); } /** * This helper method is used to extract the current locale from instance variable <i>context</i>. * * @return the current locale. */ protected final Locale getLocale() { final CallMeta meta = this.context.getMeta(); final PreferredLocale[] prefs = meta.getLanguages(); final int len = prefs != null ? prefs.length : 0; final Locale[] locales = new Locale[len]; for (int i = 0; i < len; i++) { locales[i] = prefs[i].getLocale(); } return meta.getPreferredLocale(locales); } /** * Look up \param key in i18n dictionary. * * @param key * key for which to find i18nd version. * @param def * default, returned if lookup failed. * @return value found in i18n dictionary, \param def if no value found. */ public final String msg(final String key, final String def) { return Resources.getMsg(this.context.getMeta(), key, def); } /** * Look up \param key in i18n dictionary. * * @param key * key for which to find i18nd version. * @return value found in i18n dictionary, key itself if failed. */ public final String msg(final String key) { return Resources.getMsg(this.context.getMeta(), key, key); } public final String msg(final String key, final String def, final Object... args) { return Resources.getMsg(this.context.getMeta(), key, def, args); } /** * Add datasets stored in instance variable <i>datasets</i> to plot. * <i>datasets</i> actually stores instances of AxisDataset, so each of this * datasets is mapped to a specific axis as well. * * @param plot * plot to add datasets to. */ protected void addDatasets(final XYPlot plot) { log.debug("addDatasets()"); // AxisDatasets are sorted, but some might be empty. // Thus, generate numbering on the fly. int axisIndex = 0; int datasetIndex = 0; for (final Map.Entry<Integer, AxisDataset> entry : this.datasets.entrySet()) { if (!entry.getValue().isEmpty()) { // Add axis and range information. final AxisDataset axisDataset = entry.getValue(); final NumberAxis axis = createYAxis(entry.getKey()); plot.setRangeAxis(axisIndex, axis); if (axis.getAutoRangeIncludesZero()) { axisDataset.setRange(Range.expandToInclude(axisDataset.getRange(), 0d)); } setYBounds(axisIndex, expandPointRange(axisDataset.getRange())); // Add contained datasets, mapping to axis. for (final XYDataset dataset : axisDataset.getDatasets()) { try { plot.setDataset(datasetIndex, dataset); plot.mapDatasetToRangeAxis(datasetIndex, axisIndex); applyThemes(plot, dataset, datasetIndex, axisDataset.isArea(dataset)); datasetIndex++; } catch (final RuntimeException re) { log.error(re); } } axisDataset.setPlotAxisIndex(axisIndex); axisIndex++; } } } /** * Create Y (range) axis for given index. * Shall be implemented by subclasses. */ protected abstract NumberAxis createYAxis(final int index); /** * @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. */ private void applyThemes(final XYPlot plot, final XYDataset series, final int idx, final boolean isArea) { if (isArea) applyAreaTheme(plot, (StyledAreaSeriesCollection) series, idx); else applyLineTheme(plot, series, idx); } /** * Expands a given range if it collapses into one point. * * @param range * Range to be expanded if upper == lower bound. * * @return Bounds of point plus 5 percent in each direction. */ private Bounds expandPointRange(final Range range) { if (range == null) { return null; } else if (range.getLowerBound() == range.getUpperBound()) { final Range expandedRange = ChartHelper.expandRange(range, 5d); return new DoubleBounds(expandedRange.getLowerBound(), expandedRange.getUpperBound()); } return new DoubleBounds(range.getLowerBound(), range.getUpperBound()); } /** * Creates a new instance of EnhancedLineAndShapeRenderer. * * @param plot * The plot which is set for the new renderer. * @param idx * This value is not used in the current implementation. * * @return a new instance of EnhancedLineAndShapeRenderer. */ private XYLineAndShapeRenderer createRenderer(final XYPlot plot, final int idx) { log.debug("Create EnhancedLineAndShapeRenderer for idx: " + idx); final EnhancedLineAndShapeRenderer r = new EnhancedLineAndShapeRenderer(true, false); r.setPlot(plot); return r; } /** * This method applies the themes defined in the series itself. Therefore, * <i>StyledXYSeries.applyTheme()</i> is called, which modifies the renderer * for the series. * * @param plot * The plot. * @param dataset * The XYDataset which needs to support Series objects. * @param idx * The index of the renderer / dataset. */ private void applyLineTheme(final XYPlot plot, final XYDataset dataset, final int idx) { log.debug("Apply LineTheme for dataset at index: " + idx); final LegendItemCollection lic = new LegendItemCollection(); final LegendItemCollection anno = plot.getFixedLegendItems(); final Font legendFont = createLegendLabelFont(); final XYLineAndShapeRenderer renderer = createRenderer(plot, idx); for (int s = 0, num = dataset.getSeriesCount(); s < num; s++) { final Series series = getSeriesOf(dataset, s); if (series instanceof StyledSeries) { final Style style = ((StyledSeries) series).getStyle(); style.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 (series.getItemCount() == 1) { renderer.setSeriesShapesVisible(s, true); } LegendItem legendItem = renderer.getLegendItem(idx, s); if (legendItem.getLabel().endsWith(" ")) { legendItem = null; } if (legendItem != null) { legendItem.setLabelFont(legendFont); lic.add(legendItem); } else { log.warn("Could not get LegentItem for renderer: " + idx + ", series-idx " + s); } } if (anno != null) { lic.addAll(anno); } plot.setFixedLegendItems(lic); plot.setRenderer(idx, renderer); } /** * @param plot * The plot. * @param area * A StyledAreaSeriesCollection object. * @param idx * The index of the dataset. */ private final void applyAreaTheme(final XYPlot plot, final StyledAreaSeriesCollection area, final int idx) { final LegendItemCollection lic = new LegendItemCollection(); final LegendItemCollection anno = plot.getFixedLegendItems(); final Font legendFont = createLegendLabelFont(); log.debug("Registering an 'area'renderer at idx: " + idx); final StableXYDifferenceRenderer dRenderer = new StableXYDifferenceRenderer(); if (area.getMode() == StyledAreaSeriesCollection.FILL_MODE.UNDER) { dRenderer.setPositivePaint(createTransparentPaint()); } plot.setRenderer(idx, dRenderer); area.applyTheme(dRenderer); // i18n dRenderer.setAreaLabelNumberFormat(Formatter.getFormatter(this.context.getMeta(), 2, 4)); dRenderer.setAreaLabelTemplate(Resources.getMsg(this.context.getMeta(), "area.label.template", "Area=%sm2")); final LegendItem legendItem = dRenderer.getLegendItem(idx, 0); if (legendItem != null) { legendItem.setLabelFont(legendFont); lic.add(legendItem); } else { log.warn("Could not get LegentItem for renderer: " + idx + ", series-idx " + 0); } if (anno != null) { lic.addAll(anno); } plot.setFixedLegendItems(lic); } /** * Returns a transparently textured paint. * * @return a transparently textured paint. */ private static Paint createTransparentPaint() { // TODO why not use a transparent color? final BufferedImage texture = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR); return new TexturePaint(texture, new Rectangle2D.Double(0d, 0d, 0d, 0d)); } private void preparePDFContext(final CallContext context) { final 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); } private void prepareSVGContext(final CallContext context) { final int[] dimension = getExportDimension(); context.putContextValue("chart.width", dimension[0]); context.putContextValue("chart.height", dimension[1]); context.putContextValue("chart.encoding", ChartExportHelper.DEFAULT_ENCODING); } /** * This method retrieves the chart subtitle by calling getChartSubtitle() * and adds it as TextTitle to the chart. * The default implementation of getChartSubtitle() returns the same * as getDefaultChartSubtitle() which must be implemented by derived * classes. If you want to add multiple subtitles to the chart override * this method and add your subtitles manually. * * @param chart * The JFreeChart chart object. */ protected void addSubtitles(final CallContext context, final JFreeChart chart) { final String subtitle = getChartSubtitle(this.context); if (subtitle != null && subtitle.length() > 0) { chart.addSubtitle(new TextTitle(subtitle)); } } protected abstract String getChartSubtitle(CallContext context); /** * Adds a new AxisDataset which contains <i>dataset</i> at index <i>idx</i>. * * @param dataset * An XYDataset. * @param idx * The axis index. * @param visible * Determines, if the dataset should be visible or not. */ protected final void addAxisDataset(final XYDataset dataset, final int idx, final boolean visible) { if (dataset == null || idx < 0) { return; } final AxisDataset axisDataset = getAxisDataset(idx); final Bounds[] xyBounds = ChartHelper.getBounds(dataset); if (xyBounds == null) { log.warn("Skip XYDataset for Axis (invalid ranges): " + idx); return; } if (visible) { if (log.isDebugEnabled()) { log.debug("Add new AxisDataset at index: " + idx); log.debug("X extent: " + xyBounds[0]); log.debug("Y extent: " + xyBounds[1]); } axisDataset.addDataset(dataset); } combineXBounds(xyBounds[0], 0); combineYBounds(xyBounds[1], idx); } /** * This method grants access to the AxisDatasets stored in <i>datasets</i>. * If no AxisDataset exists for index <i>idx</i>, a new AxisDataset is * created using <i>createAxisDataset()</i>. * * @param idx * The index of the desired AxisDataset. * * @return an existing or new AxisDataset. */ protected final AxisDataset getAxisDataset(final int idx) { AxisDataset axisDataset = this.datasets.get(idx); if (axisDataset == null) { axisDataset = createAxisDataset(idx); this.datasets.put(idx, axisDataset); } return axisDataset; } /** * Returns the size of a chart export as array which has been specified by * the incoming request document. * * @return the size of a chart as [width, height] or null if no width or * height are given in the request document. */ protected final int[] getSize() { final int[] size = new int[2]; final Element sizeEl = (Element) XMLUtils.xpath(this.request, XPATH_CHART_SIZE, XPathConstants.NODE, ArtifactNamespaceContext.INSTANCE); if (sizeEl != null) { final String uri = ArtifactNamespaceContext.NAMESPACE_URI; final String w = sizeEl.getAttributeNS(uri, "width"); final String h = sizeEl.getAttributeNS(uri, "height"); if (w.length() > 0 && h.length() > 0) { try { size[0] = Integer.parseInt(w); size[1] = Integer.parseInt(h); } catch (final NumberFormatException nfe) { log.warn("Wrong values for chart width/height."); } } } return size[0] > 0 && size[1] > 0 ? size : null; } /** * This method returns the format specified in the <i>request</i> document * or <i>DEFAULT_CHART_FORMAT</i> if no format is specified in * <i>request</i>. * * @return the format used to export this chart. */ private String getFormat() { final String format = (String) XMLUtils.xpath(this.request, XPATH_CHART_FORMAT, XPathConstants.STRING, ArtifactNamespaceContext.INSTANCE); return format == null || format.length() == 0 ? DEFAULT_CHART_FORMAT : format; } /** * Returns the X-Axis range as String array from request document. * If the (x|y)range elements are not found in request document, return * null (i.e. not zoomed). * * @return a String array with [lower, upper], null if not in document. */ protected final String[] getDomainAxisRangeFromRequest() { final Element xrange = (Element) XMLUtils.xpath(this.request, XPATH_CHART_X_RANGE, XPathConstants.NODE, ArtifactNamespaceContext.INSTANCE); if (xrange == null) { return null; } final String uri = ArtifactNamespaceContext.NAMESPACE_URI; final String lower = xrange.getAttributeNS(uri, "from"); final String upper = xrange.getAttributeNS(uri, "to"); return new String[] { lower, upper }; } /** * Returns null if the (x|y)range-element was not found in * request document. * This usally means that the axis are not manually zoomed, i.e. showing * full data extent. */ protected final String[] getValueAxisRangeFromRequest() { final Element yrange = (Element) XMLUtils.xpath(this.request, XPATH_CHART_Y_RANGE, XPathConstants.NODE, ArtifactNamespaceContext.INSTANCE); if (yrange == null) { return null; } final String uri = ArtifactNamespaceContext.NAMESPACE_URI; final String lower = yrange.getAttributeNS(uri, "from"); final String upper = yrange.getAttributeNS(uri, "to"); return new String[] { lower, upper }; } /** * Returns the default size of a chart export as array. * * @return the default size of a chart as [width, height]. */ protected final int[] getDefaultSize() { return new int[] { DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT }; } /** * This method returns the export dimension specified in ChartSettings as * int array [width,height]. * * @return an int array with [width,height]. */ private int[] getExportDimension() { final ChartSettings chartSettings = getChartSettings(); if (chartSettings == null) return new int[] { DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT }; final ExportSection export = chartSettings.getExportSection(); final Integer width = export.getWidth(); final Integer height = export.getHeight(); if (width != null && height != null) { return new int[] { width, height }; } return new int[] { 600, 400 }; } /** * Returns the chart title provided by <i>settings</i>. * * @param settings * A ChartSettings object. * * @return the title provided by <i>settings</i> or null if no * <i>ChartSection</i> is provided by <i>settings</i>. * * @throws NullPointerException * if <i>settings</i> is null. */ private String getChartTitle(final ChartSettings settings) { final ChartSection cs = settings.getChartSection(); return cs != null ? cs.getTitle() : null; } /** * Returns the chart subtitle provided by <i>settings</i>. * * @param settings * A ChartSettings object. * * @return the subtitle provided by <i>settings</i> or null if no * <i>ChartSection</i> is provided by <i>settings</i>. * * @throws NullPointerException * if <i>settings</i> is null. */ protected final String getChartSubtitle(final ChartSettings settings) { final ChartSection cs = settings.getChartSection(); return cs != null ? cs.getSubtitle() : null; } /** * 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(final CallContext context) { final ChartSettings chartSettings = getChartSettings(); if (chartSettings != null) { return getChartTitle(chartSettings); } return getDefaultChartTitle(context); } /** * This method always returns null. Override it in subclasses that require * subtitles. * * @return null. */ protected String getDefaultChartSubtitle(final CallContext context) { // Override this method in subclasses return null; } /** Where to place the logo. */ protected final String logoHPlace() { final ChartSettings chartSettings = getChartSettings(); if (chartSettings != null) { final ChartSection cs = chartSettings.getChartSection(); final String place = cs.getLogoHPlacement(); return place; } return "center"; } /** Where to place the logo. */ protected final String logoVPlace() { final ChartSettings chartSettings = getChartSettings(); if (chartSettings != null) { final ChartSection cs = chartSettings.getChartSection(); final String place = cs.getLogoVPlacement(); return place; } return "top"; } /** Return the logo id from settings. */ private String showLogo(final ChartSettings chartSettings) { if (chartSettings != null) { final ChartSection cs = chartSettings.getChartSection(); final String logo = cs.getDisplayLogo(); return logo; } return "none"; } /** * This method is used to determine if a logo should be added to the plot. * * @return logo name (null if none). */ protected final String showLogo() { final ChartSettings chartSettings = getChartSettings(); return showLogo(chartSettings); } /** * 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 final boolean isGridVisible() { return true; } protected final void addAnnotationsToRenderer(final XYPlot plot) { final AnnotationRenderer annotationRenderer = new AnnotationRenderer(getChartSettings(), this.datasets, DEFAULT_FONT_NAME); annotationRenderer.addAnnotationsToRenderer(plot, this.annotations); doAddFurtherAnnotations(plot, this.annotations); } /** * Allow further annotation processing, override to implement. * * Does nothing by default. */ protected void doAddFurtherAnnotations(final XYPlot plot, final List<RiverAnnotation> annotations) { } }