001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
006 * 
007 * https://github.com/jfree/orson-charts
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 org.jfree.chart3d.table;
034
035import java.awt.Insets;
036import java.awt.Graphics2D;
037import java.awt.Shape;
038import java.awt.geom.Dimension2D;
039import java.awt.geom.Rectangle2D;
040import java.io.Serializable;
041import java.util.ArrayList;
042import java.util.List;
043import java.util.Map;
044
045import org.jfree.chart3d.graphics2d.Fit2D;
046import org.jfree.chart3d.internal.Args;
047
048/**
049 * A table element that displays a list of sub-elements in a flow layout.
050 * <br><br>
051 * NOTE: This class is serializable, but the serialization format is subject 
052 * to change in future releases and should not be relied upon for persisting 
053 * instances of this class.
054 */
055@SuppressWarnings("serial")
056public class FlowElement extends AbstractTableElement 
057        implements ContainerElement, Serializable {
058
059    /** The sub-elements in this flow. */
060    private List<TableElement> elements;
061    
062    /** The horizontal alignment of each row. */
063    private HAlign horizontalAlignment;
064
065    /** 
066     * The horizontal gap between elements on the same line, specified in 
067     * Java2D units. 
068     */
069    private int hgap;
070    
071    /**
072     * Creates a new instance (equivalent to 
073     * {@code new FlowElement(HAlign.CENTER, 2)}).
074     */
075    public FlowElement() {
076        this(HAlign.CENTER, 2);
077    }
078    
079    /**
080     * Creates a new instance with the specified attributes.
081     * 
082     * @param alignment  the horizontal alignment of the elements within
083     *     each row ({@code null} not permitted).
084     * @param hgap  the gap between elements.
085     * 
086     * @since 1.1
087     */
088    public FlowElement(HAlign alignment, int hgap) {
089        super();
090        Args.nullNotPermitted(alignment, "alignment");
091        this.elements = new ArrayList<>();
092        this.horizontalAlignment = alignment;
093        this.hgap = hgap;
094    }
095    
096    /**
097     * Returns the horizontal gap between elements, specified in Java2D units.
098     * The default value is {@code 2}.
099     * 
100     * @return The horizontal gap. 
101     */
102    public int getHGap() {
103        return this.hgap;
104    }
105    
106    /**
107     * Sets the horizontal gap between elements.
108     * 
109     * @param gap  the gap (in Java2D units). 
110     */
111    public void setHGap(int gap) {
112        this.hgap = gap;
113    }
114    
115    /**
116     * Returns the horizontal alignment of items within rows.  The default
117     * value is {@link HAlign#CENTER}.
118     * 
119     * @return The horizontal alignment (never {@code null}).
120     * 
121     * @since 1.1
122     */
123    public HAlign getHorizontalAlignment() {
124        return this.horizontalAlignment;
125    }
126    
127    /**
128     * Sets the horizontal alignment.
129     * 
130     * @param alignment  the alignment ({@code null} not permitted).
131     * 
132     * @since 1.1
133     */
134    public void setHorizontalAlignment(HAlign alignment) {
135        Args.nullNotPermitted(alignment, "alignment");
136        this.horizontalAlignment = alignment;
137    }
138    
139    /**
140     * Returns a (new) list containing the elements in this flow layout.
141     * 
142     * @return A list containing the elements (possibly empty, but never 
143     *     {@code null}). 
144     */
145    public List<TableElement> getElements() {
146        return new ArrayList<>(this.elements);
147    }
148
149    /**
150     * Adds a sub-element to the list.
151     * 
152     * @param element  the element ({@code null} not permitted).
153     */
154    @Override
155    public void addElement(TableElement element) {
156        Args.nullNotPermitted(element, "element");
157        this.elements.add(element);
158    }
159
160    /**
161     * Receives a visitor.  The implementation ensures that the vistor visits
162     * all the elements belonging to the flow.
163     * 
164     * @param visitor  the visitor ({@code null} not permitted).
165     * 
166     * @since 1.2
167     */
168    @Override
169    public void receive(TableElementVisitor visitor) {
170        for (TableElement element : elements) {
171            element.receive(visitor);
172        }
173    }
174    
175    /**
176     * Returns info for as many elements as we can fit into one row.
177     * 
178     * @param first  the index of the first element.
179     * @param g2  the graphics target.
180     * @param bounds  the bounds.
181     * 
182     * @return A list of elements and dimensions. 
183     */
184    private List<ElementInfo> rowOfElements(int first, 
185            Graphics2D g2, Rectangle2D bounds) {
186        List<ElementInfo> result = new ArrayList<>();
187        int index = first;
188        boolean full = false;
189        double w = getInsets().left + getInsets().right;
190        while (index < this.elements.size() && !full) {
191            TableElement element = this.elements.get(index);
192            Dimension2D dim = element.preferredSize(g2, bounds);
193            if (w + dim.getWidth() <= bounds.getWidth() || index == first) {
194                result.add(new ElementInfo(element, dim));
195                w += dim.getWidth() + this.hgap;
196                index++;
197            } else {
198                full = true;
199            }
200        }
201        return result;
202    }
203    
204    /**
205     * Returns the height of the tallest element in the list.
206     * 
207     * @param elementInfoList  element info list
208     * 
209     * @return The height. 
210     */
211    private double calcRowHeight(List<ElementInfo> elementInfoList) {
212        double result = 0.0;
213        for (ElementInfo elementInfo : elementInfoList) {
214            result = Math.max(result, elementInfo.getDimension().getHeight());
215        }
216        return result;
217    }
218    
219    /**
220     * Calculates the total width of the elements that will form one row.
221     * 
222     * @param elementInfoList  the elements in the column.
223     * @param hgap  the gap between elements.
224     * 
225     * @return The total height. 
226     */
227    private double calcRowWidth(List<ElementInfo> elementInfoList, 
228            double hgap) {
229        double result = 0.0;
230        for (ElementInfo elementInfo : elementInfoList) {
231            result += elementInfo.getDimension().getWidth();
232        }
233        int count = elementInfoList.size();
234        if (count > 1) {
235            result += (count - 1) * hgap;
236        }
237        return result;
238    }
239    
240    /**
241     * Returns the preferred size of the element (including insets).
242     * 
243     * @param g2  the graphics target.
244     * @param bounds  the bounds.
245     * @param constraints  the constraints (ignored for now).
246     * 
247     * @return The preferred size. 
248     */
249    @Override
250    public Dimension2D preferredSize(Graphics2D g2, Rectangle2D bounds, 
251            Map<String, Object> constraints) {
252        Insets insets = getInsets();
253        double width = insets.left + insets.right;
254        double height = insets.top + insets.bottom;
255        double maxRowWidth = 0.0;
256        int elementCount = this.elements.size();
257        int i = 0;
258        while (i < elementCount) {
259            // get one row of elements...
260            List<ElementInfo> elementsInRow = rowOfElements(i, g2, 
261                    bounds);
262            double rowHeight = calcRowHeight(elementsInRow);
263            double rowWidth = calcRowWidth(elementsInRow, this.hgap);
264            maxRowWidth = Math.max(rowWidth, maxRowWidth);
265            height += rowHeight;
266            i = i + elementsInRow.size();
267        }
268        width += maxRowWidth;
269        return new ElementDimension(width, height);        
270    }
271    
272    /**
273     * Calculates the layout of the elements for the given bounds and 
274     * constraints.
275     * 
276     * @param g2  the graphics target ({@code null} not permitted).
277     * @param bounds  the bounds ({@code null} not permitted).
278     * @param constraints  the constraints (not used here).
279     * 
280     * @return A list of positions for the sub-elements. 
281     */
282    @Override
283    public List<Rectangle2D> layoutElements(Graphics2D g2, Rectangle2D bounds, 
284            Map<String, Object> constraints) {
285        int elementCount = this.elements.size();
286        List<Rectangle2D> result = new ArrayList<>(elementCount);
287        int i = 0;
288        double x = bounds.getX() + getInsets().left;
289        double y = bounds.getY() + getInsets().top;
290        while (i < elementCount) {
291            // get one row of elements...
292            List<ElementInfo> elementsInRow = rowOfElements(i, g2, 
293                    bounds);
294            double height = calcRowHeight(elementsInRow);
295            double width = calcRowWidth(elementsInRow, this.hgap);  
296            if (this.horizontalAlignment == HAlign.CENTER) {
297                x = bounds.getCenterX() - (width / 2.0);
298            } else if (this.horizontalAlignment == HAlign.RIGHT) {
299                x = bounds.getMaxX() - getInsets().right - width;
300            }
301            for (ElementInfo elementInfo : elementsInRow) {
302                Dimension2D dim = elementInfo.getDimension();
303                Rectangle2D position = new Rectangle2D.Double(x, y, 
304                        dim.getWidth(), height);
305                result.add(position);
306                x += position.getWidth() + this.hgap;
307            }
308            i = i + elementsInRow.size();
309            x = bounds.getX() + getInsets().left;
310            y += height;
311        }
312        return result;
313
314    }
315    
316    /**
317     * Draws the element within the specified bounds.
318     * 
319     * @param g2  the graphics target ({@code null} not permitted).
320     * @param bounds  the bounds ({@code null} not permitted).
321     */
322    @Override
323    public void draw(Graphics2D g2, Rectangle2D bounds) {
324        draw(g2, bounds, null);
325    }
326    
327    /**
328     * Draws the element within the specified bounds.
329     * 
330     * @param g2  the graphics target ({@code null} not permitted).
331     * @param bounds  the bounds ({@code null} not permitted).
332     * @param onDrawHandler  an object that will receive notification before 
333     *     and after the element is drawn ({@code null} permitted).
334     * 
335     * @since 1.3
336     */
337    @Override
338    public void draw(Graphics2D g2, Rectangle2D bounds, 
339            TableElementOnDraw onDrawHandler) {
340        if (onDrawHandler != null) {
341            onDrawHandler.beforeDraw(this, g2, bounds);
342        }
343        
344        Shape savedClip = g2.getClip();
345        g2.clip(bounds);
346        
347        // find the preferred size of the flow layout
348        Dimension2D prefDim = preferredSize(g2, bounds);
349        
350        // fit a rectangle of this dimension to the bounds according to the 
351        // element anchor
352        Fit2D fitter = Fit2D.getNoScalingFitter(getRefPoint());
353        Rectangle2D dest = fitter.fit(prefDim, bounds);
354        
355        // perform layout within this bounding rectangle
356        List<Rectangle2D> layoutInfo = this.layoutElements(g2, dest, null);
357        
358        // draw the elements
359        for (int i = 0; i < this.elements.size(); i++) {
360            Rectangle2D rect = layoutInfo.get(i);
361            TableElement element = this.elements.get(i);
362            element.draw(g2, rect, onDrawHandler);
363        }
364        
365        g2.setClip(savedClip);
366        if (onDrawHandler != null) {
367            onDrawHandler.afterDraw(this, g2, bounds);
368        }
369    }
370    
371    /**
372     * Tests this element for equality with an arbitrary object.
373     * 
374     * @param obj  the object ({@code null} permitted).
375     * 
376     * @return A boolean. 
377     */
378    @Override
379    public boolean equals(Object obj) {
380        if (obj == this) {
381            return true;
382        }
383        if (!(obj instanceof FlowElement)) {
384            return false;
385        }
386        FlowElement that = (FlowElement) obj;
387        if (this.hgap != that.hgap) {
388            return false;
389        }
390        if (this.horizontalAlignment != that.horizontalAlignment) {
391            return false;
392        }
393        if (!this.elements.equals(that.elements)) {
394            return false;
395        }
396        return super.equals(obj);
397    }
398
399}