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