view artifacts/src/main/java/org/dive4elements/river/jfree/EnhancedLineAndShapeRenderer.java @ 9186:eec4df8165a1

Implemented 'ShowLineLabel' for area themes.
author gernotbelger
date Thu, 28 Jun 2018 10:47:04 +0200
parents 5e38e2924c07
children
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.jfree;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.plot.CrosshairState;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.text.TextUtilities;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.TextAnchor;
import org.jfree.util.BooleanList;
import org.jfree.util.ShapeUtilities;

/**
 * Renderer with additional the additional functionality of renderering minima
 * and/or maxima of dataseries contained in datasets.
 */
public class EnhancedLineAndShapeRenderer extends XYLineAndShapeRenderer {

    /**
     *
     */
    private static final long serialVersionUID = 1L;

    /** Own log. */
    private static final Logger log = Logger.getLogger(EnhancedLineAndShapeRenderer.class);

    protected BooleanList isMinimumShapeVisible;
    protected BooleanList isMaximumShapeVisible;
    protected BooleanList showLineLabel;

    protected Map<Integer, Double> seriesMinimum;
    protected Map<Integer, Double> seriesMinimumX;
    protected Map<Integer, Double> seriesMaximum;

    protected Map<Integer, Font> lineLabelFonts;
    protected Map<Integer, Color> lineLabelTextColors;
    protected BooleanList showLineLabelBG;
    protected Map<Integer, Color> lineLabelBGColors;

    public EnhancedLineAndShapeRenderer(final boolean lines, final boolean shapes) {
        super(lines, shapes);
        this.isMinimumShapeVisible = new BooleanList();
        this.isMaximumShapeVisible = new BooleanList();
        this.showLineLabel = new BooleanList();
        this.showLineLabelBG = new BooleanList();
        this.seriesMinimum = new HashMap<>();
        this.seriesMaximum = new HashMap<>();
        this.seriesMinimumX = new HashMap<>();
        this.lineLabelFonts = new HashMap<>();
        this.lineLabelTextColors = new HashMap<>();
        this.lineLabelBGColors = new HashMap<>();
    }

    /**
     * Draw a background-box of a text to render.
     *
     * @param g2
     *            graphics device to use
     * @param text
     *            text to draw
     * @param textX
     *            x-position for text
     * @param textY
     *            y-position for text
     * @param bgColor
     *            color to fill box with.
     */
    public static void drawTextBox(final Graphics2D g2, final String text, final float textX, final float textY, final Color bgColor) {
        final Rectangle2D hotspotBox = g2.getFontMetrics().getStringBounds(text, g2);
        final float w = (float) hotspotBox.getWidth();
        final float h = (float) hotspotBox.getHeight();
        hotspotBox.setRect(textX, textY - h, w, h);
        final Color oldColor = g2.getColor();
        g2.setColor(bgColor);
        g2.fill(hotspotBox);
        g2.setColor(oldColor);
    }

    /**
     * Whether or not a specific item in a series (maybe the maxima) should
     * be rendered with shape.
     */
    private boolean getItemShapeVisible(final XYDataset dataset, final int series, final int item) {
        if (super.getItemShapeVisible(series, item)) {
            return true;
        }

        if (isMinimumShapeVisible(series) && isMinimum(dataset, series, item)) {
            return true;
        }

        if (isMaximumShapeVisible(series) && isMaximum(dataset, series, item)) {
            return true;
        }

        return false;
    }

    /**
     * Rectangle used to draw maximums shape.
     */
    private Shape getMaximumShape(final int series, final int column) {
        return new Rectangle2D.Double(-5d, -5d, 10d, 10d);
    }

    /**
     * Rectangle used to draw minimums shape.
     */
    private Shape getMinimumShape(final int series, final int column) {
        return new Rectangle2D.Double(-5d, -5d, 10d, 10d);
    }

    /** Get fill paint for the maximum indicators. */
    private Paint getMaximumFillPaint(final int series, final int column) {
        final Paint p = getItemPaint(series, column);

        if (p instanceof Color) {
            final Color c = (Color) p;
            Color b = c;

            for (int i = 0; i < 2; i++) {
                b = b.darker();
            }

            return b;
        }

        log.warn("Item paint is no instance of Color!");
        return p;
    }

    /** Get fill paint for the minimum indicators. */
    private Paint getMinimumFillPaint(final int series, final int column) {
        final Paint p = getItemPaint(series, column);

        if (p instanceof Color) {
            final Color c = (Color) p;
            Color b = c;

            for (int i = 0; i < 2; i++) {
                b = b.darker();
            }

            return b;
        }

        log.warn("Item paint is no instance of Color!");
        return p;
    }

    /**
     * Overrides XYLineAndShapeRenderer.drawSecondaryPass() to call an adapted
     * method getItemShapeVisible() which now takes an XYDataset. So, 99% of
     * code equal the code in XYLineAndShapeRenderer.
     */
    @Override
    protected void drawSecondaryPass(final Graphics2D g2, final XYPlot plot, final XYDataset dataset, final int pass, final int series, final int item,
            final ValueAxis domainAxis, final Rectangle2D dataArea, final ValueAxis rangeAxis, final CrosshairState crosshairState,
            final EntityCollection entities) {
        Shape entityArea = null;

        // get the data point...
        final double x1 = dataset.getXValue(series, item);
        final double y1 = dataset.getYValue(series, item);
        if (Double.isNaN(y1) || Double.isNaN(x1)) {
            return;
        }

        final PlotOrientation orientation = plot.getOrientation();
        final RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
        final RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
        final double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
        final double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);

        if (getItemShapeVisible(dataset, series, item)) {
            Shape shape = null;

            // OPTIMIZE: instead of calculating minimum and maximum for every
            // point, calculate it just once (assume that dataset
            // content does not change during rendering).
            // NOTE: Above OPTIMIZE might already be fulfilled to
            // most extend.
            final boolean isMinimum = isMinimumShapeVisible(series) && isMinimum(dataset, series, item);

            final boolean isMaximum = isMaximumShapeVisible(series) && isMaximum(dataset, series, item);

            if (isMinimum) {
                log.debug("Create a Minimum shape.");
                shape = getMinimumShape(series, item);
            } else if (isMaximum) {
                log.debug("Create a Maximum shape.");
                shape = getMaximumShape(series, item);
            } else {
                shape = getItemShape(series, item);
            }

            if (orientation == PlotOrientation.HORIZONTAL) {
                shape = ShapeUtilities.createTranslatedShape(shape, transY1, transX1);
            } else if (orientation == PlotOrientation.VERTICAL) {
                shape = ShapeUtilities.createTranslatedShape(shape, transX1, transY1);
            }
            entityArea = shape;
            if (shape.intersects(dataArea)) {
                if (getItemShapeFilled(series, item)) {
                    if (getUseFillPaint()) {
                        g2.setPaint(getItemFillPaint(series, item));
                    } else {
                        g2.setPaint(getItemPaint(series, item));
                    }
                    g2.fill(shape);
                }
                if (getDrawOutlines()) {
                    if (getUseOutlinePaint()) {
                        g2.setPaint(getItemOutlinePaint(series, item));
                    } else {
                        g2.setPaint(getItemPaint(series, item));
                    }
                    g2.setStroke(getItemOutlineStroke(series, item));
                    g2.draw(shape);
                }

                if (isMinimum) {
                    g2.setPaint(getMinimumFillPaint(series, item));
                    g2.fill(shape);
                    g2.setPaint(getItemOutlinePaint(series, item));
                    g2.setStroke(getItemOutlineStroke(series, item));
                    g2.draw(shape);
                } else if (isMaximum) {
                    g2.setPaint(getMaximumFillPaint(series, item));
                    g2.fill(shape);
                    g2.setPaint(getItemOutlinePaint(series, item));
                    g2.setStroke(getItemOutlineStroke(series, item));
                    g2.draw(shape);
                }
            }
        } // if (getItemShapeVisible(dataset, series, item))

        double xx = transX1;
        double yy = transY1;
        if (orientation == PlotOrientation.HORIZONTAL) {
            xx = transY1;
            yy = transX1;
        }

        // Draw the item label if there is one...
        if (isItemLabelVisible(series, item))
            drawItemLabel(g2, orientation, dataset, series, item, xx, yy, (y1 < 0.0));

        // Draw label of line.
        if (dataset instanceof XYSeriesCollection && isShowLineLabel(series) && isMinimumX(dataset, series, item)) {
            final XYSeries xYSeries = ((XYSeriesCollection) dataset).getSeries(series);
            final String waterlevelLabel = (xYSeries instanceof HasLabel) ? ((HasLabel) xYSeries).getLabel() : xYSeries.getKey().toString();
            // TODO Force water of some German rivers to flow
            // direction mountains.

            final Font labelFont = this.getLineLabelFont(series);
            final Color labelColor = this.getLineLabelTextColor(series);
            final boolean showBG = isShowLineLabelBG(series);
            final Color bgColor = getLineLabelBGColor(series);

            drawLineLabel(g2, dataArea, entities, xx, yy, labelFont, labelColor, showBG, bgColor, waterlevelLabel);
        }

        final int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
        final int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
        updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, rangeAxisIndex, transX1, transY1, orientation);

        // Add an entity for the item, but only if it falls within the data
        // area...
        if (entities != null && isPointInRect(dataArea, xx, yy)) {
            addEntity(entities, entityArea, dataset, series, item, xx, yy);
        }
    }

    public static void drawLineLabel(final Graphics2D g2, final Rectangle2D dataArea, final EntityCollection entities, final double labelX, final double labelY,
            final Font font,
            final Color fgColor,
            final boolean showBG, final Color bgColor, final String label) {

        final Font oldFont = g2.getFont();
        final Color oldColor = g2.getColor();

        g2.setFont(font);
        g2.setColor(fgColor);
        g2.setBackground(Color.black);

        // Try to always display label if the data is visible.
        double posX = labelX;
        double posY = labelY;
        if (!isPointInRect(dataArea, posX, posY)) {
            // Move into the data area.
            posX = Math.max(posX, dataArea.getMinX());
            posX = Math.min(posX, dataArea.getMaxX());
            posY = Math.max(posY, dataArea.getMinY());
            posY = Math.min(posY, dataArea.getMaxY());
        }

        // Move to right until no collisions exist anymore
        Shape hotspot = TextUtilities.calculateRotatedStringBounds(label, g2, (float) posX, (float) posY - 3f, TextAnchor.CENTER_LEFT, 0f,
                TextAnchor.CENTER_LEFT);
        while (JFreeUtil.collides(hotspot, entities, CollisionFreeLineLabelEntity.class)) {
            posX += 5f;
            hotspot = TextUtilities.calculateRotatedStringBounds(label, g2, (float) posX, (float) posY - 3f, TextAnchor.CENTER_LEFT, 0f,
                    TextAnchor.CENTER_LEFT);
        }

        // Register to avoid collissions.
        entities.add(new CollisionFreeLineLabelEntity(hotspot, 1, "", ""));

        // Fill background.
        if (showBG)
            drawTextBox(g2, label, (float) posX, (float) posY - 3f, bgColor);

        g2.drawString(label, (float) posX, (float) posY - 3f);

        g2.setFont(oldFont);
        g2.setColor(oldColor);
    }

    /**
     * Sets whether or not the minimum should be rendered with shape.
     */
    public void setIsMinimumShapeVisisble(final int series, final boolean isVisible) {
        this.isMinimumShapeVisible.setBoolean(series, isVisible);
    }

    /**
     * Whether or not the minimum should be rendered with shape.
     */
    private boolean isMinimumShapeVisible(final int series) {
        if (this.isMinimumShapeVisible.size() <= series) {
            return false;
        }

        return this.isMinimumShapeVisible.getBoolean(series);
    }

    /**
     * Sets whether or not the maximum should be rendered with shape.
     */
    public void setIsMaximumShapeVisible(final int series, final boolean isVisible) {
        this.isMaximumShapeVisible.setBoolean(series, isVisible);
    }

    /**
     * Whether or not the maximum should be rendered with shape.
     */
    private boolean isMaximumShapeVisible(final int series) {
        if (this.isMaximumShapeVisible.size() <= series) {
            return false;
        }

        return this.isMaximumShapeVisible.getBoolean(series);
    }

    /** Whether or not a label should be shown for series. */
    private boolean isShowLineLabel(final int series) {
        if (this.showLineLabel.size() <= series) {
            return false;
        }

        return this.showLineLabel.getBoolean(series);
    }

    /** Sets whether or not a label should be shown for series. */
    public void setShowLineLabel(final boolean showLineLabel, final int series) {
        this.showLineLabel.setBoolean(series, showLineLabel);
    }

    /** Whether or not a label should be shown for series. */
    private boolean isShowLineLabelBG(final int series) {
        if (this.showLineLabelBG.size() <= series) {
            return false;
        }

        return this.showLineLabelBG.getBoolean(series);
    }

    public void setShowLineLabelBG(final int series, final boolean doShow) {
        this.showLineLabelBG.setBoolean(series, doShow);
    }

    private Color getLineLabelBGColor(final int series) {
        if (this.lineLabelBGColors.size() <= series) {
            return null;
        }

        return this.lineLabelBGColors.get(series);
    }

    public void setLineLabelBGColor(final int series, final Color color) {
        this.lineLabelBGColors.put(series, color);
    }

    private Color getLineLabelTextColor(final int series) {
        if (this.lineLabelTextColors.size() <= series) {
            return null;
        }

        return this.lineLabelTextColors.get(series);
    }

    public void setLineLabelTextColor(final int series, final Color color) {
        this.lineLabelTextColors.put(series, color);
    }

    public void setLineLabelFont(final Font font, final int series) {
        this.lineLabelFonts.put(series, font);
    }

    private Font getLineLabelFont(final int series) {
        return this.lineLabelFonts.get(series);
    }

    /**
     * True if the given item of given dataset has the smallest
     * X value within this set.
     */
    private boolean isMinimumX(final XYDataset dataset, final int series, final int item) {
        return dataset.getXValue(series, item) == getMinimumX(dataset, series);
    }

    /**
     * Get Minimum X Value of a given series in a dataset.
     * The value is stored for later use if queried the first time.
     */
    private double getMinimumX(final XYDataset dataset, final int series) {
        final Integer key = Integer.valueOf(series);
        final Double old = this.seriesMinimumX.get(key);

        if (old != null) {
            return old.doubleValue();
        }

        log.debug("Compute minimum of Series: " + series);

        double min = Double.MAX_VALUE;

        for (int i = 0, n = dataset.getItemCount(series); i < n; i++) {
            final double tmpValue = dataset.getXValue(series, i);

            if (tmpValue < min) {
                min = tmpValue;
            }
        }

        this.seriesMinimumX.put(key, Double.valueOf(min));

        return min;
    }

    /**
     * True if the given item of given dataset has the smallest
     * Y value within this set.
     */
    private boolean isMinimum(final XYDataset dataset, final int series, final int item) {
        return dataset.getYValue(series, item) == getMinimum(dataset, series);
    }

    /**
     * Get Minimum Y Value of a given series in a dataset.
     * The value is stored for later use if queried the first time.
     */
    private double getMinimum(final XYDataset dataset, final int series) {
        final Integer key = Integer.valueOf(series);
        final Double old = this.seriesMinimum.get(key);

        if (old != null) {
            return old.doubleValue();
        }

        log.debug("Compute minimum of Series: " + series);

        double min = Double.MAX_VALUE;

        for (int i = 0, n = dataset.getItemCount(series); i < n; i++) {
            final double tmpValue = dataset.getYValue(series, i);

            if (tmpValue < min) {
                min = tmpValue;
            }
        }

        this.seriesMinimum.put(key, Double.valueOf(min));

        return min;
    }

    /**
     * True if the given item of given dataset has the biggest
     * Y value within this set.
     */
    private boolean isMaximum(final XYDataset dataset, final int series, final int item) {
        return dataset.getYValue(series, item) == getMaximum(dataset, series);
    }

    /**
     * Get maximum Y Value of a given series in a dataset.
     * The value is stored for later use if queried the first time.
     */
    private double getMaximum(final XYDataset dataset, final int series) {
        final Integer key = Integer.valueOf(series);
        final Double old = this.seriesMaximum.get(key);

        if (old != null) {
            return old.doubleValue();
        }

        log.debug("Compute maximum of Series: " + series);

        double max = -Double.MAX_VALUE;

        for (int i = 0, n = dataset.getItemCount(series); i < n; i++) {
            final double tmpValue = dataset.getYValue(series, i);

            if (tmpValue > max) {
                max = tmpValue;
            }
        }

        this.seriesMaximum.put(key, Double.valueOf(max));

        return max;
    }
}

http://dive4elements.wald.intevation.org