view artifacts/src/main/java/org/dive4elements/river/exports/XYChartGenerator.java @ 6332:f5bb53106ae8

Remove createBarriersLayer and createBarriers The generated mapfiles did not work and were just confusing. This looks like historical cruft that was never deleted. The real barrier mapfiles are created in the Floodmap state
author Andre Heinecke <aheinecke@intevation.de>
date Thu, 13 Jun 2013 17:24:56 +0200
parents af13ceeba52a
children 1b35b2ddfc28
line wrap: on
line source
/* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde
 * Software engineering by Intevation GmbH
 *
 * 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.Color;
import java.awt.Font;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.ImageIcon;

import org.apache.log4j.Logger;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.LegendItem;
import org.jfree.chart.annotations.XYAnnotation;
import org.jfree.chart.annotations.XYImageAnnotation;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.axis.LogarithmicAxis;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.Range;
import org.jfree.data.general.Series;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.json.JSONArray;
import org.json.JSONException;
import org.w3c.dom.Document;

import org.dive4elements.artifactdatabase.state.ArtifactAndFacet;
import org.dive4elements.river.jfree.Bounds;
import org.dive4elements.river.jfree.CollisionFreeXYTextAnnotation;
import org.dive4elements.river.jfree.DoubleBounds;
import org.dive4elements.river.jfree.RiverAnnotation;
import org.dive4elements.river.jfree.StyledAreaSeriesCollection;
import org.dive4elements.river.jfree.StyledXYSeries;


/**
 * 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 {

    public class XYAxisDataset implements AxisDataset {
        /** Symbolic integer, but also coding the priority (0 goes first). */
        protected int axisSymbol;

        /** List of assigned datasets (in order). */
        protected List<XYDataset> datasets;

        /** Range to use to include all given datasets. */
        protected Range range;

        /** Index of axis in plot. */
        protected int plotAxisIndex;

        /** Create AxisDataset. */
        public XYAxisDataset(int symb) {
            this.axisSymbol = symb;
            datasets        = new ArrayList<XYDataset>();
        }

        /** Merge (or create given range with range so far (if any). */
        private void mergeRanges(Range subRange) {
            // Avoid merging NaNs, as they take min/max place forever.
            if (subRange == null ||
                Double.isNaN(subRange.getLowerBound()) ||
                Double.isNaN(subRange.getUpperBound())) {
                return;
            }
            if (range == null) {
                range = subRange;
                return;
            }
            range = Range.combine(range, subRange);
        }


        /** Add a dataset to internal list for this axis. */
        @Override
        public void addDataset(XYDataset dataset) {
            datasets.add(dataset);
            includeYRange(((XYSeriesCollection) dataset).getSeries(0));
        }

        /** Add a dataset, include its range. */
        public void addDataset(XYSeries series) {
            addDataset(new XYSeriesCollection(series));
        }


        /** Set Range for this axis. */
        @Override
        public void setRange(Range range) {
            this.range = range;
        }


        /** Get Range for this axis. */
        @Override
        public Range getRange() {
            return range;
        }


        /** Get Array of Datasets. */
        @Override
        public XYDataset[] getDatasets() {
            return datasets.toArray(new XYDataset[datasets.size()]);
        }


        /** Add a Dataset that describes an area. */
        public void addArea(StyledAreaSeriesCollection series) {
            this.datasets.add(series);
            List<?> allSeries = series.getSeries();
            /* We do not include the bounds/ranges, if the area includes
             * points at "infinity"/BIG_DOUBLE_VALUE, the charts extents are
             * expanded to include these very small/big value.
             * This is especially used when showing "area above axis". */
        }

        /** True if to be rendered as area. */
        @Override
        public boolean isArea(XYDataset series) {
            return (series instanceof StyledAreaSeriesCollection);
        }

        /** Adjust range to include given dataset. */
        public void includeYRange(XYSeries dataset) {
            mergeRanges(new Range(dataset.getMinY(), dataset.getMaxY()));
        }

        /** True if no datasets given. */
        @Override
        public boolean isEmpty() {
            return this.datasets.isEmpty();
        }

        /** Set the 'real' axis index that this axis is mapped to. */
        @Override
        public void setPlotAxisIndex(int axisIndex) {
            this.plotAxisIndex = axisIndex;
        }

        /** Get the 'real' axis index that this axis is mapped to. */
        @Override
        public int getPlotAxisIndex() {
            return this.plotAxisIndex;
        }
    } // class AxisDataset

    /** Enumerator over existing axes. */
    @Override
    protected abstract YAxisWalker getYAxisWalker();

    public static final int AXIS_SPACE = 5;

    /** The logger that is used in this generator. */
    private static Logger logger = Logger.getLogger(XYChartGenerator.class);

    protected List<Marker> domainMarkers = new ArrayList<Marker>();

    protected List<Marker> valueMarkers = new ArrayList<Marker>();

    /** The max X range to include all X values of all series for each axis. */
    protected Map<Integer, Bounds> xBounds;

    /** The max Y range to include all Y values of all series for each axis. */
    protected Map<Integer, Bounds> yBounds;

    /** Whether or not the plot is inverted (left-right). */
    private boolean inverted;

    public XYChartGenerator() {
        super();

        xBounds  = new HashMap<Integer, Bounds>();
        yBounds  = new HashMap<Integer, Bounds>();
    }


    /**
     * Generate the chart anew (including localized axis and all).
     */
    @Override
    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();
        ValueAxis axis = createXAxis(getXAxisLabel());
        plot.setDomainAxis(axis);

        chart.setBackgroundPaint(Color.WHITE);
        plot.setBackgroundPaint(Color.WHITE);
        addSubtitles(chart);
        adjustPlot(plot);

        //debugAxis(plot);

        addDatasets(plot);

        //debugDatasets(plot);

        addMarkers(plot);

        recoverEmptyPlot(plot);
        preparePointRanges(plot);

        //debugAxis(plot);

        localizeAxes(plot);
        adjustAxes(plot);
        if (!(axis instanceof LogarithmicAxis)) {
            // XXX:
            // The auto zoom without a range tries
            // to include 0 in a logarithmic axis
            // which triggers a bug in jfreechart that causes
            // the values to be drawn carthesian
            autoZoom(plot);
        }

        //debugAxis(plot);

        // These have to go after the autozoom.
        addAnnotationsToRenderer(plot);

        // Add a logo (maybe).
        addLogo(plot);

        aggregateLegendEntries(plot);

        return chart;
    }


    /**
     * Return left most data points x value (on first axis).
     * Shortcut, especially to be overridden in (LS) charts where
     * axis could be inverted.
     */
    protected double getLeftX() {
        return (Double)getXBounds(0).getLower();
    }


    /**
     * Return right most data points x value (on first axis).
     * Shortcut, especially to be overridden in (LS) charts where
     * axis could be inverted.
     */
    protected double getRightX() {
        return (Double)getXBounds(0).getUpper();
    }


    /** Add a logo as background annotation to plot. */
    protected void addLogo(XYPlot plot) {
        String logo = showLogo();
        if (logo  == null) {
            logger.debug("No logo to show chosen");
            return;
        }

        ImageIcon imageIcon = null;
        if (logo.equals("none")) {
            return;
        }
        /*
         If you want to add images, remember to change code in these places:
         flys-artifacts:
         XYChartGenerator.java
         Timeseries*Generator.java and
         in the flys-client projects Chart*Propert*Editor.java.
         Also, these images have to be put in
         flys-artifacts/src/main/resources/images/
         flys-client/src/main/webapp/images/
         */
        java.net.URL imageURL;
        if (logo.equals("Intevation")) {
            imageURL = XYChartGenerator.class.getResource("/images/intevation.png");
        }
        else { // TODO else if ...
            imageURL = XYChartGenerator.class.getResource("/images/bfg_logo.gif");
        }
        imageIcon = new ImageIcon(imageURL);


        double xPos = 0d, yPos = 0d;

        String placeh = logoHPlace();
        String placev = logoVPlace();

        if (placev == null || placev.equals("none")) {
            placev = "top";
        }
        if (placev.equals("top")) {
            yPos = (Double)getYBounds(0).getUpper();
        }
        else if (placev.equals("bottom")) {
            yPos = (Double)getYBounds(0).getLower();
        }
        else if (placev.equals("center")) {
            yPos = ((Double)getYBounds(0).getUpper() + (Double)getYBounds(0).getLower())/2d;
        }
        else {
            logger.debug("Unknown place-v value: " + placev);
        }

        if (placeh == null || placeh.equals("none")) {
            placeh = "center";
        }
        if (placeh.equals("left")) {
            xPos = getLeftX();
        }
        else if (placeh.equals("right")) {
            xPos = getRightX();
        }
        else if (placeh.equals("center")) {
            xPos = ((Double)getXBounds(0).getUpper() + (Double)getXBounds(0).getLower())/2d;
        }
        else {
            logger.debug("Unknown place-h value: " + placeh);
        }

        logger.debug("logo position: " + xPos + "/" + yPos);

        org.jfree.ui.RectangleAnchor anchor
            = org.jfree.ui.RectangleAnchor.TOP;
        if (placev.equals("top")) {
            if (placeh.equals("left")) {
                anchor = org.jfree.ui.RectangleAnchor.TOP_LEFT;
            }
            else if (placeh.equals("right")) {
                anchor = org.jfree.ui.RectangleAnchor.TOP_RIGHT;
            }
            else if (placeh.equals("center")) {
                anchor = org.jfree.ui.RectangleAnchor.TOP;
            }
        }
        else if (placev.equals("bottom")) {
            if (placeh.equals("left")) {
                anchor = org.jfree.ui.RectangleAnchor.BOTTOM_LEFT;
            }
            else if (placeh.equals("right")) {
                anchor = org.jfree.ui.RectangleAnchor.BOTTOM_RIGHT;
            }
            else if (placeh.equals("center")) {
                anchor = org.jfree.ui.RectangleAnchor.BOTTOM;
            }
        }
        else if (placev.equals("center")) {
            if (placeh.equals("left")) {
                anchor = org.jfree.ui.RectangleAnchor.LEFT;
            }
            else if (placeh.equals("right")) {
                anchor = org.jfree.ui.RectangleAnchor.RIGHT;
            }
            else if (placeh.equals("center")) {
                anchor = org.jfree.ui.RectangleAnchor.CENTER;
            }
        }

        XYAnnotation xyannotation =
            new XYImageAnnotation(xPos, yPos, imageIcon.getImage(), anchor);
        plot.getRenderer().addAnnotation(xyannotation, org.jfree.ui.Layer.BACKGROUND);
    }


    protected NumberAxis createXAxis(String label) {
        return new NumberAxis(label);
    }


    @Override
    protected Series getSeriesOf(XYDataset dataset, int idx) {
        return ((XYSeriesCollection) dataset).getSeries(idx);
    }


    @Override
    protected AxisDataset createAxisDataset(int idx) {
        logger.debug("Create new XYAxisDataset for index: " + idx);
        return new XYAxisDataset(idx);
    }


    /**
     * Put debug output about datasets.
     */
    public void debugDatasets(XYPlot plot) {
        logger.debug("Number of datasets: " + plot.getDatasetCount());
        for (int i = 0, P = plot.getDatasetCount(); i < P; i++) {
            if (plot.getDataset(i) == null) {
                logger.debug("Dataset #" + i + " is null");
                continue;
            }
            logger.debug("Dataset #" + i + ":" + plot.getDataset(i));
            XYSeriesCollection series = (XYSeriesCollection) plot.getDataset(i);
            logger.debug("X-Extend of Dataset: " + series.getSeries(0).getMinX()
                    + " " + series.getSeries(0).getMaxX());
            logger.debug("Y-Extend of Dataset: " + series.getSeries(0).getMinY()
                    + " " + series.getSeries(0).getMaxY());
        }
    }


    /**
     * Put debug output about axes.
     */
    public void debugAxis(XYPlot plot) {
        logger.debug("...............");
        for (int i = 0, P =  plot.getRangeAxisCount(); i < P; i++) {
            if (plot.getRangeAxis(i) == null)
                logger.debug("Range-Axis #" + i + " == null");
            else {
                logger.debug("Range-Axis " + i + " != null [" +
                    plot.getRangeAxis(i).getRange().getLowerBound() +
                    "  " + plot.getRangeAxis(i).getRange().getUpperBound() +
                    "]");
            }
        }
        for (int i = 0, P =  plot.getDomainAxisCount(); i < P; i++) {
            if (plot.getDomainAxis(i) == null)
                logger.debug("Domain-Axis #" + i + " == null");
            else {
                logger.debug("Domain-Axis " + i + " != null [" +
                    plot.getDomainAxis(i).getRange().getLowerBound() +
                    "  " + plot.getDomainAxis(i).getRange().getUpperBound() +
                    "]");
            }
        }
        logger.debug("...............");
    }


    /**
     * Registers an area to be drawn.
     * @param area Area to be drawn.
     * @param index 'axis index'
     * @param visible Whether or not to be visible (important for range calculations).
     */
    public void addAreaSeries(StyledAreaSeriesCollection area, int index, boolean visible) {
        if (area == null) {
            logger.warn("Cannot yet render above/under curve.");
            return;
        }

        XYAxisDataset axisDataset = (XYAxisDataset) getAxisDataset(index);

        if (visible) {
            axisDataset.addArea(area);
        }
        else {
            /* No range merging, for areas extending to infinity this
             * causes problems. */
        }
    }


    /**
     * Add given series if visible, if not visible adjust ranges (such that
     * all points in data would be plotted once visible).
     * @param series the data series to include in plot.
     * @param index  ('symbolic') index of the series and of its axis.
     * @param visible whether or not the data should be plotted.
     */
    public void addAxisSeries(XYSeries series, int index, boolean visible) {
        if (series == null) {
            return;
        }

        logger.debug("Y Range of XYSeries: " +
            series.getMinY() + " | " + series.getMaxY());

        addAxisDataset(new XYSeriesCollection(series), index, visible);

        XYAxisDataset axisDataset = (XYAxisDataset) getAxisDataset(index);

        if (!visible) {
            // Do this also when not visible to have axis scaled by default such
            // that every data-point could be seen (except for annotations).
            axisDataset.includeYRange(series);
        }
    }


    /**
     * Add the given vertical marker to the chart.
     */
    public void addDomainMarker(Marker marker) {
        addDomainMarker(marker, true);
    }


    /**
     * Add the given vertical marker to the chart.<b>Note:</b> the marker is
     * added to the chart only if it is not null and if <i>visible</i> is true.
     * @param marker The marker that should be added to the chart.
     * @param visible The visibility of the marker.
     */
    public void addDomainMarker(Marker marker, boolean visible) {
        if (visible && marker != null) {
            domainMarkers.add(marker);
        }
    }


    /**
     * Add the given vertical marker to the chart.
     */
    public void addValueMarker(Marker marker) {
        addValueMarker(marker, true);
    }


    /**
     * Add the given horizontal marker to the chart.<b>Note:</b> the marker is
     * added to the chart only if it is not null and if <i>visible</i> is true.
     * @param marker The marker that should be added to the chart.
     * @param visible The visibility of the marker.
     */
    public void addValueMarker(Marker marker, boolean visible) {
        if (visible && marker != null) {
            valueMarkers.add(marker);
        }
    }


    protected void addMarkers(XYPlot plot) {
        for(Marker marker : domainMarkers) {
            plot.addDomainMarker(marker);
        }
        for(Marker marker : valueMarkers) {
            plot.addRangeMarker(marker);
        }
    }


    /**
     * Effect: extend range of x axis to include given limits.
     *
     * @param bounds the given ("minimal") bounds.
     * @param index index of axis to be merged.
     */
    @Override
    protected void combineXBounds(Bounds bounds, int index) {
        if (!(bounds instanceof DoubleBounds)) {
            logger.warn("Unsupported Bounds type: " + bounds.getClass());
            return;
        }

        DoubleBounds dBounds = (DoubleBounds) bounds;

        if (dBounds == null
            || Double.isNaN((Double) dBounds.getLower())
            || Double.isNaN((Double) dBounds.getUpper())) {
            return;
        }

        Bounds old = getXBounds(index);

        if (old != null) {
            dBounds = (DoubleBounds) dBounds.combine(old);
        }

        setXBounds(index, dBounds);
    }


    @Override
    protected void combineYBounds(Bounds bounds, int index) {
        if (!(bounds instanceof DoubleBounds)) {
            logger.warn("Unsupported Bounds type: " + bounds.getClass());
            return;
        }

        DoubleBounds dBounds = (DoubleBounds) bounds;

        if (dBounds == null
            || Double.isNaN((Double) dBounds.getLower())
            || Double.isNaN((Double) dBounds.getUpper())) {
            return;
        }

        Bounds old = getYBounds(index);

        if (old != null) {
            dBounds = (DoubleBounds) dBounds.combine(old);
        }

        setYBounds(index, dBounds);
    }


    /**
     * If no data is visible, draw at least empty axis.
     */
    private void recoverEmptyPlot(XYPlot plot) {
        if (plot.getRangeAxis() == null) {
            logger.debug("debug: No range axis");
            plot.setRangeAxis(createYAxis(0));
        }
    }


    /**
     * Expands X axes if only a point is shown.
     */
    private void preparePointRanges(XYPlot plot) {
        for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {

            Integer key = Integer.valueOf(i);
            Bounds  b   = getXBounds(key);


            if (b != null && b.getLower().equals(b.getUpper())) {
                logger.debug("Check whether to expand a x axis.i ("+b.getLower() + "-" + b.getUpper()+")");
                setXBounds(key, ChartHelper.expandBounds(b, 5));
            }
        }
    }


    /**
     * This method zooms the plot to the specified ranges in the attribute
     * document or to the ranges specified by the min/max values in the
     * datasets. <b>Note:</b> We determine the range manually if no zoom ranges
     * are given, because JFreeCharts auto-zoom adds a margin to the left and
     * right of the data area.
     *
     * @param plot The XYPlot.
     */
    protected void autoZoom(XYPlot plot) {
        logger.debug("Zoom to specified ranges.");

        Range xrange = getDomainAxisRange();
        Range yrange = getValueAxisRange();

        ValueAxis xAxis = plot.getDomainAxis();

        Range fixedXRange = getRangeForAxisFromSettings("X");
        if (fixedXRange != null) {
            xAxis.setRange(fixedXRange);
        }
        else {
            zoomX(plot, xAxis, getXBounds(0), xrange);
        }

        for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
            ValueAxis yaxis = plot.getRangeAxis(i);

            if (yaxis instanceof IdentifiableNumberAxis) {
                IdentifiableNumberAxis idAxis = (IdentifiableNumberAxis) yaxis;

                Range fixedRange = getRangeForAxisFromSettings(idAxis.getId());
                if (fixedRange != null) {
                    yaxis.setRange(fixedRange);
                    continue;
                }
            }

            if (yaxis == null) {
                logger.debug("Zoom problem: no Y Axis for index: " + i);
                continue;
            }

            logger.debug("Prepare zoom settings for y axis at index: " + i);
            zoomY(plot, yaxis, getYBounds(Integer.valueOf(i)), yrange);
        }
    }


    protected Range getDomainAxisRange() {
        String[] ranges = getDomainAxisRangeFromRequest();

        if (ranges == null || ranges.length < 2) {
            logger.debug("No zoom range for domain axis specified.");
            return null;
        }

        if (ranges[0].length() > 0 && ranges[1].length() > 0) {
            try {
                double from = Double.parseDouble(ranges[0]);
                double to   = Double.parseDouble(ranges[1]);

                if (from == 0 && to == 0) {
                    logger.debug("No range specified. Lower and upper X == 0");
                    return null;
                }

                if (from > to) {
                    double tmp = to;
                    to         = from;
                    from       = tmp;
                }

                return new Range(from, to);
            }
            catch (NumberFormatException nfe) {
                logger.warn("Wrong values for domain axis range.");
            }
        }

        return null;
    }


    protected Range getValueAxisRange() {
        String[] ranges = getValueAxisRangeFromRequest();

        if (ranges == null || ranges.length < 2) {
            logger.debug("No range specified. Lower and upper Y == 0");
            return null;
        }

        if (ranges[0].length() > 0 && ranges[1].length() > 0) {
            try {
                double from = Double.parseDouble(ranges[0]);
                double to   = Double.parseDouble(ranges[1]);

                if (from == 0 && to == 0) {
                    logger.debug("No range specified. Lower and upper Y == 0");
                    return null;
                }

                return from > to
                       ? new Range(to, from)
                       : new Range(from, to);
            }
            catch (NumberFormatException nfe) {
                logger.warn("Wrong values for value axis range.");
            }
        }

        return null;
    }


    protected boolean zoomX(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) {
        return zoom(plot, axis, bounds, x);
    }


    protected boolean zoomY(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) {
        return zoom(plot, axis, bounds, x);
    }


    /**
     * Zooms the x axis to the range specified in the attribute document.
     *
     * @param plot  The XYPlot.
     * @param axis  The axis the shoud be modified.
     * @param bounds The whole range specified by a dataset.
     * @param x     A user defined range (null permitted).
     *
     * @return true, if a zoom range was specified, otherwise false.
     */
    protected boolean zoom(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) {

        if (bounds == null) {
            return false;
        }

        if (x != null) {
            Bounds computed = calculateZoom(bounds, x);
            computed.applyBounds(axis, AXIS_SPACE);

            logger.debug("Zoom axis to: " + computed);

            return true;
        }

        bounds.applyBounds(axis, AXIS_SPACE);
        return false;
    }

    /**
     * Calculates the start and end km for zoomed charts.
     * @param bounds    The given total bounds (unzoomed).
     * @param range     The range specifying the zoom.
     *
     * @return The start and end km for the zoomed chart.
     */
    protected Bounds calculateZoom(Bounds bounds, Range range) {
        double min  = bounds.getLower().doubleValue();
        double max  = bounds.getUpper().doubleValue();

        if (logger.isDebugEnabled()) {
            logger.debug("Minimum is: " + min);
            logger.debug("Maximum is: " + max);
            logger.debug("Lower zoom is: " + range.getLowerBound());
            logger.debug("Upper zoom is: " + range.getUpperBound());
        }

        double diff = max > min ? max - min : min - max;

        DoubleBounds computed = new DoubleBounds(
            min + range.getLowerBound() * diff,
            min + range.getUpperBound() * diff);
        return computed;
    }

    /**
     * Extract the minimum and maximum values for x and y axes
     * which are stored in <i>xRanges</i> and <i>yRanges</i>.
     *
     * @param index The index of the y-Axis.
     *
     * @return a Range[] as follows: [x-Range, y-Range].
     */
    @Override
    public Range[] getRangesForAxis(int index) {
        logger.debug("getRangesForAxis " + index);

        Bounds rx = getXBounds(Integer.valueOf(0));
        Bounds ry = getYBounds(Integer.valueOf(index));

        if (rx == null) {
            logger.warn("Range for x axis not set." +
                        " Using default values: 0 - 1.");
            rx = new DoubleBounds(0, 1);
        }
        if (ry == null) {
            logger.warn("Range for y" + index +
                        " axis not set. Using default values: 0 - 1.");
            ry = new DoubleBounds(0, 1);
        }

        return new Range[] {
            new Range(rx.getLower().doubleValue(), rx.getUpper().doubleValue()),
            new Range(ry.getLower().doubleValue(), ry.getUpper().doubleValue())
        };
    }


    /** Get X (usually horizontal) extent for given axis. */
    @Override
    public Bounds getXBounds(int axis) {
        return xBounds.get(axis);
    }


    /** Set X (usually horizontal) extent for given axis. */
    @Override
    protected void setXBounds(int axis, Bounds bounds) {
        if (bounds.getLower() == bounds.getUpper()) {
            xBounds.put(axis, ChartHelper.expandBounds(bounds, 5d));
        }
        else {
            xBounds.put(axis, bounds);
        }
    }


    /** Get Y (usually vertical) extent for given axis. */
    @Override
    public Bounds getYBounds(int axis) {
        return yBounds.get(axis);
    }


    /** Set Y (usually vertical) extent for given axis. */
    @Override
    protected void setYBounds(int axis, Bounds bounds) {
        yBounds.put(axis, bounds);
    }


    /**
     * Adjusts the axes of a plot. This method sets the <i>labelFont</i> of the
     * X axis.
     *
     * (Duplicate in TimeseriesChartGenerator)
     *
     * @param plot The XYPlot of the chart.
     */
    protected void adjustAxes(XYPlot plot) {
        ValueAxis xaxis = plot.getDomainAxis();

        ChartSettings chartSettings = getChartSettings();
        if (chartSettings == null) {
            return;
        }

        Font labelFont = new Font(
            DEFAULT_FONT_NAME,
            Font.BOLD,
            getXAxisLabelFontSize());

        xaxis.setLabelFont(labelFont);
        xaxis.setTickLabelFont(labelFont);
    }


    /**
     * This method walks over all axes (domain and range) of <i>plot</i> and
     * calls localizeDomainAxis() for domain axes or localizeRangeAxis() for
     * range axes.
     *
     * @param plot The XYPlot.
     */
    private void localizeAxes(XYPlot plot) {
        for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
            ValueAxis axis = plot.getDomainAxis(i);

            if (axis != null) {
                localizeDomainAxis(axis);
            }
            else {
                logger.warn("Domain axis at " + i + " is null.");
            }
        }

        for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
            ValueAxis axis = plot.getRangeAxis(i);

            if (axis != null) {
                localizeRangeAxis(axis);
            }
            else {
                logger.warn("Range axis at " + i + " is null.");
            }
        }
    }


    /**
     * Overrides the NumberFormat with the NumberFormat for the current locale
     * that is provided by getLocale().
     *
     * @param domainAxis The domain axis that needs localization.
     */
    protected void localizeDomainAxis(ValueAxis domainAxis) {
        NumberFormat nf = NumberFormat.getInstance(getLocale());
        ((NumberAxis) domainAxis).setNumberFormatOverride(nf);
    }


    /**
     * Overrides the NumberFormat with the NumberFormat for the current locale
     * that is provided by getLocale().
     *
     * @param rangeAxis The domain axis that needs localization.
     */
    protected void localizeRangeAxis(ValueAxis rangeAxis) {
        NumberFormat nf = NumberFormat.getInstance(getLocale());
        ((NumberAxis) rangeAxis).setNumberFormatOverride(nf);
    }


    /**
     * Do Points out.
     */
    protected void doPoints(
        Object     o,
        ArtifactAndFacet aandf,
        Document   theme,
        boolean    visible,
        int        axisIndex
    ) {
        String seriesName = aandf.getFacetDescription();
        XYSeries series = new StyledXYSeries(seriesName, theme);

        // Add text annotations for single points.
        List<XYTextAnnotation> xy = new ArrayList<XYTextAnnotation>();

        try {
            JSONArray points = new JSONArray((String) o);
            for (int i = 0, P = points.length(); i < P; i++) {
                JSONArray array = points.getJSONArray(i);
                double x    = array.getDouble(0);
                double y    = array.getDouble(1);
                String name = array.getString(2);
                boolean act = array.getBoolean(3);
                if (!act) {
                    continue;
                }
                //logger.debug(" x " + x + " y " + y );
                series.add(x, y, false);
                xy.add(new CollisionFreeXYTextAnnotation(name, x, y));
            }
        }
        catch(JSONException e){
            logger.error("Could not decode json.");
        }

        RiverAnnotation annotations = new RiverAnnotation(null, null, null, theme);
        annotations.setTextAnnotations(xy);

        // Do not generate second legend entry. (null was passed for the aand before).
        doAnnotations(annotations, null, theme, visible);
        addAxisSeries(series, axisIndex, visible);
    }


    /**
     * Create a hash from a legenditem.
     * This hash can then be used to merge legend items labels.
     * @return hash for given legenditem to identify mergeables.
     */
    public static String legendItemHash(LegendItem li) {
        // TODO Do proper implementation. Ensure that only mergable sets are created.
        // getFillPaint()
        // getFillPaintTransformer()
        // getLabel()
        // getLine()
        // getLinePaint()
        // getLineStroke()
        // getOutlinePaint()
        // getOutlineStroke()
        // Shape getShape()
        // String getToolTipText()
        // String getURLText()
        // boolean isLineVisible()
        // boolean isShapeFilled()
        // boolean isShapeOutlineVisible()
        // boolean isShapeVisible()
        String hash = li.getLinePaint().toString();
        String label = li.getLabel();
        if (label.startsWith("W (") || label.startsWith("W(")) {
            hash += "-W-";
        }
        else if (label.startsWith("Q(") || label.startsWith("Q (")) {
            hash += "-Q-";
        }

        // WQ.java holds example of using regex Matcher/Pattern.

        return hash;
    }

    /** True if x axis has been inverted. */
    public boolean isInverted() {
        return inverted;
    }


    /** Set to true if x axis has been inverted. */
    public void setInverted(boolean inverted) {
        this.inverted = inverted;
    }


}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org