    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * ---------------------
    028     * PaintScaleLegend.java
    029     * ---------------------
    030     * (C) Copyright 2007, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   -;
    034     *
    035     * Changes
    036     * -------
    037     * 22-Jan-2007 : Version 1 (DG);
    038     *
    039     */
    040
    041    package org.jfree.chart.title;
    042
    043    import java.awt.BasicStroke;
    044    import java.awt.Color;
    045    import java.awt.Graphics2D;
    046    import java.awt.Paint;
    047    import java.awt.Stroke;
    048    import java.awt.geom.Rectangle2D;
    049    import java.io.IOException;
    050    import java.io.ObjectInputStream;
    051    import java.io.ObjectOutputStream;
    052
    053    import org.jfree.chart.axis.AxisLocation;
    054    import org.jfree.chart.axis.AxisSpace;
    055    import org.jfree.chart.axis.ValueAxis;
    056    import org.jfree.chart.block.LengthConstraintType;
    057    import org.jfree.chart.block.RectangleConstraint;
    058    import org.jfree.chart.event.TitleChangeEvent;
    059    import org.jfree.chart.plot.Plot;
    060    import org.jfree.chart.plot.PlotOrientation;
    061    import org.jfree.chart.renderer.PaintScale;
    062    import org.jfree.data.Range;
    063    import org.jfree.io.SerialUtilities;
    064    import org.jfree.ui.RectangleEdge;
    065    import org.jfree.ui.Size2D;
    066    import org.jfree.util.PaintUtilities;
    067    import org.jfree.util.PublicCloneable;
    068
    069    /**
    070     * A legend that shows a range of values and their associated colors, driven
    071     * by an underlying {@link PaintScale} implementation.
    072     *
    073     * @since 1.0.4
    074     */
    075    public class PaintScaleLegend extends Title implements PublicCloneable {
    076
    077        /** For serialization. */
    078        static final long serialVersionUID = -1365146490993227503L;
    079
    080        /** The paint scale (never <code>null</code>). */
    081        private PaintScale scale;
    082
    083        /** The value axis (never <code>null</code>). */
    084        private ValueAxis axis;
    085
    086        /**
    087         * The axis location (handles both orientations, never
    088         * <code>null</code>).
    089         */
    090        private AxisLocation axisLocation;
    091
    092        /** The offset between the axis and the paint strip (in Java2D units). */
    093        private double axisOffset;
    094
    095        /** The thickness of the paint strip (in Java2D units). */
    096        private double stripWidth;
    097
    098        /**
    099         * A flag that controls whether or not an outline is drawn around the
    100         * paint strip.
    101         */
    102        private boolean stripOutlineVisible;
    103
    104        /** The paint used to draw an outline around the paint strip. */
    105        private transient Paint stripOutlinePaint;
    106
    107        /** The stroke used to draw an outline around the paint strip. */
    108        private transient Stroke stripOutlineStroke;
    109
    110        /** The background paint (never <code>null</code>). */
    111        private transient Paint backgroundPaint;
    112
    113        /**
    114         * Creates a new instance.
    115         *
    116         * @param scale  the scale (<code>null</code> not permitted).
    117         * @param axis  the axis (<code>null</code> not permitted).
    118         */
    119        public PaintScaleLegend(PaintScale scale, ValueAxis axis) {
    120            if (axis == null) {
    121                throw new IllegalArgumentException("Null 'axis' argument.");
    122            }
    123            this.scale = scale;
    124            this.axis = axis;
    125            this.axisLocation = AxisLocation.BOTTOM_OR_LEFT;
    126            this.axisOffset = 0.0;
    127            this.stripWidth = 15.0;
    128            this.stripOutlineVisible = false;
    129            this.stripOutlinePaint = Color.gray;
    130            this.stripOutlineStroke = new BasicStroke(0.5f);
    131            this.backgroundPaint = Color.white;
    132        }
    133
    134        /**
    135         * Returns the scale used to convert values to colors.
    136         *
    137         * @return The scale (never <code>null</code>).
    138         *
    139         * @see #setScale(PaintScale)
    140         */
    141        public PaintScale getScale() {
    142            return this.scale;
    143        }
    144
    145        /**
    146         * Sets the scale and sends a {@link TitleChangeEvent} to all registered
    147         * listeners.
    148         *
    149         * @param scale  the scale (<code>null</code> not permitted).
    150         *
    151         * @see #getScale()
    152         */
    153        public void setScale(PaintScale scale) {
    154            if (scale == null) {
    155                throw new IllegalArgumentException("Null 'scale' argument.");
    156            }
    157            this.scale = scale;
    158            notifyListeners(new TitleChangeEvent(this));
    159        }
    160
    161        /**
    162         * Returns the axis for the paint scale.
    163         *
    164         * @return The axis (never <code>null</code>).
    165         *
    166         * @see #setAxis(ValueAxis)
    167         */
    168        public ValueAxis getAxis() {
    169            return this.axis;
    170        }
    171
    172        /**
    173         * Sets the axis for the paint scale and sends a {@link TitleChangeEvent}
    174         * to all registered listeners.
    175         *
    176         * @param axis  the axis (<code>null</code> not permitted).
    177         *
    178         * @see #getAxis()
    179         */
    180        public void setAxis(ValueAxis axis) {
    181            if (axis == null) {
    182                throw new IllegalArgumentException("Null 'axis' argument.");
    183            }
    184            this.axis = axis;
    185            notifyListeners(new TitleChangeEvent(this));
    186        }
    187
    188        /**
    189         * Returns the axis location.
    190         *
    191         * @return The axis location (never <code>null</code>).
    192         *
    193         * @see #setAxisLocation(AxisLocation)
    194         */
    195        public AxisLocation getAxisLocation() {
    196            return this.axisLocation;
    197        }
    198
    199        /**
    200         * Sets the axis location and sends a {@link TitleChangeEvent} to all
    201         * registered listeners.
    202         *
    203         * @param location  the location (<code>null</code> not permitted).
    204         *
    205         * @see #getAxisLocation()
    206         */
    207        public void setAxisLocation(AxisLocation location) {
    208            if (location == null) {
    209                throw new IllegalArgumentException("Null 'location' argument.");
    210            }
    211            this.axisLocation = location;
    212            notifyListeners(new TitleChangeEvent(this));
    213        }
    214
    215        /**
    216         * Returns the offset between the axis and the paint strip.
    217         *
    218         * @return The offset between the axis and the paint strip.
    219         *
    220         * @see #setAxisOffset(double)
    221         */
    222        public double getAxisOffset() {
    223            return this.axisOffset;
    224        }
    225
    226        /**
    227         * Sets the offset between the axis and the paint strip and sends a
    228         * {@link TitleChangeEvent} to all registered listeners.
    229         *
    230         * @param offset  the offset.
    231         */
    232        public void setAxisOffset(double offset) {
    233            this.axisOffset = offset;
    234            notifyListeners(new TitleChangeEvent(this));
    235        }
    236
    237        /**
    238         * Returns the width of the paint strip, in Java2D units.
    239         *
    240         * @return The width of the paint strip.
    241         *
    242         * @see #setStripWidth(double)
    243         */
    244        public double getStripWidth() {
    245            return this.stripWidth;
    246        }
    247
    248        /**
    249         * Sets the width of the paint strip and sends a {@link TitleChangeEvent}
    250         * to all registered listeners.
    251         *
    252         * @param width  the width.
    253         *
    254         * @see #getStripWidth()
    255         */
    256        public void setStripWidth(double width) {
    257            this.stripWidth = width;
    258            notifyListeners(new TitleChangeEvent(this));
    259        }
    260
    261        /**
    262         * Returns the flag that controls whether or not an outline is drawn
    263         * around the paint strip.
    264         *
    265         * @return A boolean.
    266         *
    267         * @see #setStripOutlineVisible(boolean)
    268         */
    269        public boolean isStripOutlineVisible() {
    270            return this.stripOutlineVisible;
    271        }
    272
    273        /**
    274         * Sets the flag that controls whether or not an outline is drawn around
    275         * the paint strip, and sends a {@link TitleChangeEvent} to all registered
    276         * listeners.
    277         *
    278         * @param visible  the flag.
    279         *
    280         * @see #isStripOutlineVisible()
    281         */
    282        public void setStripOutlineVisible(boolean visible) {
    283            this.stripOutlineVisible = visible;
    284            notifyListeners(new TitleChangeEvent(this));
    285        }
    286
    287        /**
    288         * Returns the paint used to draw the outline of the paint strip.
    289         *
    290         * @return The paint (never <code>null</code>).
    291         *
    292         * @see #setStripOutlinePaint(Paint)
    293         */
    294        public Paint getStripOutlinePaint() {
    295            return this.stripOutlinePaint;
    296        }
    297
    298        /**
    299         * Sets the paint used to draw the outline of the paint strip, and sends
    300         * a {@link TitleChangeEvent} to all registered listeners.
    301         *
    302         * @param paint  the paint (<code>null</code> not permitted).
    303         *
    304         * @see #getStripOutlinePaint()
    305         */
    306        public void setStripOutlinePaint(Paint paint) {
    307            if (paint == null) {
    308                throw new IllegalArgumentException("Null 'paint' argument.");
    309            }
    310            this.stripOutlinePaint = paint;
    311            notifyListeners(new TitleChangeEvent(this));
    312        }
    313
    314        /**
    315         * Returns the stroke used to draw the outline around the paint strip.
    316         *
    317         * @return The stroke (never <code>null</code>).
    318         *
    319         * @see #setStripOutlineStroke(Stroke)
    320         */
    321        public Stroke getStripOutlineStroke() {
    322            return this.stripOutlineStroke;
    323        }
    324
    325        /**
    326         * Sets the stroke used to draw the outline around the paint strip and
    327         * sends a {@link TitleChangeEvent} to all registered listeners.
    328         *
    329         * @param stroke  the stroke (<code>null</code> not permitted).
    330         *
    331         * @see #getStripOutlineStroke()
    332         */
    333        public void setStripOutlineStroke(Stroke stroke) {
    334            if (stroke == null) {
    335                throw new IllegalArgumentException("Null 'stroke' argument.");
    336            }
    337            this.stripOutlineStroke = stroke;
    338            notifyListeners(new TitleChangeEvent(this));
    339        }
    340
    341        /**
    342         * Returns the background paint.
    343         *
    344         * @return The background paint.
    345         */
    346        public Paint getBackgroundPaint() {
    347            return this.backgroundPaint;
    348        }
    349
    350        /**
    351         * Sets the background paint and sends a {@link TitleChangeEvent} to all
    352         * registered listeners.
    353         *
    354         * @param paint  the paint (<code>null</code> permitted).
    355         */
    356        public void setBackgroundPaint(Paint paint) {
    357            this.backgroundPaint = paint;
    358            notifyListeners(new TitleChangeEvent(this));
    359        }
    360
    361        /**
    362         * Arranges the contents of the block, within the given constraints, and
    363         * returns the block size.
    364         *
    365         * @param g2  the graphics device.
    366         * @param constraint  the constraint (<code>null</code> not permitted).
    367         *
    368         * @return The block size (in Java2D units, never <code>null</code>).
    369         */
    370        public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) {
    371            RectangleConstraint cc = toContentConstraint(constraint);
    372            LengthConstraintType w = cc.getWidthConstraintType();
    373            LengthConstraintType h = cc.getHeightConstraintType();
    374            Size2D contentSize = null;
    375            if (w == LengthConstraintType.NONE) {
    376                if (h == LengthConstraintType.NONE) {
    377                    contentSize = new Size2D(getWidth(), getHeight());
    378                }
    379                else if (h == LengthConstraintType.RANGE) {
    380                    throw new RuntimeException("Not yet implemented.");
    381                }
    382                else if (h == LengthConstraintType.FIXED) {
    383                    throw new RuntimeException("Not yet implemented.");
    384                }
    385            }
    386            else if (w == LengthConstraintType.RANGE) {
    387                if (h == LengthConstraintType.NONE) {
    388                    throw new RuntimeException("Not yet implemented.");
    389                }
    390                else if (h == LengthConstraintType.RANGE) {
    391                    contentSize = arrangeRR(g2, cc.getWidthRange(),
    392                            cc.getHeightRange());
    393                }
    394                else if (h == LengthConstraintType.FIXED) {
    395                    throw new RuntimeException("Not yet implemented.");
    396                }
    397            }
    398            else if (w == LengthConstraintType.FIXED) {
    399                if (h == LengthConstraintType.NONE) {
    400                    throw new RuntimeException("Not yet implemented.");
    401                }
    402                else if (h == LengthConstraintType.RANGE) {
    403                    throw new RuntimeException("Not yet implemented.");
    404                }
    405                else if (h == LengthConstraintType.FIXED) {
    406                    throw new RuntimeException("Not yet implemented.");
    407                }
    408            }
    409            return new Size2D(calculateTotalWidth(contentSize.getWidth()),
    410                    calculateTotalHeight(contentSize.getHeight()));
    411        }
    412
    413        /**
    414         * Returns the content size for the title.  This will reflect the fact that
    415         * a text title positioned on the left or right of a chart will be rotated
    416         * 90 degrees.
    417         *
    418         * @param g2  the graphics device.
    419         * @param widthRange  the width range.
    420         * @param heightRange  the height range.
    421         *
    422         * @return The content size.
    423         */
    424        protected Size2D arrangeRR(Graphics2D g2, Range widthRange,
    425                Range heightRange) {
    426
    427            RectangleEdge position = getPosition();
    428            if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) {
    429
    430
    431                float maxWidth = (float) widthRange.getUpperBound();
    432
    433                // determine the space required for the axis
    434                AxisSpace space = this.axis.reserveSpace(g2, null,
    435                        new Rectangle2D.Double(0, 0, maxWidth, 100),
    436                        RectangleEdge.BOTTOM, null);
    437
    438                return new Size2D(maxWidth, this.stripWidth + this.axisOffset
    439                        + space.getTop() + space.getBottom());
    440            }
    441            else if (position == RectangleEdge.LEFT || position
    442                    == RectangleEdge.RIGHT) {
    443                float maxHeight = (float) heightRange.getUpperBound();
    444                AxisSpace space = this.axis.reserveSpace(g2, null,
    445                        new Rectangle2D.Double(0, 0, 100, maxHeight),
    446                        RectangleEdge.RIGHT, null);
    447                return new Size2D(this.stripWidth + this.axisOffset
    448                        + space.getLeft() + space.getRight(), maxHeight);
    449            }
    450            else {
    451                throw new RuntimeException("Unrecognised position.");
    452            }
    453        }
    454
    455        /**
    456         * Draws the legend within the specified area.
    457         *
    458         * @param g2  the graphics target (<code>null</code> not permitted).
    459         * @param area  the drawing area (<code>null</code> not permitted).
    460         */
    461        public void draw(Graphics2D g2, Rectangle2D area) {
    462            draw(g2, area, null);
    463        }
    464
    465        /**
    466         * The number of subdivisions to use when drawing the paint strip.  Maybe
    467         * this need to be user controllable?
    468         */
    469        private static final int SUBDIVISIONS = 200;
    470
    471        /**
    472         * Draws the legend within the specified area.
    473         *
    474         * @param g2  the graphics target (<code>null</code> not permitted).
    475         * @param area  the drawing area (<code>null</code> not permitted).
    476         * @param params  drawing parameters (ignored here).
    477         *
    478         * @return <code>null</code>.
    479         */
    480        public Object draw(Graphics2D g2, Rectangle2D area, Object params) {
    481
    482            Rectangle2D target = (Rectangle2D) area.clone();
    483            target = trimMargin(target);
    484            if (this.backgroundPaint != null) {
    485                g2.setPaint(this.backgroundPaint);
    486                g2.fill(target);
    487            }
    488            getBorder().draw(g2, target);
    489            getBorder().getInsets().trim(target);
    490            target = trimPadding(target);
    491            double base = this.axis.getLowerBound();
    492            double increment = this.axis.getRange().getLength() / SUBDIVISIONS;
    493            Rectangle2D r = new Rectangle2D.Double();
    494
    495
    496            if (RectangleEdge.isTopOrBottom(getPosition())) {
    497                RectangleEdge axisEdge = Plot.resolveRangeAxisLocation(
    498                        this.axisLocation, PlotOrientation.HORIZONTAL);
    499                double ww = Math.ceil(target.getWidth() / SUBDIVISIONS);
    500                if (axisEdge == RectangleEdge.TOP) {
    501                    for (int i = 0; i < SUBDIVISIONS; i++) {
    502                        double v = base + (i * increment);
    503                        Paint p = this.scale.getPaint(v);
    504                        double vv = this.axis.valueToJava2D(v, target,
    505                                RectangleEdge.BOTTOM);
    506                        r.setRect(vv, target.getMaxY() - this.stripWidth, ww,
    507                                this.stripWidth);
    508                        g2.setPaint(p);
    509                        g2.fill(r);
    510                    }
    511                    g2.setPaint(this.stripOutlinePaint);
    512                    g2.setStroke(this.stripOutlineStroke);
    513                    g2.draw(new Rectangle2D.Double(target.getMinX(),
    514                            target.getMaxY() - this.stripWidth, target.getWidth(),
    515                            this.stripWidth));
    516                    this.axis.draw(g2, target.getMaxY() - this.stripWidth
    517                            - this.axisOffset, target, target, RectangleEdge.TOP,
    518                            null);
    519                }
    520                else if (axisEdge == RectangleEdge.BOTTOM) {
    521                    for (int i = 0; i < SUBDIVISIONS; i++) {
    522                        double v = base + (i * increment);
    523                        Paint p = this.scale.getPaint(v);
    524                        double vv = this.axis.valueToJava2D(v, target,
    525                                RectangleEdge.BOTTOM);
    526                        r.setRect(vv, target.getMinY(), ww, this.stripWidth);
    527                        g2.setPaint(p);
    528                        g2.fill(r);
    529                    }
    530                    g2.setPaint(this.stripOutlinePaint);
    531                    g2.setStroke(this.stripOutlineStroke);
    532                    g2.draw(new Rectangle2D.Double(target.getMinX(),
    533                            target.getMinY(), target.getWidth(), this.stripWidth));
    534                    this.axis.draw(g2, target.getMinY() + this.stripWidth
    535                            + this.axisOffset, target, target,
    536                            RectangleEdge.BOTTOM, null);
    537                }
    538            }
    539            else {
    540                RectangleEdge axisEdge = Plot.resolveRangeAxisLocation(
    541                        this.axisLocation, PlotOrientation.VERTICAL);
    542                double hh = Math.ceil(target.getHeight() / SUBDIVISIONS);
    543                if (axisEdge == RectangleEdge.LEFT) {
    544                    for (int i = 0; i < SUBDIVISIONS; i++) {
    545                        double v = base + (i * increment);
    546                        Paint p = this.scale.getPaint(v);
    547                        double vv = this.axis.valueToJava2D(v, target,
    548                                RectangleEdge.LEFT);
    549                        r.setRect(target.getMaxX() - this.stripWidth, vv - hh,
    550                                this.stripWidth, hh);
    551                        g2.setPaint(p);
    552                        g2.fill(r);
    553                    }
    554                    g2.setPaint(this.stripOutlinePaint);
    555                    g2.setStroke(this.stripOutlineStroke);
    556                    g2.draw(new Rectangle2D.Double(target.getMaxX()
    557                            - this.stripWidth, target.getMinY(), this.stripWidth,
    558                            target.getHeight()));
    559                    this.axis.draw(g2, target.getMaxX() - this.stripWidth
    560                            - this.axisOffset, target, target, RectangleEdge.LEFT,
    561                            null);
    562                }
    563                else if (axisEdge == RectangleEdge.RIGHT) {
    564                    for (int i = 0; i < SUBDIVISIONS; i++) {
    565                        double v = base + (i * increment);
    566                        Paint p = this.scale.getPaint(v);
    567                        double vv = this.axis.valueToJava2D(v, target,
    568                                RectangleEdge.LEFT);
    569                        r.setRect(target.getMinX(), vv - hh, this.stripWidth, hh);
    570                        g2.setPaint(p);
    571                        g2.fill(r);
    572                    }
    573                    g2.setPaint(this.stripOutlinePaint);
    574                    g2.setStroke(this.stripOutlineStroke);
    575                    g2.draw(new Rectangle2D.Double(target.getMinX(),
    576                            target.getMinY(), this.stripWidth, target.getHeight()));
    577                    this.axis.draw(g2, target.getMinX() + this.stripWidth
    578                            + this.axisOffset, target, target, RectangleEdge.RIGHT,
    579                            null);
    580                }
    581            }
    582            return null;
    583        }
    584
    585        /**
    586         * Tests this legend for equality with an arbitrary object.
    587         *
    588         * @param obj  the object (<code>null</code> permitted).
    589         *
    590         * @return A boolean.
    591         */
    592        public boolean equals(Object obj) {
    593            if (!(obj instanceof PaintScaleLegend)) {
    594                return false;
    595            }
    596            PaintScaleLegend that = (PaintScaleLegend) obj;
    597            if (!this.scale.equals(that.scale)) {
    598                return false;
    599            }
    600            if (!this.axis.equals(that.axis)) {
    601                return false;
    602            }
    603            if (!this.axisLocation.equals(that.axisLocation)) {
    604                return false;
    605            }
    606            if (this.axisOffset != that.axisOffset) {
    607                return false;
    608            }
    609            if (this.stripWidth != that.stripWidth) {
    610                return false;
    611            }
    612            if (this.stripOutlineVisible != that.stripOutlineVisible) {
    613                return false;
    614            }
    615            if (!PaintUtilities.equal(this.stripOutlinePaint,
    616                    that.stripOutlinePaint)) {
    617                return false;
    618            }
    619            if (!this.stripOutlineStroke.equals(that.stripOutlineStroke)) {
    620                return false;
    621            }
    622            if (!PaintUtilities.equal(this.backgroundPaint, that.backgroundPaint)) {
    623                return false;
    624            }
    625            return super.equals(obj);
    626        }
    627
    628        /**
    629         * Provides serialization support.
    630         *
    631         * @param stream  the output stream.
    632         *
    633         * @throws IOException  if there is an I/O error.
    634         */
    635        private void writeObject(ObjectOutputStream stream) throws IOException {
    636            stream.defaultWriteObject();
    637            SerialUtilities.writePaint(this.backgroundPaint, stream);
    638            SerialUtilities.writePaint(this.stripOutlinePaint, stream);
    639            SerialUtilities.writeStroke(this.stripOutlineStroke, stream);
    640        }
    641
    642        /**
    643         * Provides serialization support.
    644         *
    645         * @param stream  the input stream.
    646         *
    647         * @throws IOException  if there is an I/O error.
    648         * @throws ClassNotFoundException  if there is a classpath problem.
    649         */
    650        private void readObject(ObjectInputStream stream)
    651                throws IOException, ClassNotFoundException {
    652            stream.defaultReadObject();
    653            this.backgroundPaint = SerialUtilities.readPaint(stream);
    654            this.stripOutlinePaint = SerialUtilities.readPaint(stream);
    655            this.stripOutlineStroke = SerialUtilities.readStroke(stream);
    656        }
    657
    658    }