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