]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.g2d/src/org/simantics/g2d/elementclass/connection/EdgeSceneGraph.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.g2d / src / org / simantics / g2d / elementclass / connection / EdgeSceneGraph.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
3  * in Industry THTH ry.\r
4  * All rights reserved. This program and the accompanying materials\r
5  * are made available under the terms of the Eclipse Public License v1.0\r
6  * which accompanies this distribution, and is available at\r
7  * http://www.eclipse.org/legal/epl-v10.html\r
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.g2d.elementclass.connection;\r
13 \r
14 \r
15 import java.awt.BasicStroke;\r
16 import java.awt.Color;\r
17 import java.awt.Shape;\r
18 import java.awt.Stroke;\r
19 import java.awt.geom.AffineTransform;\r
20 import java.awt.geom.GeneralPath;\r
21 import java.awt.geom.Line2D;\r
22 import java.awt.geom.Path2D;\r
23 import java.awt.geom.PathIterator;\r
24 import java.awt.geom.Point2D;\r
25 import java.awt.geom.Rectangle2D;\r
26 import java.util.ArrayList;\r
27 import java.util.Collection;\r
28 import java.util.Iterator;\r
29 \r
30 import org.simantics.g2d.diagram.IDiagram;\r
31 import org.simantics.g2d.diagram.handler.Topology;\r
32 import org.simantics.g2d.diagram.handler.Topology.Connection;\r
33 import org.simantics.g2d.element.ElementUtils;\r
34 import org.simantics.g2d.element.IElement;\r
35 import org.simantics.g2d.element.SceneGraphNodeKey;\r
36 import org.simantics.g2d.element.handler.BendsHandler;\r
37 import org.simantics.g2d.element.handler.EdgeVisuals;\r
38 import org.simantics.g2d.element.handler.EdgeVisuals.ArrowType;\r
39 import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;\r
40 import org.simantics.g2d.element.handler.Rotate;\r
41 import org.simantics.g2d.element.handler.SceneGraph;\r
42 import org.simantics.g2d.element.handler.TerminalLayout;\r
43 import org.simantics.g2d.elementclass.BranchPoint;\r
44 import org.simantics.g2d.utils.PathUtils;\r
45 import org.simantics.scenegraph.g2d.G2DParentNode;\r
46 import org.simantics.scenegraph.g2d.nodes.EdgeNode;\r
47 import org.simantics.utils.datastructures.hints.IHintContext.Key;\r
48 \r
49 /**\r
50  * Generic edge painter\r
51  *\r
52  * @author J-P Laine\r
53  */\r
54 public class EdgeSceneGraph implements SceneGraph {\r
55 \r
56     private static final long serialVersionUID = 2914383071126238996L;\r
57 \r
58     public static final EdgeSceneGraph INSTANCE = new EdgeSceneGraph();\r
59 \r
60     public static final Stroke ARROW_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);\r
61 \r
62     public static final Key KEY_SG_NODE = new SceneGraphNodeKey(EdgeNode.class, "EDGE_SG_NODE");\r
63 \r
64     @Override\r
65     public void init(IElement e, G2DParentNode parent) {\r
66         ElementUtils.getOrCreateNode(e, parent, KEY_SG_NODE, "edge_" + e.hashCode(), EdgeNode.class);\r
67         update(e);\r
68     }\r
69 \r
70     @Override\r
71     public void cleanup(IElement e) {\r
72         ElementUtils.removePossibleNode(e, KEY_SG_NODE);\r
73     }\r
74 \r
75     public void update(final IElement e) {\r
76         EdgeNode node = e.getHint(KEY_SG_NODE);\r
77         if(node == null) return;\r
78 \r
79         EdgeVisuals vh = e.getElementClass().getSingleItem(EdgeVisuals.class);\r
80         ArrowType at1 = vh.getArrowType(e, EdgeEnd.Begin);\r
81         ArrowType at2 = vh.getArrowType(e, EdgeEnd.End);\r
82         Stroke stroke = vh.getStroke(e);\r
83         //StrokeType strokeType = vh.getStrokeType(e);\r
84         double as1 = vh.getArrowSize(e, EdgeEnd.Begin);\r
85         double as2 = vh.getArrowSize(e, EdgeEnd.End);\r
86 \r
87         Color c = ElementUtils.getFillColor(e, Color.BLACK);\r
88 \r
89         // Get terminal shape for clipping the painted edge to its bounds.\r
90         IDiagram diagram = ElementUtils.peekDiagram(e);\r
91         Shape beginTerminalShape = null;\r
92         Shape endTerminalShape = null;\r
93         if (diagram != null) {\r
94             Topology topology = diagram.getDiagramClass().getAtMostOneItemOfClass(Topology.class);\r
95             if (topology != null) {\r
96                 Connection beginConnection = topology.getConnection(e, EdgeEnd.Begin);\r
97                 Connection endConnection = topology.getConnection(e, EdgeEnd.End);\r
98                 beginTerminalShape = getTerminalShape(beginConnection);\r
99                 endTerminalShape = getTerminalShape(endConnection);\r
100                 int beginBranchDegree = getBranchPointDegree(beginConnection, topology);\r
101                 int endBranchDegree = getBranchPointDegree(endConnection, topology);\r
102                 if (beginBranchDegree > 0 && beginBranchDegree < 3) {\r
103                     at1 = ArrowType.None;\r
104                 }\r
105                 if (endBranchDegree > 0 && endBranchDegree < 3) {\r
106                     at2 = ArrowType.None;\r
107                 }\r
108             }\r
109         }\r
110 \r
111         // Read bends\r
112         BendsHandler bh = e.getElementClass().getSingleItem(BendsHandler.class);\r
113         Path2D line = bh.getPath(e);\r
114 \r
115         boolean drawArrows = at1 != ArrowType.None || at2 != ArrowType.None;\r
116         //line = clipLineEnds(line, beginTerminalShape, endTerminalShape);\r
117 \r
118         Point2D first       = new Point2D.Double();\r
119         Point2D dir1        = new Point2D.Double();\r
120         Point2D last        = new Point2D.Double();\r
121         Point2D dir2        = new Point2D.Double();\r
122         PathIterator pi     = line.getPathIterator(null);\r
123         drawArrows &= PathUtils.getPathArrows(pi, first, dir1, last, dir2);\r
124 \r
125         if (drawArrows) {\r
126             line = trimLineToArrows(line, at1, as1, at2, as2);\r
127         }\r
128 \r
129         EdgeNode.ArrowType pat1 = convert(at1);\r
130         EdgeNode.ArrowType pat2 = convert(at2);\r
131 \r
132         node.init(new GeneralPath(line), stroke, c, dir1, dir2, first, last, as1, as2, pat1, pat2, null, null);\r
133     }\r
134 \r
135     private static EdgeNode.ArrowType convert(ArrowType at) {\r
136         switch (at) {\r
137             case None: return EdgeNode.ArrowType.None;\r
138             case Stroke: return EdgeNode.ArrowType.Stroke;\r
139             case Fill: return EdgeNode.ArrowType.Fill;\r
140             default:\r
141                 throw new IllegalArgumentException("unsupported arrow type: " + at);\r
142         }\r
143     }\r
144 \r
145     private static final Rectangle2D EMPTY = new Rectangle2D.Double();\r
146 \r
147     private static Shape getTerminalShape(Connection connection) {\r
148         if (connection != null && connection.node != null && connection.terminal != null) {\r
149             TerminalLayout layout = connection.node.getElementClass().getAtMostOneItemOfClass(TerminalLayout.class);\r
150             if (layout != null) {\r
151                 //return layout.getTerminalShape(connection.node, connection.terminal);\r
152                 Shape shp = layout.getTerminalShape(connection.node, connection.terminal);\r
153                 Rotate rotate = connection.node.getElementClass().getAtMostOneItemOfClass(Rotate.class);\r
154                 if (rotate == null)\r
155                     return shp;\r
156 \r
157                 double theta = rotate.getAngle(connection.node);\r
158                 return AffineTransform.getRotateInstance(theta).createTransformedShape(shp);\r
159             }\r
160         }\r
161         return null;\r
162     }\r
163 \r
164     private final Collection<Connection> connectionsTemp = new ArrayList<Connection>();\r
165     private int getBranchPointDegree(Connection connection, Topology topology) {\r
166         if (connection != null && connection.node != null) {\r
167             if (connection.node.getElementClass().containsClass(BranchPoint.class)) {\r
168                 connectionsTemp.clear();\r
169                 topology.getConnections(connection.node, connection.terminal, connectionsTemp);\r
170                 int degree = connectionsTemp.size();\r
171                 connectionsTemp.clear();\r
172                 return degree;\r
173             }\r
174         }\r
175         return -1;\r
176     }\r
177 \r
178     private static Path2D clipLineEnds(Path2D line, Shape beginTerminalShape, Shape endTerminalShape) {\r
179         if (beginTerminalShape == null && endTerminalShape == null)\r
180             return line;\r
181 \r
182         Rectangle2D bb = beginTerminalShape != null ? beginTerminalShape.getBounds2D() : EMPTY;\r
183         Rectangle2D eb = endTerminalShape != null ? endTerminalShape.getBounds2D() : EMPTY;\r
184         // If the terminal shape doesn't contain its own coordinate system\r
185         // origin, just ignore the terminal shape.\r
186         if (bb != EMPTY && !bb.contains(0, 0))\r
187             bb = EMPTY;\r
188         if (eb != EMPTY && !eb.contains(0, 0))\r
189             eb = EMPTY;\r
190         if (bb.isEmpty() && eb.isEmpty())\r
191             return line;\r
192 \r
193         Path2D result = new Path2D.Double();\r
194 \r
195         PathIterator pi = line.getPathIterator(null);\r
196         Iterator<double[]> it = PathUtils.toLineIterator(pi);\r
197         boolean first = true;\r
198         while (it.hasNext()) {\r
199             double[] seg = it.next();\r
200             int degree = PathUtils.getLineDegree(seg);\r
201             //System.out.println("SEG: " + Arrays.toString(seg));\r
202 \r
203             if (first) {\r
204                 first = false;\r
205                 Point2D start = PathUtils.getLinePos(seg, 0);\r
206                 Point2D sp = clipToRectangle(bb, PathUtils.getLinePos(seg, 1), start);\r
207                 if (sp != null) {\r
208                     result.moveTo(sp.getX(), sp.getY());\r
209                 } else {\r
210                     result.moveTo(start.getX(), start.getY());\r
211                 }\r
212             }\r
213             if (!it.hasNext()) {\r
214                 // this is the last segment\r
215                 Point2D ep = clipToRectangle(eb, PathUtils.getLinePos(seg, 0), PathUtils.getLinePos(seg, 1));\r
216                 //System.out.println("EP: " + ep + ", " + PathUtils.getLinePos(seg, 0) + " -> " + PathUtils.getLinePos(seg, 1));\r
217                 if (ep != null) {\r
218                     seg[degree * 2] = ep.getX();\r
219                     seg[degree * 2 + 1] = ep.getY();\r
220                 }\r
221             }\r
222 \r
223             if (degree == 1) {\r
224                 result.lineTo(seg[2], seg[3]);\r
225             } else if (degree == 2) {\r
226                 result.quadTo(seg[2], seg[3], seg[4], seg[5]);\r
227             } else if (degree == 3) {\r
228                 result.curveTo(seg[2], seg[3], seg[4], seg[5], seg[6], seg[7]);\r
229             } else {\r
230                 throw new UnsupportedOperationException("invalid path segment degree: " + degree);\r
231             }\r
232         }\r
233 \r
234         result.setWindingRule(line.getWindingRule());\r
235         return result;\r
236     }\r
237 \r
238     private static Path2D trimLineToArrows(Path2D line, ArrowType beginArrowType, double beginArrowSize, ArrowType endArrowType, double endArrowSize) {\r
239         Path2D result = new Path2D.Double();\r
240         PathIterator pi = line.getPathIterator(null);\r
241         Iterator<double[]> it = PathUtils.toLineIterator(pi);\r
242         boolean first = true;\r
243 \r
244         while (it.hasNext()) {\r
245             double[] seg = it.next();\r
246             int degree = PathUtils.getLineDegree(seg);\r
247 \r
248             if (first) {\r
249                 first = false;\r
250 \r
251                 if (beginArrowType == ArrowType.Fill) {\r
252                     Point2D t = PathUtils.getLineTangent(seg, 0);\r
253                     double len = Math.sqrt(lensq(t, null));\r
254                     if (len > beginArrowSize) {\r
255                         double scale = beginArrowSize / len;\r
256                         seg[0] += t.getX() * scale;\r
257                         seg[1] += t.getY() * scale;\r
258                     } else {\r
259                         // Remove the first segment completely if the segment is too\r
260                         // small to be noticed from under the arrow.\r
261                         result.moveTo(seg[degree * 2], seg[degree * 2 + 1]);\r
262                         continue;\r
263                     }\r
264                 }\r
265 \r
266                 result.moveTo(seg[0], seg[1]);\r
267             }\r
268             if (!it.hasNext()) {\r
269                 // this is the last segment\r
270                 if (endArrowType == ArrowType.Fill) {\r
271                     Point2D t = PathUtils.getLineTangent(seg, 1);\r
272                     double len = Math.sqrt(lensq(t, null));\r
273                     if (len > endArrowSize) {\r
274                         double scale = endArrowSize / len;\r
275                         seg[degree * 2] -= t.getX() * scale;\r
276                         seg[degree * 2 + 1] -= t.getY() * scale;\r
277                     }\r
278                 }\r
279             }\r
280 \r
281             if (degree == 1) {\r
282                 result.lineTo(seg[2], seg[3]);\r
283             } else if (degree == 2) {\r
284                 result.quadTo(seg[2], seg[3], seg[4], seg[5]);\r
285             } else if (degree == 3) {\r
286                 result.curveTo(seg[2], seg[3], seg[4], seg[5], seg[6], seg[7]);\r
287             } else {\r
288                 throw new UnsupportedOperationException("invalid path segment degree: " + degree);\r
289             }\r
290         }\r
291 \r
292         result.setWindingRule(line.getWindingRule());\r
293         return result;\r
294     }\r
295 \r
296     private static Point2D clipToRectangle(Rectangle2D bounds, Point2D p1, Point2D p2) {\r
297         if (bounds.isEmpty())\r
298             return p2;\r
299 \r
300         Line2D line = new Line2D.Double(p1, p2);\r
301         Point2D vi1 = intersectWithHorizontalLine(line, bounds.getMinY() + p2.getY());\r
302         Point2D vi2 = intersectWithHorizontalLine(line, bounds.getMaxY() + p2.getY());\r
303         Point2D hi1 = intersectWithVerticalLine(line, bounds.getMinX() + p2.getX());\r
304         Point2D hi2 = intersectWithVerticalLine(line, bounds.getMaxX() + p2.getX());\r
305 \r
306         int i = 0;\r
307         Point2D[] intersections = { null, null, null, null };\r
308         if (vi1 != null)\r
309             intersections[i++] = vi1;\r
310         if (vi2 != null)\r
311             intersections[i++] = vi2;\r
312         if (hi1 != null)\r
313             intersections[i++] = hi1;\r
314         if (hi2 != null)\r
315             intersections[i++] = hi2;\r
316 \r
317         //System.out.println(bounds + ": P1(" + p1 + ") - P2(" + p2 +"): " + Arrays.toString(intersections));\r
318 \r
319         if (i == 0)\r
320             return p2;\r
321         if (i == 1)\r
322             return intersections[0];\r
323 \r
324         // Find the intersection i for which applies\r
325         // lensq(p1, p2) >= lensq(p1, i) &\r
326         // for all intersections j != i: lensq(p1, i) > lensq(p1, j)\r
327         double len = lensq(p1, p2);\r
328         //System.out.println("original line lensq: " + len);\r
329         Point2D nearestIntersection = null;\r
330         double nearestLen = -1;\r
331         for (int j = 0; j < i; ++j) {\r
332             double l = lensq(p1, intersections[j]);\r
333             //System.out.println("intersected line lensq: " + l);\r
334             if (l <= len && l > nearestLen) {\r
335                 nearestIntersection = intersections[j];\r
336                 nearestLen = l;\r
337                 //System.out.println("nearest");\r
338             }\r
339         }\r
340         return nearestIntersection;\r
341     }\r
342 \r
343     private static double lensq(Point2D p1, Point2D p2) {\r
344         double dx = p1.getX();\r
345         double dy = p1.getY();\r
346         if (p2 != null) {\r
347             dx = p2.getX() - dx;\r
348             dy = p2.getY() - dy;\r
349         }\r
350         return dx*dx + dy*dy;\r
351     }\r
352 \r
353     private static Point2D intersectWithHorizontalLine(Line2D l, double y) {\r
354         double dx = l.getX2() - l.getX1();\r
355         double dy = l.getY2() - l.getY1();\r
356 \r
357         if (Math.abs(dy) < 1e-5) {\r
358             // input line as horizontal, no intersection.\r
359             return null;\r
360         }\r
361         double a = dx / dy;\r
362         return new Point2D.Double((y - l.getY1()) * a + l.getX1(), y);\r
363     }\r
364 \r
365     private static Point2D intersectWithVerticalLine(Line2D l, double x) {\r
366         double dx = l.getX2() - l.getX1();\r
367         double dy = l.getY2() - l.getY1();\r
368 \r
369         if (Math.abs(dx) < 1e-5) {\r
370             // input line as vertical, no intersection.\r
371             return null;\r
372         }\r
373         double a = dy / dx;\r
374         return new Point2D.Double(x, a * (x - l.getX1()) + l.getY1());\r
375     }\r
376 \r
377 \r
378     public final static Path2D NORMAL_ARROW;\r
379     public final static Path2D FILLED_ARROW;\r
380 \r
381     static {\r
382         FILLED_ARROW = new Path2D.Double();\r
383         FILLED_ARROW.moveTo(-0.5, 1);\r
384         FILLED_ARROW.lineTo(   0, 0);\r
385         FILLED_ARROW.lineTo( 0.5, 1);\r
386         FILLED_ARROW.closePath();\r
387 \r
388         NORMAL_ARROW = new Path2D.Double();\r
389         NORMAL_ARROW.moveTo(-0.5, 1);\r
390         NORMAL_ARROW.lineTo(   0, 0);\r
391         NORMAL_ARROW.lineTo( 0.5, 1);\r
392     }\r
393 \r
394 }\r