001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2020, 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-2020, 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     * @since 1.0.15
152     */
153    private SortOrder sortOrder;
154
155    /**
156     * Constructs a new (empty) legend for the specified source.
157     *
158     * @param source  the source.
159     */
160    public LegendTitle(LegendItemSource source) {
161        this(source, new FlowArrangement(), new ColumnArrangement());
162    }
163
164    /**
165     * Creates a new legend title with the specified arrangement.
166     *
167     * @param source  the source.
168     * @param hLayout  the horizontal item arrangement ({@code null} not
169     *                 permitted).
170     * @param vLayout  the vertical item arrangement ({@code null} not
171     *                 permitted).
172     */
173    public LegendTitle(LegendItemSource source,
174                       Arrangement hLayout, Arrangement vLayout) {
175        this.sources = new LegendItemSource[] {source};
176        this.items = new BlockContainer(hLayout);
177        this.hLayout = hLayout;
178        this.vLayout = vLayout;
179        this.backgroundPaint = null;
180        this.legendItemGraphicEdge = RectangleEdge.LEFT;
181        this.legendItemGraphicAnchor = RectangleAnchor.CENTER;
182        this.legendItemGraphicLocation = RectangleAnchor.CENTER;
183        this.legendItemGraphicPadding = new RectangleInsets(2.0, 2.0, 2.0, 2.0);
184        this.itemFont = DEFAULT_ITEM_FONT;
185        this.itemPaint = DEFAULT_ITEM_PAINT;
186        this.itemLabelPadding = new RectangleInsets(2.0, 2.0, 2.0, 2.0);
187        this.sortOrder = SortOrder.ASCENDING;
188    }
189
190    /**
191     * Returns the legend item sources.
192     *
193     * @return The sources.
194     */
195    public LegendItemSource[] getSources() {
196        return this.sources;
197    }
198
199    /**
200     * Sets the legend item sources and sends a {@link TitleChangeEvent} to
201     * all registered listeners.
202     *
203     * @param sources  the sources ({@code null} not permitted).
204     */
205    public void setSources(LegendItemSource[] sources) {
206        Args.nullNotPermitted(sources, "sources");
207        this.sources = sources;
208        notifyListeners(new TitleChangeEvent(this));
209    }
210
211    /**
212     * Returns the background paint.
213     *
214     * @return The background paint (possibly {@code null}).
215     */
216    public Paint getBackgroundPaint() {
217        return this.backgroundPaint;
218    }
219
220    /**
221     * Sets the background paint for the legend and sends a
222     * {@link TitleChangeEvent} to all registered listeners.
223     *
224     * @param paint  the paint ({@code null} permitted).
225     */
226    public void setBackgroundPaint(Paint paint) {
227        this.backgroundPaint = paint;
228        notifyListeners(new TitleChangeEvent(this));
229    }
230
231    /**
232     * Returns the location of the shape within each legend item.
233     *
234     * @return The location (never {@code null}).
235     */
236    public RectangleEdge getLegendItemGraphicEdge() {
237        return this.legendItemGraphicEdge;
238    }
239
240    /**
241     * Sets the location of the shape within each legend item.
242     *
243     * @param edge  the edge ({@code null} not permitted).
244     */
245    public void setLegendItemGraphicEdge(RectangleEdge edge) {
246        Args.nullNotPermitted(edge, "edge");
247        this.legendItemGraphicEdge = edge;
248        notifyListeners(new TitleChangeEvent(this));
249    }
250
251    /**
252     * Returns the legend item graphic anchor.
253     *
254     * @return The graphic anchor (never {@code null}).
255     */
256    public RectangleAnchor getLegendItemGraphicAnchor() {
257        return this.legendItemGraphicAnchor;
258    }
259
260    /**
261     * Sets the anchor point used for the graphic in each legend item.
262     *
263     * @param anchor  the anchor point ({@code null} not permitted).
264     */
265    public void setLegendItemGraphicAnchor(RectangleAnchor anchor) {
266        Args.nullNotPermitted(anchor, "anchor");
267        this.legendItemGraphicAnchor = anchor;
268    }
269
270    /**
271     * Returns the legend item graphic location.
272     *
273     * @return The location (never {@code null}).
274     */
275    public RectangleAnchor getLegendItemGraphicLocation() {
276        return this.legendItemGraphicLocation;
277    }
278
279    /**
280     * Sets the legend item graphic location.
281     *
282     * @param anchor  the anchor ({@code null} not permitted).
283     */
284    public void setLegendItemGraphicLocation(RectangleAnchor anchor) {
285        this.legendItemGraphicLocation = anchor;
286    }
287
288    /**
289     * Returns the padding that will be applied to each item graphic.
290     *
291     * @return The padding (never {@code null}).
292     */
293    public RectangleInsets getLegendItemGraphicPadding() {
294        return this.legendItemGraphicPadding;
295    }
296
297    /**
298     * Sets the padding that will be applied to each item graphic in the
299     * legend and sends a {@link TitleChangeEvent} to all registered listeners.
300     *
301     * @param padding  the padding ({@code null} not permitted).
302     */
303    public void setLegendItemGraphicPadding(RectangleInsets padding) {
304        Args.nullNotPermitted(padding, "padding");
305        this.legendItemGraphicPadding = padding;
306        notifyListeners(new TitleChangeEvent(this));
307    }
308
309    /**
310     * Returns the item font.
311     *
312     * @return The font (never {@code null}).
313     */
314    public Font getItemFont() {
315        return this.itemFont;
316    }
317
318    /**
319     * Sets the item font and sends a {@link TitleChangeEvent} to
320     * all registered listeners.
321     *
322     * @param font  the font ({@code null} not permitted).
323     */
324    public void setItemFont(Font font) {
325        Args.nullNotPermitted(font, "font");
326        this.itemFont = font;
327        notifyListeners(new TitleChangeEvent(this));
328    }
329
330    /**
331     * Returns the item paint.
332     *
333     * @return The paint (never {@code null}).
334     */
335    public Paint getItemPaint() {
336        return this.itemPaint;
337    }
338
339    /**
340     * Sets the item paint.
341     *
342     * @param paint  the paint ({@code null} not permitted).
343     */
344    public void setItemPaint(Paint paint) {
345        Args.nullNotPermitted(paint, "paint");
346        this.itemPaint = paint;
347        notifyListeners(new TitleChangeEvent(this));
348    }
349
350    /**
351     * Returns the padding used for the items labels.
352     *
353     * @return The padding (never {@code null}).
354     */
355    public RectangleInsets getItemLabelPadding() {
356        return this.itemLabelPadding;
357    }
358
359    /**
360     * Sets the padding used for the item labels in the legend.
361     *
362     * @param padding  the padding ({@code null} not permitted).
363     */
364    public void setItemLabelPadding(RectangleInsets padding) {
365        Args.nullNotPermitted(padding, "padding");
366        this.itemLabelPadding = padding;
367        notifyListeners(new TitleChangeEvent(this));
368    }
369
370    /**
371     * Gets the order used to display legend items.
372     * 
373     * @return The order (never {@code null}).
374     * @since 1.0.15
375     */
376    public SortOrder getSortOrder() {
377        return this.sortOrder;
378    }
379
380    /**
381     * Sets the order used to display legend items.
382     * 
383     * @param order Specifies ascending or descending order ({@code null}
384     *              not permitted).
385     * @since 1.0.15
386     */
387    public void setSortOrder(SortOrder order) {
388        Args.nullNotPermitted(order, "order");
389        this.sortOrder = order;
390        notifyListeners(new TitleChangeEvent(this));
391    }
392
393    /**
394     * Fetches the latest legend items.
395     */
396    protected void fetchLegendItems() {
397        this.items.clear();
398        RectangleEdge p = getPosition();
399        if (RectangleEdge.isTopOrBottom(p)) {
400            this.items.setArrangement(this.hLayout);
401        }
402        else {
403            this.items.setArrangement(this.vLayout);
404        }
405
406        if (this.sortOrder.equals(SortOrder.ASCENDING)) {
407            for (int s = 0; s < this.sources.length; s++) {
408                LegendItemCollection legendItems =
409                    this.sources[s].getLegendItems();
410                if (legendItems != null) {
411                    for (int i = 0; i < legendItems.getItemCount(); i++) {
412                        addItemBlock(legendItems.get(i));
413                    }
414                }
415            }
416        }
417        else {
418            for (int s = this.sources.length - 1; s >= 0; s--) {
419                LegendItemCollection legendItems =
420                    this.sources[s].getLegendItems();
421                if (legendItems != null) {
422                    for (int i = legendItems.getItemCount()-1; i >= 0; i--) {
423                        addItemBlock(legendItems.get(i));
424                    }
425                }
426            }
427        }
428    }
429
430    private void addItemBlock(LegendItem item) {
431        Block block = createLegendItemBlock(item);
432        this.items.add(block);
433    }
434
435    /**
436     * Creates a legend item block.
437     *
438     * @param item  the legend item.
439     *
440     * @return The block.
441     */
442    protected Block createLegendItemBlock(LegendItem item) {
443        BlockContainer result;
444        LegendGraphic lg = new LegendGraphic(item.getShape(),
445                item.getFillPaint());
446        lg.setFillPaintTransformer(item.getFillPaintTransformer());
447        lg.setShapeFilled(item.isShapeFilled());
448        lg.setLine(item.getLine());
449        lg.setLineStroke(item.getLineStroke());
450        lg.setLinePaint(item.getLinePaint());
451        lg.setLineVisible(item.isLineVisible());
452        lg.setShapeVisible(item.isShapeVisible());
453        lg.setShapeOutlineVisible(item.isShapeOutlineVisible());
454        lg.setOutlinePaint(item.getOutlinePaint());
455        lg.setOutlineStroke(item.getOutlineStroke());
456        lg.setPadding(this.legendItemGraphicPadding);
457
458        LegendItemBlockContainer legendItem = new LegendItemBlockContainer(
459                new BorderArrangement(), item.getDataset(),
460                item.getSeriesKey());
461        lg.setShapeAnchor(getLegendItemGraphicAnchor());
462        lg.setShapeLocation(getLegendItemGraphicLocation());
463        legendItem.add(lg, this.legendItemGraphicEdge);
464        Font textFont = item.getLabelFont();
465        if (textFont == null) {
466            textFont = this.itemFont;
467        }
468        Paint textPaint = item.getLabelPaint();
469        if (textPaint == null) {
470            textPaint = this.itemPaint;
471        }
472        LabelBlock labelBlock = new LabelBlock(item.getLabel(), textFont,
473                textPaint);
474        labelBlock.setPadding(this.itemLabelPadding);
475        legendItem.add(labelBlock);
476        legendItem.setToolTipText(item.getToolTipText());
477        legendItem.setURLText(item.getURLText());
478
479        result = new BlockContainer(new CenterArrangement());
480        result.add(legendItem);
481
482        return result;
483    }
484
485    /**
486     * Returns the container that holds the legend items.
487     *
488     * @return The container for the legend items.
489     */
490    public BlockContainer getItemContainer() {
491        return this.items;
492    }
493
494    /**
495     * Arranges the contents of the block, within the given constraints, and
496     * returns the block size.
497     *
498     * @param g2  the graphics device.
499     * @param constraint  the constraint ({@code null} not permitted).
500     *
501     * @return The block size (in Java2D units, never {@code null}).
502     */
503    @Override
504    public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) {
505        Size2D result = new Size2D();
506        fetchLegendItems();
507        if (this.items.isEmpty()) {
508            return result;
509        }
510        BlockContainer container = this.wrapper;
511        if (container == null) {
512            container = this.items;
513        }
514        RectangleConstraint c = toContentConstraint(constraint);
515        Size2D size = container.arrange(g2, c);
516        result.height = calculateTotalHeight(size.height);
517        result.width = calculateTotalWidth(size.width);
518        return result;
519    }
520
521    /**
522     * Draws the title on a Java 2D graphics device (such as the screen or a
523     * printer).
524     *
525     * @param g2  the graphics device.
526     * @param area  the available area for the title.
527     */
528    @Override
529    public void draw(Graphics2D g2, Rectangle2D area) {
530        draw(g2, area, null);
531    }
532
533    /**
534     * Draws the block within the specified area.
535     *
536     * @param g2  the graphics device.
537     * @param area  the area.
538     * @param params  ignored ({@code null} permitted).
539     *
540     * @return An {@link org.jfree.chart.block.EntityBlockResult} or
541     *         {@code null}.
542     */
543    @Override
544    public Object draw(Graphics2D g2, Rectangle2D area, Object params) {
545        Rectangle2D target = (Rectangle2D) area.clone();
546        Rectangle2D hotspot = (Rectangle2D) area.clone();
547        StandardEntityCollection sec = null;
548        if (params instanceof EntityBlockParams
549                && ((EntityBlockParams) params).getGenerateEntities()) {
550            sec = new StandardEntityCollection();
551            sec.add(new TitleEntity(hotspot, this));
552        }
553        target = trimMargin(target);
554        if (this.backgroundPaint != null) {
555            g2.setPaint(this.backgroundPaint);
556            g2.fill(target);
557        }
558        BlockFrame border = getFrame();
559        border.draw(g2, target);
560        border.getInsets().trim(target);
561        BlockContainer container = this.wrapper;
562        if (container == null) {
563            container = this.items;
564        }
565        target = trimPadding(target);
566        Object val = container.draw(g2, target, params);
567        if (val instanceof BlockResult) {
568            EntityCollection ec = ((BlockResult) val).getEntityCollection();
569            if (ec != null && sec != null) {
570                sec.addAll(ec);
571                ((BlockResult) val).setEntityCollection(sec);
572            }
573        }
574        return val;
575    }
576
577    /**
578     * Returns the wrapper container, if any.
579     *
580     * @return The wrapper container (possibly {@code null}).
581     *
582     * @since 1.0.11
583     */
584    public BlockContainer getWrapper() {
585        return this.wrapper;
586    }
587
588    /**
589     * Sets the wrapper container for the legend.
590     *
591     * @param wrapper  the wrapper container.
592     */
593    public void setWrapper(BlockContainer wrapper) {
594        this.wrapper = wrapper;
595    }
596
597    /**
598     * Tests this title for equality with an arbitrary object.
599     *
600     * @param obj  the object ({@code null} permitted).
601     *
602     * @return A boolean.
603     */
604    @Override
605    public boolean equals(Object obj) {
606        if (obj == this) {
607            return true;
608        }
609        if (!(obj instanceof LegendTitle)) {
610            return false;
611        }
612        if (!super.equals(obj)) {
613            return false;
614        }
615        LegendTitle that = (LegendTitle) obj;
616        if (!PaintUtils.equal(this.backgroundPaint, that.backgroundPaint)) {
617            return false;
618        }
619        if (this.legendItemGraphicEdge != that.legendItemGraphicEdge) {
620            return false;
621        }
622        if (this.legendItemGraphicAnchor != that.legendItemGraphicAnchor) {
623            return false;
624        }
625        if (this.legendItemGraphicLocation != that.legendItemGraphicLocation) {
626            return false;
627        }
628        if (!this.itemFont.equals(that.itemFont)) {
629            return false;
630        }
631        if (!this.itemPaint.equals(that.itemPaint)) {
632            return false;
633        }
634        if (!this.hLayout.equals(that.hLayout)) {
635            return false;
636        }
637        if (!this.vLayout.equals(that.vLayout)) {
638            return false;
639        }
640        if (!this.sortOrder.equals(that.sortOrder)) {
641            return false;
642        }
643        return true;
644    }
645
646    /**
647     * Provides serialization support.
648     *
649     * @param stream  the output stream.
650     *
651     * @throws IOException  if there is an I/O error.
652     */
653    private void writeObject(ObjectOutputStream stream) throws IOException {
654        stream.defaultWriteObject();
655        SerialUtils.writePaint(this.backgroundPaint, stream);
656        SerialUtils.writePaint(this.itemPaint, stream);
657    }
658
659    /**
660     * Provides serialization support.
661     *
662     * @param stream  the input stream.
663     *
664     * @throws IOException  if there is an I/O error.
665     * @throws ClassNotFoundException  if there is a classpath problem.
666     */
667    private void readObject(ObjectInputStream stream)
668        throws IOException, ClassNotFoundException {
669        stream.defaultReadObject();
670        this.backgroundPaint = SerialUtils.readPaint(stream);
671        this.itemPaint = SerialUtils.readPaint(stream);
672    }
673
674}