/******************************************************************************* * Copyright (c) 2011 Association for Decentralized Information Management in * Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.diagram.connection.rendering; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.FlatteningPathIterator; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.io.Serializable; /** * @author Tuukka Lehtonen */ public class BasicConnectionStyle implements ConnectionStyle, Serializable { private static final long serialVersionUID = -5799681720482456895L; // line thickness in millimeters. final Color lineColor; final Color branchPointColor; final double branchPointRadius; final Stroke lineStroke; final Stroke routeLineStroke; final double degenerateLineLength; final double rounding; final double offset; transient Line2D line = new Line2D.Double(); transient Ellipse2D ellipse = new Ellipse2D.Double(); public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength, double rounding, double offset) { this.lineColor = lineColor; this.branchPointColor = branchPointColor; this.branchPointRadius = branchPointRadius; this.lineStroke = lineStroke; this.routeLineStroke = routeLineStroke; this.degenerateLineLength = degenerateLineLength; this.rounding = rounding; this.offset = offset; } public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength, double rounding) { this(lineColor, branchPointColor, branchPointRadius, lineStroke, routeLineStroke, degenerateLineLength, rounding, 0.0); } public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength) { this(lineColor, branchPointColor, branchPointRadius, lineStroke, routeLineStroke, degenerateLineLength, 0.0, 0.0); } public Color getLineColor() { return lineColor; } public Color getBranchPointColor() { return branchPointColor; } public double getBranchPointRadius() { return branchPointRadius; } public Stroke getLineStroke() { return lineStroke; } public Stroke getRouteLineStroke() { return routeLineStroke; } @Override public void drawLine(Graphics2D g, double x1, double y1, double x2, double y2, boolean isTransient) { if (lineColor != null) g.setColor(lineColor); if(isTransient) { g.setStroke(lineStroke); line.setLine(x1, y1, x2, y2); g.draw(line); } else { g.setStroke(routeLineStroke); line.setLine(x1, y1, x2, y2); g.draw(line); } } @Override public void drawPath(Graphics2D g, Path2D path, boolean isTransient) { if (lineColor != null) g.setColor(lineColor); if (lineStroke != null) g.setStroke(lineStroke); if(rounding > 0.0) { Object oldRenderingHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); path = round(path); if (offset != 0) { path = offsetPath(path, offset); } g.draw(path); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldRenderingHint); } else { if (offset != 0) { path = offsetPath(path, offset); } g.draw(path); } } private static Point2D getNormal(Point2D dir) { return new Point2D.Double(-dir.getY(), dir.getX()); } private static Point2D normalize(Point2D v) { double d = Math.sqrt(v.getX() * v.getX() + v.getY() * v.getY()); v.setLocation(v.getX() / d, v.getY() / d); return v; } private static Path2D offsetPath(Path2D path, double offset) { Path2D result = new Path2D.Double(); PathIterator iter = new FlatteningPathIterator(path.getPathIterator(null), 0.05, 10); double c[] = new double[6]; double initialX = 0; double initialY = 0; boolean first = true; Point2D prevDir = null; Point2D prevPos = null; while (!iter.isDone()) { int i = iter.currentSegment(c); switch (i) { case PathIterator.SEG_MOVETO: if (first) { initialX = c[0]; initialY = c[1]; first = false; } if (prevDir != null) { Point2D N = normalize(getNormal(prevDir)); result.lineTo(prevPos.getX() + N.getX() * offset , prevPos.getY() + N.getY() * offset); } prevPos = new Point2D.Double(c[0], c[1]); prevDir = null; break; case PathIterator.SEG_LINETO: case PathIterator.SEG_CLOSE: if (i == PathIterator.SEG_CLOSE) { c[0] = initialX; c[1] = initialY; } Point2D currentDir = new Point2D.Double(c[0] - prevPos.getX(), c[1] - prevPos.getY()); if (currentDir.getX() == 0.0 && currentDir.getY() == 0) break; if (prevDir == null) { Point2D N = normalize(getNormal(currentDir)); result.moveTo(prevPos.getX() + N.getX() * offset, prevPos.getY() + N.getY() * offset); prevPos = new Point2D.Double(c[0], c[i]); prevDir = currentDir; } else { Point2D N1 = normalize(getNormal(prevDir)); Point2D N2 = normalize(getNormal(currentDir)); Point2D N = normalize(new Point2D.Double(N1.getX() + N2.getX(), N1.getY() + N2.getY())); double dot = N1.getX() * N.getX() + N1.getY() * N.getY(); if (!Double.isFinite(dot) || Math.abs(dot) < 0.1) { result.lineTo(prevPos.getX() + (N1.getX() + N1.getY()) * offset, prevPos.getY() + (N1.getY() - N1.getX()) * offset); result.lineTo(prevPos.getX() + (N2.getX() + N1.getY()) * offset, prevPos.getY() + (N2.getY() - N1.getX()) * offset); prevPos = new Point2D.Double(c[0], c[i]); prevDir = currentDir; } else { double Nx = N.getX() * offset / dot; double Ny = N.getY() * offset / dot; result.lineTo(prevPos.getX() + Nx, prevPos.getY() + Ny); prevPos = new Point2D.Double(c[0], c[i]); prevDir = currentDir; } } break; } iter.next(); } if (prevDir != null) { Point2D N = normalize(getNormal(prevDir)); result.lineTo(prevPos.getX() + N.getX() * offset , prevPos.getY() + N.getY() * offset); } return result; } private Path2D round(Path2D path) { Path2D newPath = new Path2D.Double(); PathIterator it = path.getPathIterator(new AffineTransform()); double[] coords = new double[6]; double newX=0.0, newY=0.0; double curX=0.0, curY=0.0; double oldX=0.0, oldY=0.0; int state = 0; while(!it.isDone()) { int type = it.currentSegment(coords); if(type == PathIterator.SEG_LINETO) { newX = coords[0]; newY = coords[1]; if(state == 1) { double dx1 = curX-oldX; double dy1 = curY-oldY; double dx2 = curX-newX; double dy2 = curY-newY; double r1 = Math.sqrt(dx1*dx1 + dy1*dy1); double r2 = Math.sqrt(dx2*dx2 + dy2*dy2); double maxRadius = 0.5 * Math.min(r1, r2); double radius = Math.min(rounding, maxRadius); double dx1Normalized = r1 > 0 ? dx1 / r1 : 0; double dy1Normalized = r1 > 0 ? dy1 / r1 : 0; double dx2Normalized = r2 > 0 ? dx2 / r2 : 0; double dy2Normalized = r2 > 0 ? dy2 / r2 : 0; newPath.lineTo(curX - radius*dx1Normalized, curY - radius*dy1Normalized); newPath.curveTo(curX, curY, curX, curY, curX - radius*dx2Normalized, curY - radius*dy2Normalized); } else ++state; oldX = curX; oldY = curY; curX = newX; curY = newY; } else { if(state > 0) { newPath.lineTo(curX, curY); state = 0; } switch(type) { case PathIterator.SEG_MOVETO: curX = coords[0]; curY = coords[1]; newPath.moveTo(curX, curY); break; case PathIterator.SEG_QUADTO: curX = coords[2]; curY = coords[3]; newPath.quadTo(coords[0], coords[1], coords[2], coords[3]); break; case PathIterator.SEG_CUBICTO: curX = coords[4]; curY = coords[5]; newPath.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); break; case PathIterator.SEG_CLOSE: newPath.closePath(); break; } } it.next(); } if(state > 0) newPath.lineTo(curX, curY); return newPath; } @Override public void drawBranchPoint(Graphics2D g, double x, double y) { g.setColor(branchPointColor); double r = branchPointRadius; double d = 2*r; ellipse.setFrame(x-r, y-r, d, d); g.fill(ellipse); } @Override public void drawDegeneratedLine(Graphics2D g, double x, double y, boolean isHorizontal, boolean isTransient) { double d = getDegeneratedLineLength()*0.5; if(isHorizontal) { line.setLine(x-d, y, x+d, y); g.draw(line); } else { line.setLine(x, y-d, x, y+d); g.draw(line); } } @Override public double getDegeneratedLineLength() { return degenerateLineLength; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((branchPointColor == null) ? 0 : branchPointColor.hashCode()); long temp; temp = Double.doubleToLongBits(branchPointRadius); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(degenerateLineLength); result = prime * result + (int) (temp ^ (temp >>> 32)); result = prime * result + ((lineColor == null) ? 0 : lineColor.hashCode()); result = prime * result + ((lineStroke == null) ? 0 : lineStroke.hashCode()); temp = Double.doubleToLongBits(rounding); result = prime * result + (int) (temp ^ (temp >>> 32)); result = prime * result + ((routeLineStroke == null) ? 0 : routeLineStroke.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; BasicConnectionStyle other = (BasicConnectionStyle) obj; if (branchPointColor == null) { if (other.branchPointColor != null) return false; } else if (!branchPointColor.equals(other.branchPointColor)) return false; if (Double.doubleToLongBits(branchPointRadius) != Double.doubleToLongBits(other.branchPointRadius)) return false; if (Double.doubleToLongBits(degenerateLineLength) != Double.doubleToLongBits(other.degenerateLineLength)) return false; if (lineColor == null) { if (other.lineColor != null) return false; } else if (!lineColor.equals(other.lineColor)) return false; if (lineStroke == null) { if (other.lineStroke != null) return false; } else if (!lineStroke.equals(other.lineStroke)) return false; if (Double.doubleToLongBits(rounding) != Double.doubleToLongBits(other.rounding)) return false; if (routeLineStroke == null) { if (other.routeLineStroke != null) return false; } else if (!routeLineStroke.equals(other.routeLineStroke)) return false; return true; } public double getRounding() { return rounding; } public double getOffset() { return offset; } }