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