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.Graphics2D;
036import java.awt.Insets;
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 vertical flow 
050 * layout.
051 * <br><br>
052 * NOTE: This class is serializable, but the serialization format is subject 
053 * to change in future releases and should not be relied upon for persisting 
054 * instances of this class.
055 * 
056 * @since 1.1
057 */
058@SuppressWarnings("serial")
059public class VerticalFlowElement extends AbstractTableElement 
060        implements ContainerElement, Serializable {
061
062    /** The sub-elements in this flow. */
063    private List<TableElement> elements;
064    
065    /** The vertical alignment of the contents of each column. */
066    private VAlign verticalAlignment;
067
068    /** 
069     * The vertical gap between elements in the same column, specified in 
070     * Java2D units. 
071     */
072    private int vgap;
073    
074    /**
075     * Creates a new instance (equivalent to 
076     * {@code new VerticalFlowElement(VAlign.MIDDLE, 2)}).
077     */
078    public VerticalFlowElement() {
079        this(VAlign.MIDDLE, 2);
080    }
081
082    /**
083     * Creates a new instance.
084     * 
085     * @param alignment  the vertical alignment of columns ({@code null} 
086     *         not permitted).
087     * @param vgap  the gap between elements. 
088     */
089    public VerticalFlowElement(VAlign alignment, int vgap) {
090        Args.nullNotPermitted(alignment, null);
091        this.elements = new ArrayList<>();
092        this.verticalAlignment = alignment;
093        this.vgap = vgap;
094    }
095    
096    /**
097     * Returns the vertical alignment for the elements.
098     * 
099     * @return The vertical alignment (never {@code null}). 
100     */
101    public VAlign getVerticalAlignment() {
102        return this.verticalAlignment;
103    }
104    
105    /**
106     * Sets the vertical alignment of elements within columns,
107     * 
108     * @param alignment  the alignment ({@code null} not permitted). 
109     */
110    public void setVerticalAlignment(VAlign alignment) {
111        Args.nullNotPermitted(alignment, "alignment");
112        this.verticalAlignment = alignment;
113    }
114    
115    /**
116     * Returns the vertical gap between elements, in Java2D units.
117     * 
118     * @return The vertical gap. 
119     */
120    public int getVGap() {
121        return this.vgap;
122    }
123    
124    /**
125     * Sets the vertical gap between elements.
126     * 
127     * @param vgap  the gap (in Java2D units). 
128     */
129    public void setVGap(int vgap) {
130        this.vgap = vgap;
131    }
132    
133    /**
134     * Returns a (new) list containing the elements in this flow layout.
135     * 
136     * @return A list containing the elements (possibly empty, but never 
137     *     {@code null}). 
138     */
139    public List<TableElement> getElements() {
140        return new ArrayList<>(this.elements);
141    }
142    
143    /**
144     * Adds a sub-element to the list.
145     * 
146     * @param element  the element ({@code null} not permitted).
147     */
148    @Override
149    public void addElement(TableElement element) {
150        Args.nullNotPermitted(element, "element");
151        this.elements.add(element);
152    }
153
154    /**
155     * Receives a {@link TableElementVisitor} (the visitor will be received
156     * by all the elements in the flow).
157     * 
158     * @param visitor  the visitor ({@code null} not permitted).
159     * 
160     * @since 1.2
161     */
162    @Override
163    public void receive(TableElementVisitor visitor) {
164        for (TableElement element : elements) {
165            element.receive(visitor);
166        }
167    }
168
169    /**
170     * Returns the preferred size for the element.
171     * 
172     * @param g2  the graphics target ({@code null} not permitted).
173     * @param bounds  the bounds ({@code null} not permitted).
174     * @param constraints  the layout constraints (ignored here).
175     * 
176     * @return The preferred size (never {@code null}). 
177     */
178    @Override
179    public Dimension2D preferredSize(Graphics2D g2, Rectangle2D bounds, 
180            Map<String, Object> constraints) {
181        Insets insets = getInsets();
182        double width = insets.left + insets.right;
183        double height = insets.top + insets.bottom;
184        double maxColHeight = 0.0;
185        int elementCount = this.elements.size();
186        int i = 0;
187        while (i < elementCount) {
188            // get one column of elements...
189            List<ElementInfo> elementsInColumn = columnOfElements(i, g2, 
190                    bounds);
191            double colWidth = calcColumnWidth(elementsInColumn);
192            double colHeight = calcColumnHeight(elementsInColumn, this.vgap);
193            maxColHeight = Math.max(colHeight, maxColHeight);
194            width += colWidth;
195            i = i + elementsInColumn.size();
196        }
197        height += maxColHeight;
198        return new ElementDimension(width, height);
199    }
200
201    /**
202     * Returns info for as many elements as we can fit into one column.
203     * 
204     * @param first  the index of the first element.
205     * @param g2  the graphics target.
206     * @param bounds  the bounds.
207     * 
208     * @return A list of elements and dimensions. 
209     */
210    private List<ElementInfo> columnOfElements(int first, 
211            Graphics2D g2, Rectangle2D bounds) {
212        List<ElementInfo> result = new ArrayList<>();
213        int index = first;
214        boolean full = false;
215        double h = getInsets().top + getInsets().bottom;
216        while (index < this.elements.size() && !full) {
217            TableElement element = this.elements.get(index);
218            Dimension2D dim = element.preferredSize(g2, bounds);
219            if (h + dim.getHeight() <= bounds.getHeight() || index == first) {
220                result.add(new ElementInfo(element, dim));
221                h += dim.getHeight() + this.vgap;
222                index++;
223            } else {
224                full = true;
225            }
226        }
227        return result;
228    }
229    
230    /**
231     * Returns the width of the widest element in the list.
232     * 
233     * @param elementInfoList  element info list
234     * 
235     * @return The width. 
236     */
237    private double calcColumnWidth(List<ElementInfo> elementInfoList) {
238        double result = 0.0;
239        for (ElementInfo elementInfo : elementInfoList) {
240            result = Math.max(result, elementInfo.getDimension().getWidth());
241        }
242        return result;
243    }
244    
245    /**
246     * Calculates the total height of the elements that will form one column.
247     * 
248     * @param elementInfoList  the elements in the column.
249     * @param vgap  the gap between elements.
250     * 
251     * @return The total height. 
252     */
253    private double calcColumnHeight(List<ElementInfo> elementInfoList, 
254            double vgap) {
255        double result = 0.0;
256        for (ElementInfo elementInfo : elementInfoList) {
257            result += elementInfo.getDimension().getHeight();
258        }
259        int count = elementInfoList.size();
260        if (count > 1) {
261            result += (count - 1) * vgap;
262        }
263        return result;
264    }
265    
266    @Override
267    public List<Rectangle2D> layoutElements(Graphics2D g2, Rectangle2D bounds, 
268            Map<String, Object> constraints) {
269        int elementCount = this.elements.size();
270        List<Rectangle2D> result = new ArrayList<>(elementCount);
271        int i = 0;
272        double x = bounds.getX() + getInsets().left;
273        double y = bounds.getY() + getInsets().top;
274        while (i < elementCount) {
275            // get one column of elements...
276            List<ElementInfo> elementsInColumn = columnOfElements(i, g2, 
277                    bounds);
278            double width = calcColumnWidth(elementsInColumn);
279            double height = calcColumnHeight(elementsInColumn, this.vgap);  
280            if (this.verticalAlignment == VAlign.MIDDLE) {
281                y = bounds.getCenterY() - (height / 2.0);
282            } else if (this.verticalAlignment == VAlign.BOTTOM) {
283                y = bounds.getMaxY() - getInsets().bottom - height;
284            }
285            for (ElementInfo elementInfo : elementsInColumn) {
286                Dimension2D dim = elementInfo.getDimension();
287                Rectangle2D position = new Rectangle2D.Double(x, y, 
288                        width, dim.getHeight());
289                result.add(position);
290                y += position.getHeight() + this.vgap;
291            }
292            i = i + elementsInColumn.size();
293            x += width;
294            y = bounds.getY() + getInsets().top;
295        }
296        return result;
297    }
298
299    /**
300     * Draws the element and all of its subelements within the specified
301     * bounds.
302     * 
303     * @param g2  the graphics target ({@code null} not permitted).
304     * @param bounds  the bounds ({@code null} not permitted).
305     */
306    @Override
307    public void draw(Graphics2D g2, Rectangle2D bounds) {
308        draw(g2, bounds, null);
309    }
310    
311    
312    /**
313     * Draws the element within the specified bounds.  If the 
314     * {@code recordBounds} flag is set, this element and each of its
315     * children will have its {@code BOUNDS_2D} property updated with 
316     * the current bounds.
317     * 
318     * @param g2  the graphics target ({@code null} not permitted).
319     * @param bounds  the bounds ({@code null} not permitted).
320     * @param onDrawHandler  record the bounds?
321     * 
322     * @since 1.3
323     */
324    @Override
325    public void draw(Graphics2D g2, Rectangle2D bounds, 
326            TableElementOnDraw onDrawHandler) {
327        if (onDrawHandler != null) {
328            onDrawHandler.beforeDraw(this, g2, bounds);
329        }
330        Shape savedClip = g2.getClip();
331        g2.clip(bounds);
332        
333        // find the preferred size of the flow layout
334        Dimension2D prefDim = preferredSize(g2, bounds);
335        
336        // fit a rectangle of this dimension to the bounds according to the 
337        // element anchor
338        Fit2D fitter = Fit2D.getNoScalingFitter(getRefPoint());
339        Rectangle2D dest = fitter.fit(prefDim, bounds);
340        
341        // perform layout within this bounding rectangle
342        List<Rectangle2D> layoutInfo = layoutElements(g2, dest, null);
343        
344        // draw the elements
345        for (int i = 0; i < this.elements.size(); i++) {
346            Rectangle2D rect = layoutInfo.get(i);
347            TableElement element = this.elements.get(i);
348            element.draw(g2, rect, onDrawHandler);
349        }
350        g2.setClip(savedClip);
351        if (onDrawHandler != null) {
352            onDrawHandler.afterDraw(this, g2, bounds);
353        }
354    }
355    
356    /**
357     * Tests this element for equality with an arbitrary object.
358     * 
359     * @param obj  the object ({@code null} permitted).
360     * 
361     * @return A boolean. 
362     */
363    @Override
364    public boolean equals(Object obj) {
365        if (obj == this) {
366            return true;
367        }
368        if (!(obj instanceof VerticalFlowElement)) {
369            return false;
370        }
371        VerticalFlowElement that = (VerticalFlowElement) obj;
372        if (this.vgap != that.vgap) {
373            return false;
374        }
375        if (this.verticalAlignment != that.verticalAlignment) {
376            return false;
377        }
378        if (!this.elements.equals(that.elements)) {
379            return false;
380        }
381        return super.equals(obj);
382    }
383    
384}