| CellSpanActionFactory.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 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