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