]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/BasicConnectionStyle.java
Improvements to styling of connection lines
[simantics/platform.git] / bundles / org.simantics.diagram.connection / src / org / simantics / diagram / connection / rendering / BasicConnectionStyle.java
1 /*******************************************************************************
2  * Copyright (c) 2011 Association for Decentralized Information Management in
3  * 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.diagram.connection.rendering;
13
14 import java.awt.Color;
15 import java.awt.Graphics2D;
16 import java.awt.RenderingHints;
17 import java.awt.Stroke;
18 import java.awt.geom.AffineTransform;
19 import java.awt.geom.Ellipse2D;
20 import java.awt.geom.FlatteningPathIterator;
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.io.Serializable;
26
27 /**
28  * @author Tuukka Lehtonen
29  */
30 public class BasicConnectionStyle implements ConnectionStyle, Serializable {
31
32     private static final long serialVersionUID = -5799681720482456895L;
33
34     // line thickness in millimeters.
35     final Color                     lineColor;
36     final Color                     branchPointColor;
37     final double                    branchPointRadius;
38     final Stroke                    lineStroke;
39     final Stroke                    routeLineStroke;
40     final double                    degenerateLineLength;
41     final double                    rounding;
42     final double                    offset;
43
44     transient Line2D          line             = new Line2D.Double();
45     transient Ellipse2D       ellipse          = new Ellipse2D.Double();
46
47     public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength,
48             double rounding, double offset) {
49         this.lineColor = lineColor;
50         this.branchPointColor = branchPointColor;
51         this.branchPointRadius = branchPointRadius;
52         this.lineStroke = lineStroke;
53         this.routeLineStroke = routeLineStroke;
54         this.degenerateLineLength = degenerateLineLength;
55         this.rounding = rounding;
56         this.offset = offset;
57     }
58
59     public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength,
60             double rounding) {
61         this(lineColor, branchPointColor, branchPointRadius, lineStroke, routeLineStroke, degenerateLineLength, rounding, 0.0);
62     }
63     
64     public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength) {
65         this(lineColor, branchPointColor, branchPointRadius, lineStroke, routeLineStroke, degenerateLineLength, 0.0, 0.0);
66     }
67
68     public Color getLineColor() {
69         return lineColor;
70     }
71     
72     public Color getBranchPointColor() {
73         return branchPointColor;
74     }
75     
76     public double getBranchPointRadius() {
77         return branchPointRadius;
78     }
79     
80     public Stroke getLineStroke() {
81         return lineStroke;
82     }
83     
84     public Stroke getRouteLineStroke() {
85         return routeLineStroke;
86     }
87
88     @Override
89     public void drawLine(Graphics2D g, double x1, double y1, double x2,
90             double y2, boolean isTransient) {
91         if (lineColor != null)
92             g.setColor(lineColor);
93         if(isTransient) {
94             g.setStroke(lineStroke);
95             line.setLine(x1, y1, x2, y2);
96             g.draw(line);
97         } else {
98             g.setStroke(routeLineStroke);
99             line.setLine(x1, y1, x2, y2);
100             g.draw(line);
101         }
102     }
103
104     @Override
105     public void drawPath(Graphics2D g, Path2D path, boolean isTransient) {
106         if (lineColor != null)
107             g.setColor(lineColor);
108         if (lineStroke != null)
109             g.setStroke(lineStroke);
110         if(rounding > 0.0) {
111             Object oldRenderingHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
112             g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
113             path = round(path);
114             if (offset != 0) {
115                 path = offsetPath(path, offset);
116             }
117             g.draw(path);
118             g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldRenderingHint);
119         }
120         else {
121             if (offset != 0) {
122                 path = offsetPath(path, offset);
123             }
124             g.draw(path);
125         }
126     }
127     
128     private static Point2D getNormal(Point2D dir) {
129         return new Point2D.Double(-dir.getY(), dir.getX());
130     }
131     
132     private static Point2D normalize(Point2D v) {
133         double d = Math.sqrt(v.getX() * v.getX() + v.getY() * v.getY());
134         v.setLocation(v.getX() / d, v.getY() / d);
135         return v;
136     }
137     
138     private static Path2D offsetPath(Path2D path, double offset) {
139         Path2D result = new Path2D.Double();
140         PathIterator iter = new FlatteningPathIterator(path.getPathIterator(null), 0.05, 10);
141
142         double c[] = new double[6];
143         double initialX = 0;
144         double initialY = 0;
145         boolean first = true;
146         Point2D prevDir = null;
147         Point2D prevPos = null;
148
149         while (!iter.isDone()) {
150             int i = iter.currentSegment(c);
151             switch (i) {
152             case PathIterator.SEG_MOVETO:
153                 if (first) {
154                     initialX = c[0];
155                     initialY = c[1];
156                     first = false;
157                 }
158                 if (prevDir != null) {
159                     Point2D N = normalize(getNormal(prevDir)); 
160                     result.lineTo(prevPos.getX() + N.getX() * offset , prevPos.getY() + N.getY() * offset);
161                 }
162                 prevPos = new Point2D.Double(c[0], c[1]);
163                 prevDir = null;
164                 break;
165             case PathIterator.SEG_LINETO:
166             case PathIterator.SEG_CLOSE:
167                 if (i == PathIterator.SEG_CLOSE) {
168                     c[0] = initialX;
169                     c[1] = initialY;
170                 }
171                 Point2D currentDir = new Point2D.Double(c[0] - prevPos.getX(), c[1] - prevPos.getY());
172                 if (currentDir.getX() == 0.0 && currentDir.getY() == 0) break;
173                 
174                 if (prevDir == null) {
175                     Point2D N = normalize(getNormal(currentDir)); 
176                     result.moveTo(prevPos.getX() + N.getX() * offset, prevPos.getY() + N.getY() * offset);
177                     prevPos = new Point2D.Double(c[0], c[i]);
178                     prevDir = currentDir;
179                 } else {
180                     Point2D N1 = normalize(getNormal(prevDir));
181                     Point2D N2 = normalize(getNormal(currentDir));
182                     Point2D N = normalize(new Point2D.Double(N1.getX() + N2.getX(), N1.getY() + N2.getY()));
183                     double dot = N1.getX() * N.getX() + N1.getY() * N.getY();
184
185                     if (!Double.isFinite(dot) || Math.abs(dot) < 0.1) {
186                         result.lineTo(prevPos.getX() + (N1.getX() + N1.getY()) * offset, prevPos.getY() + (N1.getY() - N1.getX()) * offset);
187                         result.lineTo(prevPos.getX() + (N2.getX() + N1.getY()) * offset, prevPos.getY() + (N2.getY() - N1.getX()) * offset);
188                         prevPos = new Point2D.Double(c[0], c[i]);
189                         prevDir = currentDir;
190                     } else {
191                         double Nx = N.getX() * offset / dot;
192                         double Ny = N.getY() * offset / dot;
193                         result.lineTo(prevPos.getX() + Nx, prevPos.getY() + Ny);
194                         prevPos = new Point2D.Double(c[0], c[i]);
195                         prevDir = currentDir;
196                     }
197                 }
198
199                 break;
200             }
201             iter.next();
202         }
203         if (prevDir != null) {
204             Point2D N = normalize(getNormal(prevDir)); 
205             result.lineTo(prevPos.getX() + N.getX() * offset , prevPos.getY() + N.getY() * offset);
206         }
207         return result;
208     }
209     
210     private Path2D round(Path2D path) {
211         Path2D newPath = new Path2D.Double();
212         PathIterator it = path.getPathIterator(new AffineTransform());
213         double[] coords = new double[6];
214         double newX=0.0, newY=0.0;
215         double curX=0.0, curY=0.0;
216         double oldX=0.0, oldY=0.0;
217         int state = 0;
218         while(!it.isDone()) {
219             int type = it.currentSegment(coords);
220             if(type == PathIterator.SEG_LINETO) {
221                 newX = coords[0];
222                 newY = coords[1];
223                 if(state == 1) {
224                     double dx1 = curX-oldX;
225                     double dy1 = curY-oldY;
226                     double dx2 = curX-newX;
227                     double dy2 = curY-newY;
228                     double r1 = Math.sqrt(dx1*dx1 + dy1*dy1);
229                     double r2 = Math.sqrt(dx2*dx2 + dy2*dy2);
230                     double maxRadius = 0.5 * Math.min(r1, r2);
231                     double radius = Math.min(rounding, maxRadius);
232                     double dx1Normalized = r1 > 0 ? dx1 / r1 : 0;
233                     double dy1Normalized = r1 > 0 ? dy1 / r1 : 0;
234                     double dx2Normalized = r2 > 0 ? dx2 / r2 : 0;
235                     double dy2Normalized = r2 > 0 ? dy2 / r2 : 0;
236                     newPath.lineTo(curX - radius*dx1Normalized, curY - radius*dy1Normalized);
237                     newPath.curveTo(curX, curY,
238                                     curX, curY,
239                                     curX - radius*dx2Normalized, curY - radius*dy2Normalized);
240                 }
241                 else
242                     ++state;
243                 oldX = curX;
244                 oldY = curY;
245                 curX = newX;
246                 curY = newY;   
247             }
248             else {
249                 if(state > 0) {
250                     newPath.lineTo(curX, curY);
251                     state = 0;
252                 }
253                 switch(type) {
254                 case PathIterator.SEG_MOVETO:
255                     curX = coords[0];
256                     curY = coords[1];
257                     newPath.moveTo(curX, curY);
258                     break;
259                 case PathIterator.SEG_QUADTO:
260                     curX = coords[2];
261                     curY = coords[3];
262                     newPath.quadTo(coords[0], coords[1], coords[2], coords[3]);
263                     break;
264                 case PathIterator.SEG_CUBICTO:
265                     curX = coords[4];
266                     curY = coords[5];
267                     newPath.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
268                     break;
269                 case PathIterator.SEG_CLOSE:
270                     newPath.closePath();
271                     break;
272                 }
273             }
274             it.next();
275         }
276         if(state > 0)
277             newPath.lineTo(curX, curY);
278         return newPath;
279     }
280
281     @Override
282     public void drawBranchPoint(Graphics2D g, double x, double y) {
283         g.setColor(branchPointColor);
284         double r = branchPointRadius;
285         double d = 2*r;
286         ellipse.setFrame(x-r, y-r, d, d);
287         g.fill(ellipse);
288     }
289
290     @Override
291     public void drawDegeneratedLine(Graphics2D g, double x, double y,
292             boolean isHorizontal, boolean isTransient) {
293         double d = getDegeneratedLineLength()*0.5;
294         if(isHorizontal) {
295             line.setLine(x-d, y, x+d, y);
296             g.draw(line);
297         } else {
298             line.setLine(x, y-d, x, y+d);
299             g.draw(line);
300         }
301     }
302
303     @Override
304     public double getDegeneratedLineLength() {
305         return degenerateLineLength;
306     }
307
308     @Override
309     public int hashCode() {
310         final int prime = 31;
311         int result = 1;
312         result = prime * result + ((branchPointColor == null) ? 0 : branchPointColor.hashCode());
313         long temp;
314         temp = Double.doubleToLongBits(branchPointRadius);
315         result = prime * result + (int) (temp ^ (temp >>> 32));
316         temp = Double.doubleToLongBits(degenerateLineLength);
317         result = prime * result + (int) (temp ^ (temp >>> 32));
318         result = prime * result + ((lineColor == null) ? 0 : lineColor.hashCode());
319         result = prime * result + ((lineStroke == null) ? 0 : lineStroke.hashCode());
320         temp = Double.doubleToLongBits(rounding);
321         result = prime * result + (int) (temp ^ (temp >>> 32));
322         result = prime * result + ((routeLineStroke == null) ? 0 : routeLineStroke.hashCode());
323         return result;
324     }
325
326     @Override
327     public boolean equals(Object obj) {
328         if (this == obj)
329             return true;
330         if (obj == null)
331             return false;
332         if (getClass() != obj.getClass())
333             return false;
334         BasicConnectionStyle other = (BasicConnectionStyle) obj;
335         if (branchPointColor == null) {
336             if (other.branchPointColor != null)
337                 return false;
338         } else if (!branchPointColor.equals(other.branchPointColor))
339             return false;
340         if (Double.doubleToLongBits(branchPointRadius) != Double.doubleToLongBits(other.branchPointRadius))
341             return false;
342         if (Double.doubleToLongBits(degenerateLineLength) != Double.doubleToLongBits(other.degenerateLineLength))
343             return false;
344         if (lineColor == null) {
345             if (other.lineColor != null)
346                 return false;
347         } else if (!lineColor.equals(other.lineColor))
348             return false;
349         if (lineStroke == null) {
350             if (other.lineStroke != null)
351                 return false;
352         } else if (!lineStroke.equals(other.lineStroke))
353             return false;
354         if (Double.doubleToLongBits(rounding) != Double.doubleToLongBits(other.rounding))
355             return false;
356         if (routeLineStroke == null) {
357             if (other.routeLineStroke != null)
358                 return false;
359         } else if (!routeLineStroke.equals(other.routeLineStroke))
360             return false;
361         return true;
362     }
363
364     public double getRounding() {
365         return rounding;
366     }
367
368     public double getOffset() {
369         return offset;
370     }
371 }