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