| GroupNodeTransformerStage.java |
1 /****************************************************************************
2 * This demo file is part of yFiles for Java 2.14.
3 * Copyright (c) 2000-2017 by yWorks GmbH, Vor dem Kreuzberg 28,
4 * 72070 Tuebingen, Germany. All rights reserved.
5 *
6 * yFiles demo files exhibit yFiles for Java functionalities. Any redistribution
7 * of demo files in source code or binary form, with or without
8 * modification, is not permitted.
9 *
10 * Owners of a valid software license for a yFiles for Java version that this
11 * demo is shipped with are allowed to use the demo source code as basis
12 * for their own yFiles for Java powered applications. Use of such programs is
13 * governed by the rights and conditions as set out in the yFiles for Java
14 * license agreement.
15 *
16 * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
19 * NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
21 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 *
27 ***************************************************************************/
28 package demo.layout.hierarchic;
29
30 import y.base.DataProvider;
31 import y.base.Node;
32 import y.base.NodeCursor;
33 import y.base.NodeMap;
34 import y.geom.YInsets;
35 import y.geom.YRectangle;
36 import y.layout.AbstractLayoutStage;
37 import y.layout.LayoutGraph;
38 import y.layout.Layouter;
39 import y.layout.grid.ColumnDescriptor;
40 import y.layout.grid.PartitionCellId;
41 import y.layout.grid.PartitionGrid;
42 import y.layout.grid.RowDescriptor;
43 import y.layout.grouping.Grouping;
44 import y.layout.grouping.GroupingKeys;
45 import y.util.Maps;
46 import y.util.WrappedObjectDataProvider;
47
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.Iterator;
51
52 /**
53 * LayoutStage for class {@link y.layout.hierarchic.IncrementalHierarchicLayouter}
54 * that transforms the group nodes directly contained in a table group node
55 * (in the remainder we call such nodes <code>direct group nodes</code>) into a multi-cell
56 * (see api-doc of {@link y.layout.grid.PartitionGrid}).
57 * Therefore, for each direct group node, this stage creates a multi-cell that covers all partition cells
58 * that overlap with the group's bounding box.
59 * The corresponding multi-cell identifier is assigned to the direct group node as well as
60 * to all non-group nodes assigned to a covered partition cell.
61 * The inner group nodes are handled like common group nodes.
62 * <br>
63 * Note that this layout stage throws an <code>IllegalArgumentException</code>
64 * if multiple direct group nodes overlap with the same partition cell or
65 * if a direct group node doesn't overlap with any partition cell
66 * (i.e., if it is placed outside the partition grid structure).
67 * Furthermore, this stage does not restore the original grid structure, i.e., after applying this stage, the
68 * nodes are still mapped to the created multi-cell identifiers.
69 *
70 */
71 public class GroupNodeTransformerStage extends AbstractLayoutStage {
72 /**
73 * Initializes a new <code>GroupNodeTransformerStage</code>.
74 * @param core the layout algorithm that is decorated by this stage.
75 */
76 public GroupNodeTransformerStage(Layouter core) {
77 super(core);
78 }
79
80 /**
81 * Determines whether or not this stage can arrange the given graph.
82 * @param graph the graph to check.
83 * @return <code>true</code> if this stage can arrange the given graph;
84 * <code>false</code> otherwise.
85 */
86 public boolean canLayout(LayoutGraph graph) {
87 return canLayoutCore(graph);
88 }
89
90 /**
91 * Prepares the given graph for partition grid layout with multi-cells.
92 * This method transforms group nodes directly contained in a table group
93 * node into multi-cells.
94 * @param graph the graph to be arranged.
95 * @throws IllegalArgumentException if multiple direct group nodes overlap
96 * with the same partition cell or if a direct group node does not overlap
97 * with any partition cell (i.e., if it is placed outside the partition grid
98 * structure).
99 */
100 public void doLayout(LayoutGraph graph) {
101 final PartitionGrid grid = PartitionGrid.getPartitionGrid(graph);
102 if (grid == null || !Grouping.isGrouped(graph)) {
103 //nothing special to do
104 doLayoutCore(graph);
105 return;
106 }
107
108 //determine the direct group nodes and the associated cell span information
109 final Grouping grouping = new Grouping(graph);
110 //the top level group node represents the table
111 final Node tableGroupNode = grouping.getChildren(grouping.getRoot()).firstNode();
112 final ArrayList directGroupCellSpans = new ArrayList();
113 for (NodeCursor nc = grouping.getChildren(tableGroupNode).nodes(); nc.ok(); nc.next()) {
114 final Node n = nc.node();
115 if (grouping.isGroupNode(n)) {
116 directGroupCellSpans.add(new GroupCellSpan(n, graph));
117 }
118 }
119
120 //check if each partition cell is covered by at most one direct group node
121 if (!isConsistent(directGroupCellSpans)) {
122 throw new IllegalArgumentException("Found partition cell that is covered by multiple direct group nodes!");
123 }
124
125 //remap partition cell identifier, i.e., map simple cells (column/row-pairs) to the new multi-cells
126 final DataProvider origNode2CellIdDP = graph.getDataProvider(PartitionGrid.PARTITION_CELL_DPKEY);
127 final NodeMap newNode2CellId = Maps.createHashedNodeMap();
128 graph.addDataProvider(PartitionGrid.PARTITION_CELL_DPKEY,
129 new WrappedObjectDataProvider(newNode2CellId, origNode2CellIdDP));
130 final HashMap pair2GroupCellSpan = new HashMap();
131 for (int i = 0; i < directGroupCellSpans.size(); i++) {
132 //map direct groups to the corresponding multi-cells
133 final GroupCellSpan groupCellSpan = (GroupCellSpan) directGroupCellSpans.get(i);
134 final PartitionCellId cellId = groupCellSpan.getCellId();
135 newNode2CellId.set(groupCellSpan.getGroup(), cellId);
136
137 //store mapping between simple cells (column/row-pairs) and multi-cells
138 for (Iterator iter = cellId.getCells().iterator(); iter.hasNext(); ) {
139 pair2GroupCellSpan.put(iter.next(), cellId);
140 }
141 }
142 //update identifier of common nodes
143 for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
144 final Node n = nc.node();
145 if (grouping.isGroupNode(n)) {
146 continue;
147 }
148
149 //check if we have to remap this node
150 final PartitionCellId cellId = (PartitionCellId) origNode2CellIdDP.get(n);
151 if (cellId == null) {
152 continue;
153 }
154 final PartitionCellId.Pair pair = (PartitionCellId.Pair) cellId.getCells().iterator().next();
155 if (pair2GroupCellSpan.containsKey(pair)) {
156 newNode2CellId.set(n, pair2GroupCellSpan.get(pair));
157 }
158 }
159 grouping.dispose();
160
161 //apply core layout
162 doLayoutCore(graph);
163
164 //restore original partition cell mapping
165 graph.addDataProvider(PartitionGrid.PARTITION_CELL_DPKEY, origNode2CellIdDP);
166 }
167
168 /**
169 * Checks if the cell span objects are consistent.
170 * They are consistent if there is no pair of overlapping cell spans.
171 *
172 * @param groupCellSpans an array list of cell span objects
173 * @return <code>true</code> if the cell span objects are consistent; <code>false</code> otherwise.
174 */
175 private static boolean isConsistent(final ArrayList groupCellSpans) {
176 for (int i = 0; i < groupCellSpans.size(); i++) {
177 final GroupCellSpan span1 = (GroupCellSpan) groupCellSpans.get(i);
178 for (int j = i + 1; j < groupCellSpans.size(); j++) {
179 if (GroupCellSpan.doOverlap(span1, (GroupCellSpan) groupCellSpans.get(j))) {
180 return false;
181 }
182 }
183 }
184 return true;
185 }
186
187 /**
188 * Class that provides the cell span information of a given group node.
189 * The cell span of a group node encodes all partition cells that overlap with the group's bounding box.
190 */
191 private static class GroupCellSpan {
192 private final Node group;
193 private final LayoutGraph graph;
194 private final PartitionGrid grid;
195 private final PartitionCellId cellId;
196 //the topmost row (row with the smallest index) that overlaps with the group.
197 private RowDescriptor minRow;
198 //the bottommost row (row with the largest index) that overlaps with the group.
199 private RowDescriptor maxRow;
200 //the leftmost column (column with the smallest index) that overlaps with the group.
201 private ColumnDescriptor minColumn;
202 //the rightmost column (column with the largest index) that overlaps with the group.
203 private ColumnDescriptor maxColumn;
204
205 GroupCellSpan(final Node group, final LayoutGraph graph) {
206 this.group = group;
207 this.graph = graph;
208 this.grid = PartitionGrid.getPartitionGrid(graph);
209 determineMinMaxRowAndColumn();
210 this.cellId = grid.createCellSpanId(minRow, minColumn, maxRow, maxColumn);
211 updateCellInsets();
212 }
213
214 /**
215 * Ensures that the min/max column/row insets are greater or equal to the corresponding group node insets.
216 */
217 private void updateCellInsets() {
218 final DataProvider insetsDP = graph.getDataProvider(GroupingKeys.GROUP_NODE_INSETS_DPKEY);
219 final YInsets insets = (insetsDP == null) ? null : (YInsets) insetsDP.get(group);
220 if (insets == null || minRow == null || minColumn == null) {
221 return;
222 }
223
224 if (insets.left > minColumn.getLeftInset()) {
225 minColumn.setLeftInset(insets.left);
226 }
227 if (insets.right > maxColumn.getRightInset()) {
228 maxColumn.setRightInset(insets.right);
229 }
230 if (insets.top > minRow.getTopInset()) {
231 minRow.setTopInset(insets.top);
232 }
233 if (insets.bottom > maxRow.getBottomInset()) {
234 maxRow.setBottomInset(insets.bottom);
235 }
236 }
237
238 /**
239 * Determines the min/max row/column that overlaps with the bounding box of the given group node.
240 *
241 * @throws IllegalArgumentException if the group node does not overlap with any partition cell.
242 */
243 private void determineMinMaxRowAndColumn() {
244 final YRectangle groupBounds = graph.getRectangle(group);
245
246 final double groupLeftX = groupBounds.getX();
247 final double groupRightX = groupBounds.getX() + groupBounds.getWidth();
248 for (Iterator iter = grid.getColumns().iterator(); iter.hasNext(); ) {
249 final ColumnDescriptor column = (ColumnDescriptor) iter.next();
250 if (column.getOriginalPosition() < groupRightX
251 && groupLeftX < column.getOriginalPosition() + column.getOriginalWidth()) {
252 //group overlaps with column
253 if (minColumn == null || minColumn.getIndex() > column.getIndex()) {
254 minColumn = column;
255 }
256 if (maxColumn == null || maxColumn.getIndex() < column.getIndex()) {
257 maxColumn = column;
258 }
259 }
260 }
261
262 final double groupTopY = groupBounds.getY();
263 final double groupBottomY = groupBounds.getY() + groupBounds.getHeight();
264 for (Iterator iter = grid.getRows().iterator(); iter.hasNext(); ) {
265 final RowDescriptor row = (RowDescriptor) iter.next();
266 if (row.getOriginalPosition() < groupBottomY
267 && groupTopY < row.getOriginalPosition() + row.getOriginalHeight()) {
268 //group overlaps with row
269 if (minRow == null || minRow.getIndex() > row.getIndex()) {
270 minRow = row;
271 }
272 if (maxRow == null || maxRow.getIndex() < row.getIndex()) {
273 maxRow = row;
274 }
275 }
276 }
277
278 if (minRow == null || minColumn == null) {
279 //group does not overlap with the partition cell structure
280 throw new IllegalArgumentException();
281 }
282 }
283
284 /**
285 * Returns the group node associated with this instance.
286 *
287 * @return the group node associated with this instance.
288 */
289 public Node getGroup() {
290 return group;
291 }
292
293 /**
294 * Returns the partition cell id that encodes all partition cells that overlap with the bounding box of the
295 * associated group node (see method {@link #getGroup()}).
296 *
297 * @return the partition cell id that encodes all partition cells that overlap with the bounding box of the
298 * associated group node.
299 * @see #getGroup()
300 */
301 public PartitionCellId getCellId() {
302 return cellId;
303 }
304
305 /**
306 * Returns <code>true</code> if the two given group cell span objects overlap with each other;
307 * <code>false</code> otherwise.
308 * Two cell span objects overlap, if they have at least one partition cell in common
309 * (see method {@link #getCellId()}).
310 *
311 * @param span1 the first group cell span object.
312 * @param span2 the second group cell span object.
313 * @return <code>true</code> if the two given group cell span object overlap with each other;
314 * <code>false</code> otherwise.
315 * @see #getCellId()
316 */
317 public static boolean doOverlap(final GroupCellSpan span1, final GroupCellSpan span2) {
318 return span1.minRow.getIndex() <= span2.maxRow.getIndex()
319 && span2.minRow.getIndex() <= span1.maxRow.getIndex()
320 && span1.minColumn.getIndex() <= span2.maxColumn.getIndex()
321 && span2.minColumn.getIndex() <= span1.maxColumn.getIndex();
322 }
323 }
324 }
325