001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert 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 * LegendTitle.java
029 * ----------------
030 * (C) Copyright 2002-present, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Pierre-Marie Le Biot;
034 *                   Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier);
035 * 
036 */
037
038package org.jfree.chart.title;
039
040import java.awt.Color;
041import java.awt.Font;
042import java.awt.Graphics2D;
043import java.awt.Paint;
044import java.awt.geom.Rectangle2D;
045import java.io.IOException;
046import java.io.ObjectInputStream;
047import java.io.ObjectOutputStream;
048import java.io.Serializable;
049import java.util.Arrays;
050import java.util.Objects;
051import org.jfree.chart.HashUtils;
052
053import org.jfree.chart.LegendItem;
054import org.jfree.chart.LegendItemCollection;
055import org.jfree.chart.LegendItemSource;
056import org.jfree.chart.block.Arrangement;
057import org.jfree.chart.block.Block;
058import org.jfree.chart.block.BlockContainer;
059import org.jfree.chart.block.BlockFrame;
060import org.jfree.chart.block.BlockResult;
061import org.jfree.chart.block.BorderArrangement;
062import org.jfree.chart.block.CenterArrangement;
063import org.jfree.chart.block.ColumnArrangement;
064import org.jfree.chart.block.EntityBlockParams;
065import org.jfree.chart.block.FlowArrangement;
066import org.jfree.chart.block.LabelBlock;
067import org.jfree.chart.block.RectangleConstraint;
068import org.jfree.chart.entity.EntityCollection;
069import org.jfree.chart.entity.StandardEntityCollection;
070import org.jfree.chart.entity.TitleEntity;
071import org.jfree.chart.event.TitleChangeEvent;
072import org.jfree.chart.ui.RectangleAnchor;
073import org.jfree.chart.ui.RectangleEdge;
074import org.jfree.chart.util.PublicCloneable;
075import org.jfree.chart.util.SerialUtils;
076import org.jfree.chart.ui.RectangleInsets;
077import org.jfree.chart.ui.Size2D;
078import org.jfree.chart.util.PaintUtils;
079import org.jfree.chart.util.Args;
080import org.jfree.chart.util.SortOrder;
081
082
083/**
084 * A chart title that displays a legend for the data in the chart.
085 * <P>
086 * The title can be populated with legend items manually, or you can assign a
087 * reference to the plot, in which case the legend items will be automatically
088 * created to match the dataset(s).
089 */
090public class LegendTitle extends Title
091        implements Cloneable, PublicCloneable, Serializable {
092
093    /** For serialization. */
094    private static final long serialVersionUID = 2644010518533854633L;
095
096    /** The default item font. */
097    public static final Font DEFAULT_ITEM_FONT
098            = new Font("SansSerif", Font.PLAIN, 12);
099
100    /** The default item paint. */
101    public static final Paint DEFAULT_ITEM_PAINT = Color.BLACK;
102
103    /** The sources for legend items. */
104    private LegendItemSource[] sources;
105
106    /** The background paint (possibly {@code null}). */
107    private transient Paint backgroundPaint;
108
109    /** The edge for the legend item graphic relative to the text. */
110    private RectangleEdge legendItemGraphicEdge;
111
112    /** The anchor point for the legend item graphic. */
113    private RectangleAnchor legendItemGraphicAnchor;
114
115    /** The legend item graphic location. */
116    private RectangleAnchor legendItemGraphicLocation;
117
118    /** The padding for the legend item graphic. */
119    private RectangleInsets legendItemGraphicPadding;
120
121    /** The item font. */
122    private Font itemFont;
123
124    /** The item paint. */
125    private transient Paint itemPaint;
126
127    /** The padding for the item labels. */
128    private RectangleInsets itemLabelPadding;
129
130    /**
131     * A container that holds and displays the legend items.
132     */
133    private BlockContainer items;
134
135    /**
136     * The layout for the legend when it is positioned at the top or bottom
137     * of the chart.
138     */
139    private Arrangement hLayout;
140
141    /**
142     * The layout for the legend when it is positioned at the left or right
143     * of the chart.
144     */
145    private Arrangement vLayout;
146
147    /**
148     * An optional container for wrapping the legend items (allows for adding
149     * a title or other text to the legend).
150     */
151    private BlockContainer wrapper;
152
153    /**
154     * Whether to render legend items in ascending or descending order.
155     */
156    private SortOrder sortOrder;
157
158    /**
159     * Constructs a new (empty) legend for the specified source.
160     *
161     * @param source  the source.
162     */
163    public LegendTitle(LegendItemSource source) {
164        this(source, new FlowArrangement(), new ColumnArrangement());
165    }
166
167    /**
168     * Creates a new legend title with the specified arrangement.
169     *
170     * @param source  the source.
171     * @param hLayout  the horizontal item arrangement ({@code null} not
172     *                 permitted).
173     * @param vLayout  the vertical item arrangement ({@code null} not
174     *                 permitted).
175     */
176    public LegendTitle(LegendItemSource source,
177                       Arrangement hLayout, Arrangement vLayout) {
178        this.sources = new LegendItemSource[] {source};
179        this.items = new BlockContainer(hLayout);
180        this.hLayout = hLayout;
181        this.vLayout = vLayout;
182        this.backgroundPaint = null;
183        this.legendItemGraphicEdge = RectangleEdge.LEFT;
184        this.legendItemGraphicAnchor = RectangleAnchor.CENTER;
185        this.legendItemGraphicLocation = RectangleAnchor.CENTER;
186        this.legendItemGraphicPadding = new RectangleInsets(2.0, 2.0, 2.0, 2.0);
187        this.itemFont = DEFAULT_ITEM_FONT;
188        this.itemPaint = DEFAULT_ITEM_PAINT;
189        this.itemLabelPadding = new RectangleInsets(2.0, 2.0, 2.0, 2.0);
190        this.sortOrder = SortOrder.ASCENDING;
191    }
192
193    /**
194     * Returns the legend item sources.
195     *
196     * @return The sources.
197     */
198    public LegendItemSource[] getSources() {
199        return this.sources;
200    }
201
202    /**
203     * Sets the legend item sources and sends a {@link TitleChangeEvent} to
204     * all registered listeners.
205     *
206     * @param sources  the sources ({@code null} not permitted).
207     */
208    public void setSources(LegendItemSource[] sources) {
209        Args.nullNotPermitted(sources, "sources");
210        this.sources = sources;
211        notifyListeners(new TitleChangeEvent(this));
212    }
213
214    /**
215     * Returns the background paint.
216     *
217     * @return The background paint (possibly {@code null}).
218     */
219    public Paint getBackgroundPaint() {
220        return this.backgroundPaint;
221    }
222
223    /**
224     * Sets the background paint for the legend and sends a
225     * {@link TitleChangeEvent} to all registered listeners.
226     *
227     * @param paint  the paint ({@code null} permitted).
228     */
229    public void setBackgroundPaint(Paint paint) {
230        this.backgroundPaint = paint;
231        notifyListeners(new TitleChangeEvent(this));
232    }
233
234    /**
235     * Returns the location of the shape within each legend item.
236     *
237     * @return The location (never {@code null}).
238     */
239    public RectangleEdge getLegendItemGraphicEdge() {
240        return this.legendItemGraphicEdge;
241    }
242
243    /**
244     * Sets the location of the shape within each legend item.
245     *
246     * @param edge  the edge ({@code null} not permitted).
247     */
248    public void setLegendItemGraphicEdge(RectangleEdge edge) {
249        Args.nullNotPermitted(edge, "edge");
250        this.legendItemGraphicEdge = edge;
251        notifyListeners(new TitleChangeEvent(this));
252    }
253
254    /**
255     * Returns the legend item graphic anchor.
256     *
257     * @return The graphic anchor (never {@code null}).
258     */
259    public RectangleAnchor getLegendItemGraphicAnchor() {
260        return this.legendItemGraphicAnchor;
261    }
262
263    /**
264     * Sets the anchor point used for the graphic in each legend item.
265     *
266     * @param anchor  the anchor point ({@code null} not permitted).
267     */
268    public void setLegendItemGraphicAnchor(RectangleAnchor anchor) {
269        Args.nullNotPermitted(anchor, "anchor");
270        this.legendItemGraphicAnchor = anchor;
271    }
272
273    /**
274     * Returns the legend item graphic location.
275     *
276     * @return The location (never {@code null}).
277     */
278    public RectangleAnchor getLegendItemGraphicLocation() {
279        return this.legendItemGraphicLocation;
280    }
281
282    /**
283     * Sets the legend item graphic location.
284     *
285     * @param anchor  the anchor ({@code null} not permitted).
286     */
287    public void setLegendItemGraphicLocation(RectangleAnchor anchor) {
288        this.legendItemGraphicLocation = anchor;
289    }
290
291    /**
292     * Returns the padding that will be applied to each item graphic.
293     *
294     * @return The padding (never {@code null}).
295     */
296    public RectangleInsets getLegendItemGraphicPadding() {
297        return this.legendItemGraphicPadding;
298    }
299
300    /**
301     * Sets the padding that will be applied to each item graphic in the
302     * legend and sends a {@link TitleChangeEvent} to all registered listeners.
303     *
304     * @param padding  the padding ({@code null} not permitted).
305     */
306    public void setLegendItemGraphicPadding(RectangleInsets padding) {
307        Args.nullNotPermitted(padding, "padding");
308        this.legendItemGraphicPadding = padding;
309        notifyListeners(new TitleChangeEvent(this));
310    }
311
312    /**
313     * Returns the item font.
314     *
315     * @return The font (never {@code null}).
316     */
317    public Font getItemFont() {
318        return this.itemFont;
319    }
320
321    /**
322     * Sets the item font and sends a {@link TitleChangeEvent} to
323     * all registered listeners.
324     *
325     * @param font  the font ({@code null} not permitted).
326     */
327    public void setItemFont(Font font) {
328        Args.nullNotPermitted(font, "font");
329        this.itemFont = font;
330        notifyListeners(new TitleChangeEvent(this));
331    }
332
333    /**
334     * Returns the item paint.
335     *
336     * @return The paint (never {@code null}).
337     */
338    public Paint getItemPaint() {
339        return this.itemPaint;
340    }
341
342    /**
343     * Sets the item paint.
344     *
345     * @param paint  the paint ({@code null} not permitted).
346     */
347    public void setItemPaint(Paint paint) {
348        Args.nullNotPermitted(paint, "paint");
349        this.itemPaint = paint;
350        notifyListeners(new TitleChangeEvent(this));
351    }
352
353    /**
354     * Returns the padding used for the items labels.
355     *
356     * @return The padding (never {@code null}).
357     */
358    public RectangleInsets getItemLabelPadding() {
359        return this.itemLabelPadding;
360    }
361
362    /**
363     * Sets the padding used for the item labels in the legend.
364     *
365     * @param padding  the padding ({@code null} not permitted).
366     */
367    public void setItemLabelPadding(RectangleInsets padding) {
368        Args.nullNotPermitted(padding, "padding");
369        this.itemLabelPadding = padding;
370        notifyListeners(new TitleChangeEvent(this));
371    }
372
373    /**
374     * Gets the order used to display legend items.
375     * 
376     * @return The order (never {@code null}).
377     */
378    public SortOrder getSortOrder() {
379        return this.sortOrder;
380    }
381
382    /**
383     * Sets the order used to display legend items.
384     * 
385     * @param order Specifies ascending or descending order ({@code null}
386     *              not permitted).
387     */
388    public void setSortOrder(SortOrder order) {
389        Args.nullNotPermitted(order, "order");
390        this.sortOrder = order;
391        notifyListeners(new TitleChangeEvent(this));
392    }
393
394    /**
395     * Fetches the latest legend items.
396     */
397    protected void fetchLegendItems() {
398        this.items.clear();
399        RectangleEdge p = getPosition();
400        if (RectangleEdge.isTopOrBottom(p)) {
401            this.items.setArrangement(this.hLayout);
402        }
403        else {
404            this.items.setArrangement(this.vLayout);
405        }
406
407        if (this.sortOrder.equals(SortOrder.ASCENDING)) {
408            for (int s = 0; s < this.sources.length; s++) {
409                LegendItemCollection legendItems =
410                    this.sources[s].getLegendItems();
411                if (legendItems != null) {
412                    for (int i = 0; i < legendItems.getItemCount(); i++) {
413                        addItemBlock(legendItems.get(i));
414                    }
415                }
416            }
417        }
418        else {
419            for (int s = this.sources.length - 1; s >= 0; s--) {
420                LegendItemCollection legendItems =
421                    this.sources[s].getLegendItems();
422                if (legendItems != null) {
423                    for (int i = legendItems.getItemCount()-1; i >= 0; i--) {
424                        addItemBlock(legendItems.get(i));
425                    }
426                }
427            }
428        }
429    }
430
431    private void addItemBlock(LegendItem item) {
432        Block block = createLegendItemBlock(item);
433        this.items.add(block);
434    }
435
436    /**
437     * Creates a legend item block.
438     *
439     * @param item  the legend item.
440     *
441     * @return The block.
442     */
443    protected Block createLegendItemBlock(LegendItem item) {
444        BlockContainer result;
445        LegendGraphic lg = new LegendGraphic(item.getShape(),
446                item.getFillPaint());
447        lg.setFillPaintTransformer(item.getFillPaintTransformer());
448        lg.setShapeFilled(item.isShapeFilled());
449        lg.setLine(item.getLine());
450        lg.setLineStroke(item.getLineStroke());
451        lg.setLinePaint(item.getLinePaint());
452        lg.setLineVisible(item.isLineVisible());
453        lg.setShapeVisible(item.isShapeVisible());
454        lg.setShapeOutlineVisible(item.isShapeOutlineVisible());
455        lg.setOutlinePaint(item.getOutlinePaint());
456        lg.setOutlineStroke(item.getOutlineStroke());
457        lg.setPadding(this.legendItemGraphicPadding);
458
459        LegendItemBlockContainer legendItem = new LegendItemBlockContainer(
460                new BorderArrangement(), item.getDataset(),
461                item.getSeriesKey());
462        lg.setShapeAnchor(getLegendItemGraphicAnchor());
463        lg.setShapeLocation(getLegendItemGraphicLocation());
464        legendItem.add(lg, this.legendItemGraphicEdge);
465        Font textFont = item.getLabelFont();
466        if (textFont == null) {
467            textFont = this.itemFont;
468        }
469        Paint textPaint = item.getLabelPaint();
470        if (textPaint == null) {
471            textPaint = this.itemPaint;
472        }
473        LabelBlock labelBlock = new LabelBlock(item.getLabel(), textFont,
474                textPaint);
475        labelBlock.setPadding(this.itemLabelPadding);
476        legendItem.add(labelBlock);
477        legendItem.setToolTipText(item.getToolTipText());
478        legendItem.setURLText(item.getURLText());
479
480        result = new BlockContainer(new CenterArrangement());
481        result.add(legendItem);
482
483        return result;
484    }
485
486    /**
487     * Returns the container that holds the legend items.
488     *
489     * @return The container for the legend items.
490     */
491    public BlockContainer getItemContainer() {
492        return this.items;
493    }
494
495    /**
496     * Arranges the contents of the block, within the given constraints, and
497     * returns the block size.
498     *
499     * @param g2  the graphics device.
500     * @param constraint  the constraint ({@code null} not permitted).
501     *
502     * @return The block size (in Java2D units, never {@code null}).
503     */
504    @Override
505    public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) {
506        Size2D result = new Size2D();
507        fetchLegendItems();
508        if (this.items.isEmpty()) {
509            return result;
510        }
511        BlockContainer container = this.wrapper;
512        if (container == null) {
513            container = this.items;
514        }
515        RectangleConstraint c = toContentConstraint(constraint);
516        Size2D size = container.arrange(g2, c);
517        result.height = calculateTotalHeight(size.height);
518        result.width = calculateTotalWidth(size.width);
519        return result;
520    }
521
522    /**
523     * Draws the title on a Java 2D graphics device (such as the screen or a
524     * printer).
525     *
526     * @param g2  the graphics device.
527     * @param area  the available area for the title.
528     */
529    @Override
530    public void draw(Graphics2D g2, Rectangle2D area) {
531        draw(g2, area, null);
532    }
533
534    /**
535     * Draws the block within the specified area.
536     *
537     * @param g2  the graphics device.
538     * @param area  the area.
539     * @param params  ignored ({@code null} permitted).
540     *
541     * @return An {@link org.jfree.chart.block.EntityBlockResult} or
542     *         {@code null}.
543     */
544    @Override
545    public Object draw(Graphics2D g2, Rectangle2D area, Object params) {
546        Rectangle2D target = (Rectangle2D) area.clone();
547        Rectangle2D hotspot = (Rectangle2D) area.clone();
548        StandardEntityCollection sec = null;
549        if (params instanceof EntityBlockParams
550                && ((EntityBlockParams) params).getGenerateEntities()) {
551            sec = new StandardEntityCollection();
552            sec.add(new TitleEntity(hotspot, this));
553        }
554        target = trimMargin(target);
555        if (this.backgroundPaint != null) {
556            g2.setPaint(this.backgroundPaint);
557            g2.fill(target);
558        }
559        BlockFrame border = getFrame();
560        border.draw(g2, target);
561        border.getInsets().trim(target);
562        BlockContainer container = this.wrapper;
563        if (container == null) {
564            container = this.items;
565        }
566        target = trimPadding(target);
567        Object val = container.draw(g2, target, params);
568        if (val instanceof BlockResult) {
569            EntityCollection ec = ((BlockResult) val).getEntityCollection();
570            if (ec != null && sec != null) {
571                sec.addAll(ec);
572                ((BlockResult) val).setEntityCollection(sec);
573            }
574        }
575        return val;
576    }
577
578    /**
579     * Returns the wrapper container, if any.
580     *
581     * @return The wrapper container (possibly {@code null}).
582     */
583    public BlockContainer getWrapper() {
584        return this.wrapper;
585    }
586
587    /**
588     * Sets the wrapper container for the legend.
589     *
590     * @param wrapper  the wrapper container.
591     */
592    public void setWrapper(BlockContainer wrapper) {
593        this.wrapper = wrapper;
594    }
595
596    /**
597     * Tests this title for equality with an arbitrary object.
598     *
599     * @param obj  the object ({@code null} permitted).
600     *
601     * @return A boolean.
602     */
603    @Override
604    public boolean equals(Object obj) {
605        if (obj == this) {
606            return true;
607        }
608        if (!(obj instanceof LegendTitle)) {
609            return false;
610        }
611        LegendTitle that = (LegendTitle) obj;
612        if (!PaintUtils.equal(this.backgroundPaint, that.backgroundPaint)) {
613            return false;
614        }
615        if (!PaintUtils.equal(this.itemPaint, that.itemPaint)) {
616            return false;
617        }
618        if (!Arrays.deepEquals(this.sources, that.sources)) {
619            return false;
620        }
621        if (!Objects.equals(this.legendItemGraphicEdge, that.legendItemGraphicEdge)) {
622            return false;
623        }
624        if (!Objects.equals(this.legendItemGraphicAnchor, that.legendItemGraphicAnchor)) {
625            return false;
626        }
627        if (!Objects.equals(this.legendItemGraphicLocation, that.legendItemGraphicLocation)) {
628            return false;
629        }
630        if (!Objects.equals(this.legendItemGraphicPadding, that.legendItemGraphicPadding)) {
631            return false;
632        }
633        if (!Objects.equals(this.itemFont, that.itemFont)) {
634            return false;
635        }
636        if (!Objects.equals(this.itemLabelPadding, that.itemLabelPadding)) {
637            return false;
638        }
639        if (!Objects.equals(this.items, that.items)) {
640            return false;
641        }
642        if (!Objects.equals(this.hLayout, that.hLayout)) {
643            return false;
644        }
645        if (!Objects.equals(this.vLayout, that.vLayout)) {
646            return false;
647        }
648        if (!Objects.equals(this.wrapper, that.wrapper)) {
649            return false;
650        }
651        if (!Objects.equals(this.sortOrder, that.sortOrder)) {
652            return false;
653        }
654        return super.equals(obj);
655    }
656
657    @Override
658    public int hashCode() {
659        int hash = super.hashCode(); // equals calls superclass, hashCode must also
660        hash = 67 * hash + Arrays.deepHashCode(this.sources);
661        hash = 67 * hash + HashUtils.hashCodeForPaint(this.backgroundPaint);
662        hash = 67 * hash + Objects.hashCode(this.legendItemGraphicEdge);
663        hash = 67 * hash + Objects.hashCode(this.legendItemGraphicAnchor);
664        hash = 67 * hash + Objects.hashCode(this.legendItemGraphicLocation);
665        hash = 67 * hash + Objects.hashCode(this.legendItemGraphicPadding);
666        hash = 67 * hash + Objects.hashCode(this.itemFont);
667        hash = 67 * hash + HashUtils.hashCodeForPaint(this.itemPaint);
668        hash = 67 * hash + Objects.hashCode(this.itemLabelPadding);
669        hash = 67 * hash + Objects.hashCode(this.items);
670        hash = 67 * hash + Objects.hashCode(this.hLayout);
671        hash = 67 * hash + Objects.hashCode(this.vLayout);
672        hash = 67 * hash + Objects.hashCode(this.wrapper);
673        hash = 67 * hash + Objects.hashCode(this.sortOrder);
674        return hash;
675    }
676
677    /**
678     * Ensures symmetry between super/subclass implementations of equals. For
679     * more detail, see http://jqno.nl/equalsverifier/manual/inheritance.
680     *
681     * @param other Object
682     * 
683     * @return true ONLY if the parameter is THIS class type
684     */
685    @Override
686    public boolean canEqual(Object other) {
687        // fix the "equals not symmetric" problem
688        return (other instanceof LegendTitle);
689    }
690
691
692    /**
693     * Provides serialization support.
694     *
695     * @param stream  the output stream.
696     *
697     * @throws IOException  if there is an I/O error.
698     */
699    private void writeObject(ObjectOutputStream stream) throws IOException {
700        stream.defaultWriteObject();
701        SerialUtils.writePaint(this.backgroundPaint, stream);
702        SerialUtils.writePaint(this.itemPaint, stream);
703    }
704
705    /**
706     * Provides serialization support.
707     *
708     * @param stream  the input stream.
709     *
710     * @throws IOException  if there is an I/O error.
711     * @throws ClassNotFoundException  if there is a classpath problem.
712     */
713    private void readObject(ObjectInputStream stream)
714        throws IOException, ClassNotFoundException {
715        stream.defaultReadObject();
716        this.backgroundPaint = SerialUtils.readPaint(stream);
717        this.itemPaint = SerialUtils.readPaint(stream);
718    }
719
720}