--- /dev/null
+/*******************************************************************************\r
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
+ * in 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.g2d.elementclass.connection;\r
+\r
+\r
+import java.awt.BasicStroke;\r
+import java.awt.Color;\r
+import java.awt.Shape;\r
+import java.awt.Stroke;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.geom.GeneralPath;\r
+import java.awt.geom.Line2D;\r
+import java.awt.geom.Path2D;\r
+import java.awt.geom.PathIterator;\r
+import java.awt.geom.Point2D;\r
+import java.awt.geom.Rectangle2D;\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+import java.util.Iterator;\r
+\r
+import org.simantics.g2d.diagram.IDiagram;\r
+import org.simantics.g2d.diagram.handler.Topology;\r
+import org.simantics.g2d.diagram.handler.Topology.Connection;\r
+import org.simantics.g2d.element.ElementUtils;\r
+import org.simantics.g2d.element.IElement;\r
+import org.simantics.g2d.element.SceneGraphNodeKey;\r
+import org.simantics.g2d.element.handler.BendsHandler;\r
+import org.simantics.g2d.element.handler.EdgeVisuals;\r
+import org.simantics.g2d.element.handler.EdgeVisuals.ArrowType;\r
+import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;\r
+import org.simantics.g2d.element.handler.Rotate;\r
+import org.simantics.g2d.element.handler.SceneGraph;\r
+import org.simantics.g2d.element.handler.TerminalLayout;\r
+import org.simantics.g2d.elementclass.BranchPoint;\r
+import org.simantics.g2d.utils.PathUtils;\r
+import org.simantics.scenegraph.g2d.G2DParentNode;\r
+import org.simantics.scenegraph.g2d.nodes.EdgeNode;\r
+import org.simantics.utils.datastructures.hints.IHintContext.Key;\r
+\r
+/**\r
+ * Generic edge painter\r
+ *\r
+ * @author J-P Laine\r
+ */\r
+public class EdgeSceneGraph implements SceneGraph {\r
+\r
+ private static final long serialVersionUID = 2914383071126238996L;\r
+\r
+ public static final EdgeSceneGraph INSTANCE = new EdgeSceneGraph();\r
+\r
+ public static final Stroke ARROW_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);\r
+\r
+ public static final Key KEY_SG_NODE = new SceneGraphNodeKey(EdgeNode.class, "EDGE_SG_NODE");\r
+\r
+ @Override\r
+ public void init(IElement e, G2DParentNode parent) {\r
+ ElementUtils.getOrCreateNode(e, parent, KEY_SG_NODE, "edge_" + e.hashCode(), EdgeNode.class);\r
+ update(e);\r
+ }\r
+\r
+ @Override\r
+ public void cleanup(IElement e) {\r
+ ElementUtils.removePossibleNode(e, KEY_SG_NODE);\r
+ }\r
+\r
+ public void update(final IElement e) {\r
+ EdgeNode node = e.getHint(KEY_SG_NODE);\r
+ if(node == null) return;\r
+\r
+ EdgeVisuals vh = e.getElementClass().getSingleItem(EdgeVisuals.class);\r
+ ArrowType at1 = vh.getArrowType(e, EdgeEnd.Begin);\r
+ ArrowType at2 = vh.getArrowType(e, EdgeEnd.End);\r
+ Stroke stroke = vh.getStroke(e);\r
+ //StrokeType strokeType = vh.getStrokeType(e);\r
+ double as1 = vh.getArrowSize(e, EdgeEnd.Begin);\r
+ double as2 = vh.getArrowSize(e, EdgeEnd.End);\r
+\r
+ Color c = ElementUtils.getFillColor(e, Color.BLACK);\r
+\r
+ // Get terminal shape for clipping the painted edge to its bounds.\r
+ IDiagram diagram = ElementUtils.peekDiagram(e);\r
+ Shape beginTerminalShape = null;\r
+ Shape endTerminalShape = null;\r
+ if (diagram != null) {\r
+ Topology topology = diagram.getDiagramClass().getAtMostOneItemOfClass(Topology.class);\r
+ if (topology != null) {\r
+ Connection beginConnection = topology.getConnection(e, EdgeEnd.Begin);\r
+ Connection endConnection = topology.getConnection(e, EdgeEnd.End);\r
+ beginTerminalShape = getTerminalShape(beginConnection);\r
+ endTerminalShape = getTerminalShape(endConnection);\r
+ int beginBranchDegree = getBranchPointDegree(beginConnection, topology);\r
+ int endBranchDegree = getBranchPointDegree(endConnection, topology);\r
+ if (beginBranchDegree > 0 && beginBranchDegree < 3) {\r
+ at1 = ArrowType.None;\r
+ }\r
+ if (endBranchDegree > 0 && endBranchDegree < 3) {\r
+ at2 = ArrowType.None;\r
+ }\r
+ }\r
+ }\r
+\r
+ // Read bends\r
+ BendsHandler bh = e.getElementClass().getSingleItem(BendsHandler.class);\r
+ Path2D line = bh.getPath(e);\r
+\r
+ boolean drawArrows = at1 != ArrowType.None || at2 != ArrowType.None;\r
+ //line = clipLineEnds(line, beginTerminalShape, endTerminalShape);\r
+\r
+ Point2D first = new Point2D.Double();\r
+ Point2D dir1 = new Point2D.Double();\r
+ Point2D last = new Point2D.Double();\r
+ Point2D dir2 = new Point2D.Double();\r
+ PathIterator pi = line.getPathIterator(null);\r
+ drawArrows &= PathUtils.getPathArrows(pi, first, dir1, last, dir2);\r
+\r
+ if (drawArrows) {\r
+ line = trimLineToArrows(line, at1, as1, at2, as2);\r
+ }\r
+\r
+ EdgeNode.ArrowType pat1 = convert(at1);\r
+ EdgeNode.ArrowType pat2 = convert(at2);\r
+\r
+ node.init(new GeneralPath(line), stroke, c, dir1, dir2, first, last, as1, as2, pat1, pat2, null, null);\r
+ }\r
+\r
+ private static EdgeNode.ArrowType convert(ArrowType at) {\r
+ switch (at) {\r
+ case None: return EdgeNode.ArrowType.None;\r
+ case Stroke: return EdgeNode.ArrowType.Stroke;\r
+ case Fill: return EdgeNode.ArrowType.Fill;\r
+ default:\r
+ throw new IllegalArgumentException("unsupported arrow type: " + at);\r
+ }\r
+ }\r
+\r
+ private static final Rectangle2D EMPTY = new Rectangle2D.Double();\r
+\r
+ private static Shape getTerminalShape(Connection connection) {\r
+ if (connection != null && connection.node != null && connection.terminal != null) {\r
+ TerminalLayout layout = connection.node.getElementClass().getAtMostOneItemOfClass(TerminalLayout.class);\r
+ if (layout != null) {\r
+ //return layout.getTerminalShape(connection.node, connection.terminal);\r
+ Shape shp = layout.getTerminalShape(connection.node, connection.terminal);\r
+ Rotate rotate = connection.node.getElementClass().getAtMostOneItemOfClass(Rotate.class);\r
+ if (rotate == null)\r
+ return shp;\r
+\r
+ double theta = rotate.getAngle(connection.node);\r
+ return AffineTransform.getRotateInstance(theta).createTransformedShape(shp);\r
+ }\r
+ }\r
+ return null;\r
+ }\r
+\r
+ private final Collection<Connection> connectionsTemp = new ArrayList<Connection>();\r
+ private int getBranchPointDegree(Connection connection, Topology topology) {\r
+ if (connection != null && connection.node != null) {\r
+ if (connection.node.getElementClass().containsClass(BranchPoint.class)) {\r
+ connectionsTemp.clear();\r
+ topology.getConnections(connection.node, connection.terminal, connectionsTemp);\r
+ int degree = connectionsTemp.size();\r
+ connectionsTemp.clear();\r
+ return degree;\r
+ }\r
+ }\r
+ return -1;\r
+ }\r
+\r
+ private static Path2D clipLineEnds(Path2D line, Shape beginTerminalShape, Shape endTerminalShape) {\r
+ if (beginTerminalShape == null && endTerminalShape == null)\r
+ return line;\r
+\r
+ Rectangle2D bb = beginTerminalShape != null ? beginTerminalShape.getBounds2D() : EMPTY;\r
+ Rectangle2D eb = endTerminalShape != null ? endTerminalShape.getBounds2D() : EMPTY;\r
+ // If the terminal shape doesn't contain its own coordinate system\r
+ // origin, just ignore the terminal shape.\r
+ if (bb != EMPTY && !bb.contains(0, 0))\r
+ bb = EMPTY;\r
+ if (eb != EMPTY && !eb.contains(0, 0))\r
+ eb = EMPTY;\r
+ if (bb.isEmpty() && eb.isEmpty())\r
+ return line;\r
+\r
+ Path2D result = new Path2D.Double();\r
+\r
+ PathIterator pi = line.getPathIterator(null);\r
+ Iterator<double[]> it = PathUtils.toLineIterator(pi);\r
+ boolean first = true;\r
+ while (it.hasNext()) {\r
+ double[] seg = it.next();\r
+ int degree = PathUtils.getLineDegree(seg);\r
+ //System.out.println("SEG: " + Arrays.toString(seg));\r
+\r
+ if (first) {\r
+ first = false;\r
+ Point2D start = PathUtils.getLinePos(seg, 0);\r
+ Point2D sp = clipToRectangle(bb, PathUtils.getLinePos(seg, 1), start);\r
+ if (sp != null) {\r
+ result.moveTo(sp.getX(), sp.getY());\r
+ } else {\r
+ result.moveTo(start.getX(), start.getY());\r
+ }\r
+ }\r
+ if (!it.hasNext()) {\r
+ // this is the last segment\r
+ Point2D ep = clipToRectangle(eb, PathUtils.getLinePos(seg, 0), PathUtils.getLinePos(seg, 1));\r
+ //System.out.println("EP: " + ep + ", " + PathUtils.getLinePos(seg, 0) + " -> " + PathUtils.getLinePos(seg, 1));\r
+ if (ep != null) {\r
+ seg[degree * 2] = ep.getX();\r
+ seg[degree * 2 + 1] = ep.getY();\r
+ }\r
+ }\r
+\r
+ if (degree == 1) {\r
+ result.lineTo(seg[2], seg[3]);\r
+ } else if (degree == 2) {\r
+ result.quadTo(seg[2], seg[3], seg[4], seg[5]);\r
+ } else if (degree == 3) {\r
+ result.curveTo(seg[2], seg[3], seg[4], seg[5], seg[6], seg[7]);\r
+ } else {\r
+ throw new UnsupportedOperationException("invalid path segment degree: " + degree);\r
+ }\r
+ }\r
+\r
+ result.setWindingRule(line.getWindingRule());\r
+ return result;\r
+ }\r
+\r
+ private static Path2D trimLineToArrows(Path2D line, ArrowType beginArrowType, double beginArrowSize, ArrowType endArrowType, double endArrowSize) {\r
+ Path2D result = new Path2D.Double();\r
+ PathIterator pi = line.getPathIterator(null);\r
+ Iterator<double[]> it = PathUtils.toLineIterator(pi);\r
+ boolean first = true;\r
+\r
+ while (it.hasNext()) {\r
+ double[] seg = it.next();\r
+ int degree = PathUtils.getLineDegree(seg);\r
+\r
+ if (first) {\r
+ first = false;\r
+\r
+ if (beginArrowType == ArrowType.Fill) {\r
+ Point2D t = PathUtils.getLineTangent(seg, 0);\r
+ double len = Math.sqrt(lensq(t, null));\r
+ if (len > beginArrowSize) {\r
+ double scale = beginArrowSize / len;\r
+ seg[0] += t.getX() * scale;\r
+ seg[1] += t.getY() * scale;\r
+ } else {\r
+ // Remove the first segment completely if the segment is too\r
+ // small to be noticed from under the arrow.\r
+ result.moveTo(seg[degree * 2], seg[degree * 2 + 1]);\r
+ continue;\r
+ }\r
+ }\r
+\r
+ result.moveTo(seg[0], seg[1]);\r
+ }\r
+ if (!it.hasNext()) {\r
+ // this is the last segment\r
+ if (endArrowType == ArrowType.Fill) {\r
+ Point2D t = PathUtils.getLineTangent(seg, 1);\r
+ double len = Math.sqrt(lensq(t, null));\r
+ if (len > endArrowSize) {\r
+ double scale = endArrowSize / len;\r
+ seg[degree * 2] -= t.getX() * scale;\r
+ seg[degree * 2 + 1] -= t.getY() * scale;\r
+ }\r
+ }\r
+ }\r
+\r
+ if (degree == 1) {\r
+ result.lineTo(seg[2], seg[3]);\r
+ } else if (degree == 2) {\r
+ result.quadTo(seg[2], seg[3], seg[4], seg[5]);\r
+ } else if (degree == 3) {\r
+ result.curveTo(seg[2], seg[3], seg[4], seg[5], seg[6], seg[7]);\r
+ } else {\r
+ throw new UnsupportedOperationException("invalid path segment degree: " + degree);\r
+ }\r
+ }\r
+\r
+ result.setWindingRule(line.getWindingRule());\r
+ return result;\r
+ }\r
+\r
+ private static Point2D clipToRectangle(Rectangle2D bounds, Point2D p1, Point2D p2) {\r
+ if (bounds.isEmpty())\r
+ return p2;\r
+\r
+ Line2D line = new Line2D.Double(p1, p2);\r
+ Point2D vi1 = intersectWithHorizontalLine(line, bounds.getMinY() + p2.getY());\r
+ Point2D vi2 = intersectWithHorizontalLine(line, bounds.getMaxY() + p2.getY());\r
+ Point2D hi1 = intersectWithVerticalLine(line, bounds.getMinX() + p2.getX());\r
+ Point2D hi2 = intersectWithVerticalLine(line, bounds.getMaxX() + p2.getX());\r
+\r
+ int i = 0;\r
+ Point2D[] intersections = { null, null, null, null };\r
+ if (vi1 != null)\r
+ intersections[i++] = vi1;\r
+ if (vi2 != null)\r
+ intersections[i++] = vi2;\r
+ if (hi1 != null)\r
+ intersections[i++] = hi1;\r
+ if (hi2 != null)\r
+ intersections[i++] = hi2;\r
+\r
+ //System.out.println(bounds + ": P1(" + p1 + ") - P2(" + p2 +"): " + Arrays.toString(intersections));\r
+\r
+ if (i == 0)\r
+ return p2;\r
+ if (i == 1)\r
+ return intersections[0];\r
+\r
+ // Find the intersection i for which applies\r
+ // lensq(p1, p2) >= lensq(p1, i) &\r
+ // for all intersections j != i: lensq(p1, i) > lensq(p1, j)\r
+ double len = lensq(p1, p2);\r
+ //System.out.println("original line lensq: " + len);\r
+ Point2D nearestIntersection = null;\r
+ double nearestLen = -1;\r
+ for (int j = 0; j < i; ++j) {\r
+ double l = lensq(p1, intersections[j]);\r
+ //System.out.println("intersected line lensq: " + l);\r
+ if (l <= len && l > nearestLen) {\r
+ nearestIntersection = intersections[j];\r
+ nearestLen = l;\r
+ //System.out.println("nearest");\r
+ }\r
+ }\r
+ return nearestIntersection;\r
+ }\r
+\r
+ private static double lensq(Point2D p1, Point2D p2) {\r
+ double dx = p1.getX();\r
+ double dy = p1.getY();\r
+ if (p2 != null) {\r
+ dx = p2.getX() - dx;\r
+ dy = p2.getY() - dy;\r
+ }\r
+ return dx*dx + dy*dy;\r
+ }\r
+\r
+ private static Point2D intersectWithHorizontalLine(Line2D l, double y) {\r
+ double dx = l.getX2() - l.getX1();\r
+ double dy = l.getY2() - l.getY1();\r
+\r
+ if (Math.abs(dy) < 1e-5) {\r
+ // input line as horizontal, no intersection.\r
+ return null;\r
+ }\r
+ double a = dx / dy;\r
+ return new Point2D.Double((y - l.getY1()) * a + l.getX1(), y);\r
+ }\r
+\r
+ private static Point2D intersectWithVerticalLine(Line2D l, double x) {\r
+ double dx = l.getX2() - l.getX1();\r
+ double dy = l.getY2() - l.getY1();\r
+\r
+ if (Math.abs(dx) < 1e-5) {\r
+ // input line as vertical, no intersection.\r
+ return null;\r
+ }\r
+ double a = dy / dx;\r
+ return new Point2D.Double(x, a * (x - l.getX1()) + l.getY1());\r
+ }\r
+\r
+\r
+ public final static Path2D NORMAL_ARROW;\r
+ public final static Path2D FILLED_ARROW;\r
+\r
+ static {\r
+ FILLED_ARROW = new Path2D.Double();\r
+ FILLED_ARROW.moveTo(-0.5, 1);\r
+ FILLED_ARROW.lineTo( 0, 0);\r
+ FILLED_ARROW.lineTo( 0.5, 1);\r
+ FILLED_ARROW.closePath();\r
+\r
+ NORMAL_ARROW = new Path2D.Double();\r
+ NORMAL_ARROW.moveTo(-0.5, 1);\r
+ NORMAL_ARROW.lineTo( 0, 0);\r
+ NORMAL_ARROW.lineTo( 0.5, 1);\r
+ }\r
+\r
+}\r