/* * File Arrow.java * * Copyright (C) 2010 Remco Bouckaert remco@cs.auckland.ac.nz * * This file is part of BEAST2. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership and licensing. * * BEAST is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * BEAST is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with BEAST; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ package beast.app.draw; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.geom.GeneralPath; import java.io.PrintStream; import java.util.List; import javax.swing.JPanel; import org.w3c.dom.Node; import beast.core.util.Log; public class Arrow extends Shape { String m_sHeadID; String m_sTailID; public BEASTObjectShape m_tailShape; public InputShape m_headShape; // String m_sPenStyle; /* c'tor for creating arrow while parsing XDL format XML **/ public Arrow(Node node, Document doc, boolean reconstructBEASTObjects) { parse(node, doc, reconstructBEASTObjects); } /* c'tor for creating arrow when starting to draw new one **/ public Arrow(BEASTObjectShape tailShape, int x, int y) { m_sTailID = tailShape.getID(); m_x = x; m_y = y; m_w = 1; m_h = 1; m_tailShape = tailShape; } /* c'tor for creating arrow with all fields set properly * Used when arrows are created by Document.recalcArrows */ public Arrow(BEASTObjectShape tailShape, BEASTObjectShape headShape, String inputName) { m_sTailID = tailShape.getID(); m_tailShape = tailShape; InputShape input = headShape.getInputShape(inputName); if (input == null) { Log.warning.println("Arrow from " + tailShape.m_beastObject.getID() + " to " + headShape.m_beastObject.getID() + "." + inputName + " skipped"); } m_sHeadID = input.getID(); m_headShape = input; //m_sHeadID = headShape.m_id; m_x = 0; m_y = 0; m_w = 1; m_h = 1; m_pencolor = Color.gray; } @Override public void draw(Graphics2D g, JPanel panel) { g.setStroke(new BasicStroke(m_nPenWidth)); g.setColor(m_pencolor); g.setColor(Color.gray); GeneralPath path = new GeneralPath(); path.moveTo(m_x, m_y); path.curveTo(m_x + 20, m_y, m_x + m_w - 40, m_y + m_h, m_x + m_w, m_y + m_h); g.draw(path); drawLabel(g); } /* change head position while dragging by mouse */ public void setHead(int w, int h) { m_w = w; m_h = h; } /* set all parameters properly at end of dragging when mouse is released */ public boolean setHead(InputShape shape, List<Shape> objects, Document doc) { m_sHeadID = shape.getID(); m_headShape = shape; adjustCoordinates(); String inputName = m_headShape.getInputName(); m_headShape.getBEASTObject().setInputValue(inputName, m_tailShape.m_beastObject); return true;//setFunctionInput(objects, doc); } /* parse arrow in XDL format XML **/ @Override void parse(Node node, Document doc, boolean reconstructBEASTObjects) { super.parse(node, doc, reconstructBEASTObjects); if (node.getAttributes().getNamedItem("headid") != null) { m_sHeadID = node.getAttributes().getNamedItem("headid").getNodeValue(); } if (node.getAttributes().getNamedItem("tailid") != null) { m_sTailID = node.getAttributes().getNamedItem("tailid").getNodeValue(); } // if (node.getAttributes().getNamedItem("penstyle") != null) { // m_sPenStyle = node.getAttributes().getNamedItem("penstyle").getNodeValue(); // } } @Override String getAtts() { return " headid='" + m_sHeadID + "'" + " tailid='" + m_sTailID + "'" + // " penstyle='" + m_sPenStyle + "'" + super.getAtts(); } @Override public String getXML() { return "<" + Document.ARROW_ELEMENT + getAtts() + "/>"; } void adjustCoordinates() { Point tailCenter = new Point((m_tailShape.getX() + m_tailShape.getX2()) / 2, (m_tailShape.getY() + m_tailShape.getY2()) / 2); Point headCenter = new Point((m_headShape.getX() + m_headShape.getX2()) / 2, (m_headShape.getY() + m_headShape.getY2()) / 2); Shape rect = m_tailShape; Point roundness = new Point(0, 0); if (rect instanceof InputShape) { roundness.x = rect.m_w; roundness.y = rect.m_h; } Point tailPoint = CalcIntersectionLineAndNode( tailCenter, headCenter, rect, roundness); rect = m_headShape; roundness = new Point(0, 0); if (rect instanceof InputShape) { roundness.x = rect.m_w; roundness.y = rect.m_h; } Point headPoint = CalcIntersectionLineAndNode( headCenter, tailCenter, rect, roundness); m_x = tailPoint.x; m_y = tailPoint.y; m_w = headPoint.x - m_x; m_h = headPoint.y - m_y; } Point CalcIntersectionLineAndNode(Point p0, Point p1, Shape position, Point roundness) { // Note: a rounded rectangle is a rectangle in which the corners are quarter elipses // .p0 . // . // . // | // ............=- <- partly elipse // Point pt = new Point(); int w, h, a, b, c; // width, height, elipse width, elipse height, cut position w = Math.abs((position.getX() - position.getX2()) / 2); h = Math.abs((position.getY() - position.getY2()) / 2); a = Math.abs(roundness.x / 2); b = Math.abs(roundness.y / 2); // try intersection with horizontal line if (p1.y != p0.y) { // Don't try if Line is horizontal if (p1.y > p0.y) { c = p0.y + h; } else { c = p0.y - h; } pt.y = c; pt.x = p0.x + (p1.x - p0.x) * (c - p0.y) / (p1.y - p0.y); if ((pt.x >= p0.x - w + a) && (pt.x <= p0.x + w - a)) return pt; } // try intersection with vertical line if (p1.x != p0.x) { // Don't try if Line is vertical if (p1.x > p0.x) { c = p0.x + w; } else { c = p0.x - w; } pt.x = c; pt.y = p0.y + (p1.y - p0.y) * (c - p0.x) / (p1.x - p0.x); if ((pt.y >= p0.y - h + b) && (pt.y <= p0.y + h - b)) return pt; } // finally try intersection with one of the elips-shaped corners double ar, br, ga, gb, A, B, C, p, q; if (p1.x > p0.x) p = p0.x + w - a; else p = p0.x - w + a; if (p1.y > p0.y) q = p0.y + h - b; else q = p0.y - h + b; ar = a; br = b; if (p1.x == p0.x) // cheat to prevent divsion by zero { ga = (double) (p1.y - p0.y) / 1; } else { ga = (double) (p1.y - p0.y) / (double) (p1.x - p0.x); } gb = p0.y - ga * p0.x; A = 1 / (ar * ar) + (ga * ga) / (br * br); B = -2.0 * p / (ar * ar) + 2.0 * ga * gb / (br * br) - 2.0 * ga * q / (br * br); C = p * p / (ar * ar) + gb * gb / (br * br) - 2.0 * gb * q / (br * br) + q * q / (br * br) - 1.0; if (p1.x > p0.x) { pt.x = (int) ((-B + Math.sqrt(B * B - 4.0 * A * C)) / (2.0 * A)); } else { pt.x = (int) ((-B - Math.sqrt(B * B - 4.0 * A * C)) / (2.0 * A)); } pt.y = (int) (ga * pt.x + gb); return pt; } String m_sID = null; @Override public String getID() { return m_sID; } public void setID(String id) { m_sID = id; } @Override public String toString() { return m_sTailID + "-->" + m_sHeadID; } @Override void toSVG(PrintStream out) { out.println("<path d='M " + m_x + " " + m_y + " C " + (m_x + 20) + " " + m_y + " " + (m_x + m_w - 40) + " " + (m_y + m_h) + " " + (m_x + m_w) + " " + (m_y + m_h) + "'" + // " q 20 0 " + (m_w-40) + " " + (m_h) + " T 40 0 '" + " stroke='rgb(" + m_pencolor.getRed() + "," + m_pencolor.getGreen() + "," + m_pencolor.getBlue() + ")'" + " stroke-width='" + m_nPenWidth + "' fill='none'/>"); } } // class Arrow