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}