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