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 demo.layout.hierarchic.CellSpanLayoutDemo.CellColorManager;
31  import demo.layout.hierarchic.CellSpanLayoutDemo.Span;
32  
33  import demo.view.DemoBase;
34  import y.base.Node;
35  import y.base.NodeCursor;
36  import y.base.NodeList;
37  import y.layout.hierarchic.IncrementalHierarchicLayouter;
38  import y.layout.hierarchic.incremental.EdgeLayoutDescriptor;
39  import y.util.GraphCopier;
40  import y.view.Graph2D;
41  import y.view.Graph2DLayoutExecutor;
42  import y.view.Graph2DView;
43  import y.view.NodeRealizer;
44  import y.view.hierarchy.AutoBoundsFeature;
45  import y.view.hierarchy.HierarchyManager;
46  import y.view.tabular.TableGroupNodeRealizer;
47  import y.view.tabular.TableGroupNodeRealizer.Column;
48  import y.view.tabular.TableGroupNodeRealizer.Row;
49  import y.view.tabular.TableGroupNodeRealizer.Table;
50  import y.view.tabular.TableLayoutConfigurator;
51  
52  import java.awt.Color;
53  import java.awt.event.ActionEvent;
54  import java.util.ArrayList;
55  import java.util.Collections;
56  import java.util.Comparator;
57  import java.util.HashSet;
58  import java.util.Iterator;
59  import javax.swing.AbstractAction;
60  import javax.swing.Action;
61  import javax.swing.JToggleButton;
62  
63  /**
64   * Creates all user actions for the {@link CellSpanLayoutDemo}.
65   * Provides actions for adding/removing columns and rows, switching
66   * from design view to diagram view and vice versa, showing sample diagrams,
67   * and deleting selected elements.
68   *
69   */
70  class CellSpanActionFactory {
71    /**
72     * Client property key to store the design view graph while in diagram mode
73     * @see #switchViewState(y.view.Graph2DView)
74     */
75    private static final String KEY_DESIGN_VIEW = "CellSpanLayoutDemo.designView";
76  
77  
78    /**
79     * Prevents instantiation of factory class.
80     */
81    private CellSpanActionFactory() {
82    }
83  
84  
85    /**
86     * Creates an action for inserting new columns right before the given
87     * reference column.
88     */
89    static Action newAddBefore(
90            final Graph2DView view, final Table table, final Column col
91    ) {
92      return new AddColumn(view, table, col, true);
93    }
94  
95    /**
96     * Creates an action for inserting new columns right after the given
97     * reference column.
98     */
99    static Action newAddAfter(
100           final Graph2DView view, final Table table, final Column col
101   ) {
102     return new AddColumn(view, table, col, false);
103   }
104 
105   /**
106    * Creates an action for removing the given column.
107    */
108   static Action newRemoveColumn(
109           final Graph2D graph, final Table table, final Column col
110   ) {
111     return new RemoveColumn(graph, table, col);
112   }
113 
114   /**
115    * Creates an action for inserting new rows right before the given
116    * reference row.
117    */
118   static Action newAddBefore(
119           final Graph2DView view, final Table table, final Row row
120   ) {
121     return new AddRow(view, table, row, true);
122   }
123 
124   /**
125    * Creates an action for inserting new rows right after the given
126    * reference row.
127    */
128   static Action newAddAfter(
129           final Graph2DView view, final Table table, final Row row
130   ) {
131     return new AddRow(view, table, row, false);
132   }
133 
134   /**
135    * Creates an action for removing the given row.
136    */
137   static Action newRemoveRow(
138           final Graph2D graph, final Table table, final Row row
139   ) {
140     return new RemoveRow(graph, table, row);
141   }
142 
143   /**
144    * Creates an action for deleting selected elements in the graph
145    * associated to the given view.
146    * This action prevents top-level table nodes from being deleted.
147    * Additionally, this action ensures that top-level table nodes
148    * are not shrunk due to removing elements.
149    */
150   static Action newDeleteSelection( final Graph2DView view ) {
151     return new CellDeleteSelection(view);
152   }
153 
154   /**
155    * Creates an action that replaces the currently displayed diagram with a
156    * sample diagram.
157    * @param name the display name of the action in the application menu bar.
158    * @param parent the <code>CellSpanLayoutDemo</code> instance to display the
159    * sample diagram.
160    * @param resource the path name referencing the GraphML document with
161    * the sample diagram to display.
162    */
163   static Action newSampleAction(
164           final String name, final CellSpanLayoutDemo parent, final String resource
165   ) {
166     return new SampleAction(name, parent, resource);
167   }
168 
169   /**
170    * Creates an action for switching between design view and laid out diagram
171    * view.
172    * @param name the display name of the action in the application tool bar.
173    * @param view the view that displays either the cell span designer or the
174    * laid out diagram.
175    * @param other the toggle button to enable for the switching back to the
176    * previous view state.
177    */
178   static Action newSwitchViewStateAction(
179           final String name, final Graph2DView view, final JToggleButton other
180   ) {
181     return new SwitchViewStateAction(name, view, other);
182   }
183 
184 
185   /**
186    * Backs up the visual state of the specified node.
187    */
188   static void backupRealizer( final Graph2D graph, final Node node ) {
189     graph.backupRealizers((new NodeList(node)).nodes());
190   }
191 
192 
193   /**
194    * Switches the view state from cell span design mode to diagram mode and
195    * vice versa.
196    * @param view the view that displays either the cell span designer or the
197    * laid out diagram.
198    */
199   static void switchViewState( final Graph2DView view ) {
200     final Object value = view.getClientProperty(KEY_DESIGN_VIEW);
201     if (value instanceof Graph2D) {
202       // view is in diagram mode
203       // switch to design mode
204 
205       view.putClientProperty(KEY_DESIGN_VIEW, null);
206       view.setGraph2D((Graph2D) value);
207     } else {
208       // view is in design mode
209       // switch to laid out diagram
210 
211       // the graph holding the table node with cell spans
212       final Graph2D graph = view.getGraph2D();
213 
214       // create a copy of the designer graph for the laid out diagram
215       // this is done because the single top-level cell span designer table node
216       // is replaced with several top-level group nodes representing each cell
217       // span
218       final Graph2D target = new Graph2D();
219       target.setDefaultNodeRealizer(graph.getDefaultNodeRealizer());
220       target.setDefaultEdgeRealizer(graph.getDefaultEdgeRealizer());
221       final GraphCopier copier = new GraphCopier(graph.getGraphCopyFactory());
222       copier.copy(graph, target);
223 
224       // set up the layout algorithm used for arranging the diagram
225       final IncrementalHierarchicLayouter algorithm = new IncrementalHierarchicLayouter();
226       algorithm.setConsiderNodeLabelsEnabled(true);
227       final EdgeLayoutDescriptor eld = algorithm.getEdgeLayoutDescriptor();
228       eld.setMinimumFirstSegmentLength(25);
229       eld.setMinimumLastSegmentLength(25);
230 
231       // run the layout algorithm
232       final Graph2DLayoutExecutor executor = new Graph2DLayoutExecutor();
233       // class CellLayoutConfigurator is TableLayoutConfigurator subclass
234       // that models the cell spans in the top-level table node by
235       // introducing additional group nodes
236       executor.setTableLayoutConfigurator(new CellLayoutConfigurator());
237       executor.setConfiguringTableNodeRealizers(true);
238       executor.setPortIntersectionCalculatorEnabled(true);
239       // class GroupNodeTransformerStage re-configures partition grid data
240       // by interpreting second-level group nodes as cell spans
241       executor.doLayout(target, new GroupNodeTransformerStage(algorithm));
242 
243       // store the original cell span designer to be able to switch back to
244       // design mode later on
245       view.putClientProperty(KEY_DESIGN_VIEW, graph);
246       view.setGraph2D(target);
247     }
248 
249     view.fitContent();
250     view.updateView();
251   }
252 
253 
254   /**
255    * Action that displays a sample diagram.
256    */
257   private static final class SampleAction extends AbstractAction {
258     private final CellSpanLayoutDemo parent;
259     private final String resource;
260 
261     /**
262      * Initializes a new <code>SampleAction</code> instance.
263      * @param name the display name of the action in the application menu bar.
264      * @param parent the <code>CellSpanLayoutDemo</code> instance to display the
265      * sample diagram.
266      * @param resource the path name referencing the GraphML document with
267      * the sample diagram to display.
268      */
269     SampleAction(
270             final String name, final CellSpanLayoutDemo parent, final String resource
271     ) {
272       super(name);
273       this.parent = parent;
274       this.resource = resource;
275     }
276 
277     /**
278      * Displays the sampe diagram referenced by {@link #resource}.
279      */
280     public void actionPerformed( final ActionEvent e ) {
281       final Graph2DView view = parent.getView();
282 
283       // all sample diagrams are stored in design mode
284       // if the view is currently in diagram mode, temporarily switch to
285       // design mode for loading the diagram and switch back to diagram mode
286       // later
287       final Object value = view.getClientProperty(KEY_DESIGN_VIEW);
288       final boolean inDiagramMode = value instanceof Graph2D;
289       if (inDiagramMode) {
290         view.putClientProperty(KEY_DESIGN_VIEW, null);
291         view.setGraph2D((Graph2D) value);
292       }
293 
294       final Graph2D graph = view.getGraph2D();
295       graph.clear();
296 
297       // actually load the sample diagram
298       parent.loadGraph(resource);
299 
300       // switch back to the correct display mode if necessary
301       if (inDiagramMode) {
302         switchViewState(view);
303       }
304     }
305   }
306 
307   /**
308    * Action that switches the view state from design mode to laid out diagram
309    * mode or vice versa.
310    */
311   private static final class SwitchViewStateAction extends AbstractAction {
312     private final JToggleButton other;
313     private final Graph2DView view;
314 
315     /**
316      * Initializes a new <code>SwitchViewStateAction</code> instance.
317      * @param name the display name of the action in the application tool bar.
318      * @param view the view that displays either the cell span designer or the
319      * laid out diagram.
320      * @param other the toggle button to enable for the switching back to the
321      * previous view state.
322      */
323     SwitchViewStateAction(
324             final String name, final Graph2DView view, final JToggleButton other
325     ) {
326       super(name);
327       this.other = other;
328       this.view = view;
329     }
330 
331     /**
332      * Switches the view state from design mode to laid out diagram mode or
333      * vice versa.
334      */
335     public void actionPerformed( final ActionEvent e ) {
336       final JToggleButton src = (JToggleButton) e.getSource();
337       if (src.isSelected()) {
338         src.setEnabled(false);
339         other.setEnabled(true);
340         other.setSelected(false);
341         CellSpanActionFactory.switchViewState(view);
342       }
343     }
344   }
345 
346   /**
347    * Action that deletes selected graph elements but prevents top-level table
348    * nodes from being deleted. Additionally, this action ensures that top-level
349    * table nodes are not shrunken due to removing elements.
350    */
351   private static final class CellDeleteSelection extends DemoBase.DeleteSelection {
352     CellDeleteSelection( final Graph2DView view ) {
353       super(view);
354 
355       // prevents all table node from being deleted
356       // there should be only one table node for CellSpanLayoutDemo (i.e. the
357       // cell span designer), preventing the deletion of all table nodes should
358       // be fine, too
359       setDeletionMask(getDeletionMask() & ~TYPE_TABLE_NODE);
360     }
361 
362     /**
363      * Deletes the selected elements in graph associated to the given view.
364      * Ensures that top-level table nodes are not shrunken due to removing
365      * elements.
366      * @param view the view in which to delete graph elements.
367      */
368     public void delete( final Graph2DView view ) {
369       final Graph2D graph = view.getGraph2D();
370       final HierarchyManager hm = graph.getHierarchyManager();
371       if (hm == null) {
372         super.delete(view);
373       } else {
374         // find all top-level table nodes
375         final NodeList tables = new NodeList();
376         for (NodeCursor nc = hm.getChildren(null); nc.ok(); nc.next()) {
377           final Node node = nc.node();
378           if (hm.isGroupNode(node) &&
379               graph.getRealizer(node) instanceof TableGroupNodeRealizer) {
380             tables.add(node);
381           }
382         }
383         if (tables.isEmpty()) {
384           super.delete(view);
385         } else {
386           graph.firePreEvent();
387           // notify the undo manager of the table nodes' current sizes
388           // (actually, this step copies the complete visual states of the
389           // table nodes, even though only their sizes matter here) 
390           graph.backupRealizers(tables.nodes());
391 
392           // now, turn off the table nodes' AutoBoundsFeatures
393           // (AutoBoundsFeature is responsible for automatic size changes)
394           final NodeList disabled = new NodeList();
395           for (NodeCursor nc = tables.nodes(); nc.ok(); nc.next()) {
396             final Node node = nc.node();
397             final AutoBoundsFeature abf =
398                     graph.getRealizer(node).getAutoBoundsFeature();
399             if (abf != null && abf.isAutoBoundsEnabled()) {
400               abf.setAutoBoundsEnabled(false);
401               disabled.add(node);
402             }
403           }
404 
405           try {
406             super.delete(view);
407           } finally {
408 
409             // finally, turn on the previously disabled AutoBoundsFeatures
410             // otherwise the table nodes are no longer automatically enlarged
411             // when child nodes are moved or added
412             if (!disabled.isEmpty()) {
413               for (NodeCursor nc = disabled.nodes(); nc.ok(); nc.next()) {
414                 graph.getRealizer(nc.node())
415                         .getAutoBoundsFeature()
416                         .setAutoBoundsEnabled(true);
417               }
418             }
419           }
420 
421           graph.firePostEvent();
422         }
423       }
424     }
425   }
426 
427   /**
428    * Action that adds a new column before or after a reference column.
429    */
430   private static final class AddColumn extends AbstractAction {
431     private final Graph2DView view;
432     private final Table table;
433     private final Column column;
434     private final boolean before;
435 
436     /**
437      * Initializes a new <code>AddColumn</code> instance.
438      */
439     AddColumn(
440             final Graph2DView view,
441             final Table table, final Column column,
442             final boolean before
443     ) {
444       super("Add Column " + (before ? "Before" : "After"));
445       this.view = view;
446       this.table = table;
447       this.column = column;
448       this.before = before;
449     }
450 
451     /**
452      * Adds a new column to the referenced <code>table</code>.
453      */
454     public void actionPerformed( final ActionEvent e ) {
455       final TableGroupNodeRealizer tgnr = table.getRealizer();
456 
457       // notify the undo manager of the current table structure
458       backupRealizer(view.getGraph2D(), tgnr.getNode());
459 
460 
461       final double oldWidth = tgnr.getWidth();
462 
463       final int newIdx = column.getIndex() + (before ? 0 : 1);
464       final Column newCol = table.addColumn(newIdx);
465 
466       final double dx = tgnr.getWidth() - oldWidth;
467       // adjust table position when inserting a column before the reference
468       // column to achieve a "prepend" effect rather than an "append" effect
469       if (before && dx > 0) {
470         // AutoBoundsFeature needs to be disabled when changing the table node
471         // position, otherwise the feature's internal logic will prevent
472         // the position change
473         final AutoBoundsFeature abf = tgnr.getAutoBoundsFeature();
474         final boolean oldEnabled = abf != null && abf.isAutoBoundsEnabled();
475         if (oldEnabled) {
476           abf.setAutoBoundsEnabled(false);
477         }
478         try {
479           tgnr.setLocation(tgnr.getX() - dx, tgnr.getY());
480         } finally {
481           if (oldEnabled) {
482             abf.setAutoBoundsEnabled(true);
483           }
484         }
485       }
486 
487 
488       // update the cell to color mappings accordingly
489       // adding the new column above has increased the indices of all
490       // subsequent columns and consequently the index-based cell to color
491       // mappings have to be adjusted accordingly
492       final CellColorManager manager = CellColorManager.getInstance(table);
493       manager.shift(newCol, true);
494 
495       // color the appropriate cells in the new column such that no cell spans
496       // are split in two
497       final int nextIdx = before ? newIdx - 1 : newIdx + 1;
498       if (-1 < nextIdx && nextIdx < table.columnCount()) {
499         final Column next = table.getColumn(nextIdx);
500         for (int i = 0, n = table.rowCount(); i < n; ++i) {
501           final Row row = table.getRow(i);
502           final Color color = manager.getCellColor(column, row);
503           if (color != null && color.equals(manager.getCellColor(next, row))) {
504             manager.setCellColor(newCol, row, color);
505           }
506         }
507       }
508 
509       // ensures that the new column is completely reachable by scroll bar
510       view.updateWorldRect();
511 
512       // trigger a repaint to visualize the change
513       view.getGraph2D().updateViews();
514     }
515   }
516 
517   /**
518    * Action that removes an existing column.
519    */
520   private static final class RemoveColumn extends AbstractAction {
521     private final Graph2D graph;
522     private final Table table;
523     private final Column column;
524 
525     /**
526      * Initializes a new <code>RemoveColumn</code> instance.
527      */
528     RemoveColumn( final Graph2D graph, final Table table, final Column column ) {
529       super("Remove Column");
530       this.graph = graph;
531       this.table = table;
532       this.column = column;
533     }
534 
535     /**
536      * Removes the referenced <code>column</code>.
537      */
538     public void actionPerformed( final ActionEvent e ) {
539       // notify the undo manager of the current table structure
540       backupRealizer(graph, table.getRealizer().getNode());
541 
542       // update the cell to color mappings accordingly
543       // removing a column will decrease the indices of all subsequent columns
544       // and consequently the index-based cell to color mappings have to be
545       // adjusted accordingly
546       CellColorManager.getInstance(table).shift(column, false);
547 
548       column.remove();
549 
550       // trigger a repaint to visualize the change
551       graph.updateViews();
552     }
553   }
554 
555   /**
556    * Action that adds a new row before or after a reference row.
557    */
558   private static final class AddRow extends AbstractAction {
559     private final Graph2DView view;
560     private final Table table;
561     private final Row row;
562     private final boolean before;
563 
564     /**
565      * Initializes a new <code>AddRow</code> instance.
566      */
567     AddRow(
568             final Graph2DView view,
569             final Table table, final Row row,
570             final boolean before
571     ) {
572       super("Add Row " + (before ? "Before" : "After"));
573       this.view = view;
574       this.table = table;
575       this.row = row;
576       this.before = before;
577     }
578 
579     /**
580      * Adds a new row to the referenced <code>table</code>.
581      */
582     public void actionPerformed( final ActionEvent e ) {
583       final TableGroupNodeRealizer tgnr = table.getRealizer();
584 
585       // notify the undo manager of the current table structure
586       backupRealizer(view.getGraph2D(), tgnr.getNode());
587 
588 
589       final double oldHeight = tgnr.getHeight();
590 
591       final int newIdx = row.getIndex() + (before ? 0 : 1);
592       final Row newRow = table.addRow(newIdx);
593 
594       final double dy = tgnr.getHeight() - oldHeight;
595       // adjust table position when inserting a row before the reference row
596       // to achieve a "prepend" effect rather than an "append" effect
597       if (before && dy > 0) {
598         // AutoBoundsFeature needs to be disabled when changing the table node
599         // position, otherwise the feature's internal logic will prevent
600         // the position change
601         final AutoBoundsFeature abf = tgnr.getAutoBoundsFeature();
602         final boolean oldEnabled = abf != null && abf.isAutoBoundsEnabled();
603         if (oldEnabled) {
604           abf.setAutoBoundsEnabled(false);
605         }
606         try {
607           tgnr.setLocation(tgnr.getX(), tgnr.getY() - dy);
608         } finally {
609           if (oldEnabled) {
610             abf.setAutoBoundsEnabled(true);
611           }
612         }
613       }
614 
615       // update the cell to color mappings accordingly
616       // adding the new row above has increased the indices of all
617       // subsequent row and consequently the index-based cell to color
618       // mappings have to be adjusted accordingly
619       final CellColorManager manager = CellColorManager.getInstance(table);
620       manager.shift(newRow, true);
621 
622       // color the appropriate cells in the new row such that no cell spans
623       // are split in two
624       final int nextIdx = before ? newIdx - 1 : newIdx + 1;
625       if (-1 < nextIdx && nextIdx < table.rowCount()) {
626         final Row next = table.getRow(nextIdx);
627         for (int i = 0, n = table.columnCount(); i < n; ++i) {
628           final Column col = table.getColumn(i);
629           final Color color = manager.getCellColor(col, row);
630           if (color != null && color.equals(manager.getCellColor(col, next))) {
631             manager.setCellColor(col, newRow, color);
632           }
633         }
634       }
635 
636       // ensures that the new row is completely reachable by scroll bar
637       view.updateWorldRect();
638 
639       // trigger a repaint to visualize the change
640       view.getGraph2D().updateViews();
641     }
642   }
643 
644   /**
645    * Action that removes an existing row.
646    */
647   private static final class RemoveRow extends AbstractAction {
648     private final Graph2D graph;
649     private final Table table;
650     private final Row row;
651 
652     /**
653      * Initializes a new <code>RemoveRow</code> instance.
654      */
655     RemoveRow( final Graph2D graph, final Table table, final Row row ) {
656       super("Remove Row");
657       this.graph = graph;
658       this.table = table;
659       this.row = row;
660     }
661 
662     /**
663      * Removes the referenced <code>row</code>.
664      */
665     public void actionPerformed( final ActionEvent e ) {
666       // notify the undo manager of the current table structure
667       backupRealizer(graph, table.getRealizer().getNode());
668 
669       // update the cell to color mappings accordingly
670       // removing a row will decrease the indices of all subsequent rows
671       // and consequently the index-based cell to color mappings have to be
672       // adjusted accordingly
673       CellColorManager.getInstance(table).shift(row, false);
674 
675       row.remove();
676 
677       // trigger a repaint to visualize the change
678       graph.updateViews();
679     }
680   }
681 
682 
683   /**
684    * Converts the cell spans defined by background colors into group nodes. 
685    */
686   private static final class CellLayoutConfigurator extends TableLayoutConfigurator {
687     /**
688      * Performs all necessary layout preparations for the specified graph.
689      * Invokes
690      * <blockquote>
691      * <code>prepareCellSpans(graph);<code>
692      * <code>super.prepareAll(graph);<code>
693      * </blockquote>
694      * @param graph the <code>Graph2D</code> instance that is prepared for
695      * automated layout calculation.
696      */
697     public void prepareAll( final Graph2D graph ) {
698       prepareCellSpans(graph);
699       super.prepareAll(graph);
700     }
701 
702     /**
703      * Adds groups nodes to all top-level table nodes representing the cell
704      * spans in the table structures.
705      * @param graph the <code>Graph2D</code> instance that is prepared for
706      * automated layout calculation.
707      */
708     void prepareCellSpans( final Graph2D graph ) {
709       final HierarchyManager hm = graph.getHierarchyManager();
710       if (hm != null) {
711         for (NodeCursor nc = hm.getChildren(null); nc.ok(); nc.next()) {
712           final Node node = nc.node();
713           if (hm.isGroupNode(node)) {
714             final NodeRealizer nr = graph.getRealizer(node);
715             if (nr instanceof TableGroupNodeRealizer) {
716               prepareTable((TableGroupNodeRealizer) nr);
717             }
718           }
719         }
720       }
721     }
722 
723     /**
724      * Adds a group node for each cell span in the given realizer's associated
725      * table structure. Cells spans are defined by cell background color.
726      * @param tgnr the table realizer to process.
727      */
728     void prepareTable( final TableGroupNodeRealizer tgnr ) {
729       // disable AutoBoundsFeature to prevent the addition of group nodes
730       // from changing the table node size thereby destroying the
731       // coordinates-based child node to table cell associations
732       final AutoBoundsFeature abf = tgnr.getAutoBoundsFeature();
733       final boolean oldEnabled = abf != null && abf.isAutoBoundsEnabled();
734       if (oldEnabled) {
735         abf.setAutoBoundsEnabled(false);
736       }
737       try {
738         prepareTableImpl(tgnr);
739       } finally {
740         if (oldEnabled) {
741           abf.setAutoBoundsEnabled(true);
742         }
743       }
744     }
745 
746     /**
747      * Adds a group node for each cell span in the given realizer's associated
748      * table structure. Cells spans are defined by cell background color.
749      * This method assumes that each background color is used only for one cell
750      * span. Moreover, this method assumes that there are no nested columns or
751      * rows in the realizer's table structure.
752      * @param tgnr the table realizer to process.
753      */
754     private void prepareTableImpl( final TableGroupNodeRealizer tgnr ) {
755       final Node node = tgnr.getNode();
756       final Graph2D graph = (Graph2D) node.getGraph();
757       final HierarchyManager hm = graph.getHierarchyManager();
758 
759       final Table table = tgnr.getTable();
760 
761       final CellColorManager manager = CellColorManager.getInstance(table);
762       final HashSet used = new HashSet();
763       used.add(null);
764       for (int i = 0, n = table.columnCount(); i < n; ++i) {
765         final Column column = table.getColumn(i);
766         for (int j = 0, m = table.rowCount(); j < m; ++j) {
767           final Row row = table.getRow(j);
768           final Color color = manager.getCellColor(column, row);
769           if (used.add(color)) {
770             final Span span = Span.find(table, column, row, color);
771 
772             // determine the size of the group node representing the cell span
773             // defined by the current color
774             // the group is actually slightly smaller the union of the cells
775             // to prevent ambiguities in GroupNodeTransformerStage when
776             // translating the group nodes to partition grid cell spans
777             final double eps = 2;
778             final double minX = table.getColumn(span.minCol).calculateBounds().getX() + eps;
779             final double maxX = table.getColumn(span.maxCol).calculateBounds().getMaxX() - eps;
780             final double minY = table.getRow(span.minRow).calculateBounds().getY() + eps;
781             final double maxY = table.getRow(span.maxRow).calculateBounds().getMaxY() - eps;
782 
783             final Node group = hm.createGroupNode(node);
784       
785             final NodeRealizer nr = graph.getRealizer(group);
786             nr.setFrame(minX, minY, maxX - minX, maxY - minY);
787             nr.setFillColor(color);
788             for (int l = nr.labelCount() - 1; l > -1; --l) {
789               nr.removeLabel(l);
790             }
791           }
792         }
793       }
794     }
795 
796 
797     /**
798      * Performs all necessary resource cleanup and data translation after
799      * a layout calculation. Invokes
800      * <blockquote>
801      * <code>super.restoreAll(graph);<code>
802      * <code>restoreCellSpans(graph);<code>
803      * </blockquote>
804      * @param graph the <code>Graph2D</code> instance that was previously
805      * prepared for automated layout calculation.
806      */
807     public void restoreAll( final Graph2D graph ) {
808       super.restoreAll(graph);
809       restoreCellSpans(graph);
810     }
811 
812     /**
813      * Assigns the non-group node children of top-level table nodes to the
814      * appropriate cell span representing group nodes and removes said table
815      * nodes.
816      * @param graph the <code>Graph2D</code> instance that was previously
817      * prepared for automated layout calculation.
818      */
819     void restoreCellSpans( final Graph2D graph ) {
820       final HierarchyManager hm = graph.getHierarchyManager();
821       if (hm != null) {
822         final NodeList tables = new NodeList();
823 
824         // find all top-level table nodes (should be exactly one)
825         // and assign the non-group node children to the appropriate
826         // cell span representing group node
827         for (NodeCursor nc = hm.getChildren(null); nc.ok(); nc.next()) {
828           final Node node = nc.node();
829           if (hm.isGroupNode(node) &&
830               graph.getRealizer(node) instanceof TableGroupNodeRealizer) {
831             restoreCellSpanGroups(graph, hm, node);
832             tables.add(node);
833           }
834         }
835 
836         // remove the now unnecessary table nodes
837         // table nodes are meant only as means for defining cell spans
838         // now that the all cell spans have been translated to group nodes
839         // the tables are superfluous
840         for (NodeCursor nc = tables.nodes(); nc.ok(); nc.next()) {
841           graph.removeNode(nc.node());
842         }
843       }
844     }
845 
846     /**
847      * Assigns the non-group node children of a table node to the appropriate
848      * cell span representing group node.
849      * @param graph the <code>Graph2D</code> instance that was previously
850      * prepared for automated layout calculation.
851      * @param hm the graph's associated hierarchy information.
852      * @param node the table node.
853      */
854     void restoreCellSpanGroups(
855             final Graph2D graph, final HierarchyManager hm, final Node node
856     ) {
857       // find and separate group nodes from other (normal) nodes
858       // since there is no way to interactively create group nodes in
859       // CellSpanLayoutDemo, these groups are assumed to correspond to
860       // the previously defined cell spans
861       final NodeList groups = new NodeList();
862       final ArrayList others = new ArrayList();
863       for (NodeCursor nc = hm.getChildren(node); nc.ok(); nc.next()) {
864         final Node child = nc.node();
865         if (hm.isGroupNode(child)) {
866           groups.add(child);
867         } else {
868           others.add(child);
869         }
870       }
871 
872       // order nodes by x- and y-coordinates of their center points
873       Collections.sort(others, new NodeCenterOrder(graph));
874 
875       // for each group node determine all other (normal) nodes that lie inside
876       // the group's bounds and change those nodes to child nodes of the group
877       //
878       // assigning the other (normal) nodes to the cell span representing group
879       // nodes created in prepareTableImpl is done here, because at this point
880       // (i.e. after the layout calculation) the groups are already sized to
881       // encompass the bounds of all other (normal) nodes belonging to the
882       // cells of the groups' spans
883       //
884       // this approach relies again on the fact that it is not possible
885       // to create nested structures in the CellSpanLayoutDemo
886       for (NodeCursor nc = groups.nodes(); nc.ok(); nc.next()) {
887         final Node group = nc.node();
888         final NodeRealizer nr = graph.getRealizer(group);
889         final double minX = nr.getX();
890         final double maxX = minX + nr.getWidth();
891         final double minY = nr.getY();
892         final double maxY = minY + nr.getHeight();
893 
894         // disable AutoBoundsFeature to prevent the addition of nodes
895         // from changing the group node size thereby destroying the
896         // calculated layout of the group nodes
897         final AutoBoundsFeature abf = nr.getAutoBoundsFeature();
898         final boolean oldEnabled = abf != null && abf.isAutoBoundsEnabled();
899         if (oldEnabled) {
900           abf.setAutoBoundsEnabled(false);
901         }
902 
903         // determine which other (normal) nodes lie inside the current group
904         // node bounds
905         // since the diagram is already arranged, partition grid cells
906         // will encompass the bounds of their associated nodes completely
907         // moreover, the cell span representing groups will be sized to
908         // match the bounds of the corresponding cell spans
909         // consequently, nodes lie either completely inside or completely
910         // outside the group bounds
911         // therefore it suffices to check the center coordinates of a node
912         // to decide whether or not it should be assigned to the current group
913         boolean active = false;
914         for (Iterator it = others.iterator(); it.hasNext(); ) {
915           final Node child = (Node) it.next();
916           final double cx = graph.getCenterX(child);
917           if (minX < cx && cx < maxX) {
918             active = true;
919             final double cy = graph.getCenterY(child);
920             if (minY < cy && cy < maxY) {
921               hm.setParentNode(child, group);
922             }
923           } else if (active) {
924             // because the other (normal) nodes are ordered by x-coordinate
925             // there are no more nodes that are inside the group bounds
926             break;
927           }
928         }
929 
930         if (oldEnabled) {
931           abf.setAutoBoundsEnabled(true);
932         }
933       }
934     }
935 
936     /**
937      * Orders nodes according to their center coordinates.
938      */
939     private static class NodeCenterOrder implements Comparator {
940       private final Graph2D graph;
941 
942       /**
943        * Initializes a new <code>NodeCenterOrder</code> instance.
944        */
945       NodeCenterOrder( final Graph2D graph ) {
946         this.graph = graph;
947       }
948 
949       /**
950        * Compares the given nodes according to their center coordinates.
951        */
952       public int compare( final Object o1, final Object o2 ) {
953         final double cx1 = graph.getCenterX((Node) o1);
954         final double cx2 = graph.getCenterX((Node) o2);
955         final int order = Double.compare(cx1, cx2);
956         if (order == 0) {
957           final double cy1 = graph.getCenterY((Node) o1);
958           final double cy2 = graph.getCenterY((Node) o2);
959           return Double.compare(cy1, cy2);
960         } else {
961           return order;
962         }
963       }
964     }
965   }
966 }
967