ingo@369: package de.intevation.flys.exports;
ingo@369:
ingo@419: import java.awt.BasicStroke;
ingo@369: import java.awt.Color;
ingo@419: import java.awt.Stroke;
ingo@369:
ingo@369: import java.io.IOException;
ingo@369:
ingo@1645: import java.text.NumberFormat;
ingo@1645:
ingo@1679: import java.util.ArrayList;
ingo@1684: import java.util.HashMap;
ingo@1679: import java.util.List;
ingo@1684: import java.util.Map;
ingo@1679:
ingo@1679: import org.w3c.dom.Document;
ingo@1679:
ingo@369: import org.apache.log4j.Logger;
ingo@369:
ingo@369: import org.jfree.chart.ChartFactory;
ingo@369: import org.jfree.chart.JFreeChart;
ingo@1679: import org.jfree.chart.LegendItem;
ingo@1679: import org.jfree.chart.LegendItemCollection;
ingo@1679: import org.jfree.chart.annotations.XYTextAnnotation;
ingo@369: import org.jfree.chart.axis.NumberAxis;
ingo@652: import org.jfree.chart.axis.ValueAxis;
ingo@369: import org.jfree.chart.plot.PlotOrientation;
ingo@369: import org.jfree.chart.plot.XYPlot;
ingo@1679: import org.jfree.chart.renderer.xy.XYItemRenderer;
ingo@924: import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
ingo@652: import org.jfree.data.Range;
ingo@923: import org.jfree.data.xy.XYSeries;
ingo@923: import org.jfree.data.xy.XYSeriesCollection;
ingo@654:
ingo@654: import org.jfree.ui.RectangleInsets;
ingo@369:
ingo@369: import de.intevation.flys.exports.ChartExportHelper;
ingo@1679: import de.intevation.flys.jfree.FLYSAnnotation;
ingo@1679: import de.intevation.flys.utils.ThemeUtil;
ingo@369:
ingo@369:
ingo@369: /**
ingo@369: * An abstract base class for creating XY charts.
ingo@369: *
ingo@369: * @author Ingo Weinzierl
ingo@369: */
ingo@369: public abstract class XYChartGenerator extends ChartGenerator {
ingo@369:
felix@1048: /** The logger that is used in this generator. */
ingo@654: private static Logger logger = Logger.getLogger(XYChartGenerator.class);
ingo@369:
ingo@369:
felix@1048: /** SeriesCollection used for the first axis. */
ingo@923: protected XYSeriesCollection first;
ingo@923:
felix@1048: /** SeriesCollection used for the second axis. */
ingo@923: protected XYSeriesCollection second;
ingo@923:
ingo@1679: /** List of annotations to insert in plot. */
ingo@1679: protected List annotations;
ingo@1679:
felix@1685: /** The max X range to include all X values of all series for each axis. */
ingo@1684: protected Map xRanges;
ingo@1684:
felix@1685: /** The max Y range to include all Y values of all series for each axis. */
ingo@1684: protected Map yRanges;
ingo@1684:
ingo@419: public static final Color DEFAULT_GRID_COLOR = Color.GRAY;
ingo@419: public static final float DEFAULT_GRID_LINE_WIDTH = 0.3f;
ingo@419:
ingo@419:
ingo@369: /**
ingo@369: * Returns the title of a chart.
ingo@369: *
ingo@369: * @return the title of a chart.
ingo@369: */
ingo@369: protected abstract String getChartTitle();
ingo@369:
ingo@369: /**
ingo@369: * Returns the X-Axis label of a chart.
ingo@369: *
ingo@369: * @return the X-Axis label of a chart.
ingo@369: */
ingo@369: protected abstract String getXAxisLabel();
ingo@369:
ingo@369: /**
ingo@369: * Returns the Y-Axis label of a chart.
ingo@369: *
ingo@369: * @return the Y-Axis label of a chart.
ingo@369: */
ingo@369: protected abstract String getYAxisLabel();
ingo@369:
ingo@369:
ingo@369: public void generate()
ingo@369: throws IOException
ingo@369: {
ingo@369: logger.debug("XYChartGenerator.generate");
ingo@369:
ingo@653: JFreeChart chart = generateChart();
ingo@653:
ingo@653: int[] size = getSize();
ingo@653:
ingo@653: ChartExportHelper.exportImage(
ingo@653: out,
ingo@653: chart,
ingo@653: "png",
ingo@653: size[0], size[1]);
ingo@653: }
ingo@653:
ingo@653:
ingo@653: public JFreeChart generateChart() {
ingo@653: logger.debug("XYChartGenerator.generateChart");
ingo@653:
ingo@369: JFreeChart chart = ChartFactory.createXYLineChart(
ingo@369: getChartTitle(),
ingo@369: getXAxisLabel(),
ingo@369: getYAxisLabel(),
ingo@375: null,
ingo@369: PlotOrientation.VERTICAL,
ingo@369: true,
ingo@369: false,
ingo@369: false);
ingo@369:
ingo@369: chart.setBackgroundPaint(Color.WHITE);
ingo@369: chart.getPlot().setBackgroundPaint(Color.WHITE);
ingo@369:
ingo@419: XYPlot plot = (XYPlot) chart.getPlot();
ingo@419:
ingo@923: addDatasets(plot);
ingo@1679: addAnnotations(plot);
ingo@414: addSubtitles(chart);
ingo@419: adjustPlot(plot);
ingo@1645: localizeAxes(plot);
ingo@923:
ingo@923: removeEmptyRangeAxes(plot);
ingo@1699: adjustAxes(plot);
ingo@923:
ingo@1686: preparePointRanges(plot);
ingo@673: autoZoom(plot);
ingo@652:
ingo@924: applyThemes(plot);
ingo@924:
ingo@653: return chart;
ingo@369: }
ingo@369:
ingo@369:
felix@1685: /**
felix@1685: * Add first and second dataset to plot.
felix@1685: * @param plot plot to add datasets to.
felix@1685: */
ingo@923: protected void addDatasets(XYPlot plot) {
ingo@923: if (first != null) {
ingo@923: logger.debug("Set the first axis dataset.");
ingo@923: plot.setDataset(0, first);
ingo@923: }
ingo@923: if (second != null) {
ingo@923: logger.debug("Set the second axis dataset.");
ingo@923: plot.setDataset(1, second);
ingo@923: }
ingo@923: }
ingo@923:
ingo@923:
ingo@1684: public void addFirstAxisSeries(XYSeries series, boolean visible) {
ingo@923: if (first == null) {
ingo@923: first = new XYSeriesCollection();
ingo@923: }
ingo@923:
ingo@923: if (series != null) {
ingo@1684: if (visible) {
ingo@1684: first.addSeries(series);
ingo@1684: }
ingo@1684:
ingo@1684: combineYRanges(new Range(series.getMinY(), series.getMaxY()), 0);
ingo@1684: combineXRanges(new Range(series.getMinX(), series.getMaxX()), 0);
ingo@923: }
ingo@923: }
ingo@923:
ingo@923:
ingo@1684: public void addSecondAxisSeries(XYSeries series, boolean visible) {
ingo@923: if (second == null) {
ingo@923: second = new XYSeriesCollection();
ingo@923: }
ingo@923:
ingo@923: if (series != null) {
ingo@1684: if (visible) {
ingo@1684: second.addSeries(series);
ingo@1684: }
ingo@1684:
ingo@1684: combineYRanges(new Range(series.getMinY(), series.getMaxY()), 1);
ingo@1684: combineXRanges(new Range(series.getMinX(), series.getMaxX()), 0);
ingo@923: }
ingo@923: }
ingo@923:
ingo@923:
ingo@1684: private void combineXRanges(Range range, int index) {
sascha@1698: Integer key = Integer.valueOf(index);
ingo@1684:
ingo@1684: if (xRanges == null) {
ingo@1684: xRanges = new HashMap();
ingo@1684: xRanges.put(key, range);
ingo@1684: return;
ingo@1684: }
ingo@1684:
ingo@1684: Range newX = null;
ingo@1684: Range oldX = xRanges.get(key);
ingo@1684:
ingo@1684: if (oldX != null) {
ingo@1684: newX = Range.combine(oldX, range);
ingo@1684: }
ingo@1684: else {
ingo@1684: newX = range;
ingo@1684: }
ingo@1684:
ingo@1684: xRanges.put(key, newX);
ingo@1684: }
ingo@1684:
ingo@1684:
ingo@1684: private void combineYRanges(Range range, int index) {
sascha@1698: Integer key = Integer.valueOf(index);
ingo@1684:
ingo@1684: if (yRanges == null) {
ingo@1684: yRanges = new HashMap();
ingo@1684: yRanges.put(key, range);
ingo@1684: return;
ingo@1684: }
ingo@1684:
ingo@1684: Range newY = null;
ingo@1684: Range oldY = yRanges.get(key);
ingo@1684:
ingo@1684: if (oldY != null) {
ingo@1684: newY = Range.combine(oldY, range);
ingo@1684: }
ingo@1684: else {
ingo@1684: newY = range;
ingo@1684: }
ingo@1684:
ingo@1684: yRanges.put(key, newY);
ingo@1684: }
ingo@1684:
ingo@1684:
felix@1711: /**
felix@1711: * Adds annotations to list (if visible is true).
felix@1711: */
ingo@1684: public void addAnnotations(FLYSAnnotation annotation, boolean visible) {
ingo@1684: if (!visible) {
ingo@1684: return;
ingo@1684: }
ingo@1684:
ingo@1679: if (annotations == null) {
ingo@1679: annotations = new ArrayList();
ingo@1679: }
ingo@1679:
ingo@1679: annotations.add(annotation);
ingo@1679: }
ingo@1679:
ingo@1679:
ingo@923: private void removeEmptyRangeAxes(XYPlot plot) {
ingo@923: if (first == null) {
ingo@923: plot.setRangeAxis(0, null);
ingo@923: }
ingo@923:
ingo@923: if (second == null) {
ingo@923: plot.setRangeAxis(1, null);
ingo@923: }
ingo@923: }
ingo@923:
ingo@923:
ingo@1686: private void preparePointRanges(XYPlot plot) {
ingo@1686: for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
sascha@1698: Integer key = Integer.valueOf(i);
ingo@1686:
ingo@1686: Range r = xRanges.get(key);
ingo@1687: if (r != null && r.getLowerBound() == r.getUpperBound()) {
ingo@1686: xRanges.put(key, expandRange(r, 5));
ingo@1686: }
ingo@1686: }
ingo@1686:
ingo@1686: for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
sascha@1698: Integer key = Integer.valueOf(i);
ingo@1686:
ingo@1686: Range r = yRanges.get(key);
ingo@1687: if (r != null && r.getLowerBound() == r.getUpperBound()) {
ingo@1686: yRanges.put(key, expandRange(r, 5));
ingo@1686: }
ingo@1686: }
ingo@1686: }
ingo@1686:
ingo@1686:
ingo@1686: public static Range expandRange(Range range, double percent) {
ingo@1686: if (range == null) {
ingo@1686: return null;
ingo@1686: }
ingo@1686:
ingo@1686: double value = range.getLowerBound();
ingo@1686: double expand = value / 100 * percent;
ingo@1686:
ingo@1686: return expand != 0
ingo@1686: ? new Range(value-expand, value+expand)
ingo@1686: : new Range(-0.01 * percent, 0.01 * percent);
ingo@1686: }
ingo@1686:
ingo@1686:
ingo@653: /**
ingo@654: * This method zooms the plot to the specified ranges in the attribute
ingo@654: * document or to the ranges specified by the min/max values in the
ingo@654: * datasets. Note: We determine the range manually if no zoom ranges
ingo@654: * are given, because JFreeCharts auto-zoom adds a margin to the left and
ingo@654: * right of the data area.
ingo@653: *
ingo@653: * @param plot The XYPlot.
ingo@653: */
ingo@673: protected void autoZoom(XYPlot plot) {
ingo@652: logger.debug("Zoom to specified ranges.");
ingo@654:
ingo@673: Range xrange = getDomainAxisRange();
ingo@673: Range yrange = getValueAxisRange();
ingo@654:
ingo@1699: zoomX(plot, plot.getDomainAxis(), xRanges.get(0), xrange);
ingo@923:
ingo@1699: for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
ingo@673: ValueAxis yaxis = plot.getRangeAxis(i);
ingo@654:
ingo@673: if (yaxis == null) {
ingo@1699: logger.debug("Zoom problem: no Y Axis for index: " + i);
ingo@673: continue;
ingo@673: }
ingo@673:
ingo@1699: logger.debug("Prepare zoom settings for y axis at index: " + i);
ingo@1699: zoomY(plot, yaxis, yRanges.get(Integer.valueOf(i)), yrange);
ingo@654: }
ingo@653: }
ingo@652:
ingo@653:
ingo@718: protected boolean zoomX(XYPlot plot, ValueAxis axis, Range range, Range x) {
ingo@718: return zoom(plot, axis, range, x);
ingo@718: }
ingo@718:
ingo@718:
ingo@718: protected boolean zoomY(XYPlot plot, ValueAxis axis, Range range, Range x) {
ingo@718: return zoom(plot, axis, range, x);
ingo@718: }
ingo@718:
ingo@718:
ingo@653: /**
ingo@653: * Zooms the x axis to the range specified in the attribute document.
ingo@653: *
ingo@653: * @param plot The XYPlot.
ingo@673: * @param axis The axis the shoud be modified.
ingo@673: * @param range The whole range specified by a dataset.
ingo@673: * @param x A user defined range (null permitted).
ingo@654: *
ingo@654: * @return true, if a zoom range was specified, otherwise false.
ingo@653: */
ingo@673: protected boolean zoom(XYPlot plot, ValueAxis axis, Range range, Range x) {
ingo@673: if (x != null) {
ingo@673: double min = range.getLowerBound();
ingo@673: double max = range.getUpperBound();
ingo@673: double diff = max > min ? max - min : min - max;
ingo@652:
ingo@673: Range computed = new Range(
ingo@673: min + x.getLowerBound() * diff,
ingo@673: min + x.getUpperBound() * diff);
ingo@673:
ingo@717: axis.setRangeWithMargins(computed);
ingo@673:
ingo@673: logger.debug("Zoom axis to: " + computed);
ingo@654:
ingo@654: return true;
ingo@652: }
ingo@654:
ingo@717: axis.setRangeWithMargins(range);
ingo@654: return false;
ingo@654: }
ingo@654:
ingo@654:
ingo@654: /**
ingo@1684: * This method extracts the minimum and maximum values for x and y axes
ingo@1684: * which are stored in xRanges and yRanges.
ingo@654: *
ingo@1684: * @param index The index of the y-Axis.
ingo@654: *
ingo@654: * @return a Range[] as follows: [x-Range, y-Range].
ingo@654: */
ingo@1684: public Range[] getRangesForDataset(int index) {
ingo@1684: return new Range[] {
sascha@1698: xRanges.get(Integer.valueOf(0)),
sascha@1698: yRanges.get(Integer.valueOf(index))
ingo@1684: };
ingo@652: }
ingo@652:
ingo@652:
ingo@1679: protected void addAnnotations(XYPlot plot) {
ingo@1679: plot.clearAnnotations();
ingo@1679:
ingo@1679: if (annotations == null) {
ingo@1679: logger.debug("No Annotations given.");
ingo@1679: return;
ingo@1679: }
ingo@1679:
ingo@1679: LegendItemCollection lic = new LegendItemCollection();
ingo@1679:
ingo@1679: int idx = 0;
ingo@1679: if (plot.getRangeAxis(idx) == null && plot.getRangeAxisCount() >= 2) {
ingo@1679: idx = 1;
ingo@1679: }
ingo@1679:
ingo@1679: XYItemRenderer renderer = plot.getRenderer(idx);
ingo@1679:
ingo@1679: for (FLYSAnnotation fa: annotations) {
ingo@1679: Document theme = fa.getTheme();
ingo@1679:
ingo@1679: Color color = theme != null
ingo@1679: ? ThemeUtil.parseLineColorField(theme)
ingo@1679: : null;
ingo@1679:
ingo@1679: if (color == null) {
ingo@1679: color = Color.black;
ingo@1679: }
ingo@1679:
felix@1711: int lineWidth = theme != null
felix@1711: ? ThemeUtil.parseLineWidth(theme)
felix@1711: : 1;
felix@1711:
ingo@1679: lic.add(new LegendItem(fa.getLabel(), color));
ingo@1679:
ingo@1679: for (XYTextAnnotation ta: fa.getAnnotations()) {
ingo@1679: ta.setPaint(color);
felix@1711: ta.setOutlineStroke(new BasicStroke((float) lineWidth));
ingo@1679: renderer.addAnnotation(ta);
ingo@1679: }
ingo@1679:
ingo@1679: plot.setFixedLegendItems(lic);
ingo@1679: }
ingo@1679: }
ingo@1679:
ingo@1679:
ingo@369: /**
ingo@369: * Adjusts the axes of a plot.
ingo@369: *
ingo@369: * @param plot The XYPlot of the chart.
ingo@369: */
ingo@369: protected void adjustAxes(XYPlot plot) {
ingo@369: NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
ingo@369:
ingo@369: yAxis.setAutoRangeIncludesZero(false);
ingo@369: }
ingo@414:
ingo@414:
ingo@419: protected void adjustPlot(XYPlot plot) {
ingo@419: Stroke gridStroke = new BasicStroke(
ingo@419: DEFAULT_GRID_LINE_WIDTH,
ingo@419: BasicStroke.CAP_BUTT,
ingo@419: BasicStroke.JOIN_MITER,
ingo@419: 3.0f,
ingo@419: new float[] { 3.0f },
ingo@419: 0.0f);
ingo@419:
ingo@419: plot.setDomainGridlineStroke(gridStroke);
ingo@419: plot.setDomainGridlinePaint(DEFAULT_GRID_COLOR);
ingo@419: plot.setDomainGridlinesVisible(true);
ingo@419:
ingo@419: plot.setRangeGridlineStroke(gridStroke);
ingo@419: plot.setRangeGridlinePaint(DEFAULT_GRID_COLOR);
ingo@419: plot.setRangeGridlinesVisible(true);
ingo@654:
ingo@654: plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));
ingo@923:
ingo@923: if (plot.getDataset(0) != null) {
ingo@923: plot.mapDatasetToRangeAxis(0, 0);
ingo@923: }
ingo@923:
ingo@923: if (plot.getDataset(1) != null) {
ingo@923: plot.mapDatasetToRangeAxis(1, 1);
ingo@923: }
ingo@419: }
ingo@419:
ingo@419:
ingo@414: protected void addSubtitles(JFreeChart chart) {
ingo@414: // override this method in subclasses that need subtitles
ingo@414: }
ingo@924:
ingo@924:
ingo@1645: /**
ingo@1645: * This method walks over all axes (domain and range) of plot and
ingo@1645: * calls localizeDomainAxis() for domain axes or localizeRangeAxis() for
ingo@1645: * range axes.
ingo@1645: *
ingo@1645: * @param plot The XYPlot.
ingo@1645: */
ingo@1645: private void localizeAxes(XYPlot plot) {
ingo@1645: for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
ingo@1645: ValueAxis axis = plot.getDomainAxis(i);
ingo@1645:
ingo@1645: if (axis != null) {
ingo@1645: localizeDomainAxis(axis);
ingo@1645: }
ingo@1645: else {
ingo@1645: logger.warn("Domain axis at " + i + " is null.");
ingo@1645: }
ingo@1645: }
ingo@1645:
ingo@1645: for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
ingo@1645: ValueAxis axis = plot.getRangeAxis(i);
ingo@1645:
ingo@1645: if (axis != null) {
ingo@1645: localizeRangeAxis(axis);
ingo@1645: }
ingo@1645: else {
ingo@1645: logger.warn("Range axis at " + i + " is null.");
ingo@1645: }
ingo@1645: }
ingo@1645: }
ingo@1645:
ingo@1645:
ingo@1645: /**
ingo@1645: * Overrides the NumberFormat with the NumberFormat for the current locale
ingo@1645: * that is provided by getLocale().
ingo@1645: *
ingo@1645: * @param domainAxis The domain axis that needs localization.
ingo@1645: */
ingo@1645: protected void localizeDomainAxis(ValueAxis domainAxis) {
ingo@1645: NumberFormat nf = NumberFormat.getInstance(getLocale());
ingo@1645: ((NumberAxis) domainAxis).setNumberFormatOverride(nf);
ingo@1645: }
ingo@1645:
ingo@1645:
ingo@1645: /**
ingo@1645: * Overrides the NumberFormat with the NumberFormat for the current locale
ingo@1645: * that is provided by getLocale().
ingo@1645: *
ingo@1645: * @param domainAxis The domain axis that needs localization.
ingo@1645: */
ingo@1645: protected void localizeRangeAxis(ValueAxis rangeAxis) {
ingo@1645: NumberFormat nf = NumberFormat.getInstance(getLocale());
ingo@1645: ((NumberAxis) rangeAxis).setNumberFormatOverride(nf);
ingo@1645: }
ingo@1645:
ingo@1645:
ingo@924: protected void applyThemes(XYPlot plot) {
ingo@924: if (first != null) {
ingo@924: applyThemes(plot, first, 0);
ingo@924: }
ingo@924:
ingo@924: if (second != null) {
ingo@924: applyThemes(plot, second, 1);
ingo@924: }
ingo@924: }
ingo@924:
ingo@924:
ingo@924: protected void applyThemes(XYPlot plot, XYSeriesCollection dataset, int i) {
ingo@1679: LegendItemCollection lic = new LegendItemCollection();
ingo@1679: LegendItemCollection anno = plot.getFixedLegendItems();
ingo@1679:
ingo@924: XYLineAndShapeRenderer r = getRenderer(plot, i);
ingo@924:
ingo@924: for (int s = 0, num = dataset.getSeriesCount(); s < num; s++) {
ingo@924: XYSeries series = dataset.getSeries(s);
ingo@924:
ingo@924: if (series instanceof StyledXYSeries) {
ingo@924: ((StyledXYSeries) series).applyTheme(r, s);
ingo@924: }
ingo@1679:
ingo@1686: // special case: if there is just one single item, we need to enable
ingo@1686: // points for this series, otherwise we would not see anything in
ingo@1686: // the chart area.
ingo@1686: if (series.getItemCount() == 1) {
ingo@1686: r.setSeriesShapesVisible(s, true);
ingo@1686: }
ingo@1686:
ingo@1679: lic.add(r.getLegendItem(i, s));
ingo@924: }
ingo@924:
ingo@1679: if (anno != null) {
ingo@1679: lic.addAll(anno);
ingo@1679: }
ingo@1679:
ingo@1679: plot.setFixedLegendItems(lic);
ingo@1679:
ingo@924: plot.setRenderer(i, r);
ingo@924: }
ingo@924:
ingo@924:
ingo@924: protected XYLineAndShapeRenderer getRenderer(XYPlot plot, int idx) {
ingo@924: XYLineAndShapeRenderer r =
ingo@924: (XYLineAndShapeRenderer) plot.getRenderer(idx);
ingo@924:
ingo@924: if (r != null) {
ingo@924: return r;
ingo@924: }
ingo@924:
ingo@924: if (idx == 0) {
ingo@924: logger.warn("No default renderer set!");
ingo@924: return new XYLineAndShapeRenderer();
ingo@924: }
ingo@924:
ingo@924: r = (XYLineAndShapeRenderer) plot.getRenderer(0);
ingo@924:
ingo@924: try {
ingo@924: return (XYLineAndShapeRenderer) r.clone();
ingo@924: }
ingo@924: catch (CloneNotSupportedException cnse) {
ingo@924: logger.warn(cnse, cnse);
ingo@924: }
ingo@924:
ingo@924: logger.warn("No applicalable renderer found!");
ingo@924:
ingo@924: return new XYLineAndShapeRenderer();
ingo@924: }
ingo@369: }
ingo@369: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :