/*
* 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;
}
}
}