/* * Freeplane - mind map editor * Copyright (C) 2008 Joerg Mueller, Daniel Polansky, Christian Foltin, Dimitry Polivaev * * This file is modified by Dimitry Polivaev in 2008. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.freeplane.view.swing.map.link; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.CubicCurve2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import org.freeplane.core.ui.components.UITools; import org.freeplane.core.util.ColorUtils; import org.freeplane.features.link.ArrowType; import org.freeplane.features.link.ConnectorModel; import org.freeplane.features.link.LinkController; import org.freeplane.features.link.NodeLinkModel; import org.freeplane.features.mode.ModeController; import org.freeplane.view.swing.map.MapView; import org.freeplane.view.swing.map.NodeView; /** * This class represents a ArrowLink around a node. */ public class ConnectorView extends AConnectorView{ private static final int LOOP_INCLINE_OFFSET = 45; private static final int NORMAL_LENGTH = 50; private static final float[] DOTTED_DASH = new float[] { 4, 7}; static final Stroke DEF_STROKE = new BasicStroke(1); private static final int LABEL_GAP = 4; private static final double PRECISION = 2; private Shape arrowLinkCurve; private Rectangle sourceTextRectangle; private Rectangle middleTextRectangle; private Rectangle targetTextRectangle; final private Color textColor; final private Color color; final private BasicStroke stroke; final private Color bgColor; /* Note, that source and target are nodeviews and not nodemodels!. */ public ConnectorView(final ConnectorModel connectorModel, final NodeView source, final NodeView target, Color bgColor) { super(connectorModel, source, target); final LinkController linkController = LinkController.getController(getModeController()); textColor = linkController.getColor(connectorModel); this.bgColor =bgColor; final int alpha = linkController.getAlpha(connectorModel); color = ColorUtils.createColor(textColor, alpha); final int width = linkController.getWidth(connectorModel); if (!isSourceVisible() || !isTargetVisible()) { stroke = new BasicStroke(width); } else{ stroke = UITools.createStroke(width, linkController.getDash(connectorModel)); } } public float[] zoomDash(float[] dash) { float[] result = dash.clone(); final double zoom = getZoom(); for(float f : result){ f *= zoom; } return result; } /** */ private Point calcInclination(final NodeView node, final int dellength) { return new Point(dellength, 0); } /* (non-Javadoc) * @see org.freeplane.view.swing.map.link.ILinkView#detectCollision(java.awt.Point, boolean) */ public boolean detectCollision(final Point p, final boolean selectedOnly) { if (selectedOnly && (source == null || !source.isSelected()) && (target == null || !target.isSelected())) { return false; } if (arrowLinkCurve == null) { return false; } return new CollisionDetector().detectCollision(p, arrowLinkCurve); } private Rectangle drawEndPointText(final Graphics2D g, final String text, final Point endPoint, final Point controlPoint) { if (text == null || text.equals("")) { return null; } final TextPainter textPainter = new TextPainter(g, text); final int textWidth = textPainter.getTextWidth(); final int textHeight = textPainter.getTextHeight(); final int x; if (controlPoint.x > endPoint.x) { x = endPoint.x - textWidth - LABEL_GAP; } else { x = endPoint.x + LABEL_GAP; } final int y; if (controlPoint.y > endPoint.y) { y = endPoint.y + LABEL_GAP; } else { y = endPoint.y - textHeight - LABEL_GAP; } textPainter.draw(x, y, textColor, bgColor); return new Rectangle(x, y, textWidth, textHeight); } private Rectangle drawMiddleLabel(final Graphics2D g, final String text, final Point centerPoint) { if (text == null || text.equals("")) { return null; } final TextPainter textPainter = new TextPainter(g, text); final int textWidth = textPainter.getTextWidth(); final int x = centerPoint.x - textWidth / 2; final int textHeight = textPainter.getTextHeight(); int y = centerPoint.y - textHeight/2; textPainter.draw(x, y, textColor, bgColor); return new Rectangle(x, y, textWidth, textHeight); } Shape getArrowLinkCurve() { return arrowLinkCurve; } NodeLinkModel getArrowLinkModel() { return connectorModel; } private Point getCenterPoint() { if (arrowLinkCurve == null) { return null; } final double halfLength = getHalfLength(); final PathIterator pathIterator = arrowLinkCurve.getPathIterator(new AffineTransform(), PRECISION); double lastCoords[] = new double[6]; pathIterator.currentSegment(lastCoords); double length = 0; for (;;) { pathIterator.next(); final double nextCoords[] = new double[6]; if (pathIterator.isDone() || PathIterator.SEG_CLOSE == pathIterator.currentSegment(nextCoords)) { break; } final double dx = nextCoords[0] - lastCoords[0]; final double dy = nextCoords[1] - lastCoords[1]; final double dr = Math.sqrt(dx * dx + dy * dy); length += dr; if (length >= halfLength) { final double k; if (dr < 1) { k = 0.5; } else { k = (length - halfLength) / dr; } return new Point((int) Math.rint(nextCoords[0] - k * dx), (int) Math.rint(nextCoords[1] - k * dy)); } lastCoords = nextCoords; } throw new RuntimeException("center point not found"); } private double getHalfLength() { final PathIterator pathIterator = arrowLinkCurve.getPathIterator(new AffineTransform(), PRECISION); double lastCoords[] = new double[6]; pathIterator.currentSegment(lastCoords); double length = 0; for (;;) { pathIterator.next(); final double nextCoords[] = new double[6]; if (pathIterator.isDone() || PathIterator.SEG_CLOSE == pathIterator.currentSegment(nextCoords)) { break; } final double dx = nextCoords[0] - lastCoords[0]; final double dy = nextCoords[1] - lastCoords[1]; length += Math.sqrt(dx * dx + dy * dy); lastCoords = nextCoords; } return length / 2; } private ModeController getModeController() { NodeView nodeView = source; if (source == null) { nodeView = target; } final MapView mapView = nodeView.getMap(); return mapView.getModeController(); } /* (non-Javadoc) * @see org.freeplane.view.swing.map.link.ILinkView#getModel() */ public ConnectorModel getModel() { return connectorModel; } /** * Computes the intersection between two lines. The calculated point is approximate, * since integers are used. If you need a more precise result, use doubles * everywhere. * (c) 2007 Alexander Hristov. Use Freely (LGPL license). http://www.ahristov.com * * @param x1 Point 1 of Line 1 * @param y1 Point 1 of Line 1 * @param x2 Point 2 of Line 1 * @param y2 Point 2 of Line 1 * @param x3 Point 1 of Line 2 * @param y3 Point 1 of Line 2 * @param x4 Point 2 of Line 2 * @param y4 Point 2 of Line 2 * @return Point where the segments intersect, or null if they don't */ Point intersection(final double x1, final double y1, final double x2, final double y2, final double x3, final double y3, final double x4, final double y4) { final double d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (d == 0) { return null; } final int xi = (int) (((x3 - x4) * (x1 * y2 - y1 * x2) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d); final int yi = (int) (((y3 - y4) * (x1 * y2 - y1 * x2) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d); if (xi + 2 < Math.min(x1, x2) || xi - 2 > Math.max(x1, x2)) { return null; } return new Point(xi, yi); } /** * Computes the unitary normal vector of a segment * @param x1 Starting point of the segment * @param y1 Starting point of the segment * @param x2 Ending point of the segment * @param y2 Ending point of the segment * @return */ Point2D.Double normal(final double x1, final double y1, final double x2, final double y2) { double nx, ny; if (x1 == x2) { nx = Math.signum(y2 - y1); ny = 0; } else { final double f = (y2 - y1) / (x2 - x1); nx = f * Math.signum(x2 - x1) / Math.sqrt(1 + f * f); ny = -1 * Math.signum(x2 - x1) / Math.sqrt(1 + f * f); } return new Point2D.Double(nx, ny); } /* (non-Javadoc) * @see org.freeplane.view.swing.map.link.ILinkView#paint(java.awt.Graphics) */ public void paint(final Graphics graphics) { final boolean selfLink = getSource() == getTarget(); if (!isSourceVisible() && !isTargetVisible()) { return; } Point startPoint = null, endPoint = null, startPoint2 = null, endPoint2 = null; boolean targetIsLeft = false; boolean sourceIsLeft = false; final Graphics2D g = (Graphics2D) graphics.create(); final Color oldColor = g.getColor(); g.setColor(color); /* set stroke. */ g.setStroke(stroke); if (isSourceVisible()) { startPoint = source.getLinkPoint(connectorModel.getStartInclination()); sourceIsLeft = source.isLeft(); } if (isTargetVisible()) { endPoint = target.getLinkPoint(connectorModel.getEndInclination()); targetIsLeft = target.isLeft(); } if (connectorModel.getEndInclination() == null || connectorModel.getStartInclination() == null) { final int dellength = isSourceVisible() && isTargetVisible() ? Math.max(40, (int)(startPoint.distance(endPoint) / getZoom())) : 40; if (isSourceVisible() && connectorModel.getStartInclination() == null) { final Point incl = calcInclination(source, dellength); connectorModel.setStartInclination(incl); startPoint = source.getLinkPoint(connectorModel.getStartInclination()); } if (isTargetVisible() && connectorModel.getEndInclination() == null) { final Point incl = calcInclination(target, dellength); incl.y = -incl.y; if (selfLink) { fixInclineIfLoopNode(incl); } connectorModel.setEndInclination(incl); endPoint = target.getLinkPoint(connectorModel.getEndInclination()); } } if (startPoint != null) { startPoint2 = new Point(startPoint); Point startInclination = connectorModel.getStartInclination(); if(endPoint == null){ normalizeLength(NORMAL_LENGTH, startInclination); } startPoint2.translate(((sourceIsLeft) ? -1 : 1) * getMap().getZoomed(startInclination.x), getMap().getZoomed(startInclination.y)); } if (endPoint != null) { endPoint2 = new Point(endPoint); Point endInclination = connectorModel.getEndInclination(); if(startPoint == null){ normalizeLength(NORMAL_LENGTH, endInclination); } endPoint2.translate(((targetIsLeft) ? -1 : 1) * getMap().getZoomed(endInclination.x), getMap() .getZoomed(endInclination.y)); } paintCurve(g, startPoint, startPoint2, endPoint2, endPoint); drawLabels(g, startPoint, startPoint2, endPoint2, endPoint); g.setColor(oldColor); } private void normalizeLength(int normalLength, Point startInclination) { double k = normalLength / Math.sqrt(startInclination.x * startInclination.x + startInclination.y * startInclination.y); startInclination.x *= k; startInclination.y *= k; } private Shape createLine(Point p1, Point p2) { return new Line2D.Float(p1, p2); } private Shape createLinearPath(Point startPoint, Point startPoint2, Point endPoint2, Point endPoint) { final GeneralPath generalPath = new GeneralPath(GeneralPath.WIND_NON_ZERO, 4); generalPath.moveTo(startPoint.x, startPoint.y); generalPath.lineTo(startPoint2.x, startPoint2.y); generalPath.lineTo(endPoint2.x, endPoint2.y); generalPath.lineTo(endPoint.x, endPoint.y); return generalPath; } private void paintCurve(final Graphics2D g, Point startPoint, Point startPoint2, Point endPoint2, Point endPoint) { final boolean selfLink = getSource() == getTarget(); final boolean isLine = ConnectorModel.Shape.LINE.equals(connectorModel.getShape()); if (startPoint != null && endPoint != null) { if(isLine) { if (selfLink) { arrowLinkCurve = createLine(startPoint, startPoint2); } else { arrowLinkCurve = createLine(startPoint, endPoint); } } else if (ConnectorModel.Shape.LINEAR_PATH.equals(connectorModel.getShape())) arrowLinkCurve = createLinearPath(startPoint, startPoint2, endPoint2, endPoint); else arrowLinkCurve = createCubicCurve2D(startPoint, startPoint2, endPoint2, endPoint); } else arrowLinkCurve = null; if (arrowLinkCurve != null) { g.draw(arrowLinkCurve); } if (isSourceVisible() && !connectorModel.getStartArrow().equals(ArrowType.NONE)) { if(!selfLink && isLine && endPoint != null) paintArrow(g, endPoint, startPoint); else paintArrow(g, startPoint2, startPoint); } if (isTargetVisible() && !connectorModel.getEndArrow().equals(ArrowType.NONE)) { if(isLine && startPoint != null) { if (selfLink) paintArrow(g, startPoint, startPoint2); else paintArrow(g, startPoint, endPoint); } else paintArrow(g, endPoint2, endPoint); } if (connectorModel.getShowControlPointsFlag()) { g.setColor(textColor); g.setStroke(new BasicStroke(stroke.getLineWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, DOTTED_DASH, 0)); } if (connectorModel.getShowControlPointsFlag() || !isSourceVisible() || !isTargetVisible()) { if (startPoint != null) { g.drawLine(startPoint.x, startPoint.y, startPoint2.x, startPoint2.y); drawCircle(g, startPoint2, source.getZoomedFoldingSymbolHalfWidth()); if (arrowLinkCurve == null) { arrowLinkCurve = createLine(startPoint, startPoint2); } } if (endPoint != null && !(selfLink && isLine)) { g.drawLine(endPoint.x, endPoint.y, endPoint2.x, endPoint2.y); drawCircle(g, endPoint2, target.getZoomedFoldingSymbolHalfWidth()); if (arrowLinkCurve == null) { arrowLinkCurve = createLine(endPoint, endPoint2); } } } } private void drawCircle(Graphics2D g, Point p, int hw) { g.setStroke(DEF_STROKE); g.fillOval(p.x - hw, p.y - hw, hw*2, hw*2); } private void paintArrow(final Graphics2D g, Point from, Point to) { paintArrow(from, to, g, getZoom() * 10); } private void drawLabels(final Graphics2D g, Point startPoint, Point startPoint2, Point endPoint2, Point endPoint) { final String sourceLabel = connectorModel.getSourceLabel(); final String middleLabel = connectorModel.getMiddleLabel(); final String targetLabel = connectorModel.getTargetLabel(); if (sourceLabel == null && middleLabel == null && targetLabel == null) { return; } final Font oldFont = g.getFont(); final String fontFamily = connectorModel.getLabelFontFamily(); final int fontSize = Math.round (connectorModel.getLabelFontSize() * UITools.FONT_SCALE_FACTOR); final Font linksFont = new Font(fontFamily, 0, getZoomed(fontSize)); g.setFont(linksFont); if (startPoint != null) { sourceTextRectangle = drawEndPointText(g, sourceLabel, startPoint, startPoint2); if (endPoint == null) { middleTextRectangle = drawEndPointText(g, middleLabel, startPoint2, startPoint); } } if (endPoint != null) { targetTextRectangle = drawEndPointText(g, targetLabel, endPoint, endPoint2); if (startPoint == null) { middleTextRectangle = drawEndPointText(g, middleLabel, endPoint2, endPoint); } } if (startPoint != null && endPoint != null) { middleTextRectangle = drawMiddleLabel(g, middleLabel, getCenterPoint()); } g.setFont(oldFont); } private CubicCurve2D createCubicCurve2D(Point startPoint, Point startPoint2, Point endPoint2, Point endPoint) { final CubicCurve2D arrowLinkCurve = new CubicCurve2D.Double(); if (startPoint != null && endPoint != null) { arrowLinkCurve.setCurve(startPoint, startPoint2, endPoint2, endPoint); } else if (startPoint != null) { arrowLinkCurve.setCurve(startPoint, startPoint2, startPoint, startPoint2); } else if (endPoint != null) { arrowLinkCurve.setCurve(endPoint, endPoint2, endPoint, endPoint2); } return arrowLinkCurve; } /* (non-Javadoc) * @see org.freeplane.view.swing.map.link.ILinkView#increaseBounds(java.awt.Rectangle) */ public void increaseBounds(final Rectangle innerBounds) { final Shape arrowLinkCurve = getArrowLinkCurve(); if (arrowLinkCurve == null) { return; } final Rectangle arrowViewBigBounds = arrowLinkCurve.getBounds(); if (!innerBounds.contains(arrowViewBigBounds)) { final Rectangle arrowViewBounds = PathBBox.getBBox(arrowLinkCurve).getBounds(); innerBounds.add(arrowViewBounds); } increaseBounds(innerBounds, sourceTextRectangle); increaseBounds(innerBounds, middleTextRectangle); increaseBounds(innerBounds, targetTextRectangle); } private void increaseBounds(Rectangle innerBounds, Rectangle rect) { if (rect != null) innerBounds.add(rect); } private void fixInclineIfLoopNode(Point endIncline) { if (endIncline.y < 0) { endIncline.y -= LOOP_INCLINE_OFFSET; } else { endIncline.y += LOOP_INCLINE_OFFSET; } } }