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    }