/* =========================================================== * JFreeChart : a free chart library for the Java(tm) platform * =========================================================== * * (C) Copyright 2000-2012, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jfreechart/index.html * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * --------------------- * PaintScaleLegend.java * --------------------- * (C) Copyright 2007-2012, by Object Refinery Limited. * * Original Author: David Gilbert (for Object Refinery Limited); * Contributor(s): Peter Kolb - see patch 2686872; * * Changes * ------- * 22-Jan-2007 : Version 1 (DG); * 18-Jun-2008 : Fixed bug drawing scale with log axis (DG); * 16-Apr-2009 : Patch 2686872 implementing AxisChangeListener, and fix for * ignored stripOutlineVisible flag (DG); * 17-Jun-2012 : Removed JCommon dependencies (DG); * */ package org.jfree.chart.title; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Stroke; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import org.jfree.chart.axis.AxisLocation; import org.jfree.chart.axis.AxisSpace; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.block.LengthConstraintType; import org.jfree.chart.block.RectangleConstraint; import org.jfree.chart.ui.RectangleEdge; import org.jfree.chart.ui.Size2D; import org.jfree.chart.util.PaintUtils; import org.jfree.chart.util.PublicCloneable; import org.jfree.chart.event.AxisChangeEvent; import org.jfree.chart.event.AxisChangeListener; import org.jfree.chart.event.TitleChangeEvent; import org.jfree.chart.plot.Plot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.renderer.PaintScale; import org.jfree.chart.util.SerialUtils; import org.jfree.data.Range; /** * A legend that shows a range of values and their associated colors, driven * by an underlying {@link PaintScale} implementation. * * @since 1.0.4 */ public class PaintScaleLegend extends Title implements AxisChangeListener, PublicCloneable { /** For serialization. */ static final long serialVersionUID = -1365146490993227503L; /** The paint scale (never <code>null</code>). */ private PaintScale scale; /** The value axis (never <code>null</code>). */ private ValueAxis axis; /** * The axis location (handles both orientations, never * <code>null</code>). */ private AxisLocation axisLocation; /** The offset between the axis and the paint strip (in Java2D units). */ private double axisOffset; /** The thickness of the paint strip (in Java2D units). */ private double stripWidth; /** * A flag that controls whether or not an outline is drawn around the * paint strip. */ private boolean stripOutlineVisible; /** The paint used to draw an outline around the paint strip. */ private transient Paint stripOutlinePaint; /** The stroke used to draw an outline around the paint strip. */ private transient Stroke stripOutlineStroke; /** The background paint (never <code>null</code>). */ private transient Paint backgroundPaint; /** * The number of subdivisions for the scale when rendering. * * @since 1.0.11 */ private int subdivisions; /** * Creates a new instance. * * @param scale the scale (<code>null</code> not permitted). * @param axis the axis (<code>null</code> not permitted). */ public PaintScaleLegend(PaintScale scale, ValueAxis axis) { if (axis == null) { throw new IllegalArgumentException("Null 'axis' argument."); } this.scale = scale; this.axis = axis; this.axis.addChangeListener(this); this.axisLocation = AxisLocation.BOTTOM_OR_LEFT; this.axisOffset = 0.0; this.axis.setRange(scale.getLowerBound(), scale.getUpperBound()); this.stripWidth = 15.0; this.stripOutlineVisible = true; this.stripOutlinePaint = Color.GRAY; this.stripOutlineStroke = new BasicStroke(0.5f); this.backgroundPaint = Color.WHITE; this.subdivisions = 100; } /** * Returns the scale used to convert values to colors. * * @return The scale (never <code>null</code>). * * @see #setScale(PaintScale) */ public PaintScale getScale() { return this.scale; } /** * Sets the scale and sends a {@link TitleChangeEvent} to all registered * listeners. * * @param scale the scale (<code>null</code> not permitted). * * @see #getScale() */ public void setScale(PaintScale scale) { if (scale == null) { throw new IllegalArgumentException("Null 'scale' argument."); } this.scale = scale; notifyListeners(new TitleChangeEvent(this)); } /** * Returns the axis for the paint scale. * * @return The axis (never <code>null</code>). * * @see #setAxis(ValueAxis) */ public ValueAxis getAxis() { return this.axis; } /** * Sets the axis for the paint scale and sends a {@link TitleChangeEvent} * to all registered listeners. * * @param axis the axis (<code>null</code> not permitted). * * @see #getAxis() */ public void setAxis(ValueAxis axis) { if (axis == null) { throw new IllegalArgumentException("Null 'axis' argument."); } this.axis.removeChangeListener(this); this.axis = axis; this.axis.addChangeListener(this); notifyListeners(new TitleChangeEvent(this)); } /** * Returns the axis location. * * @return The axis location (never <code>null</code>). * * @see #setAxisLocation(AxisLocation) */ public AxisLocation getAxisLocation() { return this.axisLocation; } /** * Sets the axis location and sends a {@link TitleChangeEvent} to all * registered listeners. * * @param location the location (<code>null</code> not permitted). * * @see #getAxisLocation() */ public void setAxisLocation(AxisLocation location) { if (location == null) { throw new IllegalArgumentException("Null 'location' argument."); } this.axisLocation = location; notifyListeners(new TitleChangeEvent(this)); } /** * Returns the offset between the axis and the paint strip. * * @return The offset between the axis and the paint strip. * * @see #setAxisOffset(double) */ public double getAxisOffset() { return this.axisOffset; } /** * Sets the offset between the axis and the paint strip and sends a * {@link TitleChangeEvent} to all registered listeners. * * @param offset the offset. */ public void setAxisOffset(double offset) { this.axisOffset = offset; notifyListeners(new TitleChangeEvent(this)); } /** * Returns the width of the paint strip, in Java2D units. * * @return The width of the paint strip. * * @see #setStripWidth(double) */ public double getStripWidth() { return this.stripWidth; } /** * Sets the width of the paint strip and sends a {@link TitleChangeEvent} * to all registered listeners. * * @param width the width. * * @see #getStripWidth() */ public void setStripWidth(double width) { this.stripWidth = width; notifyListeners(new TitleChangeEvent(this)); } /** * Returns the flag that controls whether or not an outline is drawn * around the paint strip. * * @return A boolean. * * @see #setStripOutlineVisible(boolean) */ public boolean isStripOutlineVisible() { return this.stripOutlineVisible; } /** * Sets the flag that controls whether or not an outline is drawn around * the paint strip, and sends a {@link TitleChangeEvent} to all registered * listeners. * * @param visible the flag. * * @see #isStripOutlineVisible() */ public void setStripOutlineVisible(boolean visible) { this.stripOutlineVisible = visible; notifyListeners(new TitleChangeEvent(this)); } /** * Returns the paint used to draw the outline of the paint strip. * * @return The paint (never <code>null</code>). * * @see #setStripOutlinePaint(Paint) */ public Paint getStripOutlinePaint() { return this.stripOutlinePaint; } /** * Sets the paint used to draw the outline of the paint strip, and sends * a {@link TitleChangeEvent} to all registered listeners. * * @param paint the paint (<code>null</code> not permitted). * * @see #getStripOutlinePaint() */ public void setStripOutlinePaint(Paint paint) { if (paint == null) { throw new IllegalArgumentException("Null 'paint' argument."); } this.stripOutlinePaint = paint; notifyListeners(new TitleChangeEvent(this)); } /** * Returns the stroke used to draw the outline around the paint strip. * * @return The stroke (never <code>null</code>). * * @see #setStripOutlineStroke(Stroke) */ public Stroke getStripOutlineStroke() { return this.stripOutlineStroke; } /** * Sets the stroke used to draw the outline around the paint strip and * sends a {@link TitleChangeEvent} to all registered listeners. * * @param stroke the stroke (<code>null</code> not permitted). * * @see #getStripOutlineStroke() */ public void setStripOutlineStroke(Stroke stroke) { if (stroke == null) { throw new IllegalArgumentException("Null 'stroke' argument."); } this.stripOutlineStroke = stroke; notifyListeners(new TitleChangeEvent(this)); } /** * Returns the background paint. * * @return The background paint. */ public Paint getBackgroundPaint() { return this.backgroundPaint; } /** * Sets the background paint and sends a {@link TitleChangeEvent} to all * registered listeners. * * @param paint the paint (<code>null</code> permitted). */ public void setBackgroundPaint(Paint paint) { this.backgroundPaint = paint; notifyListeners(new TitleChangeEvent(this)); } /** * Returns the number of subdivisions used to draw the scale. * * @return The subdivision count. * * @since 1.0.11 */ public int getSubdivisionCount() { return this.subdivisions; } /** * Sets the subdivision count and sends a {@link TitleChangeEvent} to * all registered listeners. * * @param count the count. * * @since 1.0.11 */ public void setSubdivisionCount(int count) { if (count <= 0) { throw new IllegalArgumentException("Requires 'count' > 0."); } this.subdivisions = count; notifyListeners(new TitleChangeEvent(this)); } /** * Receives notification of an axis change event and responds by firing * a title change event. * * @param event the event. * * @since 1.0.13 */ @Override public void axisChanged(AxisChangeEvent event) { if (this.axis == event.getAxis()) { notifyListeners(new TitleChangeEvent(this)); } } /** * Arranges the contents of the block, within the given constraints, and * returns the block size. * * @param g2 the graphics device. * @param constraint the constraint (<code>null</code> not permitted). * * @return The block size (in Java2D units, never <code>null</code>). */ @Override public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) { RectangleConstraint cc = toContentConstraint(constraint); LengthConstraintType w = cc.getWidthConstraintType(); LengthConstraintType h = cc.getHeightConstraintType(); Size2D contentSize = null; if (w == LengthConstraintType.NONE) { if (h == LengthConstraintType.NONE) { contentSize = new Size2D(getWidth(), getHeight()); } else if (h == LengthConstraintType.RANGE) { throw new RuntimeException("Not yet implemented."); } else if (h == LengthConstraintType.FIXED) { throw new RuntimeException("Not yet implemented."); } } else if (w == LengthConstraintType.RANGE) { if (h == LengthConstraintType.NONE) { throw new RuntimeException("Not yet implemented."); } else if (h == LengthConstraintType.RANGE) { contentSize = arrangeRR(g2, cc.getWidthRange(), cc.getHeightRange()); } else if (h == LengthConstraintType.FIXED) { throw new RuntimeException("Not yet implemented."); } } else if (w == LengthConstraintType.FIXED) { if (h == LengthConstraintType.NONE) { throw new RuntimeException("Not yet implemented."); } else if (h == LengthConstraintType.RANGE) { throw new RuntimeException("Not yet implemented."); } else if (h == LengthConstraintType.FIXED) { throw new RuntimeException("Not yet implemented."); } } return new Size2D(calculateTotalWidth(contentSize.getWidth()), calculateTotalHeight(contentSize.getHeight())); } /** * Returns the content size for the title. This will reflect the fact that * a text title positioned on the left or right of a chart will be rotated * 90 degrees. * * @param g2 the graphics device. * @param widthRange the width range. * @param heightRange the height range. * * @return The content size. */ protected Size2D arrangeRR(Graphics2D g2, Range widthRange, Range heightRange) { RectangleEdge position = getPosition(); if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) { float maxWidth = (float) widthRange.getUpperBound(); // determine the space required for the axis AxisSpace space = this.axis.reserveSpace(g2, null, new Rectangle2D.Double(0, 0, maxWidth, 100), RectangleEdge.BOTTOM, null); return new Size2D(maxWidth, this.stripWidth + this.axisOffset + space.getTop() + space.getBottom()); } else if (position == RectangleEdge.LEFT || position == RectangleEdge.RIGHT) { float maxHeight = (float) heightRange.getUpperBound(); AxisSpace space = this.axis.reserveSpace(g2, null, new Rectangle2D.Double(0, 0, 100, maxHeight), RectangleEdge.RIGHT, null); return new Size2D(this.stripWidth + this.axisOffset + space.getLeft() + space.getRight(), maxHeight); } else { throw new RuntimeException("Unrecognised position."); } } /** * Draws the legend within the specified area. * * @param g2 the graphics target (<code>null</code> not permitted). * @param area the drawing area (<code>null</code> not permitted). */ @Override public void draw(Graphics2D g2, Rectangle2D area) { draw(g2, area, null); } /** * Draws the legend within the specified area. * * @param g2 the graphics target (<code>null</code> not permitted). * @param area the drawing area (<code>null</code> not permitted). * @param params drawing parameters (ignored here). * * @return <code>null</code>. */ @Override public Object draw(Graphics2D g2, Rectangle2D area, Object params) { Rectangle2D target = (Rectangle2D) area.clone(); target = trimMargin(target); if (this.backgroundPaint != null) { g2.setPaint(this.backgroundPaint); g2.fill(target); } getFrame().draw(g2, target); getFrame().getInsets().trim(target); target = trimPadding(target); double base = this.axis.getLowerBound(); double increment = this.axis.getRange().getLength() / this.subdivisions; Rectangle2D r = new Rectangle2D.Double(); if (RectangleEdge.isTopOrBottom(getPosition())) { RectangleEdge axisEdge = Plot.resolveRangeAxisLocation( this.axisLocation, PlotOrientation.HORIZONTAL); if (axisEdge == RectangleEdge.TOP) { for (int i = 0; i < this.subdivisions; i++) { double v = base + (i * increment); Paint p = this.scale.getPaint(v); double vv0 = this.axis.valueToJava2D(v, target, RectangleEdge.TOP); double vv1 = this.axis.valueToJava2D(v + increment, target, RectangleEdge.TOP); double ww = Math.abs(vv1 - vv0) + 1.0; r.setRect(Math.min(vv0, vv1), target.getMaxY() - this.stripWidth, ww, this.stripWidth); g2.setPaint(p); g2.fill(r); } if (isStripOutlineVisible()) { g2.setPaint(this.stripOutlinePaint); g2.setStroke(this.stripOutlineStroke); g2.draw(new Rectangle2D.Double(target.getMinX(), target.getMaxY() - this.stripWidth, target.getWidth(), this.stripWidth)); } this.axis.draw(g2, target.getMaxY() - this.stripWidth - this.axisOffset, target, target, RectangleEdge.TOP, null); } else if (axisEdge == RectangleEdge.BOTTOM) { for (int i = 0; i < this.subdivisions; i++) { double v = base + (i * increment); Paint p = this.scale.getPaint(v); double vv0 = this.axis.valueToJava2D(v, target, RectangleEdge.BOTTOM); double vv1 = this.axis.valueToJava2D(v + increment, target, RectangleEdge.BOTTOM); double ww = Math.abs(vv1 - vv0) + 1.0; r.setRect(Math.min(vv0, vv1), target.getMinY(), ww, this.stripWidth); g2.setPaint(p); g2.fill(r); } if (isStripOutlineVisible()) { g2.setPaint(this.stripOutlinePaint); g2.setStroke(this.stripOutlineStroke); g2.draw(new Rectangle2D.Double(target.getMinX(), target.getMinY(), target.getWidth(), this.stripWidth)); } this.axis.draw(g2, target.getMinY() + this.stripWidth + this.axisOffset, target, target, RectangleEdge.BOTTOM, null); } } else { RectangleEdge axisEdge = Plot.resolveRangeAxisLocation( this.axisLocation, PlotOrientation.VERTICAL); if (axisEdge == RectangleEdge.LEFT) { for (int i = 0; i < this.subdivisions; i++) { double v = base + (i * increment); Paint p = this.scale.getPaint(v); double vv0 = this.axis.valueToJava2D(v, target, RectangleEdge.LEFT); double vv1 = this.axis.valueToJava2D(v + increment, target, RectangleEdge.LEFT); double hh = Math.abs(vv1 - vv0) + 1.0; r.setRect(target.getMaxX() - this.stripWidth, Math.min(vv0, vv1), this.stripWidth, hh); g2.setPaint(p); g2.fill(r); } if (isStripOutlineVisible()) { g2.setPaint(this.stripOutlinePaint); g2.setStroke(this.stripOutlineStroke); g2.draw(new Rectangle2D.Double(target.getMaxX() - this.stripWidth, target.getMinY(), this.stripWidth, target.getHeight())); } this.axis.draw(g2, target.getMaxX() - this.stripWidth - this.axisOffset, target, target, RectangleEdge.LEFT, null); } else if (axisEdge == RectangleEdge.RIGHT) { for (int i = 0; i < this.subdivisions; i++) { double v = base + (i * increment); Paint p = this.scale.getPaint(v); double vv0 = this.axis.valueToJava2D(v, target, RectangleEdge.LEFT); double vv1 = this.axis.valueToJava2D(v + increment, target, RectangleEdge.LEFT); double hh = Math.abs(vv1 - vv0) + 1.0; r.setRect(target.getMinX(), Math.min(vv0, vv1), this.stripWidth, hh); g2.setPaint(p); g2.fill(r); } if (isStripOutlineVisible()) { g2.setPaint(this.stripOutlinePaint); g2.setStroke(this.stripOutlineStroke); g2.draw(new Rectangle2D.Double(target.getMinX(), target.getMinY(), this.stripWidth, target.getHeight())); } this.axis.draw(g2, target.getMinX() + this.stripWidth + this.axisOffset, target, target, RectangleEdge.RIGHT, null); } } return null; } /** * Tests this legend for equality with an arbitrary object. * * @param obj the object (<code>null</code> permitted). * * @return A boolean. */ @Override public boolean equals(Object obj) { if (!(obj instanceof PaintScaleLegend)) { return false; } PaintScaleLegend that = (PaintScaleLegend) obj; if (!this.scale.equals(that.scale)) { return false; } if (!this.axis.equals(that.axis)) { return false; } if (!this.axisLocation.equals(that.axisLocation)) { return false; } if (this.axisOffset != that.axisOffset) { return false; } if (this.stripWidth != that.stripWidth) { return false; } if (this.stripOutlineVisible != that.stripOutlineVisible) { return false; } if (!PaintUtils.equal(this.stripOutlinePaint, that.stripOutlinePaint)) { return false; } if (!this.stripOutlineStroke.equals(that.stripOutlineStroke)) { return false; } if (!PaintUtils.equal(this.backgroundPaint, that.backgroundPaint)) { return false; } if (this.subdivisions != that.subdivisions) { return false; } return super.equals(obj); } /** * Provides serialization support. * * @param stream the output stream. * * @throws IOException if there is an I/O error. */ private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); SerialUtils.writePaint(this.backgroundPaint, stream); SerialUtils.writePaint(this.stripOutlinePaint, stream); SerialUtils.writeStroke(this.stripOutlineStroke, stream); } /** * Provides serialization support. * * @param stream the input stream. * * @throws IOException if there is an I/O error. * @throws ClassNotFoundException if there is a classpath problem. */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); this.backgroundPaint = SerialUtils.readPaint(stream); this.stripOutlinePaint = SerialUtils.readPaint(stream); this.stripOutlineStroke = SerialUtils.readStroke(stream); } }