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