view flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java @ 2163:105097966111

Theoretically allow annotations on second y ais. Practically allow Q MainValues on Q Axis in Duration Curves. flys-artifacts/trunk@3750 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Felix Wolfsteller <felix.wolfsteller@intevation.de>
date Mon, 23 Jan 2012 14:18:53 +0000
parents c68f4f227c09
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 :

http://dive4elements.wald.intevation.org