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
041package org.jfree.chart.title;
042
043import java.awt.BasicStroke;
044import java.awt.Color;
045import java.awt.Graphics2D;
046import java.awt.Paint;
047import java.awt.Stroke;
048import java.awt.geom.Rectangle2D;
049import java.io.IOException;
050import java.io.ObjectInputStream;
051import java.io.ObjectOutputStream;
052
053import org.jfree.chart.axis.AxisLocation;
054import org.jfree.chart.axis.AxisSpace;
055import org.jfree.chart.axis.ValueAxis;
056import org.jfree.chart.block.LengthConstraintType;
057import org.jfree.chart.block.RectangleConstraint;
058import org.jfree.chart.event.TitleChangeEvent;
059import org.jfree.chart.plot.Plot;
060import org.jfree.chart.plot.PlotOrientation;
061import org.jfree.chart.renderer.PaintScale;
062import org.jfree.data.Range;
063import org.jfree.io.SerialUtilities;
064import org.jfree.ui.RectangleEdge;
065import org.jfree.ui.Size2D;
066import org.jfree.util.PaintUtilities;
067import 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 */
075public 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}