1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
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
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.g2d.elementclass.connection;
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;
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;
50 * Generic edge painter
54 public class EdgeSceneGraph implements SceneGraph {
56 private static final long serialVersionUID = 2914383071126238996L;
58 public static final EdgeSceneGraph INSTANCE = new EdgeSceneGraph();
60 public static final Stroke ARROW_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
62 public static final Key KEY_SG_NODE = new SceneGraphNodeKey(EdgeNode.class, "EDGE_SG_NODE");
65 public void init(IElement e, G2DParentNode parent) {
66 ElementUtils.getOrCreateNode(e, parent, KEY_SG_NODE, "edge_" + e.hashCode(), EdgeNode.class);
71 public void cleanup(IElement e) {
72 ElementUtils.removePossibleNode(e, KEY_SG_NODE);
75 public void update(final IElement e) {
76 EdgeNode node = e.getHint(KEY_SG_NODE);
77 if(node == null) return;
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);
87 Color c = ElementUtils.getFillColor(e, Color.BLACK);
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;
105 if (endBranchDegree > 0 && endBranchDegree < 3) {
106 at2 = ArrowType.None;
112 BendsHandler bh = e.getElementClass().getSingleItem(BendsHandler.class);
113 Path2D line = bh.getPath(e);
115 boolean drawArrows = at1 != ArrowType.None || at2 != ArrowType.None;
116 //line = clipLineEnds(line, beginTerminalShape, endTerminalShape);
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);
126 line = trimLineToArrows(line, at1, as1, at2, as2);
129 EdgeNode.ArrowType pat1 = convert(at1);
130 EdgeNode.ArrowType pat2 = convert(at2);
132 node.init(new GeneralPath(line), stroke, c, dir1, dir2, first, last, as1, as2, pat1, pat2, null, null);
135 private static EdgeNode.ArrowType convert(ArrowType at) {
137 case None: return EdgeNode.ArrowType.None;
138 case Stroke: return EdgeNode.ArrowType.Stroke;
139 case Fill: return EdgeNode.ArrowType.Fill;
141 throw new IllegalArgumentException("unsupported arrow type: " + at);
145 private static final Rectangle2D EMPTY = new Rectangle2D.Double();
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);
157 double theta = rotate.getAngle(connection.node);
158 return AffineTransform.getRotateInstance(theta).createTransformedShape(shp);
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();
178 private static Path2D clipLineEnds(Path2D line, Shape beginTerminalShape, Shape endTerminalShape) {
179 if (beginTerminalShape == null && endTerminalShape == null)
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))
188 if (eb != EMPTY && !eb.contains(0, 0))
190 if (bb.isEmpty() && eb.isEmpty())
193 Path2D result = new Path2D.Double();
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));
205 Point2D start = PathUtils.getLinePos(seg, 0);
206 Point2D sp = clipToRectangle(bb, PathUtils.getLinePos(seg, 1), start);
208 result.moveTo(sp.getX(), sp.getY());
210 result.moveTo(start.getX(), start.getY());
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));
218 seg[degree * 2] = ep.getX();
219 seg[degree * 2 + 1] = ep.getY();
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]);
230 throw new UnsupportedOperationException("invalid path segment degree: " + degree);
234 result.setWindingRule(line.getWindingRule());
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;
244 while (it.hasNext()) {
245 double[] seg = it.next();
246 int degree = PathUtils.getLineDegree(seg);
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;
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]);
266 result.moveTo(seg[0], seg[1]);
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;
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]);
288 throw new UnsupportedOperationException("invalid path segment degree: " + degree);
292 result.setWindingRule(line.getWindingRule());
296 private static Point2D clipToRectangle(Rectangle2D bounds, Point2D p1, Point2D p2) {
297 if (bounds.isEmpty())
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());
307 Point2D[] intersections = { null, null, null, null };
309 intersections[i++] = vi1;
311 intersections[i++] = vi2;
313 intersections[i++] = hi1;
315 intersections[i++] = hi2;
317 //System.out.println(bounds + ": P1(" + p1 + ") - P2(" + p2 +"): " + Arrays.toString(intersections));
322 return intersections[0];
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];
337 //System.out.println("nearest");
340 return nearestIntersection;
343 private static double lensq(Point2D p1, Point2D p2) {
344 double dx = p1.getX();
345 double dy = p1.getY();
350 return dx*dx + dy*dy;
353 private static Point2D intersectWithHorizontalLine(Line2D l, double y) {
354 double dx = l.getX2() - l.getX1();
355 double dy = l.getY2() - l.getY1();
357 if (Math.abs(dy) < 1e-5) {
358 // input line as horizontal, no intersection.
362 return new Point2D.Double((y - l.getY1()) * a + l.getX1(), y);
365 private static Point2D intersectWithVerticalLine(Line2D l, double x) {
366 double dx = l.getX2() - l.getX1();
367 double dy = l.getY2() - l.getY1();
369 if (Math.abs(dx) < 1e-5) {
370 // input line as vertical, no intersection.
374 return new Point2D.Double(x, a * (x - l.getX1()) + l.getY1());
378 public final static Path2D NORMAL_ARROW;
379 public final static Path2D FILLED_ARROW;
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();
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);