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.isometry;
29  
30  import demo.view.DemoBase;
31  import org.w3c.dom.Element;
32  import y.base.DataProvider;
33  import y.base.Edge;
34  import y.base.Node;
35  import y.base.NodeList;
36  import y.geom.YPoint;
37  import y.io.GraphMLIOHandler;
38  import y.io.IOHandler;
39  import y.io.graphml.NamespaceConstants;
40  import y.io.graphml.graph2d.EdgeLabelDeserializer;
41  import y.io.graphml.graph2d.EdgeLabelSerializer;
42  import y.io.graphml.graph2d.Graph2DGraphMLHandler;
43  import y.io.graphml.graph2d.NodeLabelDeserializer;
44  import y.io.graphml.graph2d.NodeLabelSerializer;
45  import y.io.graphml.input.DeserializationEvent;
46  import y.io.graphml.input.DeserializationHandler;
47  import y.io.graphml.input.GraphMLParseContext;
48  import y.io.graphml.input.GraphMLParseException;
49  import y.io.graphml.input.XPathUtils;
50  import y.io.graphml.output.GraphMLWriteContext;
51  import y.io.graphml.output.GraphMLWriteException;
52  import y.io.graphml.output.SerializationEvent;
53  import y.io.graphml.output.SerializationHandler;
54  import y.io.graphml.output.XmlWriter;
55  import y.layout.FixNodeLayoutStage;
56  import y.layout.LabelLayoutTranslator;
57  import y.layout.LayoutGraph;
58  import y.layout.LayoutTool;
59  import y.layout.Layouter;
60  import y.layout.NodeLayout;
61  import y.layout.hierarchic.IncrementalHierarchicLayouter;
62  import y.layout.orthogonal.OrthogonalGroupLayouter;
63  import y.util.D;
64  import y.util.DataProviderAdapter;
65  import y.view.DefaultBackgroundRenderer;
66  import y.view.DefaultOrderRenderer;
67  import y.view.EdgeLabel;
68  import y.view.EdgeRealizer;
69  import y.view.EditMode;
70  import y.view.GenericNodeRealizer;
71  import y.view.Graph2D;
72  import y.view.Graph2DLayoutExecutor;
73  import y.view.Graph2DTraversal;
74  import y.view.Graph2DView;
75  import y.view.Graph2DViewActions;
76  import y.view.HitInfo;
77  import y.view.NavigationMode;
78  import y.view.NodeLabel;
79  import y.view.NodeRealizer;
80  import y.view.NodeStateChangeHandler;
81  import y.view.ProxyShapeNodeRealizer;
82  import y.view.ViewMode;
83  import y.view.YLabel;
84  import y.view.YRenderingHints;
85  import y.view.hierarchy.GenericGroupNodeRealizer;
86  import y.view.hierarchy.HierarchyManager;
87  
88  import javax.swing.AbstractAction;
89  import javax.swing.Action;
90  import javax.swing.JToolBar;
91  import java.awt.Color;
92  import java.awt.Cursor;
93  import java.awt.EventQueue;
94  import java.awt.GradientPaint;
95  import java.awt.Graphics2D;
96  import java.awt.Paint;
97  import java.awt.Rectangle;
98  import java.awt.RenderingHints;
99  import java.awt.event.ActionEvent;
100 import java.awt.geom.AffineTransform;
101 import java.beans.PropertyChangeEvent;
102 import java.beans.PropertyChangeListener;
103 import java.io.IOException;
104 import java.net.URL;
105 import java.util.HashMap;
106 import java.util.Iterator;
107 import java.util.Locale;
108 import java.util.Map;
109 
110 /**
111  * This demo displays graphs in an isometric fashion to create an impression of a 3-dimensional view.
112  * <p>
113  *   It shows how to:
114  *   <ul>
115  *    <li>
116  *      create a layout stage ({@link IsometryTransformationLayoutStage}) that transforms the graph into a layout space
117  *      before layout and retransforms it into view space afterwards. So any {@link Layouter} can be used to
118  *      calculate the layout which is then transformed into an isometric view.
119  *    </li>
120  *    <li>write custom label configurations that display the labels isometrically transformed.</li>
121  *    <li>write custom node painter that use custom user data with 3D-information.</li>
122  *    <li>adjust the rendering order to paint objects that are further away behind.</li>
123  *   </ul>
124  * </p>
125  */
126 public class IsometryDemo extends DemoBase {
127   private static final double PAINT_DETAIL_THRESHOLD = 0.4;
128   private static final double MAX_ZOOM = 4.0;
129   private static final double MIN_ZOOM = 0.05;
130 
131   private static final int TYPE_INCREMENTAL_HIERARCHIC_LAYOUT = 0;
132   private static final int TYPE_ORTHOGONAL_GROUP_LAYOUT = 1;
133 
134   private int layoutType;
135   private boolean fractionMetricsEnabled;
136 
137   private IncrementalHierarchicLayouter ihl;
138   private OrthogonalGroupLayouter ogl;
139 
140   public IsometryDemo() {
141     this(null);
142   }
143 
144   public IsometryDemo(String helpFile) {
145     addHelpPane(helpFile);
146 
147     final Graph2D graph = view.getGraph2D();
148     new HierarchyManager(graph);
149 
150     // add data provider to make transformation data stored in the user data of the realizers available during layout
151     graph.addDataProvider(
152         IsometryTransformationLayoutStage.TRANSFORMATION_DATA_DPKEY,
153         new TransformationDataProvider(graph));
154 
155     // as the edge labels shall be painted at a different point in the rendering order than the according edges, edge
156     // label painting is disabled for edges and the graph renderer will deal with that
157     view.getRenderingHints().put(YRenderingHints.KEY_EDGE_LABEL_PAINTING, YRenderingHints.VALUE_EDGE_LABEL_PAINTING_OFF);
158     final IsometryGraphTraversal graphTraversal = new IsometryGraphTraversal();
159     view.setGraph2DRenderer(new DefaultOrderRenderer(graphTraversal, graphTraversal) {
160       public void paint(Graphics2D gfx, Graph2D graph) {
161         Rectangle clip = gfx.getClipBounds();
162         if (clip == null) {
163           clip = graph.getBoundingBox();
164         }
165 
166         final Graph2DTraversal paintOrder = getPaintOrder();
167         final int types = Graph2DTraversal.NODES | Graph2DTraversal.EDGES | Graph2DTraversal.EDGE_LABELS;
168         for (Iterator it = paintOrder.firstToLast(graph, types); it.hasNext();) {
169           final Object element = it.next();
170           if (element instanceof Edge) {
171             final EdgeRealizer er = graph.getRealizer((Edge) element);
172             if (intersects(er, clip)) {
173               er.paint(gfx);
174             }
175           } else if (element instanceof Node) {
176             final NodeRealizer nr = graph.getRealizer((Node) element);
177             if (intersects(nr, clip)) {
178               nr.paint(gfx);
179             }
180           } else if (element instanceof EdgeLabel) {
181             final EdgeLabel label = (EdgeLabel) element;
182             if (label.intersects(clip.getX(), clip.getY(), clip.getWidth(), clip.getHeight())) {
183               label.paint(gfx);
184             }
185           }
186         }
187       }
188     });
189 
190     IsometryRealizerFactory.initializeConfigurations();
191     configureLayouter();
192     configureBackgroundRenderer();
193     configureLabelRendering();
194     configureZoomThreshold();
195 
196     loadGraph("resource/iso_sample_1.graphml");
197   }
198 
199   /**
200    * Creates and configures the two possible layouters ({@link IncrementalHierarchicLayouter hierarchic} and
201    * {@link OrthogonalGroupLayouter orthogonal}).
202    */
203   private void configureLayouter() {
204     ihl = new IncrementalHierarchicLayouter();
205     ihl.setOrthogonallyRouted(true);
206     ihl.setNodeToEdgeDistance(50);
207     ihl.setMinimumLayerDistance(40);
208     ihl.setLabelLayouterEnabled(false);
209     ihl.setIntegratedEdgeLabelingEnabled(true);
210     ihl.setConsiderNodeLabelsEnabled(true);
211 
212     // this label layout translator does nothing because the TransformationLayoutStage prepares the labels for layout
213     // but OrthogonalGroupLayouter needs a label layout translator for integrated edge labeling and node label consideration
214     final LabelLayoutTranslator llt = new LabelLayoutTranslator() {
215       public void doLayout(LayoutGraph graph) {
216         final Layouter coreLayouter = getCoreLayouter();
217         if (coreLayouter != null) {
218           coreLayouter.doLayout(graph);
219         }
220       }
221     };
222 
223     ogl = new OrthogonalGroupLayouter();
224     ogl.setIntegratedEdgeLabelingEnabled(true);
225     ogl.setConsiderNodeLabelsEnabled(true);
226     ogl.setLabelLayouter(llt);
227   }
228 
229   /**
230    * Adds a isometric grid as demo background.
231    */
232   private void configureBackgroundRenderer() {
233     final DefaultBackgroundRenderer bgRenderer = new FoggyFrameBackgroundRenderer(view);
234     bgRenderer.setImageResource(getResource("resource/grid.png"));
235     bgRenderer.setMode(DefaultBackgroundRenderer.CENTERED);
236     bgRenderer.setColor(Color.WHITE);
237     view.setBackgroundRenderer(bgRenderer);
238   }
239 
240   /**
241    * Ensures that text always fits into label bounds independent of zoom level. Stores the value to be able to reset it
242    * when running the demo in the DemoBrowser, so this setting cannot effect other demos.
243    */
244   private void configureLabelRendering() {
245     fractionMetricsEnabled = YLabel.isFractionMetricsForSizeCalculationEnabled();
246     YLabel.setFractionMetricsForSizeCalculationEnabled(true);
247     view.getRenderingHints().put(
248         RenderingHints.KEY_FRACTIONALMETRICS,
249         RenderingHints.VALUE_FRACTIONALMETRICS_ON);
250   }
251 
252   /**
253    * Cleans up.
254    * This method is called by the demo browser when the demo is stopped or another demo starts.
255    */
256   public void dispose() {
257     YLabel.setFractionMetricsForSizeCalculationEnabled(fractionMetricsEnabled);
258   }
259 
260   /**
261    * Limits the range of possible zoom factors.
262    */
263   private void configureZoomThreshold() {
264     // set threshold for sloppy painting
265     view.setPaintDetailThreshold(PAINT_DETAIL_THRESHOLD);
266     // limit zooming in and out
267     view.getCanvasComponent().addPropertyChangeListener(
268         new PropertyChangeListener() {
269           public void propertyChange(PropertyChangeEvent evt) {
270             if ("Zoom".equals(evt.getPropertyName())) {
271               final double zoom = ((Double) evt.getNewValue()).doubleValue();
272               if (zoom > MAX_ZOOM) {
273                 view.setZoom(MAX_ZOOM);
274               } else if (zoom < MIN_ZOOM) {
275                 view.setZoom(MIN_ZOOM);
276               }
277             }
278           }
279         });
280   }
281 
282   /**
283    * Overwritten to replace all realizers and configurations after graph loading and before layout.
284    */
285   protected void loadGraph(URL resource) {
286     if (resource == null) {
287       String message = "Resource \"" + resource + "\" not found in classpath";
288       D.showError(message);
289       throw new RuntimeException(message);
290     }
291 
292     try {
293       IOHandler ioh = createGraphMLIOHandler();
294       view.getGraph2D().clear();
295       ioh.read(view.getGraph2D(), resource);
296     } catch (IOException e) {
297       String message = "Unexpected error while loading resource \"" + resource + "\" due to " + e.getMessage();
298       D.bug(message);
299       throw new RuntimeException(message, e);
300     }
301 
302     view.getGraph2D().setURL(resource);
303 
304     // set default configurations of this demo and add user data to nodes and labels
305     IsometryRealizerFactory.applyIsometryRealizerDefaults(view.getGraph2D());
306 
307     // calculate a new layout
308     runLayout(false);
309   }
310 
311   /**
312    * Overwritten to add a hierarchic and an orthogonal layout button to the toolbar.
313    *
314    */
315   protected JToolBar createToolBar() {
316     final JToolBar toolBar = super.createToolBar();
317     toolBar.addSeparator();
318     toolBar.add(createActionControl(createLayoutAction("Hierarchic", TYPE_INCREMENTAL_HIERARCHIC_LAYOUT), true));
319     toolBar.add(createActionControl(createLayoutAction("Orthogonal", TYPE_ORTHOGONAL_GROUP_LAYOUT), true));
320     return toolBar;
321   }
322 
323   /**
324    * Overwritten to disable deletion of graph elements because this demo is not editable.
325    */
326   protected boolean isDeletionEnabled() {
327     return false;
328   }
329 
330   /**
331    * Overwritten to disable undo and redo because this demo is not editable.
332    */
333   protected boolean isUndoRedoEnabled() {
334     return false;
335   }
336 
337   /**
338    * Overwritten to disable clipboard because this demo is not editable.
339    */
340   protected boolean isClipboardEnabled() {
341     return false;
342   }
343 
344   /**
345    * Creates an {@link Action} that starts the layout after setting its layout type (hierarchic, incremental).
346    */
347   private Action createLayoutAction(final String text, final int type) {
348     final AbstractAction action = new AbstractAction(text) {
349       public void actionPerformed(ActionEvent e) {
350         layoutType = type;
351         runLayout(false);
352       }
353     };
354     action.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);
355     return action;
356   }
357 
358   /**
359    * Overwritten to create a {@link GraphMLIOHandler} that can handle {@link IsometryData} and serialize
360    * {@link y.view.YLabel#getUserData() user data} for labels.
361    */
362   protected GraphMLIOHandler createGraphMLIOHandler() {
363     final GraphMLIOHandler graphMLIOHandler = super.createGraphMLIOHandler();
364     final Graph2DGraphMLHandler graphMLHandler = graphMLIOHandler.getGraphMLHandler();
365     final IsometryDataIOHandler dataHandler = new IsometryDataIOHandler();
366     graphMLHandler.addSerializationHandler(dataHandler);
367     graphMLHandler.addDeserializationHandler(dataHandler);
368     graphMLHandler.addSerializationHandler(new IsometryEdgeLabelSerializer());
369     graphMLHandler.addDeserializationHandler(new IsometryEdgeLabelDeserializer());
370     graphMLHandler.addSerializationHandler(new IsometryNodeLabelSerializer());
371     graphMLHandler.addDeserializationHandler(new IsometryNodeLabelDeserializer());
372     return graphMLIOHandler;
373   }
374 
375   /**
376    * Overwritten to disable {@link EditMode} because there is no interactive graph editing available.
377    *
378    * @see #registerViewModes()
379    */
380   protected EditMode createEditMode() {
381     return null;
382   }
383 
384   /**
385    * Overwritten to register {@link NavigationMode} a view mode that opens folders/closes groups.
386    */
387   protected void registerViewModes() {
388     super.registerViewModes();
389     view.addViewMode(new NavigationMode());
390     view.addViewMode(new GroupingViewMode());
391   }
392 
393   /**
394    * Runs either a {@link IncrementalHierarchicLayouter hierarchic} or an {@link OrthogonalGroupLayouter orthogonal}.
395    * layout.
396    */
397   private void runLayout(final boolean fromSketch) {
398     final Graph2DLayoutExecutor executor = new Graph2DLayoutExecutor();
399     executor.getLayoutMorpher().setKeepZoomFactor(fromSketch);
400 
401     if (layoutType == TYPE_INCREMENTAL_HIERARCHIC_LAYOUT) {
402       if (fromSketch) {
403         ihl.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_INCREMENTAL);
404         executor.doLayout(view, new FixGroupStateIconLayoutStage(new IsometryTransformationLayoutStage(ihl, fromSketch)));
405       } else {
406         ihl.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_FROM_SCRATCH);
407         executor.doLayout(view, new IsometryTransformationLayoutStage(ihl, fromSketch));
408       }
409     } else if (layoutType == TYPE_ORTHOGONAL_GROUP_LAYOUT) {
410       if (fromSketch) {
411         executor.doLayout(view, new FixGroupStateIconLayoutStage(new IsometryTransformationLayoutStage(ogl, fromSketch)));
412       } else {
413         executor.doLayout(view, new IsometryTransformationLayoutStage(ogl, fromSketch));
414       }
415     }
416   }
417 
418   /**
419    * Starts the demo.
420    */
421   public static void main(final String[] args) {
422     EventQueue.invokeLater(new Runnable() {
423       public void run() {
424         Locale.setDefault(Locale.ENGLISH);
425         initLnF();
426         (new IsometryDemo("resource/iso_help.html")).start("Isometry Demo");
427       }
428     });
429   }
430 
431   /**
432    * A {@link y.view.BackgroundRenderer} that displays an image or a plain color as background of {@link Graph2DView}
433    * and adds a foggy frame to this background.
434    */
435   private static class FoggyFrameBackgroundRenderer extends DefaultBackgroundRenderer {
436 
437     private static final Color COLOR_BLANK = new Color(255, 255, 255, 0);
438 
439     private Color bgColor;
440 
441     public FoggyFrameBackgroundRenderer(Graph2DView view) {
442       super(view);
443       bgColor = Color.WHITE;
444     }
445 
446     public void paint(Graphics2D graphics, int x, int y, int w, int h) {
447       super.paint(graphics, x, y, w, h);
448 
449       paintFoggyFrame(graphics);
450     }
451 
452     private void paintFoggyFrame(Graphics2D graphics) {
453       final Paint oldPaint = graphics.getPaint();
454       final AffineTransform oldTransform = graphics.getTransform();
455 
456       undoWorldTransform(graphics);
457 
458       final float viewX = (float) 0;
459       final float viewY = (float) 0;
460       final float viewW = (float) view.getWidth();
461       final float viewH = (float) view.getHeight();
462 
463       final float halfWidth = viewW * 0.5f;
464       final float halfHeight = viewH * 0.5f;
465       graphics.setPaint(
466           new GradientPaint(viewX + halfWidth, viewY, bgColor, viewX + halfWidth, viewY + viewH * 0.2f, COLOR_BLANK));
467       graphics.fillRect((int) viewX, (int) viewY, (int) viewW, (int) (viewH * 0.2));
468       graphics.setPaint(
469           new GradientPaint(viewX + halfWidth, viewY + viewH * 0.8f, COLOR_BLANK, viewX + halfWidth, viewY + viewH,
470               bgColor));
471       graphics.fillRect((int) viewX, (int) (viewY + viewH * 0.8), (int) viewW, (int) (viewH * 0.2));
472       graphics.setPaint(
473           new GradientPaint(viewX, viewY + halfHeight, bgColor, viewX + viewW * 0.2f, viewY + halfHeight, COLOR_BLANK));
474       graphics.fillRect((int) viewX, (int) viewY, (int) (viewW * 0.2), (int) viewH);
475       graphics.setPaint(
476           new GradientPaint(viewX + viewW * 0.8f, viewY + halfHeight, COLOR_BLANK, viewX + viewW, viewY + halfHeight,
477               bgColor));
478       graphics.fillRect((int) (viewX + viewW * 0.8), (int) viewY, (int) (viewW * 0.2), (int) viewH);
479 
480       graphics.setPaint(oldPaint);
481       graphics.setTransform(oldTransform);
482     }
483   }
484 
485   /**
486    * Provides an {@link IsometryData} instance for each node.
487    */
488   private static class TransformationDataProvider extends DataProviderAdapter {
489     private final Graph2D graph;
490 
491     public TransformationDataProvider(final Graph2D graph) {
492       this.graph = graph;
493     }
494 
495     public Object get(final Object dataHolder) {
496       if (dataHolder instanceof Node) {
497         NodeRealizer realizer = graph.getRealizer((Node) dataHolder);
498         if (realizer instanceof ProxyShapeNodeRealizer) {
499           realizer = ((ProxyShapeNodeRealizer) realizer).getRealizerDelegate();
500         }
501         if (realizer instanceof GenericNodeRealizer) {
502           return ((GenericNodeRealizer) realizer).getUserData();
503         }
504       } else if (dataHolder instanceof EdgeLabel) {
505         return ((EdgeLabel) dataHolder).getUserData();
506       }
507       return null;
508     }
509   }
510 
511   /**
512    * IOHandler that serializes and deserializes {@link IsometryData}.
513    */
514   private class IsometryDataIOHandler implements SerializationHandler, DeserializationHandler {
515     private static final String ELEMENT_NAME = "IsometryData";
516     private static final String ELEMENT_WIDTH = "width";
517     private static final String ELEMENT_HEIGHT = "height";
518     private static final String ELEMENT_DEPTH = "depth";
519     private static final String ELEMENT_HORIZONTAL = "horizontal";
520 
521     public void onHandleSerialization(SerializationEvent event) throws GraphMLWriteException {
522       final Object item = event.getItem();
523       if (item instanceof IsometryData) {
524         IsometryData isometryData = (IsometryData) item;
525         final XmlWriter writer = event.getWriter();
526         writer.writeStartElement(ELEMENT_NAME, NamespaceConstants.YFILES_JAVA_NS);
527         writer.writeAttribute(ELEMENT_WIDTH, isometryData.getWidth());
528         writer.writeAttribute(ELEMENT_HEIGHT, isometryData.getHeight());
529         writer.writeAttribute(ELEMENT_DEPTH, isometryData.getDepth());
530         writer.writeAttribute(ELEMENT_HORIZONTAL, isometryData.isHorizontal());
531         writer.writeEndElement();
532         event.setHandled(true);
533       }
534     }
535 
536     public void onHandleDeserialization(DeserializationEvent event) throws GraphMLParseException {
537       final org.w3c.dom.Node xmlNode = event.getXmlNode();
538       if (xmlNode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE
539           && NamespaceConstants.YFILES_JAVA_NS.equals(xmlNode.getNamespaceURI())
540           && ELEMENT_NAME.equals(xmlNode.getLocalName())) {
541         final Element element = (Element) xmlNode;
542         String attribute = element.getAttribute(ELEMENT_WIDTH);
543         final double width = Double.parseDouble(attribute);
544         attribute = element.getAttribute(ELEMENT_HEIGHT);
545         final double height = Double.parseDouble(attribute);
546         attribute = element.getAttribute(ELEMENT_DEPTH);
547         final double depth = Double.parseDouble(attribute);
548         attribute = element.getAttribute(ELEMENT_HORIZONTAL);
549         final boolean horizontal = Boolean.getBoolean(attribute);
550         event.setResult(new IsometryData(width, depth, height, horizontal));
551       }
552     }
553   }
554 
555   /**
556    * {@link EdgeLabelSerializer} that additionally serializes the {@link y.view.YLabel#getUserData() user data} of an
557    * {@link EdgeLabel}.
558    */
559   private static class IsometryEdgeLabelSerializer extends EdgeLabelSerializer {
560     protected void serializeContent(EdgeLabel label, XmlWriter writer, GraphMLWriteContext context) throws
561         GraphMLWriteException {
562       super.serializeContent(label, writer, context);
563       if (label.getUserData() != null) {
564         writer.writeStartElement("UserData", NamespaceConstants.YFILES_JAVA_NS);
565         context.serialize(label.getUserData());
566         writer.writeEndElement();
567       }
568     }
569   }
570 
571   /**
572    * {@link EdgeLabelSerializer} that additionally deserializes the {@link y.view.YLabel#getUserData() user data} of an
573    * {@link EdgeLabel}.
574    */
575   private static class IsometryEdgeLabelDeserializer extends EdgeLabelDeserializer {
576     protected void parseEdgeLabel(GraphMLParseContext context, org.w3c.dom.Node root, EdgeLabel label) throws
577         GraphMLParseException {
578       super.parseEdgeLabel(context, root, label);
579       final org.w3c.dom.Node userDataNode = XPathUtils.selectFirstChildElement(root, "UserData",
580           NamespaceConstants.YFILES_JAVA_NS);
581       Object userData = null;
582       if (userDataNode != null) {
583         final org.w3c.dom.Node isoData = XPathUtils.selectFirstChildElement(userDataNode, "IsometryData",
584                   NamespaceConstants.YFILES_JAVA_NS);
585         if (isoData != null) {
586           userData = context.deserialize(isoData);
587         }
588       }
589       if (userData != null) {
590         label.setUserData(userData);
591       }
592     }
593   }
594 
595   /**
596    * {@link NodeLabelSerializer} that additionally serializes the {@link y.view.YLabel#getUserData() user data} of a
597    * {@link NodeLabel}.
598    */
599   private static class IsometryNodeLabelSerializer extends NodeLabelSerializer {
600     protected void serializeContent(NodeLabel label, XmlWriter writer, GraphMLWriteContext context) throws
601         GraphMLWriteException {
602       super.serializeContent(label, writer, context);
603       if (label.getUserData() != null) {
604         writer.writeStartElement("UserData", NamespaceConstants.YFILES_JAVA_NS);
605         context.serialize(label.getUserData());
606         writer.writeEndElement();
607       }
608     }
609   }
610 
611   /**
612    * {@link NodeLabelSerializer} that additionally deserializes the {@link y.view.YLabel#getUserData() user data} of a
613    * {@link NodeLabel}.
614    */
615   private static class IsometryNodeLabelDeserializer extends NodeLabelDeserializer {
616     protected void parseNodeLabel(GraphMLParseContext context, org.w3c.dom.Node root, NodeLabel label) throws
617         GraphMLParseException {
618       super.parseNodeLabel(context, root, label);
619       final org.w3c.dom.Node userDataNode = XPathUtils.selectFirstChildElement(root, "UserData",
620           NamespaceConstants.YFILES_JAVA_NS);
621       Object userData = null;
622       if (userDataNode != null) {
623         final org.w3c.dom.Node isoData = XPathUtils.selectFirstChildElement(userDataNode, "IsometryData",
624             NamespaceConstants.YFILES_JAVA_NS);
625         if (isoData != null) {
626           userData = context.deserialize(isoData);
627         }
628       }
629       if (userData != null) {
630         label.setUserData(userData);
631       }
632     }
633   }
634 
635   /**
636    * A {@link ViewMode} that handles opening folders and closing groups that use {@link IsometryGroupPainter}.
637    */
638   private class GroupingViewMode extends ViewMode {
639     public void mouseMoved(double x, double y) {
640       if (hitsGroupStateIcon(x, y)) {
641         view.setViewCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
642       } else {
643         view.setViewCursor(Cursor.getDefaultCursor());
644       }
645     }
646 
647     public void mouseClicked(double x, double y) {
648       if (hitsGroupStateIcon(x, y)) {
649         final HitInfo hit = getHitInfo(x, y);
650         final Node hitNode = hit.getHitNode();
651         final HierarchyManager hm = view.getGraph2D().getHierarchyManager();
652         view.getGraph2D().addDataProvider(FixNodeLayoutStage.FIXED_NODE_DPKEY, new DataProviderAdapter() {
653           public boolean getBool(Object dataHolder) {
654             return dataHolder instanceof Node && hitNode == dataHolder;
655           }
656         });
657         if (hm.isFolderNode(hitNode)) {
658           openFolder(hitNode);
659         } else {
660           closeGroup(hitNode);
661         }
662 
663         // run layout incrementally
664         runLayout(true);
665 
666         view.getGraph2D().removeDataProvider(FixNodeLayoutStage.FIXED_NODE_DPKEY);
667       }
668     }
669 
670     /**
671      * Determines whether or not a group state icon is hit by the given coordinates.
672      */
673     private boolean hitsGroupStateIcon(double x, double y) {
674       final HitInfo hit = getHitInfo(x, y);
675       if (hit.hasHitNodes()) {
676         final Node hitNode = hit.getHitNode();
677         NodeRealizer realizer = view.getGraph2D().getRealizer(hitNode);
678         if (realizer instanceof ProxyShapeNodeRealizer) {
679           ProxyShapeNodeRealizer proxy = (ProxyShapeNodeRealizer) realizer;
680           realizer = proxy.getRealizerDelegate();
681         }
682         if (realizer instanceof GenericGroupNodeRealizer
683             && IsometryGroupPainter.hitsGroupStateIcon(realizer, x, y)) {
684           return true;
685         }
686       }
687       return false;
688     }
689 
690     /**
691      * Closes the specified group node.
692      * @param node    the group node that has to be converted to a folder node.
693      */
694     protected void closeGroup(final Node node) {
695       Graph2DViewActions.CloseGroupsAction helper = new Graph2DViewActions.CloseGroupsAction(view) {
696         protected boolean acceptNode(final Graph2D graph, final Node groupNode) {
697           return groupNode == node;
698         }
699       };
700       helper.setNodeStateChangeHandler(new GroupNodeStateChangeHandler(helper.getNodeStateChangeHandler()));
701       helper.closeGroups(view);
702     }
703 
704     /**
705      * Opens the specified folder node.
706      * @param node    the folder node that has to be converted to a group node.
707      */
708     protected void openFolder(final Node node) {
709       Graph2DViewActions.OpenFoldersAction helper = new Graph2DViewActions.OpenFoldersAction(view) {
710         protected boolean acceptNode(final Graph2D graph, final Node groupNode) {
711           return groupNode == node;
712         }
713       };
714       helper.setNodeStateChangeHandler(new GroupNodeStateChangeHandler(helper.getNodeStateChangeHandler()));
715       helper.openFolders(view);
716     }
717   }
718 
719   /**
720    * Normally, the state icon of a group node is placed at the upper left corner of the group node. If the group node is
721    * opened or closed the state icon remains on the same place. But the state icon of the isometric group node is placed
722    * at the lower left corner. This {@link NodeStateChangeHandler} moves the group node so, that the state icon also
723    * remains on the same place
724    */
725   private static final class GroupNodeStateChangeHandler implements NodeStateChangeHandler {
726     private final NodeStateChangeHandler handler;
727     private final Map state;
728 
729     GroupNodeStateChangeHandler(final NodeStateChangeHandler handler) {
730       this.handler = handler;
731       state = new HashMap();
732     }
733 
734     /**
735      * Overwritten to store the lower left corner of the group node before it is closed/opened.
736      */
737     public void preNodeStateChange(final Node node) {
738       state.put(node, calculateFixPoint(node));
739 
740       if (handler != null) {
741         handler.postNodeStateChange(node);
742       }
743     }
744 
745     private Object calculateFixPoint(final Node node) {
746       final NodeRealizer realizer = ((Graph2D) node.getGraph()).getRealizer(node);
747       final double[] corners = new double[16];
748       final IsometryData isometryData = IsometryRealizerFactory.getIsometryData(realizer);
749       isometryData.calculateCorners(corners);
750       IsometryData.moveTo(realizer.getX(), realizer.getY(), corners);
751 
752       return new YPoint(corners[IsometryData.C3_X], corners[IsometryData.C3_Y]);
753     }
754 
755     /**
756      * Overwritten to move the group node to the stored point after it has been closed/opened.
757      */
758     public void postNodeStateChange(final Node node) {
759       final YPoint fixPoint = (YPoint) state.remove(node);
760       if (fixPoint != null) {
761         restoreFixPoint(node, fixPoint);
762       }
763 
764       if (handler != null) {
765         handler.postNodeStateChange(node);
766       }
767     }
768 
769     private void restoreFixPoint(final Node node, final YPoint fixPoint) {
770       final Graph2D graph = (Graph2D) node.getGraph();
771       NodeRealizer realizer = graph.getRealizer(node);
772       if (realizer instanceof ProxyShapeNodeRealizer) {
773         realizer = ((ProxyShapeNodeRealizer) realizer).getRealizerDelegate();
774       }
775       final double[] corners = new double[16];
776       final IsometryData isometryData = IsometryRealizerFactory.getIsometryData(realizer);
777       isometryData.calculateCorners(corners);
778       IsometryData.moveTo(realizer.getX(), realizer.getY(), corners);
779 
780       final double newCornerX = corners[IsometryData.C3_X];
781       final double newCornerY = corners[IsometryData.C3_Y];
782 
783       final double dx = fixPoint.getX() - newCornerX;
784       final double dy = fixPoint.getY() - newCornerY;
785 
786       final HierarchyManager hm = graph.getHierarchyManager();
787       if (hm.isGroupNode(node)) {
788         final NodeList subGraph = new NodeList(hm.getChildren(node));
789         subGraph.add(node);
790         LayoutTool.moveSubgraph(graph, subGraph.nodes(), dx, dy);
791       } else {
792         LayoutTool.moveNode(graph, node, dx, dy);
793       }
794     }
795   }
796 
797   /**
798    * When the user opens/closes a folder/group node by clicking its state icon, the layouter calculates a new layout.
799    * This {@link y.layout.LayoutStage} moves the graph afterwards so, that the state icon of the group/folder node
800    * remains under the mouse cursor.
801    */
802   private class FixGroupStateIconLayoutStage extends FixNodeLayoutStage {
803     private FixGroupStateIconLayoutStage(final Layouter core) {
804       super(core);
805     }
806 
807     /**
808      * Overwritten to fix the lower left corner (where the state icon is placed) of the isometric painted folder/group
809      * node.
810      */
811     protected YPoint calculateFixPoint(final LayoutGraph graph, final NodeList fixed) {
812       final Node node = fixed.firstNode();
813       final DataProvider provider = graph.getDataProvider(IsometryTransformationLayoutStage.TRANSFORMATION_DATA_DPKEY);
814       final IsometryData isometryData = (IsometryData) provider.get(node);
815 
816       final double[] corners = new double[16];
817       isometryData.calculateCorners(corners);
818       final NodeLayout nodeLayout = graph.getNodeLayout(node);
819       IsometryData.moveTo(nodeLayout.getX(), nodeLayout.getY(), corners);
820       return new YPoint(corners[IsometryData.C3_X], corners[IsometryData.C3_Y]);
821     }
822   }
823 }
824