ingo@2074: package de.intevation.flys.jfree; ingo@2074: ingo@2591: import java.awt.Color; ingo@2074: import java.awt.Graphics2D; felix@2656: import java.awt.Font; ingo@2591: import java.awt.Paint; ingo@2074: import java.awt.Shape; ingo@2074: import java.awt.geom.Rectangle2D; ingo@2074: import java.util.HashMap; ingo@2074: import java.util.Map; ingo@2074: ingo@2074: import org.apache.log4j.Logger; ingo@2074: ingo@2074: import org.jfree.chart.axis.ValueAxis; ingo@2074: import org.jfree.chart.entity.EntityCollection; ingo@2074: import org.jfree.chart.plot.CrosshairState; ingo@2074: import org.jfree.chart.plot.PlotOrientation; ingo@2074: import org.jfree.chart.plot.XYPlot; ingo@2074: import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; ingo@2074: import org.jfree.data.xy.XYDataset; felix@2653: import org.jfree.data.xy.XYSeries; felix@2649: import org.jfree.data.xy.XYSeriesCollection; ingo@2074: import org.jfree.ui.RectangleEdge; felix@3023: import org.jfree.ui.TextAnchor; ingo@2074: import org.jfree.util.BooleanList; ingo@2074: import org.jfree.util.ShapeUtilities; felix@3023: import org.jfree.text.TextUtilities; ingo@2074: felix@2653: import de.intevation.flys.jfree.HasLabel; felix@3023: import de.intevation.flys.jfree.JFreeUtil; felix@2653: felix@2642: /** felix@2647: * Renderer with additional the additional functionality of renderering minima felix@2647: * and/or maxima of dataseries contained in datasets. felix@2642: */ ingo@2074: public class EnhancedLineAndShapeRenderer extends XYLineAndShapeRenderer { ingo@2074: felix@2647: /** Own logger. */ ingo@2074: private static final Logger logger = ingo@2074: Logger.getLogger(EnhancedLineAndShapeRenderer.class); ingo@2074: ingo@2074: protected BooleanList isMinimumShapeVisible; ingo@2074: protected BooleanList isMaximumShapeVisible; felix@2648: protected BooleanList showLineLabel; ingo@2074: ingo@2074: protected Map seriesMinimum; felix@2644: protected Map seriesMinimumX; ingo@2074: protected Map seriesMaximum; ingo@2074: felix@2656: protected Map lineLabelFonts; felix@2660: protected Map lineLabelTextColors; felix@2660: protected BooleanList showLineLabelBG; felix@2660: protected Map lineLabelBGColors; felix@2656: ingo@2074: ingo@2074: public EnhancedLineAndShapeRenderer(boolean lines, boolean shapes) { ingo@2074: super(lines, shapes); ingo@2074: this.isMinimumShapeVisible = new BooleanList(); ingo@2074: this.isMaximumShapeVisible = new BooleanList(); felix@2648: this.showLineLabel = new BooleanList(); felix@2660: this.showLineLabelBG = new BooleanList(); ingo@2074: this.seriesMinimum = new HashMap(); ingo@2074: this.seriesMaximum = new HashMap(); felix@2644: this.seriesMinimumX = new HashMap(); felix@2656: this.lineLabelFonts = new HashMap(); felix@2660: this.lineLabelTextColors = new HashMap(); felix@2660: this.lineLabelBGColors = new HashMap(); felix@2660: } felix@2660: felix@2660: felix@3015: /** felix@3015: * Draw a background-box of a text to render. felix@3015: * @param g2 graphics device to use felix@3015: * @param text text to draw felix@3015: * @param textX x-position for text felix@3015: * @param textY y-position for text felix@3015: * @param bgColor color to fill box with. felix@3015: */ felix@3015: public static void drawTextBox(Graphics2D g2, felix@3015: String text, float textX, float textY, Color bgColor felix@3015: ) { felix@2660: Rectangle2D hotspotBox = g2.getFontMetrics().getStringBounds(text, g2); felix@2660: float w = (float) hotspotBox.getWidth(), h = (float) hotspotBox.getHeight(); felix@2660: hotspotBox.setRect(textX, textY-h, w, h); felix@2660: Color oldColor = g2.getColor(); felix@2660: g2.setColor(bgColor); felix@2660: g2.fill(hotspotBox); felix@2660: g2.setColor(oldColor); ingo@2074: } ingo@2074: ingo@2074: felix@3023: /** felix@3023: * Whether or not a specific item in a series (maybe the maxima) should felix@3023: * be rendered with shape. felix@3023: */ ingo@2074: public boolean getItemShapeVisible(XYDataset dataset, int series, int item){ ingo@2074: if (super.getItemShapeVisible(series, item)) { ingo@2074: return true; ingo@2074: } ingo@2074: ingo@2074: if (isMinimumShapeVisible(series) && isMinimum(dataset, series, item)) { ingo@2074: return true; ingo@2074: } ingo@2074: ingo@2074: if (isMaximumShapeVisible(series) && isMaximum(dataset, series, item)) { ingo@2074: return true; ingo@2074: } ingo@2074: ingo@2074: return false; ingo@2074: } ingo@2074: ingo@2074: felix@3023: /** felix@3023: * Rectangle used to draw maximums shape. felix@3023: */ ingo@2591: public Shape getMaximumShape(int series, int column) { ingo@2591: return new Rectangle2D.Double(-5d, -5d, 10d, 10d); ingo@2591: } ingo@2591: ingo@2591: felix@3023: /** felix@3023: * Rectangle used to draw minimums shape. felix@3023: */ ingo@2591: public Shape getMinimumShape(int series, int column) { ingo@2591: return new Rectangle2D.Double(-5d, -5d, 10d, 10d); ingo@2591: } ingo@2591: ingo@2591: ingo@2591: public Paint getMaximumFillPaint(int series, int column) { ingo@2591: Paint p = getItemPaint(series, column); ingo@2591: ingo@2591: if (p instanceof Color) { ingo@2591: Color c = (Color) p; ingo@2591: Color b = c; ingo@2591: ingo@2591: for (int i = 0; i < 2; i++) { ingo@2591: b = b.darker(); ingo@2591: } ingo@2591: ingo@2591: return b; ingo@2591: } ingo@2591: ingo@2591: logger.warn("Item paint is no instance of Color!"); ingo@2591: return p; ingo@2591: } ingo@2591: ingo@2591: ingo@2591: public Paint getMinimumFillPaint(int series, int column) { ingo@2591: Paint p = getItemPaint(series, column); ingo@2591: ingo@2591: if (p instanceof Color) { ingo@2591: Color c = (Color) p; ingo@2591: Color b = c; ingo@2591: ingo@2591: for (int i = 0; i < 2; i++) { ingo@2591: b = b.darker(); ingo@2591: } ingo@2591: ingo@2591: return b; ingo@2591: } ingo@2591: ingo@2591: logger.warn("Item paint is no instance of Color!"); ingo@2591: return p; ingo@2591: } ingo@2591: ingo@2591: ingo@2074: /** ingo@2074: * Overrides XYLineAndShapeRenderer.drawSecondaryPass() to call an adapted ingo@2074: * method getItemShapeVisible() which now takes an XYDataset. So, 99% of ingo@2074: * code equal the code in XYLineAndShapeRenderer. ingo@2074: */ ingo@2074: @Override ingo@2074: protected void drawSecondaryPass( ingo@2074: Graphics2D g2, ingo@2074: XYPlot plot, ingo@2074: XYDataset dataset, ingo@2074: int pass, ingo@2074: int series, ingo@2074: int item, ingo@2074: ValueAxis domainAxis, ingo@2074: Rectangle2D dataArea, ingo@2074: ValueAxis rangeAxis, ingo@2074: CrosshairState crosshairState, ingo@2074: EntityCollection entities ingo@2074: ) { ingo@2074: Shape entityArea = null; ingo@2074: ingo@2074: // get the data point... ingo@2074: double x1 = dataset.getXValue(series, item); ingo@2074: double y1 = dataset.getYValue(series, item); ingo@2074: if (Double.isNaN(y1) || Double.isNaN(x1)) { ingo@2074: return; ingo@2074: } ingo@2074: ingo@2074: PlotOrientation orientation = plot.getOrientation(); ingo@2074: RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); ingo@2074: RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); ingo@2074: double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); ingo@2074: double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); ingo@2074: ingo@2074: if (getItemShapeVisible(dataset, series, item)) { ingo@2591: Shape shape = null; ingo@2591: felix@2644: // OPTIMIZE: instead of calculating minimum and maximum for every felix@2644: // point, calculate it just once (assume that dataset felix@2644: // content does not change during rendering). felix@2644: // NOTE: Above OPTIMIZE might already be fulfilled to most extend. ingo@2591: boolean isMinimum = isMinimumShapeVisible(series) ingo@2591: && isMinimum(dataset, series, item); ingo@2591: ingo@2591: boolean isMaximum = isMaximumShapeVisible(series) ingo@2591: && isMaximum(dataset, series, item); ingo@2591: ingo@2591: if (isMinimum) { ingo@2591: logger.debug("Create a Minimum shape."); ingo@2591: shape = getMinimumShape(series, item); ingo@2591: } ingo@2591: else if (isMaximum) { ingo@2591: logger.debug("Create a Maximum shape."); ingo@2591: shape = getMaximumShape(series, item); ingo@2591: } ingo@2591: else { ingo@2591: shape = getItemShape(series, item); ingo@2591: } ingo@2591: ingo@2074: if (orientation == PlotOrientation.HORIZONTAL) { ingo@2074: shape = ShapeUtilities.createTranslatedShape(shape, transY1, ingo@2074: transX1); ingo@2074: } ingo@2074: else if (orientation == PlotOrientation.VERTICAL) { ingo@2074: shape = ShapeUtilities.createTranslatedShape(shape, transX1, ingo@2074: transY1); ingo@2074: } ingo@2074: entityArea = shape; ingo@2074: if (shape.intersects(dataArea)) { ingo@2074: if (getItemShapeFilled(series, item)) { ingo@2074: if (getUseFillPaint()) { ingo@2074: g2.setPaint(getItemFillPaint(series, item)); ingo@2074: } ingo@2074: else { ingo@2074: g2.setPaint(getItemPaint(series, item)); ingo@2074: } ingo@2074: g2.fill(shape); ingo@2074: } ingo@2074: if (getDrawOutlines()) { ingo@2074: if (getUseOutlinePaint()) { ingo@2074: g2.setPaint(getItemOutlinePaint(series, item)); ingo@2074: } ingo@2074: else { ingo@2074: g2.setPaint(getItemPaint(series, item)); ingo@2074: } ingo@2074: g2.setStroke(getItemOutlineStroke(series, item)); ingo@2074: g2.draw(shape); ingo@2074: } ingo@2591: ingo@2591: if (isMinimum) { ingo@2591: g2.setPaint(getMinimumFillPaint(series, item)); ingo@2591: g2.fill(shape); ingo@2591: g2.setPaint(getItemOutlinePaint(series, item)); ingo@2591: g2.setStroke(getItemOutlineStroke(series, item)); ingo@2591: g2.draw(shape); ingo@2591: } ingo@2591: else if (isMaximum) { ingo@2591: g2.setPaint(getMaximumFillPaint(series, item)); ingo@2591: g2.fill(shape); ingo@2591: g2.setPaint(getItemOutlinePaint(series, item)); ingo@2591: g2.setStroke(getItemOutlineStroke(series, item)); ingo@2591: g2.draw(shape); ingo@2591: } ingo@2074: } felix@2644: } // if (getItemShapeVisible(dataset, series, item)) ingo@2074: ingo@2074: double xx = transX1; ingo@2074: double yy = transY1; ingo@2074: if (orientation == PlotOrientation.HORIZONTAL) { ingo@2074: xx = transY1; ingo@2074: yy = transX1; ingo@2074: } ingo@2074: ingo@2074: // draw the item label if there is one... ingo@2074: if (isItemLabelVisible(series, item)) { ingo@2074: drawItemLabel(g2, orientation, dataset, series, item, xx, yy, ingo@2074: (y1 < 0.0)); ingo@2074: } ingo@2074: felix@2649: // Draw label of line. felix@2648: if (isShowLineLabel(series) && isMinimumX (dataset, series, item)) { felix@2653: XYSeries xYSeries = ((XYSeriesCollection) dataset).getSeries(series); felix@2653: String waterlevelLabel = (xYSeries instanceof HasLabel) felix@2653: ? ((HasLabel)xYSeries).getLabel() felix@2653: : xYSeries.getKey().toString(); felix@2644: // TODO Force water of some German rivers to flow direction mountains. felix@3023: felix@2659: Font oldFont = g2.getFont(); felix@2660: felix@2660: Color oldColor = g2.getColor(); felix@2659: g2.setFont(this.getLineLabelFont(series)); felix@2660: g2.setColor(this.getLineLabelTextColor(series)); felix@2660: g2.setBackground(Color.black); felix@2660: felix@3023: // Move to right until no collisions exist anymore felix@3023: Shape hotspot = TextUtilities.calculateRotatedStringBounds( felix@3023: waterlevelLabel, g2, (float)xx, (float)yy-3f, TextAnchor.CENTER_LEFT, felix@3023: 0f, TextAnchor.CENTER_LEFT); felix@3023: while (JFreeUtil.collides(hotspot, entities, CollisionFreeLineLabelEntity.class)) { felix@3023: xx += 5f; felix@3023: hotspot = TextUtilities.calculateRotatedStringBounds( felix@3023: waterlevelLabel, g2, (float)xx, (float)yy-3f, TextAnchor.CENTER_LEFT, felix@3023: 0f, TextAnchor.CENTER_LEFT); felix@3023: } felix@3023: felix@3023: // Register to avoid collissions. felix@3023: entities.add(new CollisionFreeLineLabelEntity(hotspot, felix@3023: 1, "", "")); felix@3023: felix@2662: // Fill background. felix@2662: if (isShowLineLabelBG(series)) { felix@2662: drawTextBox(g2, waterlevelLabel, (float)xx, (float)yy-3f, felix@2662: getLineLabelBGColor(series)); felix@2662: } felix@2660: felix@2649: g2.drawString(waterlevelLabel, (float)xx, (float)yy-3f); felix@2660: felix@2659: g2.setFont(oldFont); felix@2660: g2.setColor(oldColor); felix@2644: } felix@2644: ingo@2074: int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); ingo@2074: int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); ingo@2074: updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, ingo@2074: rangeAxisIndex, transX1, transY1, orientation); ingo@2074: ingo@2074: // add an entity for the item, but only if it falls within the data ingo@2074: // area... ingo@2074: if (entities != null && isPointInRect(dataArea, xx, yy)) { ingo@2074: addEntity(entities, entityArea, dataset, series, item, xx, yy); ingo@2074: } ingo@2074: } ingo@2074: ingo@2074: felix@3023: /** felix@3023: * Sets whether or not the minimum should be rendered with shape. felix@3023: */ ingo@2074: public void setIsMinimumShapeVisisble(int series, boolean isVisible) { ingo@2074: this.isMinimumShapeVisible.setBoolean(series, isVisible); ingo@2074: } ingo@2074: ingo@2074: ingo@2074: public boolean isMinimumShapeVisible(int series) { ingo@2241: if (this.isMinimumShapeVisible.size() <= series) { ingo@2241: return false; ingo@2241: } ingo@2241: ingo@2074: return isMinimumShapeVisible.getBoolean(series); ingo@2074: } ingo@2074: ingo@2074: ingo@2074: public void setIsMaximumShapeVisible(int series, boolean isVisible) { ingo@2074: this.isMaximumShapeVisible.setBoolean(series, isVisible); ingo@2074: } ingo@2074: ingo@2074: ingo@2074: public boolean isMaximumShapeVisible(int series) { ingo@2241: if (this.isMaximumShapeVisible.size() <= series) { ingo@2241: return false; ingo@2241: } ingo@2241: ingo@2074: return isMaximumShapeVisible.getBoolean(series); ingo@2074: } ingo@2074: felix@2648: /** Whether or not a label should be shown for series. */ felix@2648: public boolean isShowLineLabel(int series) { felix@2648: if (this.showLineLabel.size() <= series) { felix@2648: return false; felix@2648: } felix@2648: felix@2648: return showLineLabel.getBoolean(series); felix@2648: } felix@2648: felix@2648: felix@2648: /** Sets whether or not a label should be shown for series. */ felix@2648: public void setShowLineLabel(boolean showLineLabel, int series) { felix@2648: this.showLineLabel.setBoolean(series, showLineLabel); felix@2648: } felix@2648: felix@2660: /** Whether or not a label should be shown for series. */ felix@2660: public boolean isShowLineLabelBG(int series) { felix@2660: if (this.showLineLabelBG.size() <= series) { felix@2660: return false; felix@2660: } felix@2660: felix@2660: return showLineLabelBG.getBoolean(series); felix@2660: } felix@2660: felix@2660: public void setShowLineLabelBG(int series, boolean doShow) { felix@2660: this.showLineLabelBG.setBoolean(series, doShow); felix@2660: } felix@2660: felix@2660: public Color getLineLabelBGColor(int series) { felix@2660: if (this.lineLabelBGColors.size() <= series) { felix@2660: return null; felix@2660: } felix@2660: felix@2660: return this.lineLabelBGColors.get(series); felix@2660: } felix@2660: felix@2660: public void setLineLabelBGColor(int series, Color color) { felix@2660: this.lineLabelBGColors.put(series, color); felix@2660: } felix@2660: felix@2660: public Color getLineLabelTextColor(int series) { felix@2660: if (this.lineLabelTextColors.size() <= series) { felix@2660: return null; felix@2660: } felix@2660: felix@2660: return this.lineLabelTextColors.get(series); felix@2660: } felix@2660: felix@2660: public void setLineLabelTextColor(int series, Color color) { felix@2660: this.lineLabelTextColors.put(series, color); felix@2660: } felix@2660: felix@2656: public void setLineLabelFont(Font font, int series) { felix@2656: this.lineLabelFonts.put(series, font); felix@2656: } felix@2656: felix@2656: public Font getLineLabelFont(int series) { felix@2656: return this.lineLabelFonts.get(series); felix@2656: } felix@2656: ingo@2074: felix@2647: /** felix@2647: * True if the given item of given dataset has the smallest felix@2647: * X value within this set. felix@2647: */ felix@2644: public boolean isMinimumX(XYDataset dataset, int series, int item) { felix@2644: return dataset.getXValue(series, item) == getMinimumX(dataset, series); felix@2644: } felix@2644: felix@2647: felix@2647: /** felix@2647: * Get Minimum X Value of a given series in a dataset. felix@2647: * The value is stored for later use if queried the first time. felix@2647: */ felix@2644: public double getMinimumX(XYDataset dataset, int series) { felix@2644: Integer key = Integer.valueOf(series); felix@2644: Double old = seriesMinimumX.get(key); felix@2644: felix@2644: if (old != null) { felix@2644: return old.doubleValue(); felix@2644: } felix@2644: felix@2644: logger.debug("Compute minimum of Series: " + series); felix@2644: felix@2644: double min = Double.MAX_VALUE; felix@2644: felix@2644: for (int i = 0, n = dataset.getItemCount(series); i < n; i++) { felix@2644: double tmpValue = dataset.getXValue(series, i); felix@2644: felix@2644: if (tmpValue < min) { felix@2644: min = tmpValue; felix@2644: } felix@2644: } felix@2644: felix@2644: seriesMinimumX.put(key, Double.valueOf(min)); felix@2644: felix@2644: return min; felix@2644: } felix@2644: felix@2647: felix@2647: /** felix@2647: * True if the given item of given dataset has the smallest felix@2647: * Y value within this set. felix@2647: */ ingo@2074: public boolean isMinimum(XYDataset dataset, int series, int item) { ingo@2074: return dataset.getYValue(series, item) == getMinimum(dataset, series); ingo@2074: } ingo@2074: ingo@2074: felix@2647: /** felix@2647: * Get Minimum Y Value of a given series in a dataset. felix@2647: * The value is stored for later use if queried the first time. felix@2647: */ ingo@2074: public double getMinimum(XYDataset dataset, int series) { ingo@2074: Integer key = Integer.valueOf(series); ingo@2075: Double old = seriesMinimum.get(key); ingo@2074: ingo@2074: if (old != null) { ingo@2074: return old.doubleValue(); ingo@2074: } ingo@2074: ingo@2074: logger.debug("Compute minimum of Series: " + series); ingo@2074: ingo@2074: double min = Double.MAX_VALUE; ingo@2074: ingo@2074: for (int i = 0, n = dataset.getItemCount(series); i < n; i++) { ingo@2074: double tmpValue = dataset.getYValue(series, i); ingo@2074: ingo@2074: if (tmpValue < min) { ingo@2074: min = tmpValue; ingo@2074: } ingo@2074: } ingo@2074: ingo@2074: seriesMinimum.put(key, Double.valueOf(min)); ingo@2074: ingo@2074: return min; ingo@2074: } ingo@2074: ingo@2074: felix@2647: /** felix@2647: * True if the given item of given dataset has the biggest felix@2647: * Y value within this set. felix@2647: */ ingo@2074: public boolean isMaximum(XYDataset dataset, int series, int item) { ingo@2074: return dataset.getYValue(series, item) == getMaximum(dataset, series); ingo@2074: } ingo@2074: ingo@2074: felix@2647: /** felix@2647: * Get maximum Y Value of a given series in a dataset. felix@2647: * The value is stored for later use if queried the first time. felix@2647: */ ingo@2074: public double getMaximum(XYDataset dataset, int series) { ingo@2074: Integer key = Integer.valueOf(series); ingo@2075: Double old = seriesMaximum.get(key); ingo@2074: ingo@2074: if (old != null) { ingo@2074: return old.doubleValue(); ingo@2074: } ingo@2074: ingo@2074: logger.debug("Compute maximum of Series: " + series); ingo@2074: ingo@2074: double max = -Double.MAX_VALUE; ingo@2074: ingo@2074: for (int i = 0, n = dataset.getItemCount(series); i < n; i++) { ingo@2074: double tmpValue = dataset.getYValue(series, i); ingo@2074: ingo@2074: if (tmpValue > max) { ingo@2074: max = tmpValue; ingo@2074: } ingo@2074: } ingo@2074: ingo@2074: seriesMaximum.put(key, Double.valueOf(max)); ingo@2074: ingo@2074: return max; ingo@2074: } ingo@2074: } ingo@2074: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :