/******************************************************************************* * 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.Collection; 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.connection.HighlightActionPointsAction.Action; import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Pick; import org.simantics.scenegraph.g2d.snap.ISnapAdvisor; import org.simantics.scenegraph.utils.GeometryUtils; import org.simantics.scenegraph.utils.InitValueSupport; import org.simantics.scenegraph.utils.NodeUtil; import gnu.trove.map.hash.THashMap; /** * @author Tuukka Lehtonen */ public class RouteGraphNode extends G2DNode implements ISelectionPainterNode, InitValueSupport { 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 dynamicStyles = null; @Override public void initValues() { dynamicColor = null; wrapRenderer(); } @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(); 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 c = baseStyle.getClass().getConstructor(Color.class, Color.class, double.class, Stroke.class, Stroke.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())); } 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())); } } 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 true 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 = NodeUtil.isSelected(this, 1); 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 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; } }