-/*******************************************************************************\r
- * Copyright (c) 2011 Association for Decentralized Information Management in\r
- * Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- * VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
-package org.simantics.scenegraph.g2d.nodes.connection;\r
-\r
-import java.awt.BasicStroke;\r
-import java.awt.Color;\r
-import java.awt.Graphics2D;\r
-import java.awt.RenderingHints;\r
-import java.awt.Shape;\r
-import java.awt.Stroke;\r
-import java.awt.event.KeyEvent;\r
-import java.awt.geom.AffineTransform;\r
-import java.awt.geom.Path2D;\r
-import java.awt.geom.Point2D;\r
-import java.awt.geom.Rectangle2D;\r
-import java.lang.reflect.Constructor;\r
-import java.util.Collection;\r
-import java.util.Map;\r
-\r
-import org.simantics.diagram.connection.RouteGraph;\r
-import org.simantics.diagram.connection.RouteLine;\r
-import org.simantics.diagram.connection.RouteLink;\r
-import org.simantics.diagram.connection.RouteTerminal;\r
-import org.simantics.diagram.connection.actions.IAction;\r
-import org.simantics.diagram.connection.actions.IReconnectAction;\r
-import org.simantics.diagram.connection.actions.MoveAction;\r
-import org.simantics.diagram.connection.actions.ReconnectLineAction;\r
-import org.simantics.diagram.connection.delta.RouteGraphDelta;\r
-import org.simantics.diagram.connection.rendering.BasicConnectionStyle;\r
-import org.simantics.diagram.connection.rendering.ConnectionStyle;\r
-import org.simantics.diagram.connection.rendering.IRouteGraphRenderer;\r
-import org.simantics.diagram.connection.rendering.StyledRouteGraphRenderer;\r
-import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle;\r
-import org.simantics.diagram.connection.splitting.SplittedRouteGraph;\r
-import org.simantics.scenegraph.INode;\r
-import org.simantics.scenegraph.ISelectionPainterNode;\r
-import org.simantics.scenegraph.g2d.G2DNode;\r
-import org.simantics.scenegraph.g2d.G2DParentNode;\r
-import org.simantics.scenegraph.g2d.IG2DNode;\r
-import org.simantics.scenegraph.g2d.events.EventTypes;\r
-import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;\r
-import org.simantics.scenegraph.g2d.events.KeyEvent.KeyReleasedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;\r
-import org.simantics.scenegraph.g2d.events.command.CommandEvent;\r
-import org.simantics.scenegraph.g2d.events.command.Commands;\r
-import org.simantics.scenegraph.g2d.nodes.GridNode;\r
-import org.simantics.scenegraph.g2d.nodes.LinkNode;\r
-import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Action;\r
-import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Pick;\r
-import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;\r
-import org.simantics.scenegraph.utils.GeometryUtils;\r
-import org.simantics.scenegraph.utils.InitValueSupport;\r
-import org.simantics.scenegraph.utils.NodeUtil;\r
-\r
-import gnu.trove.map.hash.THashMap;\r
-\r
-/**\r
- * @author Tuukka Lehtonen\r
- */\r
-public class RouteGraphNode extends G2DNode implements ISelectionPainterNode, InitValueSupport {\r
-\r
- private static final long serialVersionUID = -917194130412280965L;\r
-\r
- private static final double TOLERANCE = IAction.TOLERANCE;\r
- private static final Stroke SELECTION_STROKE = new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);\r
- private static final Color SELECTION_COLOR = new Color(255, 0, 255, 96);\r
-\r
- private static final HighlightActionPointsAction highlightActions = new HighlightActionPointsAction(null);\r
-\r
- protected RouteGraph rg;\r
- protected IRouteGraphRenderer baseRenderer;\r
- protected IRouteGraphRenderer renderer;\r
- protected double pickTolerance = TOLERANCE;\r
- protected boolean editable = true;\r
- private boolean branchable = true;\r
-\r
- protected IRouteGraphListener rgListener;\r
- protected RouteGraphDelta rgDelta;\r
-\r
- protected transient double mouseX;\r
- protected transient double mouseY;\r
- protected transient Point2D pt = new Point2D.Double();\r
-\r
- protected transient MoveAction dragAction;\r
- protected transient IAction currentAction;\r
- protected transient Rectangle2D bounds;\r
-\r
- /**\r
- * Dynamic color for connection rendering.\r
- */\r
- protected transient Color dynamicColor;\r
-\r
- /**\r
- * Dynamic stroke for connection rendering.\r
- */\r
- protected transient Stroke dynamicStroke;\r
-\r
- protected transient Path2D selectionPath = new Path2D.Double();\r
- protected transient Stroke selectionStroke = null;\r
-\r
- protected transient boolean highlightActionsEnabled = false;\r
- protected transient AffineTransform lastViewTransform = null;\r
-\r
- /**\r
- * x = NaN is used to indicate that possible branch point should not be\r
- * rendered but interaction has not ended yet.\r
- */\r
- protected transient Point2D newBranchPointPosition = null;\r
-\r
-\r
- protected transient Map<Object,ILineEndStyle> dynamicStyles = null;\r
- \r
- @Override\r
- public void initValues() {\r
- dynamicColor = null;\r
- wrapRenderer();\r
- }\r
-\r
- @PropertySetter("color")\r
- @SyncField(value = {"dynamicColor"})\r
- public void setDynamicColor(Color color) {\r
- this.dynamicColor = color;\r
- wrapRenderer();\r
- }\r
-\r
- @PropertySetter("width")\r
- @SyncField("dynamicStroke")\r
- public void setDynamicStroke(Stroke stroke) {\r
- this.dynamicStroke = stroke;\r
- wrapRenderer();\r
- createSelectionStroke();\r
- }\r
- \r
- @SyncField(value = {"dynamicStyles"})\r
- public void setDynamicLineEnd(RouteTerminal terminal, ILineEndStyle style) {\r
- if (dynamicStyles == null)\r
- dynamicStyles = new THashMap<Object, ILineEndStyle>();\r
- terminal.setDynamicStyle(style);\r
- if (terminal.getData() != null) {\r
- if (style != null)\r
- dynamicStyles.put(terminal.getData(),style);\r
- else\r
- dynamicStyles.remove(terminal.getData());\r
- }\r
- }\r
- \r
- private void updateLineEnds() {\r
- if (dynamicStyles == null)\r
- return;\r
- for (RouteTerminal t : rg.getTerminals()) {\r
- if (t.getData() == null)\r
- continue;\r
- ILineEndStyle dynamicStyle = dynamicStyles.get(t.getData());\r
- if (dynamicStyle != null)\r
- t.setDynamicStyle(dynamicStyle);\r
- }\r
- }\r
-\r
- @SyncField(value = {"rg"})\r
- public void setRouteGraph(RouteGraph graph) {\r
- this.rg = graph;\r
- updateLineEnds();\r
- updateBounds();\r
- }\r
-\r
- @SyncField(value = {"rgDelta"})\r
- public void setRouteGraphDelta(RouteGraphDelta delta) {\r
- this.rgDelta = delta;\r
- }\r
-\r
- @SyncField(value = {"renderer"})\r
- public void setRenderer(IRouteGraphRenderer renderer) {\r
-\r
- this.baseRenderer = renderer;\r
- wrapRenderer();\r
-\r
- createSelectionStroke(); \r
- }\r
- \r
- private void createSelectionStroke() {\r
- BasicConnectionStyle style = tryGetStyle();\r
- selectionStroke = null;\r
- if (style != null) {\r
- BasicStroke stroke = (BasicStroke) style.getLineStroke();\r
- if (stroke != null) {\r
- float width = Math.max(stroke.getLineWidth() + 0.75f, stroke.getLineWidth()*1.3f);\r
- selectionStroke = new BasicStroke(width, BasicStroke.CAP_BUTT, stroke.getLineJoin());\r
- }\r
- } else {\r
- selectionStroke = SELECTION_STROKE;\r
- }\r
- }\r
- \r
- private void wrapRenderer() {\r
- \r
- if(baseRenderer == null) {\r
- renderer = null;\r
- return;\r
- }\r
- \r
- if(dynamicColor != null || dynamicStroke != null) {\r
- BasicConnectionStyle baseStyle = (BasicConnectionStyle)tryGetStyle(baseRenderer);\r
- try {\r
- Constructor<? extends BasicConnectionStyle> c = baseStyle.getClass().getConstructor(Color.class, Color.class, double.class, Stroke.class, Stroke.class, double.class);\r
- renderer = new StyledRouteGraphRenderer(c.newInstance(\r
- dynamicColor != null ? dynamicColor : baseStyle.getLineColor(),\r
- baseStyle.getBranchPointColor(), baseStyle.getBranchPointRadius(),\r
- dynamicStroke != null ? dynamicStroke : baseStyle.getLineStroke(), \r
- dynamicStroke != null ? dynamicStroke : baseStyle.getRouteLineStroke(),\r
- baseStyle.getDegeneratedLineLength()));\r
- } catch (Exception e) {\r
- renderer = new StyledRouteGraphRenderer(new BasicConnectionStyle(\r
- dynamicColor != null ? dynamicColor : baseStyle.getLineColor(),\r
- baseStyle.getBranchPointColor(), baseStyle.getBranchPointRadius(),\r
- dynamicStroke != null ? dynamicStroke : baseStyle.getLineStroke(), \r
- dynamicStroke != null ? dynamicStroke : baseStyle.getRouteLineStroke(),\r
- baseStyle.getDegeneratedLineLength()));\r
- }\r
- \r
- \r
- } else {\r
- renderer = baseRenderer;\r
- }\r
- \r
- }\r
-\r
- @SyncField(value = {"pickTolerance"})\r
- public void setPickTolerance(double tolerance) {\r
- this.pickTolerance = tolerance;\r
- }\r
-\r
- @SyncField(value = {"editable"})\r
- public void setEditable(boolean editable) {\r
- this.editable = editable;\r
- }\r
-\r
- @SyncField(value = {"branchable"})\r
- public void setBranchable(boolean branchable) {\r
- this.branchable = branchable;\r
- }\r
-\r
- public RouteGraph getRouteGraph() {\r
- return rg;\r
- }\r
-\r
- public RouteGraphDelta getRouteGraphDelta() {\r
- return rgDelta;\r
- }\r
-\r
- public IRouteGraphRenderer getRenderer() {\r
- return renderer;\r
- }\r
-\r
- public boolean isEditable() {\r
- return editable;\r
- }\r
-\r
- public boolean isBranchable() {\r
- return branchable;\r
- }\r
- \r
- public double getPickTolerance() {\r
- return pickTolerance;\r
- }\r
-\r
- /**\r
- * When in client-server mode, listener is only set on the server side and\r
- * fireRouteGraphChanged will tell it when rg has changed.\r
- * \r
- * @param listener\r
- */\r
- public void setRouteGraphListener(IRouteGraphListener listener) {\r
- this.rgListener = listener;\r
- }\r
-\r
- /**\r
- * @param before\r
- * @param after\r
- * @return <code>true</code> if changes were fired\r
- */\r
- private boolean setRouteGraphAndFireChanges(RouteGraph before, RouteGraph after) {\r
- RouteGraphDelta delta = new RouteGraphDelta(before, after);\r
- if (!delta.isEmpty()) {\r
- setRouteGraph(after);\r
- setRouteGraphDelta(delta);\r
- fireRouteGraphChanged(before, after, delta);\r
- return true;\r
- }\r
- return false;\r
- }\r
-\r
- @ServerSide\r
- protected void fireRouteGraphChanged(RouteGraph before, RouteGraph after, RouteGraphDelta delta) {\r
- if (rgListener != null) {\r
- RouteGraphChangeEvent event = new RouteGraphChangeEvent(this, before, after, delta);\r
- rgListener.routeGraphChanged(event);\r
- }\r
- }\r
-\r
- public void showBranchPoint(Point2D p) {\r
- newBranchPointPosition = p;\r
- }\r
-\r
- @Override\r
- public void init() {\r
- super.init();\r
- addEventHandler(this);\r
- }\r
-\r
- @Override\r
- public void cleanup() {\r
- rgListener = null;\r
- removeEventHandler(this);\r
- super.cleanup();\r
- }\r
-\r
- protected boolean isSelected() {\r
- return NodeUtil.isSelected(this, 1);\r
- }\r
-\r
- @Override\r
- public void render(Graphics2D g) {\r
- if (renderer == null)\r
- return;\r
-\r
- AffineTransform ot = null;\r
- AffineTransform t = getTransform();\r
- if (t != null && !t.isIdentity()) {\r
- ot = g.getTransform();\r
- g.transform(getTransform());\r
- }\r
-\r
- Object aaHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);\r
- g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);\r
-\r
- boolean selected = NodeUtil.isSelected(this, 1);\r
-\r
- if (currentAction != null) {\r
- currentAction.render(g, renderer, mouseX, mouseY);\r
- } else {\r
- if (selected && selectionStroke != null) {\r
- selectionPath.reset();\r
- rg.getPath2D(selectionPath);\r
- Shape selectionShape = selectionStroke.createStrokedShape(selectionPath);\r
- g.setColor(SELECTION_COLOR);\r
- g.fill(selectionShape);\r
- }\r
-\r
- renderer.render(g, rg);\r
- if(selected)\r
- renderer.renderGuides(g, rg);\r
-\r
- if (selected && highlightActionsEnabled) {\r
- // Needed for performing actions in #mouseClicked\r
- this.lastViewTransform = g.getTransform();\r
- highlightActions.setRouteGraph(rg);\r
- highlightActions.render(g, renderer, mouseX, mouseY);\r
- highlightActions.setRouteGraph(null);\r
- }\r
- }\r
-\r
- g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, aaHint);\r
-\r
- if (editable && branchable && newBranchPointPosition != null && !Double.isNaN(newBranchPointPosition.getX())) {\r
- ConnectionStyle style = tryGetStyle();\r
- if(style != null)\r
- style.drawBranchPoint(g, newBranchPointPosition.getX(), newBranchPointPosition.getY());\r
- }\r
-\r
- if (ot != null)\r
- g.setTransform(ot);\r
- }\r
- \r
- private BasicConnectionStyle tryGetStyle() {\r
- return tryGetStyle(renderer);\r
- }\r
-\r
- private BasicConnectionStyle tryGetStyle(IRouteGraphRenderer renderer) {\r
- if (renderer instanceof StyledRouteGraphRenderer) {\r
- ConnectionStyle cs = ((StyledRouteGraphRenderer) renderer).getStyle();\r
- if (cs instanceof BasicConnectionStyle)\r
- return (BasicConnectionStyle) cs;\r
- }\r
- return null;\r
- }\r
-\r
- private double getSelectionStrokeWidth() {\r
- if (selectionStroke instanceof BasicStroke) {\r
- BasicStroke bs = (BasicStroke) selectionStroke;\r
- return bs.getLineWidth();\r
- }\r
- return 1.0;\r
- }\r
-\r
- @Override\r
- public Rectangle2D getBoundsInLocal() {\r
- return bounds;\r
- }\r
-\r
- protected void updateBounds() {\r
- Rectangle2D r = this.bounds;\r
- if (r == null)\r
- r = new Rectangle2D.Double();\r
- this.bounds = calculateBounds(r);\r
-\r
- // Need to expand to take stroke width into account.\r
- double sw = getSelectionStrokeWidth() / 2;\r
- GeometryUtils.expandRectangle(this.bounds, sw, sw);\r
- }\r
-\r
- protected Rectangle2D calculateBounds(Rectangle2D rect) {\r
- RouteGraph rg = this.rg;\r
- if (currentAction instanceof MoveAction)\r
- rg = ((MoveAction) currentAction).getRouteGraph();\r
- rg.getBounds(rect);\r
- return rect;\r
- }\r
-\r
- protected void getMouseLocalPos(MouseEvent e) {\r
- //System.out.println("m: " + e.controlPosition);\r
- pt.setLocation(e.controlPosition);\r
- //System.out.println("parent: " + pt);\r
- pt = NodeUtil.worldToLocal(this, pt, pt);\r
- //System.out.println("local: " + pt);\r
- mouseX = pt.getX();\r
- mouseY = pt.getY();\r
- }\r
-\r
- @Override\r
- protected boolean mouseDragged(MouseDragBegin e) {\r
- if (dragAction != null && !e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK) && e.button == MouseEvent.LEFT_BUTTON) {\r
- currentAction = dragAction;\r
- dragAction = null;\r
- }\r
- return updateCurrentAction(e, true);\r
- }\r
-\r
- @Override\r
- protected boolean mouseMoved(MouseMovedEvent e) {\r
- //System.out.println("mouse moved: " + e);\r
-\r
- // Handle connection branching visualization.\r
- getMouseLocalPos(e);\r
- if (newBranchPointPosition == null && e.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK)) {\r
- if (bounds.contains(mouseX, mouseY)) {\r
- newBranchPointPosition = new Point2D.Double(Double.NaN, Double.NaN);\r
- }\r
- }\r
- if (newBranchPointPosition != null) {\r
- RouteLine line = rg.pickLine(mouseX, mouseY, pickTolerance);\r
- if (line != null) {\r
- newBranchPointPosition.setLocation(mouseX, mouseY);\r
- SplittedRouteGraph.snapToLine(newBranchPointPosition, line);\r
- repaint();\r
- } else if (!Double.isNaN(newBranchPointPosition.getX())) {\r
- newBranchPointPosition.setLocation(Double.NaN, Double.NaN);\r
- repaint();\r
- }\r
- }\r
-\r
- // Make sure that highlight action rendering is according to mouse hover.\r
- if (highlightActionsEnabled) {\r
- if (NodeUtil.isSelected(this, 1)) {\r
- repaint();\r
- }\r
- }\r
-\r
- return updateCurrentAction(e, false);\r
- }\r
-\r
- protected boolean updateCurrentAction(MouseEvent e, boolean updateMousePos) {\r
- boolean oldHighlight = highlightActionsEnabled;\r
- highlightActionsEnabled = e.hasAllModifiers(MouseEvent.CTRL_MASK);\r
- if (oldHighlight != highlightActionsEnabled)\r
- repaint();\r
-\r
- if (currentAction != null) {\r
- if (updateMousePos)\r
- getMouseLocalPos(e);\r
- updateBounds();\r
-\r
- // Repaint, but only if absolutely necessary.\r
- if (currentAction instanceof MoveAction || bounds.contains(mouseX, mouseY))\r
- repaint();\r
-\r
- return true;\r
- }\r
- return false;\r
- }\r
-\r
- @Override\r
- protected boolean mouseClicked(MouseClickEvent e) {\r
- if (!editable)\r
- return false;\r
-\r
- if (e.button == MouseEvent.LEFT_BUTTON) {\r
- if (isSelected() && highlightActionsEnabled) {\r
- // Reconnection / segment deletion only available for branched connections. \r
- if (rg.getTerminals().size() > 2) {\r
- Pick pick = highlightActions.pickAction(rg, lastViewTransform, mouseX, mouseY);\r
- if (pick.hasAction(Action.REMOVE)) {\r
- RemoveLineAction remove = RemoveLineAction.perform(rg, pick.line.getLine(), mouseX, mouseY);\r
- if (remove != null) {\r
- setRouteGraphAndFireChanges(remove.getOriginalRouteGraph(), remove.getRouteGraph());\r
- repaint();\r
- return true;\r
- }\r
- }\r
- if (pick.hasAction(Action.RECONNECT)) {\r
- currentAction = ReconnectLineAction.create(rg, mouseX, mouseY);\r
- if (currentAction != null) {\r
- repaint();\r
- }\r
- }\r
- }\r
- }\r
- }\r
-\r
- return false;\r
- }\r
-\r
- @Override\r
- protected boolean mouseButtonPressed(MouseButtonPressedEvent e) {\r
- if (!editable)\r
- return false;\r
-\r
- if (e.button == MouseEvent.LEFT_BUTTON) {\r
- // Visualize new branch point no longer.\r
- newBranchPointPosition = null;\r
-\r
- getMouseLocalPos(e);\r
- dragAction = null;\r
-// if(currentAction instanceof HighlightActionPointsAction) {\r
-// RemoveLineAction remove = RemoveLineAction.perform(rg, mouseX, mouseY);\r
-// if (remove != null) {\r
-// setRouteGraphAndFireChanges(remove.getOriginalRouteGraph(), remove.getRouteGraph());\r
-// repaint();\r
-// } else {\r
-// currentAction = ReconnectLineAction.create(rg, mouseX, mouseY);\r
-// if (currentAction != null)\r
-// repaint();\r
-// }\r
-// }\r
-// else\r
- if(currentAction instanceof IReconnectAction) {\r
- RouteGraph originalRg = rg.copy();\r
- ((IReconnectAction)currentAction).finish(mouseX, mouseY);\r
- currentAction = null;\r
-\r
- setRouteGraphAndFireChanges(originalRg, rg);\r
-\r
- currentAction = null;\r
- repaint();\r
- return true;\r
- }\r
- else {\r
- if (!allowConnectionRerouting()) {\r
- return false;\r
- }\r
- //System.out.println("move action");\r
- dragAction = SnappingMoveAction.create(rg, mouseX, mouseY, pickTolerance, moveFilter, getSnapAdvisor());\r
- //System.out.println("DRAG ACTION: " + dragAction);\r
- }\r
-\r
- //System.out.println(this + " NEW action: " + currentAction);\r
- if (currentAction != null)\r
- return true;\r
- }\r
- return false;\r
- }\r
-\r
- /**\r
- * Checks the selections data node in the scene graph for any links \r
- * @return\r
- */\r
- private boolean allowConnectionRerouting() {\r
- final int maxOtherNodesSelected = 1;\r
-\r
- INode selections = NodeUtil.tryLookup(this, "selections");\r
- if (!(selections instanceof G2DParentNode))\r
- return true;\r
- G2DParentNode p = (G2DParentNode) selections;\r
- for (IG2DNode selection : p.getNodes()) {\r
- if (!(selection instanceof G2DParentNode))\r
- continue;\r
-\r
- G2DParentNode sp = (G2DParentNode) selection;\r
- Collection<IG2DNode> links = sp.getNodes();\r
- if (links.isEmpty())\r
- return true;\r
- int othersSelected = 0;\r
- for (IG2DNode link : links) {\r
- if (link instanceof LinkNode) {\r
- INode node = ((LinkNode) link).getDelegate();\r
- if (!NodeUtil.isParentOf(node, this)) {\r
- othersSelected++;\r
- if (othersSelected > maxOtherNodesSelected)\r
- return false;\r
- }\r
- }\r
- }\r
- if (othersSelected > maxOtherNodesSelected)\r
- return false;\r
- }\r
- return true;\r
- }\r
-\r
- protected ISnapAdvisor getSnapAdvisor() {\r
- GridNode grid = lookupNode(GridNode.GRID_NODE_ID, GridNode.class);\r
- return grid != null ? grid.getSnapAdvisor() : null;\r
- }\r
-\r
- MoveAction.TargetFilter moveFilter = new MoveAction.TargetFilter() {\r
- @Override\r
- public boolean accept(Object target) {\r
- return (target instanceof RouteLine) || (target instanceof RouteLink);\r
- }\r
- };\r
-\r
- @Override\r
- protected boolean handleCommand(CommandEvent e) {\r
- /*if (Commands.DELETE.equals(e.command)) {\r
- Object target = rg.pick(mouseX, mouseY, pickTolerance);\r
- return deleteTarget(target);\r
- } else if (Commands.SPLIT_CONNECTION.equals(e.command)) {\r
- Object target = rg.pick(mouseX, mouseY, pickTolerance);\r
- return splitTarget(target);\r
- } else */\r
- if (Commands.CANCEL.equals(e.command)) {\r
- return cancelCurrentAction();\r
- }\r
- return false;\r
- }\r
-\r
- protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) {\r
- if (currentAction instanceof MoveAction) {\r
- MoveAction move = (MoveAction) currentAction;\r
- RouteGraph originalRg = rg.copy();\r
- move.finish(mouseX, mouseY);\r
-\r
- setRouteGraphAndFireChanges(originalRg, rg);\r
-\r
- currentAction = null;\r
- repaint();\r
- return true;\r
- }\r
- return false;\r
- }\r
-\r
- @Override\r
- protected boolean keyPressed(KeyPressedEvent e) {\r
- if (!editable)\r
- return false;\r
-\r
- if (!e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK) && e.keyCode == KeyEvent.VK_S) {\r
- Object target = rg.pick(mouseX, mouseY, pickTolerance, RouteGraph.PICK_PERSISTENT_LINES | RouteGraph.PICK_TRANSIENT_LINES);\r
- return splitTarget(target);\r
- }\r
- else if (!e.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK | MouseEvent.CTRL_MASK) && (e.keyCode == KeyEvent.VK_R || e.keyCode == KeyEvent.VK_D)) {\r
- Object target = rg.pick(mouseX, mouseY, pickTolerance, RouteGraph.PICK_PERSISTENT_LINES);\r
- return deleteTarget(target);\r
- }\r
- else if (e.keyCode == KeyEvent.VK_ESCAPE) {\r
- return cancelCurrentAction();\r
- }\r
-// else if (e.keyCode == KeyEvent.VK_D) {\r
-// if (target instanceof RouteTerminal) {\r
-// RouteTerminal terminal = (RouteTerminal) target;\r
-// RouteGraph before = rg.copy();\r
-// rg.toggleDirectLines(terminal);\r
-// setRouteGraphAndFireChanges(before, rg);\r
-// repaint();\r
-// }\r
-// }\r
-// else if (target != null && e.getKeyCode() == KeyEvent.VK_P) {\r
-// rg.print();\r
-// }\r
- else if (e.keyCode == KeyEvent.VK_CONTROL) {\r
- highlightActionsEnabled = true;\r
- repaint();\r
- }\r
- else if (e.keyCode == KeyEvent.VK_ALT) {\r
- // Begin connection branching visualization.\r
- RouteLine line = rg.pickLine(mouseX, mouseY, pickTolerance);\r
- if (branchable && line != null) {\r
- newBranchPointPosition = new Point2D.Double(mouseX, mouseY);\r
- SplittedRouteGraph.snapToLine(newBranchPointPosition, line);\r
- repaint();\r
- }\r
- }\r
-\r
- return false;\r
- }\r
-\r
- @Override\r
- protected boolean keyReleased(KeyReleasedEvent e) {\r
- if (e.keyCode == KeyEvent.VK_ALT) {\r
- // End connection branching visualization.\r
- if (newBranchPointPosition != null) {\r
- newBranchPointPosition = null;\r
- repaint();\r
- }\r
- }\r
- if (e.keyCode == KeyEvent.VK_CONTROL) {\r
- highlightActionsEnabled = false;\r
- repaint();\r
- }\r
- return false;\r
- }\r
-\r
-\r
- private boolean cancelCurrentAction() {\r
- if (currentAction != null) {\r
- currentAction = null;\r
- repaint();\r
- return true;\r
- }\r
- return false;\r
- }\r
-\r
- private boolean splitTarget(Object target) {\r
- if (target instanceof RouteLine) {\r
- RouteLine rLine = (RouteLine)target;\r
- RouteGraph before = rg.copy();\r
- rg.split(rLine, rLine.isHorizontal() ? mouseX : mouseY);\r
- setRouteGraphAndFireChanges(before, rg);\r
- repaint();\r
- return true;\r
- }\r
- return false;\r
- }\r
-\r
- private boolean deleteTarget(Object target) {\r
- boolean changed = false;\r
- if (target instanceof RouteLine) {\r
- RouteLine line = (RouteLine) target;\r
- RouteGraph before = rg.copy();\r
- rg.merge(line);\r
- changed = setRouteGraphAndFireChanges(before, rg);\r
- }\r
- else if (target instanceof RouteLink) {\r
- RouteGraph before = rg.copy();\r
- rg.deleteCorner((RouteLink) target);\r
- changed = setRouteGraphAndFireChanges(before, rg);\r
- }\r
-// else if (target instanceof RouteTerminal) {\r
-// RouteGraph before = rg.copy();\r
-// rg.remove((RouteTerminal) target);\r
-// changed = setRouteGraphAndFireChanges(before, rg);\r
-// }\r
- if (changed)\r
- repaint();\r
- return changed;\r
- }\r
-\r
- /**\r
- * A version of MoveAction that snaps movements using the specified\r
- * ISnapAdvisor.\r
- */\r
- static class SnappingMoveAction extends MoveAction {\r
-\r
- private ISnapAdvisor snapAdvisor;\r
- private Point2D point = new Point2D.Double();\r
-\r
- public SnappingMoveAction(RouteGraph rg, Object target, ISnapAdvisor snapAdvisor) {\r
- super(rg, target);\r
- this.snapAdvisor = snapAdvisor;\r
- }\r
-\r
- protected void move(RouteGraph rg, Object target, double x, double y) {\r
- point.setLocation(x, y);\r
- snapAdvisor.snap(point);\r
- super.move(rg, target, point.getX(), point.getY());\r
- }\r
-\r
- public static MoveAction create(RouteGraph rg, double x, double y, double tolerance, TargetFilter filter, ISnapAdvisor snapAdvisor) {\r
- Object target = rg.pick(x, y, tolerance, RouteGraph.PICK_LINES | RouteGraph.PICK_INTERIOR_POINTS);\r
- if (target != null && (filter == null || filter.accept(target))) {\r
- if (snapAdvisor != null)\r
- return new SnappingMoveAction(rg, target, snapAdvisor);\r
- return new MoveAction(rg, target);\r
- }\r
- return null;\r
- }\r
-\r
- }\r
-\r
- @Override\r
- public int getEventMask() {\r
- return EventTypes.CommandMask | EventTypes.KeyMask | EventTypes.MouseMask;\r
- }\r
-\r
-}\r
+/*******************************************************************************
+ * Copyright (c) 2011 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.scenegraph.g2d.nodes.connection;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.Stroke;
+import java.awt.event.KeyEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.simantics.diagram.connection.RouteGraph;
+import org.simantics.diagram.connection.RouteLine;
+import org.simantics.diagram.connection.RouteLink;
+import org.simantics.diagram.connection.RouteTerminal;
+import org.simantics.diagram.connection.actions.IAction;
+import org.simantics.diagram.connection.actions.IReconnectAction;
+import org.simantics.diagram.connection.actions.MoveAction;
+import org.simantics.diagram.connection.actions.ReconnectLineAction;
+import org.simantics.diagram.connection.delta.RouteGraphDelta;
+import org.simantics.diagram.connection.rendering.BasicConnectionStyle;
+import org.simantics.diagram.connection.rendering.ConnectionStyle;
+import org.simantics.diagram.connection.rendering.IRouteGraphRenderer;
+import org.simantics.diagram.connection.rendering.StyledRouteGraphRenderer;
+import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle;
+import org.simantics.diagram.connection.splitting.SplittedRouteGraph;
+import org.simantics.scenegraph.INode;
+import org.simantics.scenegraph.ISelectionPainterNode;
+import org.simantics.scenegraph.g2d.G2DNode;
+import org.simantics.scenegraph.g2d.G2DParentNode;
+import org.simantics.scenegraph.g2d.IG2DNode;
+import org.simantics.scenegraph.g2d.events.EventTypes;
+import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
+import org.simantics.scenegraph.g2d.events.KeyEvent.KeyReleasedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
+import org.simantics.scenegraph.g2d.events.command.CommandEvent;
+import org.simantics.scenegraph.g2d.events.command.Commands;
+import org.simantics.scenegraph.g2d.nodes.GridNode;
+import org.simantics.scenegraph.g2d.nodes.LinkNode;
+import org.simantics.scenegraph.g2d.nodes.SVGNodeAssignment;
+import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Action;
+import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Pick;
+import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
+import org.simantics.scenegraph.utils.ColorUtil;
+import org.simantics.scenegraph.utils.GeometryUtils;
+import org.simantics.scenegraph.utils.InitValueSupport;
+import org.simantics.scenegraph.utils.NodeUtil;
+import org.slf4j.LoggerFactory;
+import org.slf4j.Logger;
+
+import gnu.trove.map.hash.THashMap;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public class RouteGraphNode extends G2DNode implements ISelectionPainterNode, InitValueSupport {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RouteGraphNode.class);
+
+ private static final long serialVersionUID = -917194130412280965L;
+
+ private static final double TOLERANCE = IAction.TOLERANCE;
+ private static final Stroke SELECTION_STROKE = new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
+ private static final Color SELECTION_COLOR = new Color(255, 0, 255, 96);
+
+ private static final HighlightActionPointsAction highlightActions = new HighlightActionPointsAction(null);
+
+ protected RouteGraph rg;
+ protected IRouteGraphRenderer baseRenderer;
+ protected IRouteGraphRenderer renderer;
+ protected double pickTolerance = TOLERANCE;
+ protected boolean editable = true;
+ private boolean branchable = true;
+
+ protected IRouteGraphListener rgListener;
+ protected RouteGraphDelta rgDelta;
+
+ protected transient double mouseX;
+ protected transient double mouseY;
+ protected transient Point2D pt = new Point2D.Double();
+
+ protected transient MoveAction dragAction;
+ protected transient IAction currentAction;
+ protected transient Rectangle2D bounds;
+
+ /**
+ * Dynamic color for connection rendering.
+ */
+ protected transient Color dynamicColor;
+
+ /**
+ * Dynamic stroke for connection rendering.
+ */
+ protected transient Stroke dynamicStroke;
+
+ protected transient Path2D selectionPath = new Path2D.Double();
+ protected transient Stroke selectionStroke = null;
+
+ protected transient boolean highlightActionsEnabled = false;
+ protected transient AffineTransform lastViewTransform = null;
+
+ /**
+ * x = NaN is used to indicate that possible branch point should not be
+ * rendered but interaction has not ended yet.
+ */
+ protected transient Point2D newBranchPointPosition = null;
+
+
+ protected transient Map<Object,ILineEndStyle> dynamicStyles = null;
+
+ private transient boolean ignoreSelection = false;
+
+ @Override
+ public void initValues() {
+ dynamicColor = null;
+ wrapRenderer();
+ }
+
+ private static float tryParseFloat(String s, float def) {
+ try {
+ return Float.parseFloat(s);
+ } catch (NumberFormatException e) {
+ LOGGER.error("Could not parse '" + s + "' into float.");
+ return def;
+ }
+ }
+
+ /*
+ * 1.0 BUTT MITER 1.0 0.0
+ */
+ private static Stroke parseStroke(String definition) {
+
+ float width = 1.0f;
+ int cap = BasicStroke.CAP_BUTT;
+ int join = BasicStroke.JOIN_MITER;
+ float miterLimit = 1.0f;
+ float[] dash = { 1, 0};
+ float dash_phase = 0;
+
+ String[] parts = definition.split(" ");
+
+ if(parts.length > 0) {
+ width = tryParseFloat(parts[0], width);
+ }
+ if(parts.length > 1) {
+ if("BUTT".equals(parts[1])) cap = BasicStroke.CAP_BUTT;
+ else if("ROUND".equals(parts[1])) cap = BasicStroke.CAP_ROUND;
+ else if("SQUARE".equals(parts[1])) cap = BasicStroke.CAP_SQUARE;
+ }
+ if(parts.length > 2) {
+ if("BEVEL".equals(parts[2])) cap = BasicStroke.JOIN_BEVEL;
+ else if("MITER".equals(parts[2])) cap = BasicStroke.JOIN_MITER;
+ else if("ROUND".equals(parts[2])) cap = BasicStroke.JOIN_ROUND;
+ }
+ if(parts.length > 3) {
+ miterLimit = tryParseFloat(parts[3], miterLimit);
+ }
+ if(parts.length > 4) {
+ dash_phase = tryParseFloat(parts[4], dash_phase);
+ }
+ if(parts.length > 6) {
+ dash = new float[parts.length - 5];
+ for(int i=5;i<parts.length;i++) {
+ dash[i-5] = tryParseFloat(parts[i], 1.0f);
+ }
+ }
+
+
+ try {
+ return new BasicStroke(width, cap, join, miterLimit, dash, dash_phase);
+ } catch (IllegalArgumentException e) {
+ return new BasicStroke();
+ }
+
+ }
+
+ public void setAssignments(List<SVGNodeAssignment> assignments) {
+ for(SVGNodeAssignment ass : assignments) {
+ if("dynamicColor".equals(ass.elementId)) {
+ setDynamicColor(ColorUtil.hexColor(ass.value));
+ } else if("dynamicStroke".equals(ass.elementId)) {
+ setDynamicStroke(parseStroke(ass.value));
+ }
+ }
+ }
+
+ public void setIgnoreSelection(boolean value) {
+ ignoreSelection = value;
+ }
+
+ public boolean getIgnoreSelection() {
+ return ignoreSelection;
+ }
+
+ @PropertySetter("color")
+ @SyncField(value = {"dynamicColor"})
+ public void setDynamicColor(Color color) {
+ this.dynamicColor = color;
+ wrapRenderer();
+ }
+
+ @PropertySetter("width")
+ @SyncField("dynamicStroke")
+ public void setDynamicStroke(Stroke stroke) {
+ this.dynamicStroke = stroke;
+ wrapRenderer();
+ createSelectionStroke();
+ }
+
+ @SyncField(value = {"dynamicStyles"})
+ public void setDynamicLineEnd(RouteTerminal terminal, ILineEndStyle style) {
+ if (dynamicStyles == null)
+ dynamicStyles = new THashMap<Object, ILineEndStyle>();
+ terminal.setDynamicStyle(style);
+ if (terminal.getData() != null) {
+ if (style != null)
+ dynamicStyles.put(terminal.getData(),style);
+ else
+ dynamicStyles.remove(terminal.getData());
+ }
+ }
+
+ private void updateLineEnds() {
+ if (dynamicStyles == null)
+ return;
+ for (RouteTerminal t : rg.getTerminals()) {
+ if (t.getData() == null)
+ continue;
+ ILineEndStyle dynamicStyle = dynamicStyles.get(t.getData());
+ if (dynamicStyle != null)
+ t.setDynamicStyle(dynamicStyle);
+ }
+ }
+
+ @SyncField(value = {"rg"})
+ public void setRouteGraph(RouteGraph graph) {
+ this.rg = graph;
+ updateLineEnds();
+ updateBounds();
+ }
+
+ @SyncField(value = {"rgDelta"})
+ public void setRouteGraphDelta(RouteGraphDelta delta) {
+ this.rgDelta = delta;
+ }
+
+ @SyncField(value = {"renderer"})
+ public void setRenderer(IRouteGraphRenderer renderer) {
+
+ this.baseRenderer = renderer;
+ wrapRenderer();
+
+ createSelectionStroke();
+ }
+
+ private void createSelectionStroke() {
+ BasicConnectionStyle style = tryGetStyle();
+ selectionStroke = null;
+ if (style != null) {
+ BasicStroke stroke = (BasicStroke) style.getLineStroke();
+ if (stroke != null) {
+ float width = Math.max(stroke.getLineWidth() + 0.75f, stroke.getLineWidth()*1.3f);
+ selectionStroke = new BasicStroke(width, BasicStroke.CAP_BUTT, stroke.getLineJoin());
+ }
+ } else {
+ selectionStroke = SELECTION_STROKE;
+ }
+ }
+
+ private void wrapRenderer() {
+
+ if(baseRenderer == null) {
+ renderer = null;
+ return;
+ }
+
+ if(dynamicColor != null || dynamicStroke != null) {
+ BasicConnectionStyle baseStyle = (BasicConnectionStyle)tryGetStyle(baseRenderer);
+ try {
+ Constructor<? extends BasicConnectionStyle> c = baseStyle.getClass().getConstructor(Color.class, Color.class, double.class, Stroke.class, Stroke.class, double.class, double.class);
+ renderer = new StyledRouteGraphRenderer(c.newInstance(
+ dynamicColor != null ? dynamicColor : baseStyle.getLineColor(),
+ baseStyle.getBranchPointColor(), baseStyle.getBranchPointRadius(),
+ dynamicStroke != null ? dynamicStroke : baseStyle.getLineStroke(),
+ dynamicStroke != null ? dynamicStroke : baseStyle.getRouteLineStroke(),
+ baseStyle.getDegeneratedLineLength(), baseStyle.getRounding()));
+ } catch (Exception e) {
+ renderer = new StyledRouteGraphRenderer(new BasicConnectionStyle(
+ dynamicColor != null ? dynamicColor : baseStyle.getLineColor(),
+ baseStyle.getBranchPointColor(), baseStyle.getBranchPointRadius(),
+ dynamicStroke != null ? dynamicStroke : baseStyle.getLineStroke(),
+ dynamicStroke != null ? dynamicStroke : baseStyle.getRouteLineStroke(),
+ baseStyle.getDegeneratedLineLength(), baseStyle.getRounding()));
+ }
+
+
+ } else {
+ renderer = baseRenderer;
+ }
+
+ }
+
+ @SyncField(value = {"pickTolerance"})
+ public void setPickTolerance(double tolerance) {
+ this.pickTolerance = tolerance;
+ }
+
+ @SyncField(value = {"editable"})
+ public void setEditable(boolean editable) {
+ this.editable = editable;
+ }
+
+ @SyncField(value = {"branchable"})
+ public void setBranchable(boolean branchable) {
+ this.branchable = branchable;
+ }
+
+ public RouteGraph getRouteGraph() {
+ return rg;
+ }
+
+ public RouteGraphDelta getRouteGraphDelta() {
+ return rgDelta;
+ }
+
+ public IRouteGraphRenderer getRenderer() {
+ return renderer;
+ }
+
+ public boolean isEditable() {
+ return editable;
+ }
+
+ public boolean isBranchable() {
+ return branchable;
+ }
+
+ public double getPickTolerance() {
+ return pickTolerance;
+ }
+
+ /**
+ * When in client-server mode, listener is only set on the server side and
+ * fireRouteGraphChanged will tell it when rg has changed.
+ *
+ * @param listener
+ */
+ public void setRouteGraphListener(IRouteGraphListener listener) {
+ this.rgListener = listener;
+ }
+
+ /**
+ * @param before
+ * @param after
+ * @return <code>true</code> if changes were fired
+ */
+ private boolean setRouteGraphAndFireChanges(RouteGraph before, RouteGraph after) {
+ RouteGraphDelta delta = new RouteGraphDelta(before, after);
+ if (!delta.isEmpty()) {
+ setRouteGraph(after);
+ setRouteGraphDelta(delta);
+ fireRouteGraphChanged(before, after, delta);
+ return true;
+ }
+ return false;
+ }
+
+ @ServerSide
+ protected void fireRouteGraphChanged(RouteGraph before, RouteGraph after, RouteGraphDelta delta) {
+ if (rgListener != null) {
+ RouteGraphChangeEvent event = new RouteGraphChangeEvent(this, before, after, delta);
+ rgListener.routeGraphChanged(event);
+ }
+ }
+
+ public void showBranchPoint(Point2D p) {
+ newBranchPointPosition = p;
+ }
+
+ @Override
+ public void init() {
+ super.init();
+ addEventHandler(this);
+ }
+
+ @Override
+ public void cleanup() {
+ rgListener = null;
+ removeEventHandler(this);
+ super.cleanup();
+ }
+
+ protected boolean isSelected() {
+ return NodeUtil.isSelected(this, 1);
+ }
+
+ @Override
+ public void render(Graphics2D g) {
+ if (renderer == null)
+ return;
+
+ AffineTransform ot = null;
+ AffineTransform t = getTransform();
+ if (t != null && !t.isIdentity()) {
+ ot = g.getTransform();
+ g.transform(getTransform());
+ }
+
+ Object aaHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
+
+ boolean selected = ignoreSelection ? false : NodeUtil.isSelected(this, 1);
+
+ rg.updateTerminals();
+
+ if (currentAction != null) {
+ currentAction.render(g, renderer, mouseX, mouseY);
+ } else {
+ if (selected && selectionStroke != null) {
+ selectionPath.reset();
+ rg.getPath2D(selectionPath);
+ Shape selectionShape = selectionStroke.createStrokedShape(selectionPath);
+ g.setColor(SELECTION_COLOR);
+ g.fill(selectionShape);
+ }
+
+ renderer.render(g, rg);
+ if(selected)
+ renderer.renderGuides(g, rg);
+
+ if (selected && highlightActionsEnabled) {
+ // Needed for performing actions in #mouseClicked
+ this.lastViewTransform = g.getTransform();
+ highlightActions.setRouteGraph(rg);
+ highlightActions.render(g, renderer, mouseX, mouseY);
+ highlightActions.setRouteGraph(null);
+ }
+ }
+
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, aaHint);
+
+ if (editable && branchable && newBranchPointPosition != null && !Double.isNaN(newBranchPointPosition.getX())) {
+ ConnectionStyle style = tryGetStyle();
+ if(style != null)
+ style.drawBranchPoint(g, newBranchPointPosition.getX(), newBranchPointPosition.getY());
+ }
+
+ if (ot != null)
+ g.setTransform(ot);
+ }
+
+ private BasicConnectionStyle tryGetStyle() {
+ return tryGetStyle(renderer);
+ }
+
+ private BasicConnectionStyle tryGetStyle(IRouteGraphRenderer renderer) {
+ if (renderer instanceof StyledRouteGraphRenderer) {
+ ConnectionStyle cs = ((StyledRouteGraphRenderer) renderer).getStyle();
+ if (cs instanceof BasicConnectionStyle)
+ return (BasicConnectionStyle) cs;
+ }
+ return null;
+ }
+
+ private double getSelectionStrokeWidth() {
+ if (selectionStroke instanceof BasicStroke) {
+ BasicStroke bs = (BasicStroke) selectionStroke;
+ return bs.getLineWidth();
+ }
+ return 1.0;
+ }
+
+ @Override
+ public Rectangle2D getBoundsInLocal() {
+ return bounds;
+ }
+
+ protected void updateBounds() {
+ Rectangle2D r = this.bounds;
+ if (r == null)
+ r = new Rectangle2D.Double();
+ this.bounds = calculateBounds(r);
+
+ // Need to expand to take stroke width into account.
+ double sw = getSelectionStrokeWidth() / 2;
+ GeometryUtils.expandRectangle(this.bounds, sw, sw);
+ }
+
+ protected Rectangle2D calculateBounds(Rectangle2D rect) {
+ RouteGraph rg = this.rg;
+ if (currentAction instanceof MoveAction)
+ rg = ((MoveAction) currentAction).getRouteGraph();
+ rg.getBounds(rect);
+ return rect;
+ }
+
+ protected void getMouseLocalPos(MouseEvent e) {
+ //System.out.println("m: " + e.controlPosition);
+ pt.setLocation(e.controlPosition);
+ //System.out.println("parent: " + pt);
+ pt = NodeUtil.worldToLocal(this, pt, pt);
+ //System.out.println("local: " + pt);
+ mouseX = pt.getX();
+ mouseY = pt.getY();
+ }
+
+ @Override
+ protected boolean mouseDragged(MouseDragBegin e) {
+ if (dragAction != null && !e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK) && e.button == MouseEvent.LEFT_BUTTON) {
+ currentAction = dragAction;
+ dragAction = null;
+ }
+ return updateCurrentAction(e, true);
+ }
+
+ @Override
+ protected boolean mouseMoved(MouseMovedEvent e) {
+ //System.out.println("mouse moved: " + e);
+
+ // Handle connection branching visualization.
+ getMouseLocalPos(e);
+ if (newBranchPointPosition == null && e.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK)) {
+ if (bounds.contains(mouseX, mouseY)) {
+ newBranchPointPosition = new Point2D.Double(Double.NaN, Double.NaN);
+ }
+ }
+ if (newBranchPointPosition != null) {
+ RouteLine line = rg.pickLine(mouseX, mouseY, pickTolerance);
+ if (line != null) {
+ newBranchPointPosition.setLocation(mouseX, mouseY);
+ SplittedRouteGraph.snapToLine(newBranchPointPosition, line);
+ repaint();
+ } else if (!Double.isNaN(newBranchPointPosition.getX())) {
+ newBranchPointPosition.setLocation(Double.NaN, Double.NaN);
+ repaint();
+ }
+ }
+
+ // Make sure that highlight action rendering is according to mouse hover.
+ if (highlightActionsEnabled) {
+ if (NodeUtil.isSelected(this, 1)) {
+ repaint();
+ }
+ }
+
+ return updateCurrentAction(e, false);
+ }
+
+ protected boolean updateCurrentAction(MouseEvent e, boolean updateMousePos) {
+ boolean oldHighlight = highlightActionsEnabled;
+ highlightActionsEnabled = e.hasAllModifiers(MouseEvent.CTRL_MASK);
+ if (oldHighlight != highlightActionsEnabled)
+ repaint();
+
+ if (currentAction != null) {
+ if (updateMousePos)
+ getMouseLocalPos(e);
+ updateBounds();
+
+ // Repaint, but only if absolutely necessary.
+ if (currentAction instanceof MoveAction || bounds.contains(mouseX, mouseY))
+ repaint();
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean mouseClicked(MouseClickEvent e) {
+ if (!editable)
+ return false;
+
+ if (e.button == MouseEvent.LEFT_BUTTON) {
+ if (isSelected() && highlightActionsEnabled) {
+ // Reconnection / segment deletion only available for branched connections.
+ if (rg.getTerminals().size() > 2) {
+ Pick pick = highlightActions.pickAction(rg, lastViewTransform, mouseX, mouseY);
+ if (pick.hasAction(Action.REMOVE)) {
+ RemoveLineAction remove = RemoveLineAction.perform(rg, pick.line.getLine(), mouseX, mouseY);
+ if (remove != null) {
+ setRouteGraphAndFireChanges(remove.getOriginalRouteGraph(), remove.getRouteGraph());
+ repaint();
+ return true;
+ }
+ }
+ if (pick.hasAction(Action.RECONNECT)) {
+ currentAction = ReconnectLineAction.create(rg, mouseX, mouseY);
+ if (currentAction != null) {
+ repaint();
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ protected boolean mouseButtonPressed(MouseButtonPressedEvent e) {
+ if (!editable)
+ return false;
+
+ if (e.button == MouseEvent.LEFT_BUTTON) {
+ // Visualize new branch point no longer.
+ newBranchPointPosition = null;
+
+ getMouseLocalPos(e);
+ dragAction = null;
+// if(currentAction instanceof HighlightActionPointsAction) {
+// RemoveLineAction remove = RemoveLineAction.perform(rg, mouseX, mouseY);
+// if (remove != null) {
+// setRouteGraphAndFireChanges(remove.getOriginalRouteGraph(), remove.getRouteGraph());
+// repaint();
+// } else {
+// currentAction = ReconnectLineAction.create(rg, mouseX, mouseY);
+// if (currentAction != null)
+// repaint();
+// }
+// }
+// else
+ if(currentAction instanceof IReconnectAction) {
+ RouteGraph originalRg = rg.copy();
+ ((IReconnectAction)currentAction).finish(mouseX, mouseY);
+ currentAction = null;
+
+ setRouteGraphAndFireChanges(originalRg, rg);
+
+ currentAction = null;
+ repaint();
+ return true;
+ }
+ else {
+ if (!allowConnectionRerouting()) {
+ return false;
+ }
+ //System.out.println("move action");
+ dragAction = SnappingMoveAction.create(rg, mouseX, mouseY, pickTolerance, moveFilter, getSnapAdvisor());
+ //System.out.println("DRAG ACTION: " + dragAction);
+ }
+
+ //System.out.println(this + " NEW action: " + currentAction);
+ if (currentAction != null)
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks the selections data node in the scene graph for any links
+ * @return
+ */
+ private boolean allowConnectionRerouting() {
+ final int maxOtherNodesSelected = 1;
+
+ INode selections = NodeUtil.tryLookup(this, "selections");
+ if (!(selections instanceof G2DParentNode))
+ return true;
+ G2DParentNode p = (G2DParentNode) selections;
+ for (IG2DNode selection : p.getNodes()) {
+ if (!(selection instanceof G2DParentNode))
+ continue;
+
+ G2DParentNode sp = (G2DParentNode) selection;
+ Collection<IG2DNode> links = sp.getNodes();
+ if (links.isEmpty())
+ return true;
+ int othersSelected = 0;
+ for (IG2DNode link : links) {
+ if (link instanceof LinkNode) {
+ INode node = ((LinkNode) link).getDelegate();
+ if (!NodeUtil.isParentOf(node, this)) {
+ othersSelected++;
+ if (othersSelected > maxOtherNodesSelected)
+ return false;
+ }
+ }
+ }
+ if (othersSelected > maxOtherNodesSelected)
+ return false;
+ }
+ return true;
+ }
+
+ protected ISnapAdvisor getSnapAdvisor() {
+ GridNode grid = lookupNode(GridNode.GRID_NODE_ID, GridNode.class);
+ return grid != null ? grid.getSnapAdvisor() : null;
+ }
+
+ MoveAction.TargetFilter moveFilter = new MoveAction.TargetFilter() {
+ @Override
+ public boolean accept(Object target) {
+ return (target instanceof RouteLine) || (target instanceof RouteLink);
+ }
+ };
+
+ @Override
+ protected boolean handleCommand(CommandEvent e) {
+ /*if (Commands.DELETE.equals(e.command)) {
+ Object target = rg.pick(mouseX, mouseY, pickTolerance);
+ return deleteTarget(target);
+ } else if (Commands.SPLIT_CONNECTION.equals(e.command)) {
+ Object target = rg.pick(mouseX, mouseY, pickTolerance);
+ return splitTarget(target);
+ } else */
+ if (Commands.CANCEL.equals(e.command)) {
+ return cancelCurrentAction();
+ }
+ return false;
+ }
+
+ protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) {
+ if (currentAction instanceof MoveAction) {
+ MoveAction move = (MoveAction) currentAction;
+ RouteGraph originalRg = rg.copy();
+ move.finish(mouseX, mouseY);
+
+ setRouteGraphAndFireChanges(originalRg, rg);
+
+ currentAction = null;
+ repaint();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean keyPressed(KeyPressedEvent e) {
+ if (!editable)
+ return false;
+
+ if (!e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK) && e.keyCode == KeyEvent.VK_S) {
+ Object target = rg.pick(mouseX, mouseY, pickTolerance, RouteGraph.PICK_PERSISTENT_LINES | RouteGraph.PICK_TRANSIENT_LINES);
+ return splitTarget(target);
+ }
+ else if (!e.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK | MouseEvent.CTRL_MASK) && (e.keyCode == KeyEvent.VK_R || e.keyCode == KeyEvent.VK_D)) {
+ Object target = rg.pick(mouseX, mouseY, pickTolerance, RouteGraph.PICK_PERSISTENT_LINES);
+ return deleteTarget(target);
+ }
+ else if (e.keyCode == KeyEvent.VK_ESCAPE) {
+ return cancelCurrentAction();
+ }
+// else if (e.keyCode == KeyEvent.VK_D) {
+// if (target instanceof RouteTerminal) {
+// RouteTerminal terminal = (RouteTerminal) target;
+// RouteGraph before = rg.copy();
+// rg.toggleDirectLines(terminal);
+// setRouteGraphAndFireChanges(before, rg);
+// repaint();
+// }
+// }
+// else if (target != null && e.getKeyCode() == KeyEvent.VK_P) {
+// rg.print();
+// }
+ else if (e.keyCode == KeyEvent.VK_CONTROL) {
+ highlightActionsEnabled = true;
+ repaint();
+ }
+ else if (e.keyCode == KeyEvent.VK_ALT) {
+ // Begin connection branching visualization.
+ RouteLine line = rg.pickLine(mouseX, mouseY, pickTolerance);
+ if (branchable && line != null) {
+ newBranchPointPosition = new Point2D.Double(mouseX, mouseY);
+ SplittedRouteGraph.snapToLine(newBranchPointPosition, line);
+ repaint();
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ protected boolean keyReleased(KeyReleasedEvent e) {
+ if (e.keyCode == KeyEvent.VK_ALT) {
+ // End connection branching visualization.
+ if (newBranchPointPosition != null) {
+ newBranchPointPosition = null;
+ repaint();
+ }
+ }
+ if (e.keyCode == KeyEvent.VK_CONTROL) {
+ highlightActionsEnabled = false;
+ repaint();
+ }
+ return false;
+ }
+
+
+ private boolean cancelCurrentAction() {
+ if (currentAction != null) {
+ currentAction = null;
+ repaint();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean splitTarget(Object target) {
+ if (target instanceof RouteLine) {
+ RouteLine rLine = (RouteLine)target;
+ RouteGraph before = rg.copy();
+ rg.split(rLine, rLine.isHorizontal() ? mouseX : mouseY);
+ setRouteGraphAndFireChanges(before, rg);
+ repaint();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean deleteTarget(Object target) {
+ boolean changed = false;
+ if (target instanceof RouteLine) {
+ RouteLine line = (RouteLine) target;
+ RouteGraph before = rg.copy();
+ rg.merge(line);
+ changed = setRouteGraphAndFireChanges(before, rg);
+ }
+ else if (target instanceof RouteLink) {
+ RouteGraph before = rg.copy();
+ rg.deleteCorner((RouteLink) target);
+ changed = setRouteGraphAndFireChanges(before, rg);
+ }
+// else if (target instanceof RouteTerminal) {
+// RouteGraph before = rg.copy();
+// rg.remove((RouteTerminal) target);
+// changed = setRouteGraphAndFireChanges(before, rg);
+// }
+ if (changed)
+ repaint();
+ return changed;
+ }
+
+ /**
+ * A version of MoveAction that snaps movements using the specified
+ * ISnapAdvisor.
+ */
+ static class SnappingMoveAction extends MoveAction {
+
+ private ISnapAdvisor snapAdvisor;
+ private Point2D point = new Point2D.Double();
+
+ public SnappingMoveAction(RouteGraph rg, Object target, ISnapAdvisor snapAdvisor) {
+ super(rg, target);
+ this.snapAdvisor = snapAdvisor;
+ }
+
+ protected void move(RouteGraph rg, Object target, double x, double y) {
+ point.setLocation(x, y);
+ snapAdvisor.snap(point);
+ super.move(rg, target, point.getX(), point.getY());
+ }
+
+ public static MoveAction create(RouteGraph rg, double x, double y, double tolerance, TargetFilter filter, ISnapAdvisor snapAdvisor) {
+ Object target = rg.pick(x, y, tolerance, RouteGraph.PICK_LINES | RouteGraph.PICK_INTERIOR_POINTS);
+ if (target != null && (filter == null || filter.accept(target))) {
+ if (snapAdvisor != null)
+ return new SnappingMoveAction(rg, target, snapAdvisor);
+ return new MoveAction(rg, target);
+ }
+ return null;
+ }
+
+ }
+
+ @Override
+ public int getEventMask() {
+ return EventTypes.CommandMask | EventTypes.KeyMask | EventTypes.MouseMask;
+ }
+
+}