diff flys-artifacts/src/main/java/org/dive4elements/river/exports/TimeseriesChartGenerator.java @ 5831:bd047b71ab37

Repaired internal references
author Sascha L. Teichmann <teichmann@intevation.de>
date Thu, 25 Apr 2013 12:06:39 +0200
parents flys-artifacts/src/main/java/de/intevation/flys/exports/TimeseriesChartGenerator.java@7eebd9e58641
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/flys-artifacts/src/main/java/org/dive4elements/river/exports/TimeseriesChartGenerator.java	Thu Apr 25 12:06:39 2013 +0200
@@ -0,0 +1,892 @@
+package org.dive4elements.river.exports;
+
+import org.dive4elements.artifactdatabase.state.ArtifactAndFacet;
+import org.dive4elements.river.artifacts.resources.Resources;
+import org.dive4elements.river.jfree.Bounds;
+import org.dive4elements.river.jfree.CollisionFreeXYTextAnnotation;
+import org.dive4elements.river.jfree.DoubleBounds;
+import org.dive4elements.river.jfree.FLYSAnnotation;
+import org.dive4elements.river.jfree.StyledTimeSeries;
+import org.dive4elements.river.jfree.TimeBounds;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+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.LegendItemCollection;
+import org.jfree.chart.annotations.XYAnnotation;
+import org.jfree.chart.annotations.XYImageAnnotation;
+import org.jfree.chart.annotations.XYTextAnnotation;
+import org.jfree.chart.axis.ValueAxis;
+import org.jfree.chart.plot.Marker;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.Range;
+import org.jfree.data.general.Series;
+import org.jfree.data.time.Day;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.data.xy.XYDataset;
+import org.jfree.ui.Layer;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.w3c.dom.Document;
+
+/**
+ * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
+ */
+public abstract class TimeseriesChartGenerator extends ChartGenerator {
+
+
+    /**
+     * Inner class TimeseriesAxisDataset stores TimeSeriesCollection.
+     */
+    public class TimeseriesAxisDataset implements AxisDataset {
+
+        protected int axisSymbol;
+
+        protected List<TimeSeriesCollection> datasets;
+
+        protected Range range;
+
+        protected int plotAxisIndex;
+
+        public TimeseriesAxisDataset(int axisSymbol) {
+            this.axisSymbol = axisSymbol;
+            this.datasets   = new ArrayList<TimeSeriesCollection>();
+        }
+
+
+        @Override
+        public void addDataset(XYDataset dataset) {
+            if (!(dataset instanceof TimeSeriesCollection)) {
+                logger.warn("Skip non TimeSeriesCollection dataset.");
+                return;
+            }
+
+            TimeSeriesCollection tsc = (TimeSeriesCollection) dataset;
+
+            datasets.add(tsc);
+            mergeRanges(tsc);
+        }
+
+
+        @Override
+        public XYDataset[] getDatasets() {
+            return datasets.toArray(new XYDataset[datasets.size()]);
+        }
+
+
+        @Override
+        public boolean isEmpty() {
+            return datasets.isEmpty();
+        }
+
+
+        @Override
+        public void setRange(Range range) {
+            this.range = range;
+        }
+
+
+        @Override
+        public Range getRange() {
+            return range;
+        }
+
+
+        @Override
+        public void setPlotAxisIndex(int plotAxisIndex) {
+            this.plotAxisIndex = plotAxisIndex;
+        }
+
+
+        @Override
+        public int getPlotAxisIndex() {
+            return plotAxisIndex;
+        }
+
+
+        @Override
+        public boolean isArea(XYDataset dataset) {
+            logger.warn("This AxisDataset doesn't support Areas yet!");
+            return false;
+        }
+
+
+        protected void mergeRanges(TimeSeriesCollection dataset) {
+            logger.debug("Range before merging: " + range);
+            Range subRange = null;
+
+            // Determine min/max of range axis.
+            for (int i = 0; i < dataset.getSeriesCount(); i++) {
+                if (dataset.getSeries(i).getItemCount() == 0) {
+                    continue;
+                }
+                double min = Double.MAX_VALUE;
+                double max = -Double.MAX_VALUE;
+                TimeSeries series = dataset.getSeries(i);
+                for (int j = 0; j < series.getItemCount(); j++) {
+                    double tmp = series.getValue(j).doubleValue();
+                    min = tmp < min ? tmp : min;
+                    max = tmp > max ? tmp : max;
+                }
+                if (subRange != null) {
+                    subRange = new Range(
+                        min < subRange.getLowerBound() ?
+                            min : subRange.getLowerBound(),
+                        max > subRange.getUpperBound() ?
+                            max : subRange.getUpperBound());
+                }
+                else {
+                    subRange = new Range(min, max);
+                }
+            }
+
+            // 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);
+        }
+
+    } // end of TimeseriesAxisDataset class
+
+    protected List<Marker> domainMarker;
+
+    protected List<Marker> valueMarker;
+
+    protected Map<String, String> attributes;
+
+    protected boolean domainZeroLineVisible;
+
+    private static final Logger logger =
+        Logger.getLogger(TimeseriesChartGenerator.class);
+
+    public static final int AXIS_SPACE = 5;
+
+    protected Map<Integer, Bounds> xBounds;
+
+    protected Map<Integer, Bounds> yBounds;
+
+
+    /**
+     * The default constructor that initializes internal datastructures.
+     */
+    public TimeseriesChartGenerator() {
+        super();
+
+        xBounds = new HashMap<Integer, Bounds>();
+        yBounds = new HashMap<Integer, Bounds>();
+        domainMarker = new ArrayList<Marker>();
+        valueMarker = new ArrayList<Marker>();
+        attributes = new HashMap<String, String>();
+    }
+
+
+
+    @Override
+    public JFreeChart generateChart() {
+        logger.info("Generate Timeseries Chart.");
+
+        JFreeChart chart = ChartFactory.createTimeSeriesChart(
+            getChartTitle(),
+            getXAxisLabel(),
+            getYAxisLabel(0),
+            null,
+            isLegendVisible(),
+            false,
+            false);
+
+        XYPlot plot = (XYPlot) chart.getPlot();
+
+        chart.setBackgroundPaint(Color.WHITE);
+        plot.setBackgroundPaint(Color.WHITE);
+
+        addSubtitles(chart);
+        adjustPlot(plot);
+        addDatasets(plot);
+        adjustAxes(plot);
+        addDomainAxisMarker(plot);
+        addValueAxisMarker(plot);
+        adaptZoom(plot);
+
+        applySeriesAttributes(plot);
+
+        addAnnotationsToRenderer(plot);
+        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 (Long)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 (Long)getXBounds(0).getUpper();
+    }
+
+
+    /**
+     * Add a logo as background annotation to plot.
+     * Copy from XYChartGenerator.
+     */
+    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 = ((Long)getXBounds(0).getUpper() + (Long)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);
+    }
+
+
+    @Override
+    protected Series getSeriesOf(XYDataset dataset, int idx) {
+        return ((TimeSeriesCollection) dataset).getSeries(idx);
+    }
+
+
+    /**
+     * This method creates new instances of TimeseriesAxisDataset.
+     *
+     * @param idx The symbol for the new TimeseriesAxisDataset.
+     */
+    @Override
+    protected AxisDataset createAxisDataset(int idx) {
+        logger.debug("Create a new AxisDataset for index: " + idx);
+        return new TimeseriesAxisDataset(idx);
+    }
+
+
+    @Override
+    protected void combineXBounds(Bounds bounds, int index) {
+        if (bounds != null) {
+            Bounds old = getXBounds(index);
+
+            if (old != null) {
+                bounds = bounds.combine(old);
+            }
+
+            setXBounds(index, bounds);
+        }
+    }
+
+
+    @Override
+    protected void combineYBounds(Bounds bounds, int index) {
+        if (bounds != null) {
+            Bounds old = getYBounds(index);
+
+            if (old != null) {
+                bounds = bounds.combine(old);
+            }
+
+            setYBounds(index, bounds);
+        }
+    }
+
+
+    // TODO REPLACE THIS METHOD WITH getBoundsForAxis(index)
+    @Override
+    public Range[] getRangesForAxis(int index) {
+        // TODO
+        Bounds[] bounds = getBoundsForAxis(index);
+
+        return new Range[] {
+            new Range(
+                bounds[0].getLower().doubleValue(),
+                bounds[0].getUpper().doubleValue()),
+            new Range(
+                bounds[1].getLower().doubleValue(),
+                bounds[1].getUpper().doubleValue())
+        };
+    }
+
+
+    @Override
+    public Bounds getXBounds(int axis) {
+        return xBounds.get(axis);
+    }
+
+
+    @Override
+    protected void setXBounds(int axis, Bounds bounds) {
+        xBounds.put(axis, bounds);
+    }
+
+
+    @Override
+    public Bounds getYBounds(int axis) {
+        return yBounds.get(axis);
+    }
+
+
+    @Override
+    protected void setYBounds(int axis, Bounds bounds) {
+        if (bounds != null) {
+            yBounds.put(axis, bounds);
+        }
+    }
+
+
+    public Bounds[] getBoundsForAxis(int index) {
+        logger.debug("Return x and y bounds for axis at: " + index);
+
+        Bounds rx = getXBounds(Integer.valueOf(index));
+        Bounds ry = getYBounds(Integer.valueOf(index));
+
+        if (rx == null) {
+            logger.warn("Range for x axis not set." +
+                        " Using default values: 0 - 1.");
+            rx = new TimeBounds(0l, 1l);
+        }
+
+        if (ry == null) {
+            logger.warn("Range for y axis not set." +
+                        " Using default values: 0 - 1.");
+            ry = new DoubleBounds(0l, 1l);
+        }
+
+        logger.debug("X Bounds at index " + index + " is: " + rx);
+        logger.debug("Y Bounds at index " + index + " is: " + ry);
+
+        return new Bounds[] {rx, ry};
+    }
+
+
+    /** Get (zoom)values from request. */
+    public Bounds getDomainAxisRange() {
+        String[] ranges = getDomainAxisRangeFromRequest();
+
+        if (ranges == null || ranges.length < 2) {
+            logger.debug("No zoom range for domain axis specified.");
+            return null;
+        }
+
+        if (ranges[0] == null || ranges[1] == null) {
+            logger.warn("Invalid ranges for domain axis specified!");
+            return null;
+        }
+
+        try {
+            double lower = Double.parseDouble(ranges[0]);
+            double upper = Double.parseDouble(ranges[1]);
+
+            return new DoubleBounds(lower, upper);
+        }
+        catch (NumberFormatException nfe) {
+            logger.warn("Invalid ranges for domain axis specified: " + nfe);
+        }
+
+        return null;
+    }
+
+
+    public Bounds getValueAxisRange() {
+        String[] ranges = getValueAxisRangeFromRequest();
+
+        if (ranges == null || ranges.length < 2) {
+            logger.debug("No zoom range for domain axis specified.");
+            return null;
+        }
+
+        if (ranges[0] == null || ranges[1] == null) {
+            logger.warn("Invalid ranges for domain axis specified!");
+            return null;
+        }
+
+        try {
+            double lower = Double.parseDouble(ranges[0]);
+            double upper = Double.parseDouble(ranges[1]);
+
+            return new DoubleBounds(lower, upper);
+        }
+        catch (NumberFormatException nfe) {
+            logger.warn("Invalid ranges for domain axis specified: " + nfe);
+        }
+
+        return null;
+    }
+
+
+    protected void adaptZoom(XYPlot plot) {
+        logger.debug("Adapt zoom of Timeseries chart.");
+
+        zoomX(plot, plot.getDomainAxis(), getXBounds(0), getDomainAxisRange());
+
+        Bounds valueAxisBounds = getValueAxisRange();
+
+        for (int j = 0, n = plot.getRangeAxisCount(); j < n; j++) {
+            zoomY(
+                plot,
+                plot.getRangeAxis(j),
+                getYBounds(j),
+                valueAxisBounds);
+        }
+    }
+
+
+    /**
+     * @param plot the plot.
+     * @param axis the value (x, time) axis of which to set bounds.
+     * @param total the current bounds (?).
+     */
+    protected void zoomX(
+        XYPlot    plot,
+        ValueAxis axis,
+        Bounds    total,//we could equally nicely getXBounds(0)
+        Bounds    user
+    ) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("== Zoom X axis ==");
+            logger.debug("    Total axis range  : " + total);
+            logger.debug("    User defined range: " + user);
+        }
+
+        if (user != null) {
+            long min  = total.getLower().longValue();
+            long max  = total.getUpper().longValue();
+            long diff = max > min ? max - min : min - max;
+
+            long newMin = Math.round(min + user.getLower().doubleValue() * diff);
+            long newMax = Math.round(min + user.getUpper().doubleValue() * diff);
+
+            TimeBounds newBounds = new TimeBounds(newMin, newMax);
+
+            logger.debug("    Zoom axis to: " + newBounds);
+
+            newBounds.applyBounds(axis, AXIS_SPACE);
+        }
+        else {
+            logger.debug("No user specified zoom values found!");
+            if (total != null && axis != null) {
+                total.applyBounds(axis, AXIS_SPACE);
+            }
+        }
+    }
+
+
+    /**
+     * @param user zoom values in percent.
+     */
+    protected void zoomY(
+        XYPlot    plot,
+        ValueAxis axis,
+        Bounds    total,
+        Bounds    user
+    ) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("== Zoom Y axis ==");
+            logger.debug("    Total axis range  : " + total);
+            logger.debug("    User defined range: " + user);
+        }
+
+        if (user != null) {
+            double min  = total.getLower().doubleValue();
+            double max  = total.getUpper().doubleValue();
+            double diff = max > min ? max - min : min - max;
+
+            double newMin = min + user.getLower().doubleValue() * diff;
+            double newMax = min + user.getUpper().doubleValue() * diff;
+
+            DoubleBounds newBounds = new DoubleBounds(newMin, newMax);
+
+            logger.debug("    Zoom axis to: " + newBounds);
+
+            newBounds.applyBounds(axis, AXIS_SPACE);
+        }
+        else {
+            logger.debug("No user specified zoom values found!");
+            if (total != null && axis != null) {
+                total.applyBounds(axis, AXIS_SPACE);
+            }
+        }
+    }
+
+
+    /**
+     * Adjusts the axes of a plot. This method sets the <i>labelFont</i> of the
+     * X axis.
+     *
+     * (Duplicate in XYChartGenerator).
+     *
+     * @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);
+    }
+
+
+    protected Date decodeXAxisValue(JSONArray array) throws JSONException, ParseException {
+        try {
+            double x = array.getDouble(0);
+            long l = (new Double(x)).longValue();
+            return new Date(l);
+        }
+        catch(JSONException ex) {
+            String str = array.getString(0);
+            DateFormat df = DateFormat.getDateInstance(
+                    DateFormat.MEDIUM, Resources.getLocale(context.getMeta()));
+            return df.parse(str);
+        }
+    }
+
+    /**
+     * Do Points out.
+     */
+    protected void doPoints(
+        Object     o,
+        ArtifactAndFacet aandf,
+        Document   theme,
+        boolean    visible,
+        int        axisIndex
+    ) {
+        String seriesName = aandf.getFacetDescription();
+        TimeSeries series = new StyledTimeSeries(seriesName, theme);
+
+        // Add text annotations for single points.
+        List<XYTextAnnotation> xy = new ArrayList<XYTextAnnotation>();
+        HashMap<Day, String> names = new HashMap<Day, String>();
+
+        try {
+            JSONArray points = new JSONArray((String) o);
+            for (int i = 0, P = points.length(); i < P; i++) {
+                JSONArray array = points.getJSONArray(i);
+
+                double y    = array.getDouble(1);
+                String name = array.getString(2);
+                boolean act = array.getBoolean(3);
+                if (!act) {
+                    continue;
+                }
+
+                Date date = decodeXAxisValue(array);
+
+                Day day = new Day(date);
+                series.add(day, y, false);
+                names.put(day, name);
+            }
+        }
+        catch(JSONException ex) {
+            logger.error("Could not decode json");
+        }
+        catch(ParseException ex) {
+            logger.error("Could not parse date string");
+        }
+
+        TimeSeriesCollection tsc = new TimeSeriesCollection();
+        tsc.addSeries(series);
+        // Add Annotations.
+        for (int i = 0, S = series.getItemCount(); i < S; i++) {
+            double x = tsc.getXValue(0, i);
+            double y = tsc.getYValue(0, i);
+            xy.add(new CollisionFreeXYTextAnnotation(
+                       names.get(series.getTimePeriod(i)), x, y));
+            logger.debug("doPoints(): x=" + x + " y=" + y);
+        }
+        FLYSAnnotation annotations =
+            new FLYSAnnotation(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);
+
+        addAxisDataset(tsc, axisIndex, visible);
+    }
+
+    public void addDomainAxisMarker(XYPlot plot) {
+        logger.debug("domainmarkers: " + domainMarker.size());
+        for (Marker marker: domainMarker) {
+            logger.debug("adding domain marker");
+            plot.addDomainMarker(marker, Layer.BACKGROUND);
+        }
+        domainMarker.clear();
+    }
+
+    public void addValueAxisMarker(XYPlot plot) {
+        for (Marker marker: valueMarker) {
+            logger.debug("adding value marker..");
+            plot.addRangeMarker(marker, Layer.BACKGROUND);
+        }
+        valueMarker.clear();
+    }
+
+    public void addAttribute(String seriesKey, String name) {
+        attributes.put(seriesKey, name);
+    }
+
+    private LegendItem getLegendItemFor(XYPlot plot, String interSeriesKey) {
+        LegendItemCollection litems = plot.getLegendItems();
+        Iterator<LegendItem> iter = litems.iterator();
+        while(iter.hasNext()) {
+            LegendItem item = iter.next();
+            if(interSeriesKey.startsWith(item.getSeriesKey().toString())) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    protected void applySeriesAttributes(XYPlot plot) {
+        int count  = plot.getDatasetCount();
+        for (int i = 0; i < count; i++) {
+            XYDataset data = plot.getDataset(i);
+            if (data == null) {
+                continue;
+            }
+
+            int seriesCount = data.getSeriesCount();
+            for (int j = 0; j < seriesCount; j++) {
+                StyledTimeSeries series =
+                    (StyledTimeSeries)getSeriesOf(data, j);
+                String key = series.getKey().toString();
+
+                if (attributes.containsKey(key)) {
+                    // Interpolated points are drawn unfilled
+                    if (attributes.get(key).equals("interpolate")) {
+                        XYLineAndShapeRenderer renderer =
+                                series.getStyle().getRenderer();
+                        renderer.setSeriesPaint(
+                            j,
+                            renderer.getSeriesFillPaint(j));
+                        renderer.setSeriesShapesFilled(j, false);
+
+                        LegendItem legendItem = getLegendItemFor(plot, key);
+                        if(legendItem != null) {
+                            LegendItem interLegend = new LegendItem(
+                                    legendItem.getLabel(),
+                                    legendItem.getDescription(),
+                                    legendItem.getToolTipText(),
+                                    legendItem.getURLText(),
+                                    legendItem.isShapeVisible(),
+                                    legendItem.getShape(),
+                                    false, // shapeFilled?
+                                    legendItem.getFillPaint(),
+                                    true,  // shapeOutlineVisible?
+                                    renderer.getSeriesFillPaint(j),
+                                    legendItem.getOutlineStroke(),
+                                    legendItem.isLineVisible(),
+                                    legendItem.getLine(),
+                                    legendItem.getLineStroke(),
+                                    legendItem.getLinePaint()
+                                    );
+                            interLegend.setSeriesKey(series.getKey());
+                            logger.debug("applySeriesAttributes: draw unfilled legend item");
+                            plot.getLegendItems().add(interLegend);
+                        }
+                    }
+                }
+
+                if (attributes.containsKey(key)) {
+                    if(attributes.get(key).equals("outline")) {
+                        XYLineAndShapeRenderer renderer =
+                            series.getStyle().getRenderer();
+                        renderer.setSeriesPaint(
+                            j,
+                            renderer.getSeriesFillPaint(j));
+                        renderer.setDrawOutlines(true);
+                    }
+                }
+            }
+        }
+    }
+
+    /** 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();
+        }
+
+        public double above(double percent, double base) {
+            return base + yRange.getLength() * percent;
+        }
+    }
+
+}
+// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org