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