001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
006 * 
007 * http://www.object-refinery.com/orsoncharts/index.html
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package com.orsoncharts.legend;
034
035import java.awt.Color;
036import java.awt.Font;
037import java.awt.Shape;
038import java.util.List;
039import java.io.Serializable;
040
041import com.orsoncharts.Chart3D;
042import com.orsoncharts.interaction.InteractiveElementType;
043import com.orsoncharts.plot.Plot3D;
044import com.orsoncharts.plot.CategoryPlot3D;
045import com.orsoncharts.plot.PiePlot3D;
046import com.orsoncharts.plot.XYZPlot;
047import com.orsoncharts.style.ChartStyle;
048import com.orsoncharts.table.ContainerElement;
049import com.orsoncharts.table.FlowElement;
050import com.orsoncharts.table.GridElement;
051import com.orsoncharts.table.HAlign;
052import com.orsoncharts.table.ShapeElement;
053import com.orsoncharts.table.TableElement;
054import com.orsoncharts.table.TextElement;
055import com.orsoncharts.table.VAlign;
056import com.orsoncharts.table.VerticalFlowElement;
057import com.orsoncharts.util.Anchor2D;
058import com.orsoncharts.util.ArgChecks;
059import com.orsoncharts.util.ObjectUtils;
060import com.orsoncharts.util.Orientation;
061
062/**
063 * The standard legend builder, which creates a simple legend
064 * with a flow layout and optional header and footer text.
065 * <br><br>
066 * NOTE: This class is serializable, but the serialization format is subject 
067 * to change in future releases and should not be relied upon for persisting 
068 * instances of this class.
069 */
070@SuppressWarnings("serial")
071public final class StandardLegendBuilder implements LegendBuilder, 
072        Serializable {
073
074    /** An optional header/title for the legend (can be {@code null}). */
075    private String header;
076    
077    /** The header alignment (never {@code null}). */
078    private HAlign headerAlignment;
079    
080    /** An optional footer for the legend (can be {@code null}). */
081    private String footer;
082    
083    /** The footer alignment (never {@code null}). */
084    private HAlign footerAlignment;
085
086    /** 
087     * The row alignment (if {@code null}, the row alignment will be
088     * derived from the anchor point).
089     */
090    private HAlign rowAlignment;
091    
092    /**
093     * The column alignment (if {@code null}, the column alignment will
094     * be derived from the anchor point).
095     */
096    private VAlign columnAlignment;
097    
098    /**
099     * Creates a builder for a simple legend with no header and no footer.
100     */
101    public StandardLegendBuilder() {
102        this(null, null);
103    }
104    
105    /**
106     * Creates a builder for a simple legend with the specified header and/or
107     * footer.
108     * 
109     * @param header  the legend header ({@code null} permitted).
110     * @param footer  the legend footer ({@code null} permitted).
111     */
112    public StandardLegendBuilder(String header, String footer) {
113        this.header = header;
114        this.headerAlignment = HAlign.LEFT;
115        this.footer = footer;
116        this.footerAlignment = HAlign.RIGHT;
117        this.rowAlignment = null;
118        this.columnAlignment = null;
119    }
120    
121    /**
122     * Returns the header text.
123     * 
124     * @return The header text (possibly {@code null}).
125     */
126    public String getHeader() {
127        return this.header;
128    }
129    
130    /**
131     * Sets the header text.
132     * 
133     * @param header  the header ({@code null} permitted). 
134     */
135    public void setHeader(String header) {
136        this.header = header;
137    }
138
139    /**
140     * Returns the header alignment.
141     * 
142     * @return The header alignment (never {@code null}).
143     */
144    public HAlign getHeaderAlignment() {
145        return this.headerAlignment;
146    }
147    
148    /**
149     * Sets the header alignment.
150     * 
151     * @param align  the header alignment ({@code null} not permitted). 
152     */
153    public void setHeaderAlignment(HAlign align) {
154        ArgChecks.nullNotPermitted(align, "align");
155        this.headerAlignment = align;
156    }
157    
158    /**
159     * Returns the footer text.
160     * 
161     * @return The footer text (possibly {@code null}).
162     */
163    public String getFooter() {
164        return this.footer;
165    }
166    
167    /**
168     * Sets the footer text.
169     * 
170     * @param footer  the footer ({@code null} permitted). 
171     */
172    public void setFooter(String footer) {
173        this.footer = footer;
174    }
175    
176    /**
177     * Returns the footer alignment.
178     * 
179     * @return The footer alignment (never {@code null}).
180     */
181    public HAlign getFooterAlignment() {
182        return this.footerAlignment;
183    }
184    
185    /**
186     * Sets the footer alignment.
187     * 
188     * @param align  the footer alignment ({@code null} not permitted). 
189     */
190    public void setFooterAlignment(HAlign align) {
191        ArgChecks.nullNotPermitted(align, "align");
192        this.footerAlignment = align;
193    }
194    
195    /**
196     * Returns the row alignment.  The default value is {@code null} 
197     * which means that the row alignment is derived from the anchor point 
198     * (left aligned for anchors on the left side, center alignment for 
199     * anchors in the middle, and right aligned for anchors on the right side).
200     * 
201     * @return The row alignment (possibly {@code null}). 
202     * 
203     * @since 1.1
204     */
205    public HAlign getRowAlignment() {
206        return this.rowAlignment;
207    }
208    
209    /**
210     * Sets the row alignment (to override the default alignment that is
211     * derived from the legend anchor point).  In most circumstances you 
212     * should be able to rely on the default behaviour, so leave this
213     * attribute set to {@code null}.
214     * 
215     * @param alignment  the row alignment ({@code null} permitted).
216     * 
217     * @since 1.1
218     */
219    public void setRowAlignment(HAlign alignment) {
220        this.rowAlignment = alignment;    
221    }
222    
223    /**
224     * Returns the column alignment.  The default value is {@code null} 
225     * which means that the column alignment is derived from the anchor point 
226     * (top aligned for anchors at the top, center alignment for 
227     * anchors in the middle, and bottom aligned for anchors at the bottom).
228     * 
229     * @return The column alignment (possibly {@code null}). 
230     * 
231     * @since 1.1
232     */
233    public VAlign getColumnAlignment() {
234        return this.columnAlignment;
235    }
236    
237    /**
238     * Sets the column alignment (to override the default alignment that is
239     * derived from the legend anchor point).  In most circumstances you 
240     * should be able to rely on the default behaviour, so leave this
241     * attribute set to {@code null}.
242     * 
243     * @param alignment  the column alignment ({@code null} permitted).
244     * 
245     * @since 1.1
246     */
247    public void setColumnAlignment(VAlign alignment) {
248        this.columnAlignment = alignment;
249    }
250    
251    /**
252     * Creates and returns a legend (instance of {@link TableElement}) that
253     * provides a visual key for the data series in the specified plot.  The
254     * plot can be any of the built-in plot types: {@link PiePlot3D}, 
255     * {@link CategoryPlot3D} or {@link XYZPlot}.  
256     * <br><br>
257     * Certain subelements will have the following properties set so that 
258     * downstream code is able to identify which elements relate to particular
259     * data series: CLASS : 'LegendItem', SERIES_KEY : the series key.
260     * 
261     * @param plot  the plot ({@code null} not permitted).
262     * @param anchor  the anchor ({@code null} not permitted).
263     * @param orientation  the orientation ({@code null} not permitted).
264     * @param style  the chart style ({@code null} not permitted).
265     * 
266     * @return The legend.
267     * 
268     * @since 1.2
269     */
270    @Override
271    public TableElement createLegend(Plot3D plot, Anchor2D anchor, 
272            Orientation orientation, ChartStyle style) {
273        
274        TableElement legend = createSimpleLegend(plot.getLegendInfo(), anchor,
275                orientation, style);
276        if (this.header != null || this.footer != null) {
277            GridElement compositeLegend = new GridElement();
278            compositeLegend.setBackground(null);
279            if (header != null) {
280                TextElement he = new TextElement(this.header, 
281                        style.getLegendHeaderFont());
282                he.setHorizontalAligment(this.headerAlignment);
283                he.setBackgroundColor(style.getLegendHeaderBackgroundColor());
284                compositeLegend.setElement(he, "R0", "C1");                
285            }
286            compositeLegend.setElement(legend, "R1", "C1");
287            if (this.footer != null) {
288                TextElement fe = new TextElement(this.footer, 
289                        style.getLegendFooterFont());
290                fe.setHorizontalAligment(this.footerAlignment);
291                fe.setBackgroundColor(style.getLegendFooterBackgroundColor());
292                compositeLegend.setElement(fe, "R2", "C1");
293            }
294            return compositeLegend;
295        } else {
296            return legend;
297        }
298    }
299    
300    /**
301     * Creates a simple legend based on a flow layout of the individual legend 
302     * items.
303     * 
304     * @param items  the items to be added to the legend ({@code null} 
305     *     not permitted).
306     * @param plot  the plot ({@code null} not permitted).
307     * @param anchor  the anchor point ({@code null} not permitted).
308     * @param orientation  the orientation ({@code null} not permitted).
309     * 
310     * @return The simple legend.
311     */
312    private TableElement createSimpleLegend(List<LegendItemInfo> items,
313            Anchor2D anchor, Orientation orientation, ChartStyle style) {
314        ArgChecks.nullNotPermitted(items, "items");
315        ArgChecks.nullNotPermitted(orientation, "orientation");
316        ContainerElement legend;
317        if (orientation == Orientation.HORIZONTAL) {
318            FlowElement fe = new FlowElement(horizontalAlignment(anchor), 2);
319            fe.setRefPoint(anchor.getRefPt());
320            legend = fe;
321        } else {
322            VerticalFlowElement vfe = new VerticalFlowElement(
323                    verticalAlignment(anchor), 2); 
324            vfe.setRefPoint(anchor.getRefPt());
325            legend = vfe;        
326        }
327        for (LegendItemInfo item : items) {
328            Shape shape = item.getShape();
329            if (shape == null) {
330                shape = style.getLegendItemShape();
331            }
332            TableElement legendItem = createLegendItem(item.getLabel(), 
333                    style.getLegendItemFont(), style.getLegendItemColor(), 
334                    shape, item.getColor(), 
335                    style.getLegendItemBackgroundColor());
336            legendItem.setProperty(TableElement.CLASS, 
337                    InteractiveElementType.LEGEND_ITEM);
338            legendItem.setProperty(Chart3D.SERIES_KEY, item.getSeriesKey());
339            legend.addElement(legendItem);
340        }
341        return legend;
342    }
343    
344    /**
345     * Returns the horizontal alignment that should be used.
346     * 
347     * @param anchor  the anchor ({@code null} not permitted).
348     * 
349     * @return The horizontal alignment. 
350     */
351    private HAlign horizontalAlignment(Anchor2D anchor) {
352        if (this.rowAlignment != null) {
353            return this.rowAlignment;
354        }
355        if (anchor.getRefPt().isLeft()) {
356            return HAlign.LEFT;
357        }
358        if (anchor.getRefPt().isRight()) {
359            return HAlign.RIGHT;
360        }
361        return HAlign.CENTER;
362    }
363        
364    /**
365     * Returns the vertical alignment that should be used.
366     * 
367     * @param anchor  the anchor ({@code null} not permitted).
368     * 
369     * @return The vertical alignment. 
370     */
371    private VAlign verticalAlignment(Anchor2D anchor) {
372        if (this.columnAlignment != null) {
373            return this.columnAlignment;
374        }
375        if (anchor.getRefPt().isTop()) {
376            return VAlign.TOP;
377        }
378        if (anchor.getRefPt().isBottom()) {
379            return VAlign.BOTTOM;
380        }
381        return VAlign.MIDDLE;
382    }
383        
384    /**
385     * Creates a single item in the legend (normally this represents one
386     * data series from the dataset).
387     * 
388     * @param text  the legend item text ({@code null} not permitted).
389     * @param font  the font ({@code null} not permitted).
390     * @param textcolor  the text color ({@code null} not permitted).
391     * @param shape  the shape ({@code null} not permitted).
392     * @param shapeColor  the shape color ({@code null} not permitted).
393     * @param background  the background color ({@code null} not 
394     *     permitted).
395     * 
396     * @return A legend item (never {@code null}). 
397     */
398    private TableElement createLegendItem(String text, Font font, 
399            Color textColor, Shape shape, Color shapeColor, Color background) {
400        // defer argument checks...
401        ShapeElement se = new ShapeElement(shape, shapeColor);
402        se.setBackgroundColor(background);
403        TextElement te = new TextElement(text, font);
404        te.setColor(textColor);
405        te.setBackgroundColor(background);
406        GridElement ge = new GridElement();
407        ge.setElement(se, "R1", "C1");
408        ge.setElement(te, "R1", "C2");
409        return ge;
410    }
411    
412    /**
413     * Tests this legend builder for equality with an arbitrary object.
414     * 
415     * @param obj  the object ({@code null} permitted).
416     * 
417     * @return A boolean. 
418     */
419    @Override
420    public boolean equals(Object obj) {
421        if (obj == this) {
422            return true;
423        }
424        if (!(obj instanceof StandardLegendBuilder)) {
425            return false;
426        }
427        StandardLegendBuilder that = (StandardLegendBuilder) obj;
428        if (!ObjectUtils.equals(this.header, that.header)) {
429            return false;
430        }
431        if (this.headerAlignment != that.headerAlignment) {
432            return false;
433        }     
434        if (!ObjectUtils.equals(this.footer, that.footer)) {
435            return false;
436        }
437        if (this.footerAlignment != that.footerAlignment) {
438            return false;
439        }
440        return true;
441    }
442
443}