]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/NodeUtil.java
Fonts are now embedded in diagram, wiki, etc PDF exports.
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / utils / NodeUtil.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.scenegraph.utils;
13
14 import java.awt.AWTEvent;
15 import java.awt.Container;
16 import java.awt.event.MouseEvent;
17 import java.awt.event.MouseWheelEvent;
18 import java.awt.geom.AffineTransform;
19 import java.awt.geom.NoninvertibleTransformException;
20 import java.awt.geom.Point2D;
21 import java.awt.geom.Rectangle2D;
22 import java.io.PrintStream;
23 import java.lang.reflect.InvocationTargetException;
24 import java.lang.reflect.Method;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.locks.Condition;
33 import java.util.concurrent.locks.Lock;
34 import java.util.concurrent.locks.ReentrantLock;
35
36 import org.simantics.scenegraph.IDynamicSelectionPainterNode;
37 import org.simantics.scenegraph.ILookupService;
38 import org.simantics.scenegraph.INode;
39 import org.simantics.scenegraph.INode.PropertySetter;
40 import org.simantics.scenegraph.ISelectionPainterNode;
41 import org.simantics.scenegraph.ParentNode;
42 import org.simantics.scenegraph.g2d.G2DParentNode;
43 import org.simantics.scenegraph.g2d.G2DSceneGraph;
44 import org.simantics.scenegraph.g2d.IG2DNode;
45 import org.simantics.scenegraph.g2d.events.EventDelegator;
46 import org.simantics.scenegraph.g2d.events.NodeEventHandler;
47 import org.simantics.scenegraph.g2d.events.SGMouseEvent;
48 import org.simantics.scenegraph.g2d.events.SGMouseWheelEvent;
49 import org.simantics.scenegraph.g2d.nodes.ConnectionNode;
50 import org.simantics.scenegraph.g2d.nodes.FlagNode;
51 import org.simantics.scenegraph.g2d.nodes.SingleElementNode;
52 import org.simantics.scl.runtime.function.Function1;
53 import org.simantics.scl.runtime.function.FunctionImpl1;
54 import org.simantics.utils.datastructures.Pair;
55 import org.simantics.utils.threads.IThreadWorkQueue;
56
57 /**
58  * Utilities for debugging/printing the contents of a scenegraph.
59  * 
60  * @author Tuukka Lehtonen
61  */
62 public final class NodeUtil {
63
64     /**
65      * @param <T>
66      */
67     public static interface Filter<T> {
68         public boolean accept(T t);
69     }
70
71     public static class PrefixFilter implements Filter<String> {
72         private final String prefix;
73         public PrefixFilter(String prefix) {
74             this.prefix = prefix;
75         }
76         @Override
77         public boolean accept(String t) {
78             return t.startsWith(prefix);
79         }
80     }
81
82     /**
83      * The name of the sibling-node that is used to represent that a node is selected.
84      */
85     public static final String SELECTION_NODE_NAME = "selection";
86
87     public static INode getNearestParentOfType(INode node, Class<?> clazz) {
88         ParentNode<?> parent = null;
89         while (true) {
90             parent = node.getParent();
91             if (parent == null)
92                 return node;
93             node = parent;
94             if (clazz.isInstance(node))
95                 return node;
96         }
97     }
98
99     public static INode getRootNode(INode node) {
100         ParentNode<?> parent = null;
101         while (true) {
102             parent = node.getParent();
103             if (parent == null)
104                 return node;
105             node = parent;
106         }
107     }
108
109     public static G2DSceneGraph getRootNode(IG2DNode node) {
110         INode root = getRootNode((INode) node);
111         return (G2DSceneGraph) root;
112     }
113
114     public static G2DSceneGraph getPossibleRootNode(IG2DNode node) {
115         INode root = getRootNode((INode) node);
116         return (root instanceof G2DSceneGraph) ? (G2DSceneGraph) root : null;
117     }
118
119     /**
120      * Method for seeking node from scenegraph by class
121      * 
122      * @param <T>
123      * @param parent
124      * @param clazz
125      * @return
126      */
127     public static <T> T getNearestChildByClass(G2DParentNode parent, Class<T> clazz) {
128         return getNearestChildByClass(parent.getNodes(), clazz);
129     }
130
131     /**
132      * Breadth-first-search implementation to be used by getNearestChildByClass method
133      * 
134      * @param <T>
135      * @param nodes
136      * @param clazz
137      * @return
138      */
139     @SuppressWarnings("unchecked")
140     public static <T> T getNearestChildByClass(Collection<IG2DNode> nodes, Class<T> clazz) {
141         Collection<IG2DNode> list = null;
142         for (IG2DNode n : nodes) {
143             if (clazz.isInstance(n)) {
144                 return (T) n;
145             } else if (n instanceof G2DParentNode) {
146                 if (list == null)
147                     list = new ArrayList<IG2DNode>();
148                 list.addAll(((G2DParentNode)n).getNodes());
149             }
150         }
151         if (list == null || list.isEmpty()) return null;
152         return getNearestChildByClass(list, clazz);
153     }
154
155     /**
156      * Tries to look for a child node from the specified node with the specified
157      * ID. Returns <code>null</code> if the specified node is a not a
158      * {@link ParentNode}.
159      * 
160      * @param node
161      * @param id
162      * @return
163      */
164     public static INode getChildById(INode node, String id) {
165         if (node instanceof ParentNode<?>) {
166             return ((ParentNode<?>) node).getNode(id);
167         }
168         return null;
169     }
170
171     /**
172      * Looks for the first child node of the specified node based on 2D Z-order.
173      * The specified node must be a {@link G2DParentNode} for the method to
174      * succeed.
175      * 
176      * @param node the node to get for the first child from
177      * @return <code>null</code> if the specified node is not a
178      *         {@link G2DParentNode} or has no children.
179      */
180     public static INode getFirstChild(INode node) {
181         if (node instanceof G2DParentNode) {
182             G2DParentNode pn = (G2DParentNode) node;
183             IG2DNode[] sorted = pn.getSortedNodes();
184             if (sorted.length > 0)
185                 return sorted[0];
186         }
187         return null;
188     }
189
190     /**
191      * Returns a single child node of the specified node or <code>null</code> if
192      * there are more than one or zero children.
193      * 
194      * @param node the node to get a possible single child from
195      * @return single child node or <code>null</code> if specified node has more
196      *         than one or zero children
197      */
198     public static INode getPossibleChild(INode node) {
199         if (node instanceof ParentNode<?>) {
200             ParentNode<?> pn = (ParentNode<?>) node;
201             if (pn.getNodeCount() == 1)
202                 return pn.getNodes().iterator().next();
203         }
204         return null;
205     }
206
207     /**
208      * Counts the depth of the specified node in its scene graph node tree.
209      * Depth 1 equals root level.
210      * 
211      * @param node the node for which to count a depth
212      * @return the depth of the node
213      */
214     public static int getDepth(INode node) {
215         int result = 1;
216         ParentNode<?> parent = null;
217         while (true) {
218             parent = node.getParent();
219             if (parent == null)
220                 return result;
221             node = parent;
222             ++result;
223         }
224     }
225
226     private static final void printSceneGraph(PrintStream stream, int indentLevel, INode node, String id) {
227         for (int i = 0; i < indentLevel; ++i)
228             stream.print("\t");
229         stream.print(node.getSimpleClassName());
230         if (id != null) {               
231                 String lookupId = tryLookupId(node);
232                 if (lookupId != null) {
233                         stream.print(" {" + id + ", lookupId = "+lookupId+"}");
234                 } else {
235                         stream.print(" {" + id + "}");
236                 }
237         }
238         stream.println(node);
239         if (node instanceof G2DParentNode) {
240             G2DParentNode parentNode = (G2DParentNode) node;
241             for (String cid : parentNode.getSortedNodesById()) {
242                 Object child = parentNode.getNode(cid);
243                 if (child instanceof INode)
244                     printSceneGraph(stream, indentLevel + 1, (INode) child, cid);
245             }
246         } else if (node instanceof ParentNode<?>) {
247             ParentNode<? extends INode> parentNode = (ParentNode<?>) node;
248             for (String cid : parentNode.getNodeIds()) {
249                 INode child = parentNode.getNode(cid);
250                 printSceneGraph(stream, indentLevel + 1, (INode) child);
251             }
252         }
253     }
254
255     public static final void printSceneGraph(PrintStream stream, int indentLevel, INode node) {
256         String id = null;
257         ParentNode<?> parent = node.getParent();
258         if (parent != null) {
259             Collection<String> ids = parent.getNodeIds();
260             for (String i : ids) {
261                 INode n = parent.getNode(i);
262                 if (n == node) {
263                     id = i;
264                     break;
265                 }
266             }
267         }
268         printSceneGraph(stream, indentLevel, node, id);
269     }
270
271     public static final void printSceneGraph(int indentLevel, INode node) {
272         printSceneGraph(System.out, indentLevel, node);
273     }
274
275     public static final void printSceneGraph(INode node) {
276         printSceneGraph(System.out, 0, node);
277     }
278
279     public static interface NodeProcedure<T> {
280         T execute(INode node, String id);
281     }
282
283     /**
284      * @param node the node to iterate possible children for
285      * @param procedure invoked for each child node, if returns a
286      *        <code>non-null</code> value, the return value is collected into the result
287      *        list
288      * @return the list of collected children
289      */
290     public static final <T> List<T> forChildren(INode node, NodeProcedure<T> procedure) {
291         return forChildren(node, procedure, new ArrayList<T>());
292     }
293
294     /**
295      * @param node the node to iterate possible children for
296      * @param procedure invoked for each child node, if returns a
297      *        <code>non-null</code> value, the node is collected into the result
298      *        list
299      * @param result the result list into which selected children are collected
300      *        or <code>null</code> to not collect
301      * @return the list of collected children or null if provided result list
302      *         was <code>null</code>
303      */
304     public static final <T> List<T> forChildren(INode node, NodeProcedure<T> procedure, List<T> result) {
305         if (node instanceof ParentNode<?>) {
306             ParentNode<?> pn = (ParentNode<?>) node;
307             if (node instanceof G2DParentNode) {
308                 G2DParentNode g2dpn = (G2DParentNode) node;
309                 for (String id : g2dpn.getSortedNodesById()) {
310                     INode n = pn.getNode(id);
311                     T t = procedure.execute(n, id);
312                     if (t != null && result != null)
313                         result.add(t);
314                 }
315             } else {
316                 for (String id : pn.getNodeIds()) {
317                     INode n = pn.getNode(id);
318                     T t = procedure.execute(n, id);
319                     if (t != null && result != null)
320                         result.add(t);
321                 }
322             }
323         }
324         return result;
325     }
326
327     public static final int countTreeNodes(INode node) {
328         int result = 1;
329         if (node instanceof ParentNode<?>) {
330             ParentNode<? extends INode> pn = (ParentNode<?>) node;
331             Collection<? extends INode> ns = pn.getNodes();
332             for (INode n : ns) {
333                 result += countTreeNodes(n);
334             }
335         }
336         return result;
337     }
338
339     public static final StringBuilder printTreeNodes(INode node, StringBuilder builder) {
340         printTreeNodes(node, 0, builder);
341         return builder;
342     }
343
344     public static final StringBuilder printTreeNodes(INode node, int indent, StringBuilder builder) {
345         for (int i = 0; i < indent; i++)
346             builder.append(" ");
347         builder.append(node.toString() + "\n");
348         if (node instanceof ParentNode<?>) {
349             ParentNode<? extends INode> pn = (ParentNode<?>) node;
350             Collection<? extends INode> ns = pn.getNodes();
351             for (INode n : ns) {
352                 printTreeNodes(n, indent+2, builder);
353             }
354         }
355         return builder;
356     }
357
358     public static final <T extends INode> Set<T> collectNodes(INode node, Class<T> clazz) {
359         Set<T> result = new HashSet<T>();
360         collectNodes(node, clazz, result);
361         return result;
362     }
363
364     @SuppressWarnings("unchecked")
365     public static final <T extends INode> void collectNodes(INode node, Class<T> clazz, Set<T> result) {
366         if (clazz.isInstance(node))
367             result.add((T) node);
368         if (node instanceof ParentNode<?>) {
369             ParentNode<? extends INode> pn = (ParentNode<?>) node;
370             Collection<? extends INode> ns = pn.getNodes();
371             for (INode n : ns) {
372                 collectNodes(n, clazz, result);
373             }
374         }
375     }
376     
377     public static <T extends INode> T getSingleNode(INode node, Class<T> clazz) {
378         Set<T> all = collectNodes(node, clazz);
379         if(all.size() != 1) throw new RuntimeException("Expected exactly 1 instance of class " + clazz.getCanonicalName() + ", got " + all.size());
380         return (T)all.iterator().next();
381     }
382     
383     public static final boolean hasChildren(INode node) {
384         if (node instanceof ParentNode<?>) {
385             ParentNode<?> pn = (ParentNode<?>) node;
386             return !pn.getNodes().isEmpty();
387         }
388         return false;
389     }
390
391     /**
392      * Look for a single scene graph node by its ID in a path under a specified
393      * node.
394      * 
395      * @param parent the parent node under which to start looking
396      * @param idPath the node ID path
397      * @return <code>null</code> if node was not found
398      * @throws ClassCastException if the found node was not of the expected type
399      *         T extending INode
400      */
401     @SuppressWarnings("unchecked")
402     public static <T extends INode> T findNodeById(INode parent, String... idPath) {
403         INode n = parent;
404         for (int i = 0;; ++i) {
405             if (i >= idPath.length)
406                 return (T) n;
407             if (n instanceof ParentNode<?>) {
408                 n = ((ParentNode<?>) n).getNode(idPath[i]);
409             } else {
410                 return null;
411             }
412         }
413     }
414
415     /**
416      * Tries to find out whether a node is selected or not.
417      * 
418      * DISCLAIMER: this is a hack for the current
419      * org.simantics.g2d.diagram.participant.ElementPainter implementation that
420      * will stop working if the implementation changes.
421      * 
422      * @param node
423      * @param ascendLimit the max. amount of steps towards parent nodes to take
424      *        while looking for selection information
425      * @return <code>true</code> if evidence of selection is found
426      */
427     public static boolean isSelected(INode node, int ascendLimit) {
428         int steps = 0;
429         ParentNode<?> pn = null;
430         if (node instanceof ParentNode<?>) {
431             pn = (ParentNode<?>) node;
432         } else {
433             pn = node.getParent();
434             ++steps;
435         }
436         for (; pn != null && steps <= ascendLimit; pn = pn.getParent(), ++steps) {
437             INode child = pn.getNode(SELECTION_NODE_NAME);
438             if (child != null)
439                 return true;
440         }
441         return false;
442     }
443
444     public static Container findRootPane(INode node) {
445         G2DSceneGraph parent = findNearestParentNode(node, G2DSceneGraph.class);
446         if (parent == null)
447             return null;
448         return ((G2DSceneGraph) parent).getRootPane();
449     }
450
451     private static boolean isSelectionPainter(INode node) {
452         if (node instanceof ISelectionPainterNode) {
453             if (node instanceof IDynamicSelectionPainterNode)
454                 return ((IDynamicSelectionPainterNode)node).showsSelection();
455             return true;
456         }
457         return false;
458     }
459
460     /**
461      * @param elementNode
462      * @return
463      */
464     public static boolean needSelectionPaint(INode elementNode) {
465         // FIXME: there should be a cleaner way to implement this.
466
467         if (isSelectionPainter(elementNode)) {
468 //          System.out.println("skipped selection painting for connection node child ISelectionPainterNode");
469             return false;
470         }
471
472         if (elementNode instanceof ConnectionNode) {
473 //          System.out.println("connectionNode");
474             for (IG2DNode child : ((ConnectionNode) elementNode).getNodes()) {
475 //              System.out.println(" child " + child);
476                 if (isSelectionPainter(child)) {
477 //                  System.out.println("skipped selection painting for connection node child ISelectionPainterNode");
478                     return false;
479                 }
480                 if (child instanceof SingleElementNode) {
481                     for(IG2DNode child2 : ((SingleElementNode) child).getNodes()) {
482 //                      System.out.println(" child2 " + child2);
483                         if (isSelectionPainter(child2)) {
484 //                          System.out.println("skipped selection painting for edge ISelectionPainterNode");
485                             return false;
486                         }
487                     }
488                 }
489             }
490         } else if (elementNode instanceof SingleElementNode) {
491             for (INode child : ((SingleElementNode) elementNode).getNodes()) {
492                 if (isSelectionPainter(child))
493                     return false;
494             }
495         }
496
497         return true;
498     }
499
500     private static final String SET_THREAD_CALLBACKS_NAME = "CGLIB$SET_THREAD_CALLBACKS"; // If this method is found, the class is enhanced by cglib, thus does not contain annotations
501
502     public static Method getSetterForProperty(String property, INode node) {
503         assert(node != null);
504         Class<?> cl = node.getClass();
505
506         while(true) {
507             boolean isEnhanced = false;
508             for(Method method : cl.getMethods()) {
509                 if(method.isAnnotationPresent(PropertySetter.class)) {
510                     PropertySetter ann = method.getAnnotation(PropertySetter.class);
511                     if(ann.value().equals(property)) {
512                         return method;
513                     }
514                 } else if(method.getName().equals(SET_THREAD_CALLBACKS_NAME) && method.getGenericParameterTypes().length == 1) {
515                     // The class seems to be enhanced by cglib, hence annotations are not present. Check superclass for annotations..
516                     isEnhanced = true;
517                     cl = cl.getSuperclass();
518                     break; // We are not going to find any annotations, stop loop and try with the parent class
519                 }
520             }
521             if(!isEnhanced || cl == null) break;
522         }
523         return null;
524     }
525
526     /**
527      * TODO: log exceptions for debug purposes
528      * 
529      * @param property name of the property
530      * @param value    can be null..
531      * @param node
532      * @return
533      */
534     public static boolean setPropertyIfSupported(String property, Object value, INode node) {
535         Method setter = getSetterForProperty(property, node);
536         if(setter != null) {
537             Class<?> pc[] = setter.getParameterTypes();
538             if(pc.length == 1 && (value == null || pc[0].isAssignableFrom(value.getClass()))) {
539                 try {
540                     setter.invoke(node, value);
541                     return true;
542                 } catch (IllegalArgumentException e) {
543                     // TODO Auto-generated catch block
544                     e.printStackTrace();
545                 } catch (IllegalAccessException e) {
546                     // TODO Auto-generated catch block
547                     e.printStackTrace();
548                 } catch (InvocationTargetException e) {
549                     // TODO Auto-generated catch block
550                     e.getCause().printStackTrace();
551                 }
552             } else {
553
554                 if(pc.length > 0) {
555                     System.err.println("Method " + setter.getName() + " expects " + pc[0].getCanonicalName() + " (got " + value.getClass().getCanonicalName() + ").");
556                 }
557
558             }
559         }
560
561         return false;
562     }
563
564     public static INode findChildById(ParentNode<?> parent, String key) {
565         INode result = parent.getNode(key);
566         if (result != null)
567             return result;
568
569         for (String entry : parent.getNodeIds()) {
570             if (entry.startsWith(key))
571                 return parent.getNode(key);
572         }
573
574         for (INode node : parent.getNodes()) {
575             if (node instanceof ParentNode) {
576                 result = findChildById((ParentNode<?>) node, key);
577                 if (result != null)
578                     return result;
579             }
580         }
581
582         return null;
583     }
584
585     
586     private static int getSegmentEnd(String suffix) {
587         int pos;
588         for(pos=1;pos<suffix.length();++pos) {
589             char c = suffix.charAt(pos);
590             if(c == '/' || c == '#')
591                 break;
592         }
593         return pos;
594     }
595     
596     public static String decodeString(String string) {
597         return string;
598     }
599     
600     public static INode browsePossible(INode node, String suffix) {
601         if(suffix.isEmpty()) 
602             return node;        
603         switch(suffix.charAt(0)) {
604         case '.': {
605                 INode parent = node.getParent();
606             if(parent == null)
607                 return null;
608             return browsePossible(parent, suffix.substring(1));
609         }
610         case '#': {
611             /*int segmentEnd = getSegmentEnd(suffix);
612             Variable property = getPossibleProperty(graph, 
613                     decodeString(suffix.substring(1, segmentEnd)));
614             if(property == null) 
615                 return null;
616             return property.browsePossible(graph, suffix.substring(segmentEnd));*/
617                 return node;
618         }
619         case '/': {
620             int segmentEnd = getSegmentEnd(suffix);
621             INode child = findChildById((ParentNode<?>)node, decodeString(suffix.substring(1, segmentEnd)));
622             if(child == null) 
623                 return null;
624             return browsePossible(child, suffix.substring(segmentEnd));
625         }
626         default:
627             return null;
628         }
629     }    
630     
631     public static Pair<INode, String> browsePossibleReference(INode node, String suffix) {
632         if(suffix.isEmpty()) 
633             throw new RuntimeException("Did not find a reference.");        
634         switch(suffix.charAt(0)) {
635         case '.': {
636                 INode parent = node.getParent();
637             if(parent == null)
638                 return null;
639             return browsePossibleReference(parent, suffix.substring(1));
640         }
641         case '#': {
642                 return Pair.make(node, suffix.substring(1));
643         }
644         case '/': {
645             int segmentEnd = getSegmentEnd(suffix);
646             INode child = findChildById((ParentNode<?>)node, decodeString(suffix.substring(1, segmentEnd)));
647             if(child == null) 
648                 return null;
649             return browsePossibleReference(child, suffix.substring(segmentEnd));
650         }
651         default:
652             return null;
653         }
654     }    
655
656     public static INode findChildByPrefix(G2DParentNode parent, String prefix) {
657         INode result = parent.getNode(prefix);
658         if (result != null)
659             return result;
660
661         for (String entry : parent.getNodeIds()) {
662             if (entry.startsWith(prefix))
663                 return parent.getNode(entry);
664         }
665
666         for (IG2DNode node : parent.getNodes()) {
667             if (node instanceof G2DParentNode) {
668                 result = findChildByPrefix((G2DParentNode) node, prefix);
669                 if (result != null)
670                     return result;
671             }
672         }
673
674         return null;
675     }
676
677     /**
678      * @param parent
679      * @param prefix
680      * @return
681      */
682     public static Collection<String> filterDirectChildIds(ParentNode<?> parent, String prefix) {
683         return filterDirectChildIds(parent, new PrefixFilter(prefix));
684     }
685
686     /**
687      * @param parent
688      * @param prefix
689      * @return
690      */
691     public static Collection<String> filterDirectChildIds(ParentNode<?> parent, Filter<String> childFilter) {
692         Collection<String> childIds = parent.getNodeIds();
693         ArrayList<String> result = new ArrayList<String>(childIds.size());
694
695         for (String id : childIds)
696             if (childFilter.accept(id))
697                 result.add(id);
698
699         return result;
700     }
701
702     /**
703      * @param parent
704      * @param prefix
705      * @return
706      */
707     public static Collection<INode> filterDirectChildren(ParentNode<?> parent, Filter<String> childFilter) {
708         Collection<String> childIds = parent.getNodeIds();
709         ArrayList<INode> result = new ArrayList<INode>(childIds.size());
710
711         for (String id : childIds)
712             if (childFilter.accept(id))
713                 result.add( parent.getNode(id) );
714
715         return result;
716     }
717
718     /**
719      * @param node
720      * @return the lookup service for the specified node
721      * @throws UnsupportedOperationException if ILookupService is not available
722      */
723     public static ILookupService getLookupService(INode node) {
724         ParentNode<?> root = node.getRootNode();
725         if (!(root instanceof ILookupService))
726             throw new UnsupportedOperationException("ILookupService not supported by root node " + root + " attained from " + node);
727         return (ILookupService) root;
728     }
729
730     /**
731      * @param node
732      * @return <code>null</code> if lookup service is not available
733      */
734     public static ILookupService tryGetLookupService(INode node) {
735         ParentNode<?> root = node.getRootNode();
736         return root instanceof ILookupService ? (ILookupService) root : null;
737     }
738
739     /**
740      * @param node
741      * @param id
742      * @return <code>null</code> if lookup failed, i.e. mapping does not exist
743      * @throws UnsupportedOperationException if lookup is not supported
744      * @see #getLookupService(INode)
745      * @see ILookupService
746      */
747     public static INode lookup(INode node, String id) {
748         ILookupService lookup = getLookupService(node);
749         return lookup.lookupNode(id);
750     }
751
752     /**
753      * @param node
754      * @param id
755      * @return <code>null</code> if lookup not supported or lookup failed
756      * @see #tryGetLookupService(INode)
757      * @see ILookupService
758      */
759     public static INode tryLookup(INode node, String id) {
760         ILookupService lookup = tryGetLookupService(node);
761         return lookup != null ? lookup.lookupNode(id) : null;
762     }
763
764     /**
765      * @param node
766      * @param id
767      * @param clazz
768      * @return <code>null</code> if lookup failed, i.e. mapping does not exist
769      * @throws UnsupportedOperationException if lookup is not supported
770      * @throws ClassCastException if the found node cannot be cast to the
771      *         specified class
772      * @see #getLookupService(INode)
773      * @see ILookupService
774      */
775     public static <T> T lookup(INode node, String id, Class<T> clazz) {
776         ILookupService lookup = getLookupService(node);
777         INode found = lookup.lookupNode(id);
778         return found != null ? clazz.cast(found) : null;
779     }
780
781     /**
782      * @param node
783      * @param id
784      * @return <code>null</code> if lookup not supported or lookup failed
785      * @see #tryGetLookupService(INode)
786      * @see ILookupService
787      */
788     public static <T> T tryLookup(INode node, String id, Class<T> clazz) {
789         ILookupService lookup = tryGetLookupService(node);
790         if (lookup == null)
791             return null;
792         INode found = lookup.lookupNode(id);
793         return found != null ? clazz.cast(found) : null;
794     }
795
796     /**
797      * @param node
798      * @param id
799      * @return <code>null</code> if lookup failed, i.e. mapping does not exist
800      * @throws UnsupportedOperationException if lookup is not supported
801      * @see #getLookupService(INode)
802      * @see ILookupService
803      */
804     public static String lookupId(INode node) {
805         ILookupService lookup = getLookupService(node);
806         return lookup.lookupId(node);
807     }
808
809     /**
810      * @param node
811      * @param id
812      * @return <code>null</code> if lookup not supported or lookup failed
813      * @see #tryGetLookupService(INode)
814      * @see ILookupService
815      */
816     public static String tryLookupId(INode node) {
817         ILookupService lookup = tryGetLookupService(node);
818         return lookup != null ? lookup.lookupId(node) : null;
819     }
820
821     /**
822      * Map the specified node to the specified ID in the {@link ILookupService}
823      * provided by the root node of the specified node.
824      * 
825      * @param node
826      * @param id
827      * @throws UnsupportedOperationException if {@link ILookupService} is not
828      *         available for the specified node
829      * @see #getLookupService(INode)
830      * @see ILookupService
831      */
832     public static void map(INode node, String id) {
833         getLookupService(node).map(id, node);
834     }
835
836     /**
837      * Remove possible ILookupService mapping for the specified node.
838      * 
839      * @param node the node to try to remove mappings for
840      * @return mapped ID or <code>null</code> if no mapping existed
841      * @throws UnsupportedOperationException if {@link ILookupService} is not
842      *         available for the specified node
843      * @see ILookupService
844      * @see #getLookupService(INode)
845      */
846     public static String unmap(INode node) {
847         return getLookupService(node).unmap(node);
848     }
849
850     /**
851      * Try to remove possible ILookupService mapping for the specified node.
852      * 
853      * @param node the node to try to remove mappings for
854      * @return mapped ID or <code>null</code> if {@link ILookupService} is not
855      *         supported or no mapping existed
856      * @see ILookupService
857      * @see #tryGetLookupService(INode)
858      */
859     public static String tryUnmap(INode node) {
860         ILookupService lookup = tryGetLookupService(node);
861         return lookup != null ? lookup.unmap(node) : null;
862     }
863
864     public static EventDelegator getEventDelegator(INode node) {
865         ParentNode<?> n = node.getRootNode();
866         if (n instanceof G2DSceneGraph) {
867             return ((G2DSceneGraph) n).getEventDelegator();
868         }
869         return null;
870     }
871
872     public static NodeEventHandler getNodeEventHandler(INode node) {
873         ParentNode<?> n = node.getRootNode();
874         return (n instanceof G2DSceneGraph) ? ((G2DSceneGraph) n).getEventHandler() : null;
875 //        INodeEventHandlerProvider provider = findNearestParentNode(node, INodeEventHandlerProvider.class);
876 //        return provider != null ? provider.getEventHandler() : null;
877     }
878
879     public static AWTEvent transformEvent(AWTEvent event, IG2DNode node) {
880         if (event instanceof MouseEvent) {
881             // Find node transform..
882             AffineTransform transform = getGlobalToLocalTransform(node, null);
883             if (transform == null) {
884                 System.err.println("WARNING: Non-invertible transform for node: " + node);
885                 return event;
886             }
887             MouseEvent me = (MouseEvent)event;
888             // Use double coordinates if available
889             Point2D p = new Point2D.Double((double)me.getX(), (double)me.getY());
890             transform.transform(p, p);
891
892             MouseEvent e = null;
893             // Giving event.getSource() as a parameter for the new events will cause major delay for the event instantiation, hence dummy component is used
894             if (event instanceof MouseWheelEvent) {
895                 e = new SGMouseWheelEvent(new DummyComponent(), me.getID(), me.getWhen(), me.getModifiers(), p.getX(), p.getY(), me.getClickCount(), me.isPopupTrigger(), ((MouseWheelEvent)me).getScrollType(), ((MouseWheelEvent)me).getScrollAmount(), ((MouseWheelEvent)me).getWheelRotation(), me);
896             } else {
897                 e = new SGMouseEvent(new DummyComponent(), me.getID(), me.getWhen(), me.getModifiers(), p.getX(), p.getY(), me.getClickCount(), me.isPopupTrigger(), me.getButton(), me);
898             }
899             return e;
900         }
901         return event;
902     }
903
904     private static final boolean DEBUG_BOUNDS = false;
905
906     private static Rectangle2D getLocalBoundsImpl(INode node, Function1<INode, Boolean> filter, int indent) {
907         if (node instanceof IG2DNode) {
908             if (node instanceof G2DParentNode) {
909                 G2DParentNode pNode = (G2DParentNode)node;
910                 Iterator<IG2DNode> it = pNode.getNodes().iterator();
911                 if (!it.hasNext())
912                     return null;
913                 Rectangle2D bounds = null;
914                 while (it.hasNext()) {
915                     IG2DNode next = it.next();
916                     if (filter != null && !filter.apply(next))
917                         continue;
918
919                     Rectangle2D bl = getLocalBoundsImpl(next, filter, indent+2);
920
921                     if(DEBUG_BOUNDS) {
922                         for(int i=0;i<indent;i++) System.err.print(" ");
923                         System.err.println("+getLocalBoundsImpl " + next  + " => " + bl);
924                     }
925
926                     if(bl != null) {
927                         if(bounds == null) {
928                             bounds = next.localToParent(bl.getFrame());
929                         } else {
930                             bounds.add(next.localToParent(bl));
931                         }
932                     }
933                 }
934
935                 if(DEBUG_BOUNDS) {
936                     for(int i=0;i<indent;i++) System.err.print(" ");
937                     System.err.println("=getLocalBoundsImpl " + node  + " => " + bounds);
938                 }
939
940                 return bounds;
941             } else {
942                 Rectangle2D result = ((IG2DNode)node).getBoundsInLocal(true);
943                 if(result != null) {
944                     if(DEBUG_BOUNDS) {
945                         for(int i=0;i<indent;i++) System.err.print(" ");
946                         System.err.println("=getLocalBoundsImpl " + node  + " => " + result);
947                     }
948                     return result;
949                 }
950             }
951         }
952         return null;
953     }
954
955     public static Rectangle2D getLocalBounds(INode node) {
956         return getLocalBoundsImpl(node, null, 0);
957     }
958
959     public static Rectangle2D getLocalBounds(INode node, final Set<INode> excluding) {
960         return getLocalBoundsImpl(node, new FunctionImpl1<INode, Boolean>() {
961
962                         @Override
963                         public Boolean apply(INode node) {
964                                 return !excluding.contains(node);
965                         }
966         }, 0);
967     }
968
969     public static Rectangle2D getLocalBounds(INode node, final Class<?> excluding) {
970         return getLocalBoundsImpl(node, new FunctionImpl1<INode, Boolean>() {
971
972                         @Override
973                         public Boolean apply(INode node) {
974                                 return !excluding.isInstance(node);
975                         }
976         }, 0);
977     }
978
979     public static Rectangle2D getLocalElementBounds(INode node) {
980         if(node instanceof ConnectionNode) {
981             return getLocalBounds(node);
982         } else if(node instanceof SingleElementNode) {
983             // For normal symbols
984             INode image = NodeUtil.findChildByPrefix((SingleElementNode)node, "composite_image");
985             if (image == null)
986                 // For generic text nodes
987                 image = NodeUtil.findChildByPrefix((SingleElementNode) node, "text");
988             if (image == null)
989                 // For I/O table diagram flags (value of org.simantics.diagram.flag.FlagSceneGraph.VISUAL_ROOT)
990                 image = NodeUtil.findChildByPrefix((SingleElementNode) node, "visual");
991             if (image == null)
992                 image = NodeUtil.getNearestChildByClass((SingleElementNode) node, FlagNode.class);
993             if (image != null)
994                 return getLocalElementBounds(image);
995             else
996                 return getLocalBounds(node);
997         } else {
998             return getLocalBounds(node);
999         }
1000     }
1001
1002     public static <T> T findNearestParentNode(INode node, Class<T> ofClass) {
1003         ParentNode<?> parent = null;
1004         while (true) {
1005             parent = node.getParent();
1006             if (parent == null)
1007                 return null;
1008             if (ofClass.isInstance(parent))
1009                 return ofClass.cast(parent);
1010             node = parent;
1011         }
1012     }
1013  
1014     private static class PendingTester implements Runnable {
1015
1016         private boolean             pending     = true;
1017         private final G2DSceneGraph sg;
1018
1019         private final Lock          pendingLock = new ReentrantLock();
1020         private final Condition     pendingSet  = pendingLock.newCondition();
1021
1022         public PendingTester(G2DSceneGraph sg) {
1023             this.sg = sg;
1024         }
1025
1026         @Override
1027         public void run() {
1028             pendingLock.lock();
1029             try {
1030                 pending = sg.isPending();
1031                 pendingSet.signalAll();
1032             } finally {
1033                 pendingLock.unlock();
1034             }
1035         }
1036
1037         public boolean isPending() {
1038             return pending;
1039         }
1040
1041         public void await() {
1042             pendingLock.lock();
1043             try {
1044                 if (pending)
1045                     pendingSet.await(10, TimeUnit.MILLISECONDS);
1046             } catch (InterruptedException e) {
1047                 // Ignore.
1048             } finally {
1049                 pendingLock.unlock();
1050             }
1051         }
1052
1053     }
1054
1055     public static void waitPending(IThreadWorkQueue thread, G2DSceneGraph sg) {
1056         // Wait for 30s by default
1057         waitPending(thread, sg, 30000);
1058     }
1059
1060     /**
1061      * Synchronously waits until the the specified scene graph is no longer in
1062      * pending state.
1063      * 
1064      * @param thread the thread to schedule pending checks into
1065      * @param sg the scene graph to wait upon
1066      */
1067     public static void waitPending(IThreadWorkQueue thread, G2DSceneGraph sg, int timeoutMs) {
1068         PendingTester tester = new PendingTester(sg);
1069         long start = System.currentTimeMillis();
1070         while (tester.isPending()) {
1071             thread.asyncExec(tester);
1072             if (tester.isPending())
1073                 tester.await();
1074             long duration = System.currentTimeMillis() - start;
1075             if(duration > timeoutMs)
1076                 throw new IllegalStateException("Timeout in resolving pending nodes.");
1077         }
1078     }
1079
1080     public static void increasePending(INode node) {
1081         G2DSceneGraph sg = (G2DSceneGraph) node.getRootNode();
1082         if(sg != null)
1083                 sg.increasePending(node);
1084     }
1085
1086     public static void decreasePending(INode node) {
1087         G2DSceneGraph sg = (G2DSceneGraph) node.getRootNode();
1088         if(sg != null)
1089                 sg.decreasePending(node);
1090     }
1091
1092     // TRANSFORMATIONS
1093
1094     public static AffineTransform getLocalToGlobalTransform(IG2DNode node, AffineTransform result) {
1095         result.setToIdentity();
1096         ParentNode<?> parent = node.getParent();
1097         while (parent != null) {
1098             result.preConcatenate(((IG2DNode) parent).getTransform());
1099             parent = parent.getParent();
1100         }
1101         return result;
1102     }
1103
1104     public static AffineTransform getLocalToGlobalTransform(IG2DNode node) {
1105         return getLocalToGlobalTransform(node, new AffineTransform());
1106     }
1107
1108     public static AffineTransform getGlobalToLocalTransform(IG2DNode node) throws NoninvertibleTransformException {
1109         AffineTransform transform = getLocalToGlobalTransform(node);
1110         transform.invert();
1111         return transform;
1112     }
1113
1114     public static AffineTransform getGlobalToLocalTransform(IG2DNode node, AffineTransform returnIfNonInvertible) {
1115         AffineTransform transform = getLocalToGlobalTransform(node);
1116         try {
1117             transform.invert();
1118             return transform;
1119         } catch (NoninvertibleTransformException e) {
1120             return returnIfNonInvertible;
1121         }
1122     }
1123
1124     public static Point2D worldToLocal(IG2DNode local, Point2D pt, Point2D pt2) {
1125         AffineTransform at = getGlobalToLocalTransform(local, null);
1126         if (at == null) {
1127                 pt2.setLocation(pt);
1128             return pt2;
1129         }
1130         return at.transform(pt, pt2);
1131     }
1132
1133     public static Point2D localToWorld(IG2DNode local, Point2D pt, Point2D pt2) {
1134         AffineTransform at = getLocalToGlobalTransform(local);
1135         return at.transform(pt, pt2);
1136     }
1137
1138     public static String getNodeName(INode nn) {
1139         INode node = nn.getParent();
1140         ParentNode<?> pn = (ParentNode<?>) node;
1141         if (node instanceof G2DParentNode) {
1142             G2DParentNode g2dpn = (G2DParentNode) node;
1143             for (String id : g2dpn.getSortedNodesById()) 
1144             {
1145                 INode n = pn.getNode(id);
1146                 if ( nn == n ) {
1147                         return id;
1148                 }
1149             }
1150         }
1151         return null;
1152     }
1153
1154     /**
1155      * For asking whether specified parent node really is a parent of the
1156      * specified child node.
1157      * 
1158      * @param parent
1159      *            the parent
1160      * @param child
1161      *            alleged child of parent
1162      * @return <code>true</code> if parent really is a parent of child
1163      */
1164     public static boolean isParentOf(INode parent, INode child) {
1165         while (true) {
1166             if (parent == child)
1167                 return true;
1168             child = child.getParent();
1169             if (child == null)
1170                 return false;
1171         }
1172     }
1173
1174 }