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.view.mindmap;
29  
30  import demo.view.DemoBase;
31  import y.algo.GraphConnectivity;
32  import y.algo.Trees;
33  import y.base.Command;
34  import y.base.Edge;
35  import y.base.EdgeCursor;
36  import y.base.EdgeMap;
37  import y.base.Graph;
38  import y.base.Node;
39  import y.base.NodeMap;
40  import y.base.WrongGraphStructure;
41  import y.io.GraphMLIOHandler;
42  import y.io.IOHandler;
43  import y.io.graphml.KeyScope;
44  import y.io.graphml.KeyType;
45  import y.view.ArcEdgeRealizer;
46  import y.view.Arrow;
47  import y.view.BezierPathCalculator;
48  import y.view.DefaultBackgroundRenderer;
49  import y.view.EdgeLabel;
50  import y.view.EditMode;
51  import y.view.GenericEdgeRealizer;
52  import y.view.GenericNodeRealizer;
53  import y.view.Graph2D;
54  import y.view.Graph2DView;
55  import y.view.Graph2DViewMouseWheelZoomListener;
56  import y.view.LineType;
57  import y.view.ShapeNodeRealizer;
58  import y.view.SmartEdgeLabelModel;
59  import y.view.YRenderingHints;
60  
61  import javax.swing.AbstractAction;
62  import javax.swing.Action;
63  import javax.swing.JFileChooser;
64  import javax.swing.JMenu;
65  import javax.swing.JMenuBar;
66  import javax.swing.JMenuItem;
67  import javax.swing.filechooser.FileFilter;
68  import java.awt.Color;
69  import java.awt.EventQueue;
70  import java.awt.Graphics2D;
71  import java.awt.Rectangle;
72  import java.awt.event.ActionEvent;
73  import java.awt.event.MouseWheelListener;
74  import java.io.File;
75  import java.io.IOException;
76  import java.net.MalformedURLException;
77  import java.net.URL;
78  import java.util.ArrayList;
79  import java.util.Iterator;
80  import java.util.Locale;
81  import java.util.Map;
82  
83  /**
84   * Demonstrates how to build a mind mapping tool with yFiles for Java.
85   * The design follows the suggestions of Tony Buzan, the inventor of mind maps.
86   * <p>
87   * You can add and delete items, move items, collapse and expand children,
88   * change item colors and label texts, choose different icons and add
89   * cross-references between independent items.
90   * </p><p>
91   * User Interaction
92   * </p>
93   * <dl>
94   * <dt>Adding items</dt>
95   * <dd>
96   *   Either move the mouse over an existing item and click the green plus symbol
97   *   of the overlay controls, or select an existing item and type
98   *   <code>INSERT</code> to add a child item and <code>ENTER</code> to add
99   *   a sibling item.
100  * </dd>
101  * <dt>Deleting items</dt>
102  * <dd>
103  *   Either move the mouse over an existing item and click the red minus symbol
104  *   of the overlay controls, or select an existing item and type
105  *   <code>DELETE</code> or <code>BACKSPACE</code> to remove the selected item
106  *   and all of its children.
107  * </dd>
108  * <dt>Expanding and collapsing items</dt>
109  * <dd>
110  *   Either click on the blue arrow symbol below an item with children or
111  *   select an item with children and type <code>NUMPAD -</code> to collapse the
112  *   item or <code>NUMPAD +</code> to expand the previously collapsed item. 
113  * </dd>
114  * <dt>Creating cross-references</dt>
115  * <dd>
116  *   Either move the mouse over an existing item and click the light blue arrow
117  *   symbol or select an existing item, then press and hold <code>SHIFT</code>
118  *   and click on another item or move the mouse over an existing item, press
119  *   and hold <code>SHIFT</code>, then drag the mouse to another item.
120  * </dd>
121  * <dt>Editing text</dt>
122  * <dd>
123  *   Either double-click an existing item or select an existing item and type
124  *   <code>F2</code>.
125  *   Text for cross-references may be added/edited the same way.
126  * </dd>
127  * <dt>Changing colors</dt>
128  * <dd>
129  *   Move the mouse over an existing item and click the multi-colored symbol.
130  * </dd>
131  * <dt>Changing icons</dt>
132  * <dd>
133  *   Move the mouse over an existing item and click the smiley symbol.
134  *   Choosing the crossed out box removes a previously selected icon.
135  * </dd>
136  * <dt>Changing the mind map structure</dt>
137  * <dd>
138  *   Dragging an existing item close to another item will reassign it to a
139  *   new parent item.
140  *   Dragging an existing item away from all other items will delete it once
141  *   the mouse is released.
142  * </dd>
143  * <dt>Loading and Saving mind maps</dt>
144  * <dd>
145  *   Mind maps may be loaded from and saved as GraphML and FreeMind's native
146  *   XML format.
147  * </dd>
148  * </dl>
149  */
150 public class MindMapDemo extends DemoBase {
151   public static void main(String[] args) {
152     EventQueue.invokeLater(new Runnable() {
153       public void run() {
154         Locale.setDefault(Locale.ENGLISH);
155         initLnF();
156         (new MindMapDemo("resource/mindmaphelp.html")).start("MindMapDemo");
157       }
158     });
159   }
160 
161   public MindMapDemo() {
162     this(null);
163   }
164 
165   public MindMapDemo(final String helpFilePath) {
166     new CollapseButton.Handler(view);
167 
168     loadFile(getResource("resource/hobbies.graphml"));
169 
170     KeyboardHandling.setKeyActions(view);
171     configureRendering();
172     //restrict zoom level
173     final Graph2DViewMouseWheelZoomListener zl = new Graph2DViewMouseWheelZoomListener();
174     zl.setMaximumZoom(2.5);
175     zl.setMinimumZoom(0.08);
176     zl.setCenterZooming(false);
177     //prevent scrolling when max/min zoom reached
178     MouseWheelListener[] mouseWheelListeners = view.getCanvasComponent().getMouseWheelListeners();
179     view.getCanvasComponent().removeMouseWheelListener(mouseWheelListeners[0]);
180     zl.addToCanvas(view);
181 
182     addHelpPane(helpFilePath);
183 
184     //nicer background
185     configureBackgroundRenderer();
186     getUndoManager().resetQueue();
187   }
188 
189   protected Graph2DView createGraphView() {
190     final Graph2DView view = new Graph2DView(ViewModel.instance.graph);
191     view.setFitContentOnResize(true);
192     return view;
193   }
194 
195   /**
196    * creates and sets a BackgroundRenderer, that draws a college block like background.
197    */
198   private void configureBackgroundRenderer() {
199     view.setBackgroundRenderer(new DefaultBackgroundRenderer(view) {
200       public void paint( Graphics2D gfx, int x, int y, int w, int h ) {
201         super.paint(gfx, x, y, w, h);
202         final int SQUARE_SIZE = 25;
203         final int HOLE_SIZE = 30;
204         final Rectangle bounds = view.getBounds();
205 
206         gfx.setColor(new Color(174, 174, 174));
207 
208         undoWorldTransform(gfx);
209         final int width = 5;
210         gfx.fillRect(7 * SQUARE_SIZE - (width / 2), 0, width, bounds.y + bounds.height);
211         for (int i = 0; i < bounds.height; i += SQUARE_SIZE) {
212           gfx.drawLine(0, i, bounds.width + bounds.x, i);
213         }
214         for (int i = 0; i < bounds.width; i += SQUARE_SIZE) {
215           gfx.drawLine(i, 0, i, bounds.y + bounds.height);
216         }
217         double height = bounds.height / 8;
218 
219         gfx.setColor(Color.WHITE);
220 
221         gfx.fillOval((int) (1.5 * SQUARE_SIZE), (int) height, HOLE_SIZE, HOLE_SIZE);
222         gfx.fillOval((int) (1.5 * SQUARE_SIZE), (int) (height * 3), HOLE_SIZE, HOLE_SIZE);
223         gfx.fillOval((int) (1.5 * SQUARE_SIZE), (int) (height * 5), HOLE_SIZE, HOLE_SIZE);
224         gfx.fillOval((int) (1.5 * SQUARE_SIZE), (int) (height * 7), HOLE_SIZE, HOLE_SIZE);
225 
226         gfx.setColor(new Color(167, 167, 167));
227         gfx.drawOval((int) (1.5 * SQUARE_SIZE), (int) height, HOLE_SIZE, HOLE_SIZE);
228         gfx.drawOval((int) (1.5 * SQUARE_SIZE), (int) (height * 3), HOLE_SIZE, HOLE_SIZE);
229         gfx.drawOval((int) (1.5 * SQUARE_SIZE), (int) (height * 5), HOLE_SIZE, HOLE_SIZE);
230         gfx.drawOval((int) (1.5 * SQUARE_SIZE), (int) (height * 7), HOLE_SIZE, HOLE_SIZE);
231 
232         redoWorldTransform(gfx);
233       }
234     });
235   }
236 
237   /**
238    * Configures the rendering order for the mind map.
239    * For mind maps, the custom rendering order
240    * <ol>
241    * <li>normal edges,</li>
242    * <li>nodes,</li>
243    * <li>cross-reference edges, and</li>
244    * <li>node labels</li>
245    * </ol>
246    * is used.
247    */
248   private void configureRendering() {
249     view.setGraph2DRenderer(new MindMapRenderer());
250 
251     // turns off simplified sloppy painting for ShapeNodeRealizer
252     // ShapeNodeRealizer is used to represent the root item of the mind map
253     view.getRenderingHints().put(
254         ShapeNodeRealizer.KEY_SLOPPY_RECT_PAINTING,
255         ShapeNodeRealizer.VALUE_SLOPPY_RECT_PAINTING_OFF);
256 
257     // turns off node label painting in node realizers
258     // MindMapRenderer handles label painting
259     view.getRenderingHints().put(
260             YRenderingHints.KEY_NODE_LABEL_PAINTING,
261             YRenderingHints.VALUE_NODE_LABEL_PAINTING_OFF);
262   }
263 
264   /**
265    * configure custom node and edge realizers
266    */
267   protected void configureDefaultRealizers() {
268     final GenericNodeRealizer.Factory nodeFactory = GenericNodeRealizer.getFactory();
269     final Map nodeMap = nodeFactory.createDefaultConfigurationMap();
270     nodeMap.put(GenericNodeRealizer.Painter.class, new MindMapNodePainter());
271     nodeFactory.addConfiguration("MindMapUnderline", nodeMap);
272     final GenericNodeRealizer gnr = new GenericNodeRealizer();
273     gnr.setConfiguration("MindMapUnderline");
274     gnr.setHeight(24);
275     view.getGraph2D().setDefaultNodeRealizer(gnr);
276 
277     final GenericEdgeRealizer.Factory edgeFactory = GenericEdgeRealizer.getFactory();
278     final Map edgeMap = edgeFactory.createDefaultConfigurationMap();
279     edgeMap.put(GenericEdgeRealizer.PathCalculator.class, new BezierPathCalculator());
280     edgeMap.put(GenericEdgeRealizer.Painter.class,new GradientEdgePainter());
281         edgeFactory.addConfiguration("BezierGradientEdge", edgeMap);
282     final GenericEdgeRealizer edgeRealizer = new GenericEdgeRealizer();
283     edgeRealizer.setConfiguration("BezierGradientEdge");
284     final ArcEdgeRealizer realizer = new ArcEdgeRealizer(ArcEdgeRealizer.FIXED_RATIO);
285 
286     final EdgeLabel label = realizer.getLabel();
287     final SmartEdgeLabelModel model = new SmartEdgeLabelModel();
288     final SmartEdgeLabelModel.ModelParameter modelParameter = new SmartEdgeLabelModel.ModelParameter(0, 1, 0, false,
289         SmartEdgeLabelModel.ModelParameter.LEFT, 0);
290     label.setLabelModel(model,modelParameter);
291     label.setBackgroundColor(Color.WHITE);
292     realizer.setTargetArrow(Arrow.DELTA);
293     realizer.setLineColor(MindMapUtil.CROSS_EDGE_COLOR);
294     realizer.setLineType(LineType.LINE_7);
295     view.getGraph2D().setDefaultEdgeRealizer(realizer);
296   }
297 
298   /**
299    * Registers view modes to handle mouse interaction.
300    */
301   protected void registerViewModes() {
302     // registers HoverButton internal view mode
303     // said mode has to be the first mode that is registered to ensure that
304     // it receives events before all other view modes  
305     new HoverButton(view);
306 
307     final EditMode editMode = new MoveNodeMode(getUndoManager());
308     editMode.getMouseInputMode().setDrawableSearchingEnabled(true);
309     editMode.allowMouseInput(true);
310     editMode.allowBendCreation(false);
311     editMode.allowMovePorts(false);
312     editMode.allowNodeCreation(false);
313     editMode.allowEdgeCreation(false);
314     editMode.allowResizeNodes(false);
315     editMode.allowMoveLabels(false);
316     view.addViewMode(editMode);
317     view.addViewMode(new KeyboardHandling.LabelChangeViewMode());
318   }
319 
320   /**
321    * Creates a custom delete selection action that removes all decendant
322    * items along with the selected item.
323    */
324   protected Action createDeleteSelectionAction() {
325     return KeyboardHandling.createDeleteSelectionAction(view);
326   }
327 
328   /**
329    * Saves the current mind map as GraphML or in FreeMind's mind map format.
330    */
331   public class SaveFileAction extends AbstractAction {
332     JFileChooser chooser;
333 
334     public SaveFileAction() {
335       super("Save...");
336       putValue(Action.SHORT_DESCRIPTION, "Save...");
337     }
338 
339     public void actionPerformed(final ActionEvent e) {
340       if (chooser == null) {
341         chooser = new JFileChooser();
342         chooser.setDialogTitle("Save Mindmap...");
343         //only .mm and .graphml files are allowed
344         chooser.setAcceptAllFileFilterUsed(false);
345         chooser.addChoosableFileFilter(new FileFilter() {
346           public boolean accept(File f) {
347             return f.isDirectory() || f.getName().endsWith(".mm");
348           }
349 
350           public String getDescription() {
351             return "FreeMind Mindmap (.mm)";
352           }
353         });
354         chooser.addChoosableFileFilter(new FileFilter() {
355           public boolean accept(File f) {
356             return f.isDirectory() || f.getName().endsWith(".graphml");
357           }
358 
359           public String getDescription() {
360             return "GraphML (.graphml)";
361           }
362         });
363       }
364 
365       if (chooser.showSaveDialog(contentPane) == JFileChooser.APPROVE_OPTION) {
366         final boolean useNativeFormat =
367                 chooser.getFileFilter().getDescription().startsWith("GraphML");
368 
369         String fileName = chooser.getSelectedFile().getAbsolutePath();
370         //enforce file extension
371         if (useNativeFormat) {
372           final String suffix = ".graphml";
373           if (!fileName.toLowerCase().endsWith(suffix)) {
374             fileName += suffix;
375           }
376         } else {
377           final String suffix = ".mm";
378           if (!fileName.toLowerCase().endsWith(suffix)) {
379             fileName += suffix;
380           }
381         }
382 
383         //choose how to save depending on the file extension
384         final IOHandler writer =
385                 useNativeFormat
386                 ? (IOHandler) createGraphMLIOHandler()
387                 : new FreeMindIOHandler();
388         try {
389           writer.write(view.getGraph2D(), fileName);
390         } catch (IOException ioe) {
391           ioe.printStackTrace();
392         }
393       }
394     }
395   }
396 
397   /**
398    * Loads a mind map from GraphML or FreeMind's mind map format.
399    */
400   public class OpenFileAction extends AbstractAction {
401     JFileChooser chooser;
402 
403     public OpenFileAction() {
404       super("Load...");
405       putValue(Action.SHORT_DESCRIPTION, "Load...");
406     }
407 
408     public void actionPerformed(final ActionEvent e) {
409       if (chooser == null) {
410         chooser = new JFileChooser();
411         chooser.setAcceptAllFileFilterUsed(false);
412         chooser.setDialogTitle("Open Mindmap...");
413         chooser.addChoosableFileFilter(new FileFilter() {
414           public boolean accept( final File f ) {
415             return f.isDirectory() ||
416                    f.getName().endsWith(".mm") ||
417                    f.getName().endsWith(".graphml");
418           }
419 
420           public String getDescription() {
421             return "Mindmap (.graphml, .mm)";
422           }
423         });
424       }
425 
426       if (chooser.showOpenDialog(contentPane) == JFileChooser.APPROVE_OPTION) {
427         try {
428           final URL url = chooser.getSelectedFile().toURI().toURL();
429           loadFile(url);
430         } catch (MalformedURLException mue) {
431           mue.printStackTrace();
432         }
433       }
434     }
435   }
436 
437   private void loadFile( final URL resource ) {
438     final ArrayList exceptions = new ArrayList();
439     final ViewModel model = ViewModel.instance;
440 
441     final Graph2D graph = view.getGraph2D();
442     graph.firePreEvent();
443 
444     graph.clear();
445 
446     final boolean useNativeFormat = resource.getFile().toLowerCase().endsWith(".graphml");
447     if (useNativeFormat) {
448       try {
449         createGraphMLIOHandler().read(graph, resource);
450       } catch (IOException ioe) {
451         exceptions.add(ioe);
452       }
453     } else {
454       try {
455         (new FreeMindIOHandler()).read(graph, resource);
456       } catch (IOException ioe) {
457         exceptions.add(ioe);
458       }
459     }
460 
461     if (exceptions.isEmpty()) {
462       try {
463         final Node root = findRoot(graph);
464         model.setRoot(root);
465 
466         if (!useNativeFormat) {
467           MindMapUtil.setRootRealizer(graph, root, graph.getLabelText(root));
468           LayoutUtil.layout(graph);
469         }
470       } catch (WrongGraphStructure wgse) {
471         exceptions.add(wgse);
472       }
473     }
474 
475     if (!exceptions.isEmpty()) {
476       graph.clear();
477       final Node root = graph.createNode();
478       MindMapUtil.setRootRealizer(graph, root, "Mind Map");
479       model.setRoot(root);
480 
481       for (Iterator it = exceptions.iterator(); it.hasNext(); ) {
482         ((Exception) it.next()).printStackTrace();
483       }
484 
485       view.fitContent();
486       view.updateView();
487     } else if (useNativeFormat) {
488       view.fitContent();
489       view.updateView();
490     }
491 
492     graph.firePostEvent();
493     model.clearHiddenCrossReferences();
494     getUndoManager().resetQueue();
495   }
496 
497   /**
498    * Determines the root item in a mind map.
499    * To be considered a mind map, the specified graph has to be a directed tree.
500    * However, additional, non-tree edges are allowed if these edges are
501    * explicitly marked as such, i.e.
502    * {@link ViewModel#isCrossReference(y.base.Edge)} has to return
503    * <code>true</code> for all non-tree edges.
504    * @param graph the mind map whose root item is to be determined.
505    * @return the root item in a mind map.
506    * @throws WrongGraphStructure if the specified graph is not a directed tree.
507    */
508   private Node findRoot( final Graph2D graph ) {
509     if (GraphConnectivity.isConnected(graph)) {
510       final Graph copy = new Graph();
511       final Node[] cNodes = new Node[graph.nodeCount()];
512       for (int i = 0; i < cNodes.length; ++i) {
513         cNodes[i] = copy.createNode();
514       }
515 
516       final ViewModel model = ViewModel.instance;
517       for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
518         final Edge edge = ec.edge();
519         if (!model.isCrossReference(edge)) {
520           copy.createEdge(
521                   cNodes[edge.source().index()],
522                   cNodes[edge.target().index()]);
523         }
524       }
525 
526       if (Trees.isRootedTree(copy)) {
527         final Node cRoot = Trees.getRoot(copy);
528         return graph.getNodeArray()[cRoot.index()];
529       }
530     }
531     throw new WrongGraphStructure("Graph is not a mind map");
532   }
533 
534   protected Action createLoadAction() {
535     return new OpenFileAction();
536   }
537 
538   protected Action createSaveAction() {
539     return new SaveFileAction();
540   }
541 
542   private class OpenExampleAction extends AbstractAction {
543     private String filename;
544 
545     public OpenExampleAction(String filename) {
546       super(filename);
547       this.filename = filename;
548     }
549 
550     public void actionPerformed(ActionEvent e) {
551       loadFile(getResource("resource/" + filename + ".graphml"));
552     }
553   }
554 
555   protected JMenuBar createMenuBar() {
556     JMenuBar menuBar = super.createMenuBar();
557     JMenu menu = menuBar.getMenu(0);
558     JMenuItem item = new JMenuItem(new NewFileAction());
559     menu.add(item,0);
560     menu = new JMenu("Example Mind Maps");
561     menuBar.add(menu);
562     menu.add(new OpenExampleAction("hobbies"));
563     menu.add(new OpenExampleAction("yFiles"));
564     menu.add(new OpenExampleAction("packages"));
565     return menuBar;
566   }
567 
568   private class NewFileAction extends AbstractAction {
569     public NewFileAction() {
570       super("New");
571     }
572 
573     public void actionPerformed(ActionEvent e) {
574       final Graph2D graph = view.getGraph2D();
575       graph.firePreEvent();
576       graph.backupRealizers();
577       graph.clear();
578 
579       final Node root = graph.createNode();
580       MindMapUtil.setRootRealizer(graph, root, "Mind Map");
581       final ViewModel model = ViewModel.instance;
582       getUndoManager().push(new SetRoot(model.getRoot(), root));
583       model.setRoot(root);
584       graph.firePostEvent();
585 
586       view.fitContent();
587       view.updateView();
588     }
589   }
590 
591   private static final class SetRoot implements Command {
592     private final Node oldRoot;
593     private final Node newRoot;
594 
595     SetRoot( final Node oldRoot, final Node newRoot ) {
596       this.oldRoot = oldRoot;
597       this.newRoot = newRoot;
598     }
599 
600     public void execute() {
601     }
602 
603     public void undo() {
604       ViewModel.instance.setRoot(oldRoot);
605     }
606 
607     public void redo() {
608       ViewModel.instance.setRoot(newRoot);
609     }
610   }
611 
612 
613   protected boolean isClipboardEnabled() {
614     return false;
615   }
616 
617 
618   protected GraphMLIOHandler createGraphMLIOHandler() {
619     final GraphMLIOHandler ioHandler = super.createGraphMLIOHandler();
620 
621     final NodeMap leftRightMap = LayoutUtil.getLeftRightMap(ViewModel.instance.graph);
622     ioHandler.getGraphMLHandler().addOutputDataProvider("Mindmap.LeftSide", leftRightMap, KeyScope.NODE, KeyType.BOOLEAN);
623     ioHandler.getGraphMLHandler().addInputDataAcceptor("Mindmap.LeftSide", leftRightMap, KeyScope.NODE, KeyType.BOOLEAN);
624 
625     final EdgeMap crossReferences = LayoutUtil.getCrossReferencesMap(ViewModel.instance.graph);
626     ioHandler.getGraphMLHandler().addOutputDataProvider("Mindmap.CrossReference", crossReferences, KeyScope.EDGE, KeyType.BOOLEAN);
627     ioHandler.getGraphMLHandler().addInputDataAcceptor("Mindmap.CrossReference", crossReferences, KeyScope.EDGE, KeyType.BOOLEAN);
628 
629     return ioHandler;
630   }
631 }
632