    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.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 com.orsoncharts.util.ArgChecks;
    046import com.orsoncharts.util.Fit2D;
    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        ArgChecks.nullNotPermitted(alignment, null);
    091        this.elements = new ArrayList<TableElement>();
    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        ArgChecks.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<TableElement>(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        ArgChecks.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<ElementInfo>();
    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<Rectangle2D>(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}