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