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.view.DemoBase;
31  import y.io.GraphMLIOHandler;
32  import y.view.EditMode;
33  import y.view.Graph2D;
34  import y.view.Graph2DView;
35  import y.view.Graph2DViewActions;
36  import y.view.MoveSelectionMode;
37  import y.view.hierarchy.HierarchyManager;
38  import y.view.tabular.TableGroupNodeRealizer;
39  import y.view.tabular.TableGroupNodeRealizer.Column;
40  import y.view.tabular.TableGroupNodeRealizer.Row;
41  import y.view.tabular.TableGroupNodeRealizer.Table;
42  
43  import java.awt.Color;
44  import java.awt.EventQueue;
45  import java.util.ArrayList;
46  import java.util.Collection;
47  import java.util.Collections;
48  import java.util.Comparator;
49  import java.util.HashMap;
50  import java.util.HashSet;
51  import java.util.Iterator;
52  import java.util.Locale;
53  import java.util.Map;
54  import javax.swing.Action;
55  import javax.swing.ActionMap;
56  import javax.swing.JMenu;
57  import javax.swing.JMenuBar;
58  import javax.swing.JToggleButton;
59  import javax.swing.JToolBar;
60  
61  /**
62   * Demonstrates {@link y.layout.hierarchic.IncrementalHierarchicLayouter}'s
63   * support for multi-cells in {@link y.layout.grid.PartitionGrid}s.
64   * <p>
65   * Multi-cells impose less restrictions on node placement than normal cells:
66   * A node that belongs to a multi-cell may be placed in each of the multi-cell's
67   * columns and rows.
68   * </p><p>
69   * A new cell span may be created by dragging the mouse across the cells to
70   * combine while holding down <code>CTRL</code>.
71   * An existing span may be removed by dragging the mouse across the combined
72   * cells while holding down <code>CTRL</code> and <code>ALT</code>.
73   * </p>
74   * 
75   */
76  public class CellSpanLayoutDemo extends DemoBase {
77    // registers the configuration used for cell designer table nodes
78    static {
79      CellSpanRealizerFactory.initConfigurations();
80    }
81  
82    /**
83     * Initializes a new <code>CellSpanLayoutDemo</code> instance.
84     * Displays a sample diagram by default.
85     */
86    public CellSpanLayoutDemo() {
87      this(null);
88    }
89  
90    /**
91     * Initializes a new <code>CellSpanLayoutDemo</code> instance.
92     * Displays sample 1 and the documentation referenced by the given file path.
93     * @param helpFilePath the file path for the HTML documentation to display. 
94     */
95    public CellSpanLayoutDemo( final String helpFilePath ) {
96      initGraph(view.getGraph2D());
97      addHelpPane(helpFilePath);
98    }
99  
100   /**
101    * Creates a new {@link HierarchyManager} for the displayed graph.
102    */
103   protected void initialize() {
104     new HierarchyManager(view.getGraph2D());
105   }
106 
107   /**
108    * Creates a custom delete selection action that prevents table nodes from
109    * being deleted.
110    */
111   protected Action createDeleteSelectionAction() {
112     return CellSpanActionFactory.newDeleteSelection(view);
113   }
114 
115   /**
116    * Registers keyboard actions.
117    * Overwritten to remove grouping related keyboard shortcuts.
118    * This demo requires exactly two hierarchy levels with a single top-level
119    * table node and an arbitrary number of normal child nodes.
120    * With grouping keyboard shortcuts, this assumption would be easily violated.
121    */
122   protected void registerViewActions() {
123     super.registerViewActions();
124 
125     final ActionMap amap = view.getCanvasComponent().getActionMap();
126     if (amap != null) {
127       final Object[] keys = {
128           Graph2DViewActions.CLOSE_GROUPS,
129           Graph2DViewActions.OPEN_FOLDERS,
130           Graph2DViewActions.GROUP_SELECTION,
131           Graph2DViewActions.FOLD_SELECTION,
132           Graph2DViewActions.UNGROUP_SELECTION,
133           Graph2DViewActions.UNFOLD_SELECTION,
134       };
135       for (int i = 0; i < keys.length; ++i) {
136         amap.remove(keys[i]);
137       }
138     }
139   }
140 
141   /**
142    * Creates a custom mode for interactive editing.
143    * This custom mode ...
144    * <ul>
145    * <li>
146    *   ... uses marquee selection with <code>CTRL</code> pressed to color
147    *   table cells
148    * </li><li>
149    *   ... uses marquee selection with <code>CTRL</code> and <code>ALT</code>
150    *   pressed to reset the color of table cells
151    * </li><li>
152    *   ... provides a custom context menu with actions for creating and
153    *   removing table columns and rows 
154    * </li><li>
155    *   ... prevents node creation outside of group/table nodes
156    * </li>
157    * </ul>
158    */
159   protected EditMode createEditMode() {
160     final EditMode editMode = configureEditMode(
161             CellSpanControllerFactory.newCellEditMode());
162 
163     // prevents nodes from being moved out of the demo's table node
164     final MoveSelectionMode msm = new MoveSelectionMode();
165     msm.setGroupReassignmentEnabled(false);
166     editMode.setMoveSelectionMode(msm);
167 
168     // enable resizing table columns and rows
169     editMode.getMouseInputMode().setNodeSearchingEnabled(true);
170 
171     // enable node creation when clicking on a table/group node
172     // necessary because CellEditMode prevents node creation when clicking
173     // on empty space
174     editMode.setChildNodeCreationEnabled(true);
175 
176     return editMode;
177   }
178 
179   /**
180    * Creates a {@link GraphMLIOHandler} that supports reading/writing
181    * the individual background colors of table nodes stored in
182    * {@link CellColorManager} instances.
183    */
184   protected GraphMLIOHandler createGraphMLIOHandler() {
185     return CellSpanIoSupport.configure(super.createGraphMLIOHandler());
186   }
187 
188   /**
189    * Turns off clipboard support.
190    * This demo assumes a single top-level table node. With clipboard support,
191    * this assumption would be easily violated.
192    */
193   protected boolean isClipboardEnabled() {
194     return false;
195   }
196 
197   /**
198    * Provides controls for displaying different sample diagrams.
199    */
200   protected JMenuBar createMenuBar() {
201     final JMenu jm = new JMenu("Samples");
202     jm.add(CellSpanActionFactory.newSampleAction(
203             "Sample 1", this, "resource/CellSpanLayoutDemoS01.graphml"));
204     jm.add(CellSpanActionFactory.newSampleAction(
205             "Sample 2", this, "resource/CellSpanLayoutDemoS02.graphml"));
206     jm.add(CellSpanActionFactory.newSampleAction(
207             "Sample 3", this, "resource/CellSpanLayoutDemoS03.graphml"));
208     jm.add(CellSpanActionFactory.newSampleAction(
209             "Sample 4", this, "resource/CellSpanLayoutDemoS04.graphml"));
210     jm.add(CellSpanActionFactory.newSampleAction(
211             "Sample 5", this, "resource/CellSpanLayoutDemoS05.graphml"));
212 
213     final JMenuBar jmb = super.createMenuBar();
214     jmb.add(jm);
215     return jmb;
216   }
217 
218   /**
219    * Provides controls for switching from design to diagram and vice versa.
220    * In design mode, the tabular cell structure of the diagram may be modified.
221    * In diagram mode, the previously defined cell structure is laid out. 
222    */
223   protected JToolBar createToolBar() {
224     final JToolBar jtb = super.createToolBar();
225     jtb.addSeparator();
226 
227     // use two toggle buttons which enable/disable each other to signal clearly
228     // that it is possible to switch between laid out diagram and design view
229     final JToggleButton tb1 = new JToggleButton();
230     final JToggleButton tb2 = new JToggleButton();
231     tb1.setAction(CellSpanActionFactory.newSwitchViewStateAction("Diagram", view, tb2));
232     jtb.add(tb1);
233     tb2.setSelected(true);
234     tb2.setAction(CellSpanActionFactory.newSwitchViewStateAction("Design", view, tb1));
235     tb2.setEnabled(false);
236     jtb.add(tb2);
237     return jtb;
238   }
239 
240   /**
241    * Loads graph data from the resource with the given name.
242    * Overwritten for access from {@link CellSpanActionFactory}.
243    * @param resource the path name of the resource to load.
244    */
245   protected void loadGraph( final String resource ) {
246     super.loadGraph(resource);
247   }
248 
249   /**
250    * Returns the main diagram view.
251    * Exists for access from {@link CellSpanActionFactory}.
252    * @return the main diagram view.
253    */
254   protected Graph2DView getView() {
255     return view;
256   }
257 
258   /**
259    * Displays a sample diagram.
260    */
261   private void initGraph(final Graph2D graph) {
262     graph.clear();
263     loadGraph("resource/CellSpanLayoutDemoS01.graphml");
264     getUndoManager().resetQueue();
265   }
266 
267   public static void main(String[] args) {
268     EventQueue.invokeLater(new Runnable() {
269       public void run() {
270         Locale.setDefault(Locale.ENGLISH);
271         initLnF();
272         (new CellSpanLayoutDemo("resource/cellspanlayouthelp.html")).start();
273       }
274     });
275   }
276 
277 
278   /**
279    * Returns the graph holding the given table structure.
280    * @return the graph holding the given table structure.
281    */
282   static Graph2D getGraph2D( final Table table ) {
283     return (Graph2D) table.getRealizer().getNode().getGraph();
284   }
285 
286 
287   /**
288    * Stores the background color for each table cell.
289    * Provides methods for managing stored colors and choosing new colors.
290    * Background colors are mapped to partition cell spans in class
291    * {@link CellSpanActionFactory.CellLayoutConfigurator}.
292    * <p>
293    * Instances of this class are stored as
294    * {@link y.view.GenericNodeRealizer#setUserData(Object) user data} in
295    * {@link TableGroupNodeRealizer} instances and are copied from one realizer
296    * instance to another on undo/redo, clipboard operations, etc. Since
297    * <code>TableGroupNodeRealizer</code>'s user data copying is inherited from
298    * its super class {@link y.view.GenericNodeRealizer}, user data copying
299    * happens before table structure copying. Consequently, storing references
300    * to columns and rows of a <code>TableGroupNodeRealizer</code> does not work
301    * for copy operations. For this reason, cell to background mappings in this
302    * class are based on column and row indices.
303    * </p>
304    */
305   static final class CellColorManager {
306     /** All possible background colors. */
307     private static final Color[] COLORS = newColors();
308 
309 
310     /**
311      * Stores the background color for each cell.
312      * Cells are stored as pairs of column index and row index. This is
313      * necessary, to be able to copy <code>CellColorManager</code> instances
314      * from one {@link TableGroupNodeRealizer} instance to another.
315      */
316     private final Map data;
317 
318     /**
319      * Initializes a new <code>CellColorManager</code> instance with no
320      * background colors.
321      */
322     CellColorManager() {
323       data = new HashMap();
324     }
325 
326     /**
327      * Initializes a new <code>CellColorManager</code> instance as copy of
328      * the given prototype instance.
329      */
330     CellColorManager( final CellColorManager prototype ) {
331       data = new HashMap(prototype.data);
332     }
333 
334     /**
335      * Returns a background color that is not yet used in this instance.
336      * @return a background color that is not yet used in this instance or
337      * <code>null</code> if all background colors are already in use.
338      */
339     Color nextUnused() {
340       final HashSet used = new HashSet(data.values());
341       Color color = null;
342       for (int i = 0; i < COLORS.length; ++i) {
343         if (!used.contains(COLORS[i])) {
344           color = COLORS[i];
345           break;
346         }
347       }
348       return color;
349     }
350 
351     /**
352      * Returns the background color for the given cell.
353      * @param col the horizontal position of the cell.
354      * @param row the vertical position of the cell.
355      * @return the background color for the given cell or <code>null</code>
356      * if the given cell has no background color.
357      */
358     Color getCellColor( final Column col, final Row row ) {
359       if (col != null && row != null) {
360         return (Color) data.get(new CellKey(col, row));
361       } else {
362         return null;
363       }
364     }
365 
366     /**
367      * Sets the background color for the given cell.
368      * @param col the horizontal position of the cell.
369      * @param row the vertical position of the cell.
370      * @param color the new background color for the given cell. May be
371      * <code>null</code>.
372      */
373     void setCellColor( final Column col, final Row row, final Color color ) {
374       if (col != null && row != null) {
375         if (color == null) {
376           data.remove(new CellKey(col, row));
377         } else {
378           data.put(new CellKey(col, row), color);
379         }
380       }
381     }
382 
383     /**
384      * Sets the background color for the given cell span.
385      * @param span the cell span for which to set the background color.
386      * @param color the new background color for the given cell. May be
387      * <code>null</code>.
388      */
389     void setCellColor( final Span span, final Color color ) {
390       if (color == null) {
391         for (int i = span.minCol, n = span.maxCol + 1; i < n; ++i) {
392           for (int j = span.minRow, m = span.maxRow + 1; j < m; ++j) {
393             data.remove(new CellKey(i, j));
394           }
395         }
396       } else {
397         for (int i = span.minCol, n = span.maxCol + 1; i < n; ++i) {
398           for (int j = span.minRow, m = span.maxRow + 1; j < m; ++j) {
399             data.put(new CellKey(i, j), color);
400           }
401         }
402       }
403     }
404 
405     /**
406      * Adjusts the column indices in this manager's cell to color mappings
407      * for added/removed columns.
408      * @param column the column that was added or will be removed.
409      * @param up if <code>true</code>, indices will be adjusted for added
410      * columns otherwise for removed columns.
411      */
412     void shift( final Column column, final boolean up ) {
413       shiftImpl(column.getIndex(), false, up);
414     }
415 
416     /**
417      * Adjusts the row indices in this manager's cell to color mappings
418      * for added/removed rows.
419      * @param row the row that was added or will be removed.
420      * @param up if <code>true</code>, indices will be adjusted for added
421      * rows otherwise for removed rows.
422      */
423     void shift( final Row row, final boolean up ) {
424       shiftImpl(row.getIndex(), true, up);
425     }
426 
427     /**
428      * Adjusts the indices in this manager's cell to color mappings
429      * for added/removed columns or rows.
430      * @param idx the index of the column or row that was added or will be
431      * removed.
432      * @param row if <code>true</code>, indices will be adjusted for rows;
433      * otherwise for columns.
434      * @param up if <code>true</code> indices will be adjusted for added
435      * columns/rows; otherwise for removed columns/rows.
436      */
437     private void shiftImpl( final int idx, final boolean row, final boolean up ) {
438       final ArrayList keys = new ArrayList(data.keySet());
439       Collections.sort(keys, new CellComparator(row, up));
440       final int offset = up ? 1 : -1;
441       for (Iterator it = keys.iterator(); it.hasNext();) {
442         final CellKey key = (CellKey) it.next();
443         final int value = row ? key.row : key.column;
444         if (value >= idx) {
445           final Object color = data.remove(key);
446           if (up || value > idx) {
447             if (row) {
448               data.put(new CellKey(key.column, key.row + offset), color);
449             } else {
450               data.put(new CellKey(key.column + offset, key.row), color);
451             }
452           }
453         }
454       }
455     }
456 
457     /**
458      * Returns the <code>CellColorManager</code> instance that stores the
459      * background colors for the cells in the given table. 
460      */
461     static CellColorManager getInstance( final Table table ) {
462       final TableGroupNodeRealizer tgnr = table.getRealizer();
463       return (CellColorManager) tgnr.getUserData();
464     }
465 
466     /**
467      * Creates a small set of colors for use as cell background colors.
468      */
469     private static Color[] newColors() {
470       return new Color[] {
471         new Color(192,   0,   0),
472         new Color(255, 102,   0),
473         new Color(251, 176,   7),
474         new Color(  0, 153,  57),
475         new Color(  0, 137, 205),
476 
477         new Color(238,  53,  81),
478         new Color(255, 134,  56),
479         new Color(242, 228,  21),
480         new Color(149, 209,  39),
481         new Color( 17, 175, 252),
482       };
483 
484       // alternative color set
485 //      return new Color[] {
486 //        new Color(181, 137,   0),
487 //        new Color(203,  75,  22),
488 //        new Color(220,  50,  47),
489 //        new Color(211,  54, 130),
490 //        new Color(108, 113, 196),
491 //        new Color( 38, 139, 210),
492 //        new Color( 42, 161, 152),
493 //        new Color(133, 153,   0),
494 //      };      
495     }
496   }
497 
498   /**
499    * Orders {@link CellKey} instances by their column or row index.
500    */
501   private static final class CellComparator implements Comparator {
502     private final boolean row;
503     private final int lessThan;
504     private int greaterThan;
505 
506     CellComparator( final boolean row, final boolean descending ) {
507       this.row = row;
508       lessThan = descending ? 1 : -1;
509       greaterThan = descending ? -1 : 1;
510     }
511 
512     public int compare( final Object o1, final Object o2 ) {
513       final int v1 = getValue((CellKey) o1);
514       final int v2 = getValue((CellKey) o2);
515       if (v1 < v2) {
516         return lessThan;
517       } else if (v1 > v2) {
518         return greaterThan;
519       } else {
520         return 0;
521       }
522     }
523 
524     int getValue( final CellKey key ) {
525       return row ? key.row : key.column;
526     }
527   }
528 
529   /**
530    * Represents a table cell by storing the cell's column and row indices.
531    */
532   private static final class CellKey {
533     // The horizontal position of the cell.
534     private final int column;
535     // The vertical position of the cell.
536     private final int row;
537 
538     CellKey( final Column column, final Row row ) {
539       this(column.getIndex(), row.getIndex());
540     }
541 
542     CellKey( final int column, final int row ) {
543       this.column = column;
544       this.row = row;
545     }
546 
547     public boolean equals( final Object o ) {
548       if (this == o) return true;
549       if (o == null || getClass() != o.getClass()) return false;
550 
551       final CellKey cellKey = (CellKey) o;
552 
553       if (column != cellKey.column) return false;
554       if (row != cellKey.row) return false;
555 
556       return true;
557     }
558 
559     public int hashCode() {
560       int result = column;
561       result = 31 * result + row;
562       return result;
563     }
564 
565     public String toString() {
566       return "[c=" + column + ";" + "r=" + row + "]";
567     }
568   }
569 
570   /**
571    * Represents a table cell by storing the cell's column and row instances.
572    */
573   static final class Cell {
574     // The horizontal position of the cell.
575     private final Column column;
576     // The vertical position of the cell.
577     private final Row row;
578 
579     Cell( final Column column, final Row row ) {
580       this.column = column;
581       this.row = row;
582     }
583 
584     public boolean equals( final Object o ) {
585       if (this == o) return true;
586       if (o == null || getClass() != o.getClass()) return false;
587 
588       final Cell cell = (Cell) o;
589 
590       if (!column.equals(cell.column)) return false;
591       if (!row.equals(cell.row)) return false;
592 
593       return true;
594     }
595 
596     public int hashCode() {
597       int result = column.hashCode();
598       result = 31 * result + row.hashCode();
599       return result;
600     }
601 
602     public String toString() {
603       return "[c=" + column.getIndex() + ";" + "r=" + row.getIndex() + "]";
604     }
605   }
606 
607   /**
608    * Represents a "rectangular", two-dimensional cell range.
609    */
610   static final class Span {
611     // The index of the left-most column in this span. 
612     final int minCol;
613     // The index of the right-most column in this span. 
614     final int maxCol;
615     // The index of the top-most row in this span. 
616     final int minRow;
617     // The index of the bottom-most row in this span. 
618     final int maxRow;
619 
620     private Span(
621             final int minCol, final int maxCol,
622             final int minRow, final int maxRow
623     ) {
624       this.minCol = minCol;
625       this.maxCol = maxCol;
626       this.minRow = minRow;
627       this.maxRow = maxRow;
628     }
629 
630     /**
631      * Determines whether or not the given cell is included in this cell span.
632      * @param col the column (index) of the cell to check.
633      * @param row the row (index) of the cell to check.
634      * @return <code>true</code> if the given cell is included in this cell
635      * span; <code>false</code> otherwise.
636      */
637     boolean contains( final Column col, final Row row ) {
638       final int cIdx = col.getIndex();
639       final int rIdx = row.getIndex();
640       return minCol <= cIdx && cIdx <= maxCol &&
641              minRow <= rIdx && rIdx <= maxRow;
642     }
643 
644     /**
645      * Determines whether or not this cell span contains all the cells of the
646      * given span.
647      * @param span the cell span to check.
648      * @return <code>true</code> if this cell span contains all the cells of the
649      * given span; <code>false</code> otherwise.
650      */
651     boolean contains( final Span span ) {
652       return minCol <= span.minCol && span.maxCol <= maxCol &&
653              minRow <= span.minRow && span.maxRow <= maxRow;
654     }
655 
656     /**
657      * Creates a new cell span instance by finding the greatest cell span that
658      * includes the given cell and has only cells of the given color.
659      * Note, the color of the given cell is ignored and may differ from the
660      * given color.
661      * @param table the table structure to check.
662      * @param col the column (index) of the starting cell.
663      * @param row the row (index) of the starting cell.
664      * @param color the color of the cells to be included in the span.
665      * @return a new cell span instance.
666      */
667     static Span find(
668             final Table table, final Column col, final Row row,
669             final Color color
670     ) {
671       final CellColorManager manager = CellColorManager.getInstance(table);
672 
673       int minCIdx = col.getIndex();
674       int maxCIdx = minCIdx;
675       int minRIdx = row.getIndex();
676       int maxRIdx = minRIdx;
677 
678       // view the table as graph structure with each cell representing a node
679       // and consider cell A to be connected to cell B if 
680       //  B is directly above/below/to the left/to the right of A and
681       //  B has the given color
682       // with this assumption the below code is essentially the well-known
683       // graph algorithm depth first search 
684       final HashSet seen = new HashSet();
685       final ArrayList stack = new ArrayList();
686       stack.add(new Cell(col, row));
687       while (!stack.isEmpty()) {
688         final Cell cell = (Cell) stack.remove(stack.size() - 1);
689         if (seen.add(cell)) {
690           final Column c = cell.column;
691           final int cIdx = c.getIndex();
692           final Row r = cell.row;
693           final int rIdx = r.getIndex();
694 
695           if (rIdx > 0) {
696             final Row north = table.getRow(rIdx - 1);
697             if (color.equals(manager.getCellColor(c, north))) {
698               stack.add(new Cell(c, north));
699 
700               if (minRIdx > rIdx - 1) {
701                 minRIdx = rIdx - 1;
702               }
703             }
704           }
705           if (cIdx > 0) {
706             final Column west = table.getColumn(cIdx - 1);
707             if (color.equals(manager.getCellColor(west, r))) {
708               stack.add(new Cell(west, r));
709 
710               if (minCIdx > cIdx - 1) {
711                 minCIdx = cIdx - 1;
712               }
713             }
714           }
715           if (rIdx + 1 < table.rowCount()) {
716             final Row south = table.getRow(rIdx + 1);
717             if (color.equals(manager.getCellColor(c, south))) {
718               stack.add(new Cell(c, south));
719 
720               if (maxRIdx < rIdx + 1) {
721                 maxRIdx = rIdx + 1;
722               }
723             }
724           }
725           if (cIdx + 1 < table.columnCount()) {
726             final Column east = table.getColumn(cIdx + 1);
727             if (color.equals(manager.getCellColor(east, r))) {
728               stack.add(new Cell(east, r));
729 
730               if (maxCIdx < cIdx + 1) {
731                 maxCIdx = cIdx + 1;
732               }
733             }
734           }
735         }
736       }
737 
738       return new Span(minCIdx, maxCIdx, minRIdx, maxRIdx);
739     }
740 
741     /**
742      * Creates a new cell span instance that includes the given cells.
743      * @param cells a collection of {@link Cell} instances.
744      * @return a new cell span instance.
745      */
746     static Span span( final Collection cells ) {
747       final Iterator it = cells.iterator();
748       final Cell first = (Cell) it.next();
749 
750       int minCIdx = first.column.getIndex();
751       int maxCIdx = minCIdx;
752       int minRIdx = first.row.getIndex();
753       int maxRIdx = minRIdx;
754 
755       while (it.hasNext()) {
756         final Cell cell = (Cell) it.next();
757 
758         final int cIdx = cell.column.getIndex();
759         final int rIdx = cell.row.getIndex();
760         if (minCIdx > cIdx) {
761           minCIdx = cIdx;
762         }
763         if (maxCIdx < cIdx) {
764           maxCIdx = cIdx;
765         }
766         if (minRIdx > rIdx) {
767           minRIdx = rIdx;
768         }
769         if (maxRIdx < rIdx) {
770           maxRIdx = rIdx;
771         }
772       }
773 
774       return new Span(minCIdx, maxCIdx, minRIdx, maxRIdx);
775     }
776 
777     /**
778      * Creates a new cell span instance.
779      * @param minCol the index of the left-most column in this span. 
780      * @param maxCol the index of the right-most column in this span.
781      * @param minRow the index of the top-most row in this span.
782      * @param maxRow the index of the bottom-most row in this span.
783      * @return a new cell span instance.
784      */
785     static Span manual(
786             final int minCol, final int maxCol,
787             final int minRow, final int maxRow
788     ) {
789       return new Span(minCol, maxCol, minRow, maxRow);
790     }
791   }
792 }
793