Dynamic terminals and connections
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / connection / RouteGraphNode.java
1 /*******************************************************************************
2  * Copyright (c) 2011 Association for Decentralized Information Management in
3  * 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.g2d.nodes.connection;
13
14 import java.awt.BasicStroke;
15 import java.awt.Color;
16 import java.awt.Graphics2D;
17 import java.awt.RenderingHints;
18 import java.awt.Shape;
19 import java.awt.Stroke;
20 import java.awt.event.KeyEvent;
21 import java.awt.geom.AffineTransform;
22 import java.awt.geom.Path2D;
23 import java.awt.geom.Point2D;
24 import java.awt.geom.Rectangle2D;
25 import java.lang.reflect.Constructor;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.List;
29 import java.util.Map;
30
31 import org.simantics.diagram.connection.RouteGraph;
32 import org.simantics.diagram.connection.RouteLine;
33 import org.simantics.diagram.connection.RouteLink;
34 import org.simantics.diagram.connection.RouteTerminal;
35 import org.simantics.diagram.connection.actions.IAction;
36 import org.simantics.diagram.connection.actions.IReconnectAction;
37 import org.simantics.diagram.connection.actions.MoveAction;
38 import org.simantics.diagram.connection.actions.ReconnectLineAction;
39 import org.simantics.diagram.connection.delta.RouteGraphDelta;
40 import org.simantics.diagram.connection.rendering.BasicConnectionStyle;
41 import org.simantics.diagram.connection.rendering.ConnectionStyle;
42 import org.simantics.diagram.connection.rendering.IRouteGraphRenderer;
43 import org.simantics.diagram.connection.rendering.StyledRouteGraphRenderer;
44 import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle;
45 import org.simantics.diagram.connection.splitting.SplittedRouteGraph;
46 import org.simantics.scenegraph.INode;
47 import org.simantics.scenegraph.ISelectionPainterNode;
48 import org.simantics.scenegraph.g2d.G2DNode;
49 import org.simantics.scenegraph.g2d.G2DParentNode;
50 import org.simantics.scenegraph.g2d.IG2DNode;
51 import org.simantics.scenegraph.g2d.events.EventTypes;
52 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
53 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyReleasedEvent;
54 import org.simantics.scenegraph.g2d.events.MouseEvent;
55 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
56 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
57 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
58 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
59 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
60 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
61 import org.simantics.scenegraph.g2d.events.command.Commands;
62 import org.simantics.scenegraph.g2d.nodes.GridNode;
63 import org.simantics.scenegraph.g2d.nodes.LinkNode;
64 import org.simantics.scenegraph.g2d.nodes.SVGNodeAssignment;
65 import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Action;
66 import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Pick;
67 import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
68 import org.simantics.scenegraph.utils.ColorUtil;
69 import org.simantics.scenegraph.utils.GeometryUtils;
70 import org.simantics.scenegraph.utils.InitValueSupport;
71 import org.simantics.scenegraph.utils.NodeUtil;
72 import org.slf4j.LoggerFactory;
73 import org.slf4j.Logger;
74
75 import gnu.trove.map.hash.THashMap;
76
77 /**
78  * @author Tuukka Lehtonen
79  */
80 public class RouteGraphNode extends G2DNode implements ISelectionPainterNode, InitValueSupport  {
81
82     private static final Logger LOGGER = LoggerFactory.getLogger(RouteGraphNode.class);
83
84         private static final long       serialVersionUID = -917194130412280965L;
85
86     private static final double     TOLERANCE        = IAction.TOLERANCE;
87     private static final Stroke     SELECTION_STROKE = new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
88     private static final Color      SELECTION_COLOR  = new Color(255, 0, 255, 96);
89
90     private static final HighlightActionPointsAction highlightActions = new HighlightActionPointsAction(null);
91
92     protected RouteGraph            rg;
93     protected IRouteGraphRenderer   baseRenderer;
94     protected IRouteGraphRenderer   renderer;
95     protected double                pickTolerance    = TOLERANCE;
96     protected boolean               editable         = true;
97     private boolean                 branchable       = true;
98
99     protected IRouteGraphListener   rgListener;
100     protected RouteGraphDelta       rgDelta;
101
102     protected transient double      mouseX;
103     protected transient double      mouseY;
104     protected transient Point2D     pt               = new Point2D.Double();
105
106     protected transient MoveAction  dragAction;
107     protected transient IAction     currentAction;
108     protected transient Rectangle2D bounds;
109
110     /**
111      * Dynamic color for connection rendering.
112      */
113     protected transient Color       dynamicColor;
114
115     /**
116      * Dynamic stroke for connection rendering.
117      */
118     protected transient Stroke      dynamicStroke;
119
120     protected transient Path2D      selectionPath    = new Path2D.Double();
121     protected transient Stroke      selectionStroke  = null;
122
123     protected transient boolean     highlightActionsEnabled = false;
124     protected transient AffineTransform lastViewTransform = null;
125
126     /**
127      * x = NaN is used to indicate that possible branch point should not be
128      * rendered but interaction has not ended yet.
129      */
130     protected transient Point2D     newBranchPointPosition = null;
131
132
133     protected transient Map<Object,ILineEndStyle> dynamicStyles = null;
134
135     private transient boolean ignoreSelection = false;
136     
137     @Override
138     public void initValues() {
139         dynamicColor = null;
140         wrapRenderer();
141     }
142
143     private static float tryParseFloat(String s, float def) {
144         try {
145                 return Float.parseFloat(s);
146         } catch (NumberFormatException e) {
147                 LOGGER.error("Could not parse '" + s + "' into float.");
148                 return def;
149         }
150     }
151     
152     /*
153      * 1.0 BUTT MITER 1.0 0.0
154      */
155     private static Stroke parseStroke(String definition) {
156         
157         float width = 1.0f;
158         int cap = BasicStroke.CAP_BUTT;
159         int join = BasicStroke.JOIN_MITER;
160         float miterLimit = 1.0f;
161         float[] dash = { 1, 0};
162         float dash_phase = 0;
163
164         String[] parts = definition.split(" ");
165         
166         if(parts.length > 0) {
167                 width = tryParseFloat(parts[0], width);
168         }
169         if(parts.length > 1) { 
170                 if("BUTT".equals(parts[1])) cap = BasicStroke.CAP_BUTT;
171                 else if("ROUND".equals(parts[1])) cap = BasicStroke.CAP_ROUND;
172                 else if("SQUARE".equals(parts[1])) cap = BasicStroke.CAP_SQUARE;
173         }
174         if(parts.length > 2) { 
175                 if("BEVEL".equals(parts[2])) cap = BasicStroke.JOIN_BEVEL;
176                 else if("MITER".equals(parts[2])) cap = BasicStroke.JOIN_MITER;
177                 else if("ROUND".equals(parts[2])) cap = BasicStroke.JOIN_ROUND;
178         }
179         if(parts.length > 3) {
180                 miterLimit = tryParseFloat(parts[3], miterLimit);
181         } 
182         if(parts.length > 4) {
183                 dash_phase = tryParseFloat(parts[4], dash_phase);
184         }
185         if(parts.length > 6) {
186                 dash = new float[parts.length - 5];
187                 for(int i=5;i<parts.length;i++) {
188                         dash[i-5] = tryParseFloat(parts[i], 1.0f);
189                 }
190         }
191                         
192         
193         try {
194                 return new BasicStroke(width, cap, join, miterLimit, dash, dash_phase);
195         } catch (IllegalArgumentException e) {
196                 return new BasicStroke();
197         }
198         
199     }
200     
201     public void setAssignments(List<SVGNodeAssignment> assignments) {
202         for(SVGNodeAssignment ass : assignments) {
203                 if("dynamicColor".equals(ass.elementId)) {
204                         setDynamicColor(ColorUtil.hexColor(ass.value));
205                 } else if("dynamicStroke".equals(ass.elementId)) {
206                         setDynamicStroke(parseStroke(ass.value));
207                 }
208         }
209     }
210
211     public void setIgnoreSelection(boolean value) {
212         ignoreSelection = value;
213     }
214     
215     public boolean getIgnoreSelection() {
216         return ignoreSelection;
217     }  
218   
219     @PropertySetter("color")
220     @SyncField(value = {"dynamicColor"})
221     public void setDynamicColor(Color color) {
222         this.dynamicColor = color;
223         wrapRenderer();
224     }
225
226     @PropertySetter("width")
227     @SyncField("dynamicStroke")
228     public void setDynamicStroke(Stroke stroke) {
229         this.dynamicStroke = stroke;
230         wrapRenderer();
231         createSelectionStroke();
232     }
233     
234     @SyncField(value = {"dynamicStyles"})
235     public void setDynamicLineEnd(RouteTerminal terminal, ILineEndStyle style) {
236         if (dynamicStyles == null)
237                 dynamicStyles = new THashMap<Object, ILineEndStyle>();
238         terminal.setDynamicStyle(style);
239         if (terminal.getData() != null) {
240                 if (style != null)
241                         dynamicStyles.put(terminal.getData(),style);
242                 else
243                         dynamicStyles.remove(terminal.getData());
244         }
245     }
246     
247     private void updateLineEnds() {
248         if (dynamicStyles == null)
249                 return;
250         for (RouteTerminal t : rg.getTerminals()) {
251                 if (t.getData() == null)
252                         continue;
253                 ILineEndStyle dynamicStyle = dynamicStyles.get(t.getData());
254                 if (dynamicStyle != null)
255                         t.setDynamicStyle(dynamicStyle);
256         }
257     }
258
259     @SyncField(value = {"rg"})
260     public void setRouteGraph(RouteGraph graph) {
261         this.rg = graph;
262         updateLineEnds();
263         updateBounds();
264     }
265
266     @SyncField(value = {"rgDelta"})
267     public void setRouteGraphDelta(RouteGraphDelta delta) {
268         this.rgDelta = delta;
269     }
270
271     @SyncField(value = {"renderer"})
272     public void setRenderer(IRouteGraphRenderer renderer) {
273
274         this.baseRenderer = renderer;
275         wrapRenderer();
276
277         createSelectionStroke();      
278     }
279     
280     private void createSelectionStroke() {
281          BasicConnectionStyle style = tryGetStyle();
282          selectionStroke = null;
283          if (style != null) {
284              BasicStroke stroke = (BasicStroke) style.getLineStroke();
285              if (stroke != null) {
286                  float width = Math.max(stroke.getLineWidth() + 0.75f, stroke.getLineWidth()*1.3f);
287                  selectionStroke = new BasicStroke(width, BasicStroke.CAP_BUTT, stroke.getLineJoin());
288              }
289          } else {
290              selectionStroke = SELECTION_STROKE;
291          }
292     }
293     
294     private void wrapRenderer() {
295         
296         if(baseRenderer == null) {
297             renderer = null;
298             return;
299         }
300         
301         if(dynamicColor != null || dynamicStroke != null) {
302             BasicConnectionStyle baseStyle = (BasicConnectionStyle)tryGetStyle(baseRenderer);
303             try {
304                 Constructor<? extends BasicConnectionStyle> c = baseStyle.getClass().getConstructor(Color.class, Color.class, double.class, Stroke.class, Stroke.class, double.class, double.class);
305                 renderer = new StyledRouteGraphRenderer(c.newInstance(
306                         dynamicColor != null ? dynamicColor : baseStyle.getLineColor(),
307                                 baseStyle.getBranchPointColor(), baseStyle.getBranchPointRadius(),
308                                     dynamicStroke != null ? dynamicStroke : baseStyle.getLineStroke(), 
309                                             dynamicStroke != null ? dynamicStroke : baseStyle.getRouteLineStroke(),
310                                                     baseStyle.getDegeneratedLineLength(), baseStyle.getRounding()));
311             } catch (Exception e) {
312                 renderer = new StyledRouteGraphRenderer(new BasicConnectionStyle(
313                         dynamicColor != null ? dynamicColor : baseStyle.getLineColor(),
314                                 baseStyle.getBranchPointColor(), baseStyle.getBranchPointRadius(),
315                                     dynamicStroke != null ? dynamicStroke : baseStyle.getLineStroke(), 
316                                             dynamicStroke != null ? dynamicStroke : baseStyle.getRouteLineStroke(),
317                                                     baseStyle.getDegeneratedLineLength(), baseStyle.getRounding()));
318             }
319             
320             
321         } else {
322             renderer = baseRenderer;
323         }
324         
325     }
326
327     @SyncField(value = {"pickTolerance"})
328     public void setPickTolerance(double tolerance) {
329         this.pickTolerance = tolerance;
330     }
331
332     @SyncField(value = {"editable"})
333     public void setEditable(boolean editable) {
334         this.editable = editable;
335     }
336
337     @SyncField(value = {"branchable"})
338     public void setBranchable(boolean branchable) {
339         this.branchable = branchable;
340     }
341
342     public RouteGraph getRouteGraph() {
343         return rg;
344     }
345
346     public RouteGraphDelta getRouteGraphDelta() {
347         return rgDelta;
348     }
349
350     public IRouteGraphRenderer getRenderer() {
351         return renderer;
352     }
353
354     public boolean isEditable() {
355         return editable;
356     }
357
358     public boolean isBranchable() {
359         return branchable;
360     }
361     
362     public double getPickTolerance() {
363                 return pickTolerance;
364         }
365
366     /**
367      * When in client-server mode, listener is only set on the server side and
368      * fireRouteGraphChanged will tell it when rg has changed.
369      * 
370      * @param listener
371      */
372     public void setRouteGraphListener(IRouteGraphListener listener) {
373         this.rgListener = listener;
374     }
375
376     /**
377      * @param before
378      * @param after
379      * @return <code>true</code> if changes were fired
380      */
381     private boolean setRouteGraphAndFireChanges(RouteGraph before, RouteGraph after) {
382         RouteGraphDelta delta = new RouteGraphDelta(before, after);
383         if (!delta.isEmpty()) {
384             setRouteGraph(after);
385             setRouteGraphDelta(delta);
386             fireRouteGraphChanged(before, after, delta);
387             return true;
388         }
389         return false;
390     }
391
392     @ServerSide
393     protected void fireRouteGraphChanged(RouteGraph before, RouteGraph after, RouteGraphDelta delta) {
394         if (rgListener != null) {
395             RouteGraphChangeEvent event = new RouteGraphChangeEvent(this, before, after, delta);
396             rgListener.routeGraphChanged(event);
397         }
398     }
399
400     public void showBranchPoint(Point2D p) {
401         newBranchPointPosition = p;
402     }
403
404     @Override
405     public void init() {
406         super.init();
407         addEventHandler(this);
408     }
409
410     @Override
411     public void cleanup() {
412         rgListener = null;
413         removeEventHandler(this);
414         super.cleanup();
415     }
416
417     protected boolean isSelected() {
418         return NodeUtil.isSelected(this, 1);
419     }
420
421     @Override
422     public void render(Graphics2D g) {
423         if (renderer == null)
424             return;
425
426         AffineTransform ot = null;
427         AffineTransform t = getTransform();
428         if (t != null && !t.isIdentity()) {
429             ot = g.getTransform();
430             g.transform(getTransform());
431         }
432
433         Object aaHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
434         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
435
436         boolean selected = ignoreSelection ? false : NodeUtil.isSelected(this, 1);
437
438         rg.updateTerminals();
439
440         if (currentAction != null) {
441             currentAction.render(g, renderer, mouseX, mouseY);
442         } else {
443             if (selected && selectionStroke != null) {
444                 selectionPath.reset();
445                 rg.getPath2D(selectionPath);
446                 Shape selectionShape = selectionStroke.createStrokedShape(selectionPath);
447                 g.setColor(SELECTION_COLOR);
448                 g.fill(selectionShape);
449             }
450
451             renderer.render(g, rg);
452             if(selected)
453                 renderer.renderGuides(g, rg);
454
455             if (selected && highlightActionsEnabled) {
456                 // Needed for performing actions in #mouseClicked
457                 this.lastViewTransform = g.getTransform();
458                 highlightActions.setRouteGraph(rg);
459                 highlightActions.render(g, renderer, mouseX, mouseY);
460                 highlightActions.setRouteGraph(null);
461             }
462         }
463
464         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, aaHint);
465
466         if (editable && branchable && newBranchPointPosition != null && !Double.isNaN(newBranchPointPosition.getX())) {
467             ConnectionStyle style = tryGetStyle();
468             if(style != null)
469                 style.drawBranchPoint(g, newBranchPointPosition.getX(), newBranchPointPosition.getY());
470         }
471
472         if (ot != null)
473             g.setTransform(ot);
474     }
475     
476     private BasicConnectionStyle tryGetStyle() {
477         return tryGetStyle(renderer);
478     }
479
480     private BasicConnectionStyle tryGetStyle(IRouteGraphRenderer renderer) {
481         if (renderer instanceof StyledRouteGraphRenderer) {
482             ConnectionStyle cs = ((StyledRouteGraphRenderer) renderer).getStyle();
483             if (cs instanceof BasicConnectionStyle)
484                 return (BasicConnectionStyle) cs;
485         }
486         return null;
487     }
488
489     private double getSelectionStrokeWidth() {
490         if (selectionStroke instanceof BasicStroke) {
491             BasicStroke bs = (BasicStroke) selectionStroke;
492             return bs.getLineWidth();
493         }
494         return 1.0;
495     }
496
497     @Override
498     public Rectangle2D getBoundsInLocal() {
499         return bounds;
500     }
501
502     protected void updateBounds() {
503         Rectangle2D r = this.bounds;
504         if (r == null)
505             r = new Rectangle2D.Double();
506         this.bounds = calculateBounds(r);
507
508         // Need to expand to take stroke width into account.
509         double sw = getSelectionStrokeWidth() / 2;
510         GeometryUtils.expandRectangle(this.bounds, sw, sw);
511     }
512
513     protected Rectangle2D calculateBounds(Rectangle2D rect) {
514         RouteGraph rg = this.rg;
515         if (currentAction instanceof MoveAction)
516             rg = ((MoveAction) currentAction).getRouteGraph();
517         rg.getBounds(rect);
518         return rect;
519     }
520
521     protected void getMouseLocalPos(MouseEvent e) {
522         //System.out.println("m: " + e.controlPosition);
523         pt.setLocation(e.controlPosition);
524         //System.out.println("parent: " + pt);
525         pt = NodeUtil.worldToLocal(this, pt, pt);
526         //System.out.println("local: " + pt);
527         mouseX = pt.getX();
528         mouseY = pt.getY();
529     }
530
531     @Override
532     protected boolean mouseDragged(MouseDragBegin e) {
533         if (dragAction != null && !e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK) && e.button == MouseEvent.LEFT_BUTTON) {
534             currentAction = dragAction;
535             dragAction = null;
536         }
537         return updateCurrentAction(e, true);
538     }
539
540     @Override
541     protected boolean mouseMoved(MouseMovedEvent e) {
542         //System.out.println("mouse moved: " + e);
543
544         // Handle connection branching visualization.
545         getMouseLocalPos(e);
546         if (newBranchPointPosition == null && e.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK)) {
547             if (bounds.contains(mouseX, mouseY)) {
548                 newBranchPointPosition = new Point2D.Double(Double.NaN, Double.NaN);
549             }
550         }
551         if (newBranchPointPosition != null) {
552             RouteLine line = rg.pickLine(mouseX, mouseY, pickTolerance);
553             if (line != null) {
554                 newBranchPointPosition.setLocation(mouseX, mouseY);
555                 SplittedRouteGraph.snapToLine(newBranchPointPosition, line);
556                 repaint();
557             } else if (!Double.isNaN(newBranchPointPosition.getX())) {
558                 newBranchPointPosition.setLocation(Double.NaN, Double.NaN);
559                 repaint();
560             }
561         }
562
563         // Make sure that highlight action rendering is according to mouse hover.
564         if (highlightActionsEnabled) {
565             if (NodeUtil.isSelected(this, 1)) {
566                 repaint();
567             }
568         }
569
570         return updateCurrentAction(e, false);
571     }
572
573     protected boolean updateCurrentAction(MouseEvent e, boolean updateMousePos) {
574         boolean oldHighlight = highlightActionsEnabled;
575         highlightActionsEnabled = e.hasAllModifiers(MouseEvent.CTRL_MASK);
576         if (oldHighlight != highlightActionsEnabled)
577             repaint();
578
579         if (currentAction != null) {
580             if (updateMousePos)
581                 getMouseLocalPos(e);
582             updateBounds();
583
584             // Repaint, but only if absolutely necessary.
585             if (currentAction instanceof MoveAction || bounds.contains(mouseX, mouseY))
586                 repaint();
587
588             return true;
589         }
590         return false;
591     }
592
593     @Override
594     protected boolean mouseClicked(MouseClickEvent e) {
595         if (!editable)
596             return false;
597
598         if (e.button == MouseEvent.LEFT_BUTTON) {
599             if (isSelected() && highlightActionsEnabled) {
600                 // Reconnection / segment deletion only available for branched connections. 
601                 if (rg.getTerminals().size() > 2) {
602                     Pick pick = highlightActions.pickAction(rg, lastViewTransform, mouseX, mouseY);
603                     if (pick.hasAction(Action.REMOVE)) {
604                         RemoveLineAction remove = RemoveLineAction.perform(rg, pick.line.getLine(), mouseX, mouseY);
605                         if (remove != null) {
606                             setRouteGraphAndFireChanges(remove.getOriginalRouteGraph(), remove.getRouteGraph());
607                             repaint();
608                             return true;
609                         }
610                     }
611                     if (pick.hasAction(Action.RECONNECT)) {
612                         currentAction = ReconnectLineAction.create(rg, mouseX, mouseY);
613                         if (currentAction != null) {
614                             repaint();
615                         }
616                     }
617                 }
618             }
619         }
620
621         return false;
622     }
623
624     @Override
625     protected boolean mouseButtonPressed(MouseButtonPressedEvent e) {
626         if (!editable)
627             return false;
628
629         if (e.button == MouseEvent.LEFT_BUTTON) {
630             // Visualize new branch point no longer.
631             newBranchPointPosition = null;
632
633             getMouseLocalPos(e);
634             dragAction = null;
635 //          if(currentAction instanceof HighlightActionPointsAction) {
636 //              RemoveLineAction remove = RemoveLineAction.perform(rg, mouseX, mouseY);
637 //              if (remove != null) {
638 //                  setRouteGraphAndFireChanges(remove.getOriginalRouteGraph(), remove.getRouteGraph());
639 //                  repaint();
640 //              } else {
641 //                  currentAction = ReconnectLineAction.create(rg, mouseX, mouseY);
642 //                  if (currentAction != null)
643 //                      repaint();
644 //              }
645 //          }
646 //          else
647             if(currentAction instanceof IReconnectAction) {
648                 RouteGraph originalRg = rg.copy();
649                 ((IReconnectAction)currentAction).finish(mouseX, mouseY);
650                 currentAction = null;
651
652                 setRouteGraphAndFireChanges(originalRg, rg);
653
654                 currentAction = null;
655                 repaint();
656                 return true;
657             }
658             else {
659                 if (!allowConnectionRerouting()) {
660                     return false;
661                 }
662                 //System.out.println("move action");
663                 dragAction = SnappingMoveAction.create(rg, mouseX, mouseY, pickTolerance, moveFilter, getSnapAdvisor());
664                 //System.out.println("DRAG ACTION: " + dragAction);
665             }
666
667             //System.out.println(this + " NEW action: " + currentAction);
668             if (currentAction != null)
669                 return true;
670         }
671         return false;
672     }
673
674     /**
675      * Checks the selections data node in the scene graph for any links 
676      * @return
677      */
678     private boolean allowConnectionRerouting() {
679         final int maxOtherNodesSelected = 1;
680
681         INode selections = NodeUtil.tryLookup(this, "selections");
682         if (!(selections instanceof G2DParentNode))
683             return true;
684         G2DParentNode p = (G2DParentNode) selections;
685         for (IG2DNode selection : p.getNodes()) {
686             if (!(selection instanceof G2DParentNode))
687                 continue;
688
689             G2DParentNode sp = (G2DParentNode) selection;
690             Collection<IG2DNode> links = sp.getNodes();
691             if (links.isEmpty())
692                 return true;
693             int othersSelected = 0;
694             for (IG2DNode link : links) {
695                 if (link instanceof LinkNode) {
696                     INode node = ((LinkNode) link).getDelegate();
697                     if (!NodeUtil.isParentOf(node, this)) {
698                         othersSelected++;
699                         if (othersSelected > maxOtherNodesSelected)
700                             return false;
701                     }
702                 }
703             }
704             if (othersSelected > maxOtherNodesSelected)
705                 return false;
706         }
707         return true;
708     }
709
710     protected ISnapAdvisor getSnapAdvisor() {
711         GridNode grid = lookupNode(GridNode.GRID_NODE_ID, GridNode.class);
712         return grid != null ? grid.getSnapAdvisor() : null;
713     }
714
715     MoveAction.TargetFilter moveFilter = new MoveAction.TargetFilter() {
716         @Override
717         public boolean accept(Object target) {
718             return (target instanceof RouteLine) || (target instanceof RouteLink);
719         }
720     };
721
722     @Override
723     protected boolean handleCommand(CommandEvent e) {
724         /*if (Commands.DELETE.equals(e.command)) {
725             Object target = rg.pick(mouseX, mouseY, pickTolerance);
726             return deleteTarget(target);
727         } else if (Commands.SPLIT_CONNECTION.equals(e.command)) {
728             Object target = rg.pick(mouseX, mouseY, pickTolerance);
729             return splitTarget(target);
730         } else */
731         if (Commands.CANCEL.equals(e.command)) {
732             return cancelCurrentAction();
733         }
734         return false;
735     }
736
737     protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) {
738         if (currentAction instanceof MoveAction) {
739             MoveAction move = (MoveAction) currentAction;
740             RouteGraph originalRg = rg.copy();
741             move.finish(mouseX, mouseY);
742
743             setRouteGraphAndFireChanges(originalRg, rg);
744
745             currentAction = null;
746             repaint();
747             return true;
748         }
749         return false;
750     }
751
752     @Override
753     protected boolean keyPressed(KeyPressedEvent e) {
754         if (!editable)
755             return false;
756
757         if (!e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK) && e.keyCode == KeyEvent.VK_S) {
758             Object target = rg.pick(mouseX, mouseY, pickTolerance, RouteGraph.PICK_PERSISTENT_LINES | RouteGraph.PICK_TRANSIENT_LINES);
759             return splitTarget(target);
760         }
761         else if (!e.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK | MouseEvent.CTRL_MASK) && (e.keyCode == KeyEvent.VK_R || e.keyCode == KeyEvent.VK_D)) {
762             Object target = rg.pick(mouseX, mouseY, pickTolerance, RouteGraph.PICK_PERSISTENT_LINES);
763             return deleteTarget(target);
764         }
765         else if (e.keyCode == KeyEvent.VK_ESCAPE) {
766             return cancelCurrentAction();
767         }
768 //        else if (e.keyCode == KeyEvent.VK_D) {
769 //            if (target instanceof RouteTerminal) {
770 //                RouteTerminal terminal = (RouteTerminal) target;
771 //                RouteGraph before = rg.copy();
772 //                rg.toggleDirectLines(terminal);
773 //                setRouteGraphAndFireChanges(before, rg);
774 //                repaint();
775 //            }
776 //        }
777 //        else if (target != null && e.getKeyCode() == KeyEvent.VK_P) {
778 //            rg.print();
779 //        }
780         else if (e.keyCode == KeyEvent.VK_CONTROL) {
781             highlightActionsEnabled = true;
782             repaint();
783         }
784         else if (e.keyCode == KeyEvent.VK_ALT) {
785             // Begin connection branching visualization.
786             RouteLine line = rg.pickLine(mouseX, mouseY, pickTolerance);
787             if (branchable && line != null) {
788                 newBranchPointPosition = new Point2D.Double(mouseX, mouseY);
789                 SplittedRouteGraph.snapToLine(newBranchPointPosition, line);
790                 repaint();
791             }
792         }
793
794         return false;
795     }
796
797     @Override
798     protected boolean keyReleased(KeyReleasedEvent e) {
799         if (e.keyCode == KeyEvent.VK_ALT) {
800             // End connection branching visualization.
801             if (newBranchPointPosition != null) {
802                 newBranchPointPosition = null;
803                 repaint();
804             }
805         }
806         if (e.keyCode == KeyEvent.VK_CONTROL) {
807             highlightActionsEnabled = false;
808             repaint();
809         }
810         return false;
811     }
812
813
814     private boolean cancelCurrentAction() {
815         if (currentAction != null) {
816             currentAction = null;
817             repaint();
818             return true;
819         }
820         return false;
821     }
822
823     private boolean splitTarget(Object target) {
824         if (target instanceof RouteLine) {
825             RouteLine rLine = (RouteLine)target;
826             RouteGraph before = rg.copy();
827             rg.split(rLine, rLine.isHorizontal() ? mouseX : mouseY);
828             setRouteGraphAndFireChanges(before, rg);
829             repaint();
830             return true;
831         }
832         return false;
833     }
834
835     private boolean deleteTarget(Object target) {
836         boolean changed = false;
837         if (target instanceof RouteLine) {
838             RouteLine line = (RouteLine) target;
839             RouteGraph before = rg.copy();
840             rg.merge(line);
841             changed = setRouteGraphAndFireChanges(before, rg);
842         }
843         else if (target instanceof RouteLink) {
844             RouteGraph before = rg.copy();
845             rg.deleteCorner((RouteLink) target);
846             changed = setRouteGraphAndFireChanges(before, rg);
847         }
848 //        else if (target instanceof RouteTerminal) {
849 //            RouteGraph before = rg.copy();
850 //            rg.remove((RouteTerminal) target);
851 //            changed = setRouteGraphAndFireChanges(before, rg);
852 //        }
853         if (changed)
854             repaint();
855         return changed;
856     }
857
858     /**
859      * A version of MoveAction that snaps movements using the specified
860      * ISnapAdvisor.
861      */
862     static class SnappingMoveAction extends MoveAction {
863
864         private ISnapAdvisor snapAdvisor;
865         private Point2D      point = new Point2D.Double();
866
867         public SnappingMoveAction(RouteGraph rg, Object target, ISnapAdvisor snapAdvisor) {
868             super(rg, target);
869             this.snapAdvisor = snapAdvisor;
870         }
871
872         protected void move(RouteGraph rg, Object target, double x, double y) {
873             point.setLocation(x, y);
874             snapAdvisor.snap(point);
875             super.move(rg, target, point.getX(), point.getY());
876         }
877
878         public static MoveAction create(RouteGraph rg, double x, double y, double tolerance, TargetFilter filter, ISnapAdvisor snapAdvisor) {
879             Object target = rg.pick(x, y, tolerance, RouteGraph.PICK_LINES | RouteGraph.PICK_INTERIOR_POINTS);
880             if (target != null && (filter == null || filter.accept(target))) {
881                 if (snapAdvisor != null)
882                     return new SnappingMoveAction(rg, target, snapAdvisor);
883                 return new MoveAction(rg, target);
884             }
885             return null;
886         }
887
888     }
889
890     @Override
891     public int getEventMask() {
892         return EventTypes.CommandMask | EventTypes.KeyMask | EventTypes.MouseMask;
893     }
894
895 }