/******************************************************************************* * Copyright (c) 2007, 2010 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.g2d.elementclass.connection; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.handler.Topology; import org.simantics.g2d.diagram.handler.Topology.Connection; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.SceneGraphNodeKey; import org.simantics.g2d.element.handler.BendsHandler; import org.simantics.g2d.element.handler.EdgeVisuals; import org.simantics.g2d.element.handler.EdgeVisuals.ArrowType; import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd; import org.simantics.g2d.element.handler.Rotate; import org.simantics.g2d.element.handler.SceneGraph; import org.simantics.g2d.element.handler.TerminalLayout; import org.simantics.g2d.elementclass.BranchPoint; import org.simantics.g2d.utils.PathUtils; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.nodes.EdgeNode; import org.simantics.utils.datastructures.hints.IHintContext.Key; /** * Generic edge painter * * @author J-P Laine */ public class EdgeSceneGraph implements SceneGraph { private static final long serialVersionUID = 2914383071126238996L; public static final EdgeSceneGraph INSTANCE = new EdgeSceneGraph(); public static final Stroke ARROW_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); public static final Key KEY_SG_NODE = new SceneGraphNodeKey(EdgeNode.class, "EDGE_SG_NODE"); @Override public void init(IElement e, G2DParentNode parent) { ElementUtils.getOrCreateNode(e, parent, KEY_SG_NODE, "edge_" + e.hashCode(), EdgeNode.class); update(e); } @Override public void cleanup(IElement e) { ElementUtils.removePossibleNode(e, KEY_SG_NODE); } public void update(final IElement e) { EdgeNode node = e.getHint(KEY_SG_NODE); if(node == null) return; EdgeVisuals vh = e.getElementClass().getSingleItem(EdgeVisuals.class); ArrowType at1 = vh.getArrowType(e, EdgeEnd.Begin); ArrowType at2 = vh.getArrowType(e, EdgeEnd.End); Stroke stroke = vh.getStroke(e); //StrokeType strokeType = vh.getStrokeType(e); double as1 = vh.getArrowSize(e, EdgeEnd.Begin); double as2 = vh.getArrowSize(e, EdgeEnd.End); Color c = ElementUtils.getFillColor(e, Color.BLACK); // Get terminal shape for clipping the painted edge to its bounds. IDiagram diagram = ElementUtils.peekDiagram(e); Shape beginTerminalShape = null; Shape endTerminalShape = null; if (diagram != null) { Topology topology = diagram.getDiagramClass().getAtMostOneItemOfClass(Topology.class); if (topology != null) { Connection beginConnection = topology.getConnection(e, EdgeEnd.Begin); Connection endConnection = topology.getConnection(e, EdgeEnd.End); beginTerminalShape = getTerminalShape(beginConnection); endTerminalShape = getTerminalShape(endConnection); int beginBranchDegree = getBranchPointDegree(beginConnection, topology); int endBranchDegree = getBranchPointDegree(endConnection, topology); if (beginBranchDegree > 0 && beginBranchDegree < 3) { at1 = ArrowType.None; } if (endBranchDegree > 0 && endBranchDegree < 3) { at2 = ArrowType.None; } } } // Read bends BendsHandler bh = e.getElementClass().getSingleItem(BendsHandler.class); Path2D line = bh.getPath(e); boolean drawArrows = at1 != ArrowType.None || at2 != ArrowType.None; //line = clipLineEnds(line, beginTerminalShape, endTerminalShape); Point2D first = new Point2D.Double(); Point2D dir1 = new Point2D.Double(); Point2D last = new Point2D.Double(); Point2D dir2 = new Point2D.Double(); PathIterator pi = line.getPathIterator(null); drawArrows &= PathUtils.getPathArrows(pi, first, dir1, last, dir2); if (drawArrows) { line = trimLineToArrows(line, at1, as1, at2, as2); } EdgeNode.ArrowType pat1 = convert(at1); EdgeNode.ArrowType pat2 = convert(at2); node.init(new GeneralPath(line), stroke, c, dir1, dir2, first, last, as1, as2, pat1, pat2, null, null); } private static EdgeNode.ArrowType convert(ArrowType at) { switch (at) { case None: return EdgeNode.ArrowType.None; case Stroke: return EdgeNode.ArrowType.Stroke; case Fill: return EdgeNode.ArrowType.Fill; default: throw new IllegalArgumentException("unsupported arrow type: " + at); } } private static final Rectangle2D EMPTY = new Rectangle2D.Double(); private static Shape getTerminalShape(Connection connection) { if (connection != null && connection.node != null && connection.terminal != null) { TerminalLayout layout = connection.node.getElementClass().getAtMostOneItemOfClass(TerminalLayout.class); if (layout != null) { //return layout.getTerminalShape(connection.node, connection.terminal); Shape shp = layout.getTerminalShape(connection.node, connection.terminal); Rotate rotate = connection.node.getElementClass().getAtMostOneItemOfClass(Rotate.class); if (rotate == null) return shp; double theta = rotate.getAngle(connection.node); return AffineTransform.getRotateInstance(theta).createTransformedShape(shp); } } return null; } private final Collection connectionsTemp = new ArrayList(); private int getBranchPointDegree(Connection connection, Topology topology) { if (connection != null && connection.node != null) { if (connection.node.getElementClass().containsClass(BranchPoint.class)) { connectionsTemp.clear(); topology.getConnections(connection.node, connection.terminal, connectionsTemp); int degree = connectionsTemp.size(); connectionsTemp.clear(); return degree; } } return -1; } private static Path2D clipLineEnds(Path2D line, Shape beginTerminalShape, Shape endTerminalShape) { if (beginTerminalShape == null && endTerminalShape == null) return line; Rectangle2D bb = beginTerminalShape != null ? beginTerminalShape.getBounds2D() : EMPTY; Rectangle2D eb = endTerminalShape != null ? endTerminalShape.getBounds2D() : EMPTY; // If the terminal shape doesn't contain its own coordinate system // origin, just ignore the terminal shape. if (bb != EMPTY && !bb.contains(0, 0)) bb = EMPTY; if (eb != EMPTY && !eb.contains(0, 0)) eb = EMPTY; if (bb.isEmpty() && eb.isEmpty()) return line; Path2D result = new Path2D.Double(); PathIterator pi = line.getPathIterator(null); Iterator it = PathUtils.toLineIterator(pi); boolean first = true; while (it.hasNext()) { double[] seg = it.next(); int degree = PathUtils.getLineDegree(seg); //System.out.println("SEG: " + Arrays.toString(seg)); if (first) { first = false; Point2D start = PathUtils.getLinePos(seg, 0); Point2D sp = clipToRectangle(bb, PathUtils.getLinePos(seg, 1), start); if (sp != null) { result.moveTo(sp.getX(), sp.getY()); } else { result.moveTo(start.getX(), start.getY()); } } if (!it.hasNext()) { // this is the last segment Point2D ep = clipToRectangle(eb, PathUtils.getLinePos(seg, 0), PathUtils.getLinePos(seg, 1)); //System.out.println("EP: " + ep + ", " + PathUtils.getLinePos(seg, 0) + " -> " + PathUtils.getLinePos(seg, 1)); if (ep != null) { seg[degree * 2] = ep.getX(); seg[degree * 2 + 1] = ep.getY(); } } if (degree == 1) { result.lineTo(seg[2], seg[3]); } else if (degree == 2) { result.quadTo(seg[2], seg[3], seg[4], seg[5]); } else if (degree == 3) { result.curveTo(seg[2], seg[3], seg[4], seg[5], seg[6], seg[7]); } else { throw new UnsupportedOperationException("invalid path segment degree: " + degree); } } result.setWindingRule(line.getWindingRule()); return result; } private static Path2D trimLineToArrows(Path2D line, ArrowType beginArrowType, double beginArrowSize, ArrowType endArrowType, double endArrowSize) { Path2D result = new Path2D.Double(); PathIterator pi = line.getPathIterator(null); Iterator it = PathUtils.toLineIterator(pi); boolean first = true; while (it.hasNext()) { double[] seg = it.next(); int degree = PathUtils.getLineDegree(seg); if (first) { first = false; if (beginArrowType == ArrowType.Fill) { Point2D t = PathUtils.getLineTangent(seg, 0); double len = Math.sqrt(lensq(t, null)); if (len > beginArrowSize) { double scale = beginArrowSize / len; seg[0] += t.getX() * scale; seg[1] += t.getY() * scale; } else { // Remove the first segment completely if the segment is too // small to be noticed from under the arrow. result.moveTo(seg[degree * 2], seg[degree * 2 + 1]); continue; } } result.moveTo(seg[0], seg[1]); } if (!it.hasNext()) { // this is the last segment if (endArrowType == ArrowType.Fill) { Point2D t = PathUtils.getLineTangent(seg, 1); double len = Math.sqrt(lensq(t, null)); if (len > endArrowSize) { double scale = endArrowSize / len; seg[degree * 2] -= t.getX() * scale; seg[degree * 2 + 1] -= t.getY() * scale; } } } if (degree == 1) { result.lineTo(seg[2], seg[3]); } else if (degree == 2) { result.quadTo(seg[2], seg[3], seg[4], seg[5]); } else if (degree == 3) { result.curveTo(seg[2], seg[3], seg[4], seg[5], seg[6], seg[7]); } else { throw new UnsupportedOperationException("invalid path segment degree: " + degree); } } result.setWindingRule(line.getWindingRule()); return result; } private static Point2D clipToRectangle(Rectangle2D bounds, Point2D p1, Point2D p2) { if (bounds.isEmpty()) return p2; Line2D line = new Line2D.Double(p1, p2); Point2D vi1 = intersectWithHorizontalLine(line, bounds.getMinY() + p2.getY()); Point2D vi2 = intersectWithHorizontalLine(line, bounds.getMaxY() + p2.getY()); Point2D hi1 = intersectWithVerticalLine(line, bounds.getMinX() + p2.getX()); Point2D hi2 = intersectWithVerticalLine(line, bounds.getMaxX() + p2.getX()); int i = 0; Point2D[] intersections = { null, null, null, null }; if (vi1 != null) intersections[i++] = vi1; if (vi2 != null) intersections[i++] = vi2; if (hi1 != null) intersections[i++] = hi1; if (hi2 != null) intersections[i++] = hi2; //System.out.println(bounds + ": P1(" + p1 + ") - P2(" + p2 +"): " + Arrays.toString(intersections)); if (i == 0) return p2; if (i == 1) return intersections[0]; // Find the intersection i for which applies // lensq(p1, p2) >= lensq(p1, i) & // for all intersections j != i: lensq(p1, i) > lensq(p1, j) double len = lensq(p1, p2); //System.out.println("original line lensq: " + len); Point2D nearestIntersection = null; double nearestLen = -1; for (int j = 0; j < i; ++j) { double l = lensq(p1, intersections[j]); //System.out.println("intersected line lensq: " + l); if (l <= len && l > nearestLen) { nearestIntersection = intersections[j]; nearestLen = l; //System.out.println("nearest"); } } return nearestIntersection; } private static double lensq(Point2D p1, Point2D p2) { double dx = p1.getX(); double dy = p1.getY(); if (p2 != null) { dx = p2.getX() - dx; dy = p2.getY() - dy; } return dx*dx + dy*dy; } private static Point2D intersectWithHorizontalLine(Line2D l, double y) { double dx = l.getX2() - l.getX1(); double dy = l.getY2() - l.getY1(); if (Math.abs(dy) < 1e-5) { // input line as horizontal, no intersection. return null; } double a = dx / dy; return new Point2D.Double((y - l.getY1()) * a + l.getX1(), y); } private static Point2D intersectWithVerticalLine(Line2D l, double x) { double dx = l.getX2() - l.getX1(); double dy = l.getY2() - l.getY1(); if (Math.abs(dx) < 1e-5) { // input line as vertical, no intersection. return null; } double a = dy / dx; return new Point2D.Double(x, a * (x - l.getX1()) + l.getY1()); } public final static Path2D NORMAL_ARROW; public final static Path2D FILLED_ARROW; static { FILLED_ARROW = new Path2D.Double(); FILLED_ARROW.moveTo(-0.5, 1); FILLED_ARROW.lineTo( 0, 0); FILLED_ARROW.lineTo( 0.5, 1); FILLED_ARROW.closePath(); NORMAL_ARROW = new Path2D.Double(); NORMAL_ARROW.moveTo(-0.5, 1); NORMAL_ARROW.lineTo( 0, 0); NORMAL_ARROW.lineTo( 0.5, 1); } }