net.sourceforge.plantuml.svek.Line Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of plantuml Show documentation
Show all versions of plantuml Show documentation
PlantUML is a component that allows to quickly write :
* sequence diagram,
* use case diagram,
* class diagram,
* activity diagram,
* component diagram,
* state diagram
* object diagram
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2013, Arnaud Roques
*
* Project Info: http://plantuml.sourceforge.net
*
* This file is part of PlantUML.
*
* PlantUML 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 3 of the License, or
* (at your option) any later version.
*
* PlantUML 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
* Original Author: Arnaud Roques
*
* Revision $Revision: 4236 $
*
*/
package net.sourceforge.plantuml.svek;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.util.Collection;
import java.util.List;
import net.sourceforge.plantuml.Direction;
import net.sourceforge.plantuml.Hideable;
import net.sourceforge.plantuml.ISkinParam;
import net.sourceforge.plantuml.OptionFlags;
import net.sourceforge.plantuml.StringUtils;
import net.sourceforge.plantuml.Url;
import net.sourceforge.plantuml.command.Position;
import net.sourceforge.plantuml.cucadiagram.Display;
import net.sourceforge.plantuml.cucadiagram.IEntity;
import net.sourceforge.plantuml.cucadiagram.IGroup;
import net.sourceforge.plantuml.cucadiagram.Link;
import net.sourceforge.plantuml.cucadiagram.LinkArrow;
import net.sourceforge.plantuml.cucadiagram.LinkDecor;
import net.sourceforge.plantuml.cucadiagram.LinkHat;
import net.sourceforge.plantuml.cucadiagram.LinkMiddleDecor;
import net.sourceforge.plantuml.graphic.FontConfiguration;
import net.sourceforge.plantuml.graphic.HorizontalAlignement;
import net.sourceforge.plantuml.graphic.HtmlColor;
import net.sourceforge.plantuml.graphic.StringBounder;
import net.sourceforge.plantuml.graphic.TextBlock;
import net.sourceforge.plantuml.graphic.TextBlockArrow;
import net.sourceforge.plantuml.graphic.TextBlockUtils;
import net.sourceforge.plantuml.graphic.UDrawable;
import net.sourceforge.plantuml.posimo.BezierUtils;
import net.sourceforge.plantuml.posimo.DotPath;
import net.sourceforge.plantuml.posimo.Moveable;
import net.sourceforge.plantuml.posimo.Positionable;
import net.sourceforge.plantuml.posimo.PositionableUtils;
import net.sourceforge.plantuml.svek.SvekUtils.PointListIterator;
import net.sourceforge.plantuml.svek.extremity.ExtremityFactory;
import net.sourceforge.plantuml.svek.image.EntityImageNoteLink;
import net.sourceforge.plantuml.ugraphic.UGraphic;
import net.sourceforge.plantuml.ugraphic.UPolygon;
import net.sourceforge.plantuml.ugraphic.UShape;
import net.sourceforge.plantuml.ugraphic.UStroke;
public class Line implements Moveable, Hideable {
private final String ltail;
private final String lhead;
private final Link link;
private final String startUid;
private final String endUid;
private final TextBlock startTailText;
private final TextBlock endHeadText;
private final TextBlock noteLabelText;
private final int lineColor;
private final int noteLabelColor;
private final int startTailColor;
private final int endHeadColor;
private final StringBounder stringBounder;
private final Bibliotekon bibliotekon;
private DotPath dotPath;
private Positionable startTailLabelXY;
private Positionable endHeadLabelXY;
private Positionable noteLabelXY;
private UDrawable extremity2;
private UDrawable extremity1;
private double dx;
private double dy;
private boolean opale;
private Cluster projectionCluster;
class DirectionalTextBlock implements TextBlock {
private final TextBlock right;
private final TextBlock left;
private final TextBlock up;
private final TextBlock down;
DirectionalTextBlock(TextBlock right, TextBlock left, TextBlock up, TextBlock down) {
this.right = right;
this.left = left;
this.up = up;
this.down = down;
}
public void drawU(UGraphic ug, double x, double y) {
Direction dir = getDirection();
if (getLinkArrow() == LinkArrow.BACKWARD) {
dir = dir.getInv();
}
switch (dir) {
case RIGHT:
right.drawU(ug, x, y);
break;
case LEFT:
left.drawU(ug, x, y);
break;
case UP:
up.drawU(ug, x, y);
break;
case DOWN:
down.drawU(ug, x, y);
break;
default:
throw new UnsupportedOperationException();
}
}
public List getUrls() {
if (getDirection() == Direction.RIGHT) {
return right.getUrls();
}
return left.getUrls();
}
public Dimension2D calculateDimension(StringBounder stringBounder) {
return right.calculateDimension(stringBounder);
}
private Direction getDirection() {
if (isAutolink()) {
final double startAngle = dotPath.getStartAngle();
return Direction.LEFT;
}
final Point2D start = dotPath.getStartPoint();
final Point2D end = dotPath.getEndPoint();
final double ang = Math.atan2(end.getX() - start.getX(), end.getY() - start.getY());
if (ang > -Math.PI / 4 && ang < Math.PI / 4) {
return Direction.DOWN;
}
if (ang > Math.PI * 3 / 4 || ang < -Math.PI * 3 / 4) {
return Direction.UP;
}
return end.getX() > start.getX() ? Direction.RIGHT : Direction.LEFT;
}
}
// private boolean projectionStart() {
// return startUid.startsWith(Cluster.CENTER_ID);
// }
public Line(String startUid, String endUid, Link link, ColorSequence colorSequence, String ltail, String lhead,
ISkinParam skinParam, StringBounder stringBounder, FontConfiguration labelFont, Bibliotekon bibliotekon) {
if (startUid == null || endUid == null || link == null) {
throw new IllegalArgumentException();
}
this.bibliotekon = bibliotekon;
this.stringBounder = stringBounder;
this.link = link;
this.startUid = startUid;
this.endUid = endUid;
this.ltail = ltail;
this.lhead = lhead;
this.lineColor = colorSequence.getValue();
this.noteLabelColor = colorSequence.getValue();
this.startTailColor = colorSequence.getValue();
this.endHeadColor = colorSequence.getValue();
final TextBlock labelOnly;
if (link.getLabel() == null) {
if (getLinkArrow() == LinkArrow.NONE) {
labelOnly = null;
} else {
final TextBlockArrow right = new TextBlockArrow(Direction.RIGHT, labelFont);
final TextBlockArrow left = new TextBlockArrow(Direction.LEFT, labelFont);
final TextBlockArrow up = new TextBlockArrow(Direction.UP, labelFont);
final TextBlockArrow down = new TextBlockArrow(Direction.DOWN, labelFont);
labelOnly = new DirectionalTextBlock(right, left, up, down);
}
} else {
final double marginLabel = startUid.equals(endUid) ? 6 : 1;
final TextBlock label = TextBlockUtils.withMargin(
TextBlockUtils.create(link.getLabel(), labelFont, HorizontalAlignement.CENTER, skinParam),
marginLabel, marginLabel);
if (getLinkArrow() == LinkArrow.NONE) {
labelOnly = label;
} else {
TextBlock right = new TextBlockArrow(Direction.RIGHT, labelFont);
right = TextBlockUtils.mergeLR(label, right);
TextBlock left = new TextBlockArrow(Direction.LEFT, labelFont);
left = TextBlockUtils.mergeLR(left, label);
TextBlock up = new TextBlockArrow(Direction.UP, labelFont);
up = TextBlockUtils.mergeTB(up, label, HorizontalAlignement.CENTER);
TextBlock down = new TextBlockArrow(Direction.DOWN, labelFont);
down = TextBlockUtils.mergeTB(label, down, HorizontalAlignement.CENTER);
labelOnly = new DirectionalTextBlock(right, left, up, down);
}
}
final TextBlock noteOnly;
if (link.getNote() == null) {
noteOnly = null;
} else {
noteOnly = TextBlockUtils.fromIEntityImage(new EntityImageNoteLink(link.getNote(), link.getNoteColor(),
skinParam));
}
if (labelOnly != null && noteOnly != null) {
if (link.getNotePosition() == Position.LEFT) {
noteLabelText = TextBlockUtils.mergeLR(noteOnly, labelOnly);
} else if (link.getNotePosition() == Position.RIGHT) {
noteLabelText = TextBlockUtils.mergeLR(labelOnly, noteOnly);
} else if (link.getNotePosition() == Position.TOP) {
noteLabelText = TextBlockUtils.mergeTB(noteOnly, labelOnly, HorizontalAlignement.CENTER);
} else {
noteLabelText = TextBlockUtils.mergeTB(labelOnly, noteOnly, HorizontalAlignement.CENTER);
}
} else if (labelOnly != null) {
noteLabelText = labelOnly;
} else if (noteOnly != null) {
noteLabelText = noteOnly;
} else {
noteLabelText = null;
}
if (link.getQualifier1() == null) {
startTailText = null;
} else {
startTailText = TextBlockUtils.create(Display.getWithNewlines(link.getQualifier1()), labelFont,
HorizontalAlignement.CENTER, skinParam);
}
if (link.getQualifier2() == null) {
endHeadText = null;
} else {
endHeadText = TextBlockUtils.create(Display.getWithNewlines(link.getQualifier2()), labelFont,
HorizontalAlignement.CENTER, skinParam);
}
}
public boolean hasNoteLabelText() {
return noteLabelText != null;
}
private LinkArrow getLinkArrow() {
return link.getLinkArrow();
}
public void appendLine(StringBuilder sb) {
// Log.println("inverted=" + isInverted());
// if (isInverted()) {
// sb.append(endUid);
// sb.append("->");
// sb.append(startUid);
// } else {
sb.append(startUid);
sb.append("->");
sb.append(endUid);
// }
sb.append("[");
String decoration = link.getType().getSpecificDecorationSvek();
if (decoration.endsWith(",") == false) {
decoration += ",";
}
sb.append(decoration);
if (OptionFlags.HORIZONTAL_LINE_BETWEEN_DIFFERENT_PACKAGE_ALLOWED || link.getLength() != 1) {
sb.append("minlen=" + (link.getLength() - 1));
sb.append(",");
}
sb.append("color=\"" + StringUtils.getAsHtml(lineColor) + "\"");
if (noteLabelText != null) {
sb.append(",");
sb.append("label=<");
appendTable(sb, noteLabelText.calculateDimension(stringBounder), noteLabelColor);
sb.append(">");
// sb.append(",labelfloat=true");
}
if (startTailText != null) {
sb.append(",");
sb.append("taillabel=<");
appendTable(sb, startTailText.calculateDimension(stringBounder), startTailColor);
sb.append(">");
// sb.append(",labelangle=0");
}
if (endHeadText != null) {
sb.append(",");
sb.append("headlabel=<");
appendTable(sb, endHeadText.calculateDimension(stringBounder), endHeadColor);
sb.append(">");
// sb.append(",labelangle=0");
}
if (ltail != null) {
sb.append(",");
sb.append("ltail=");
sb.append(ltail);
}
if (lhead != null) {
sb.append(",");
sb.append("lhead=");
sb.append(lhead);
}
if (link.isInvis()) {
sb.append(",");
sb.append("style=invis");
}
if (link.isConstraint() == false || link.hasTwoEntryPointsSameContainer()) {
sb.append("constraint=false,");
}
// if (link.getLabeldistance() != null) {
// sb.append("labeldistance=" + link.getLabeldistance() + ",");
// }
// if (link.getLabelangle() != null) {
// sb.append("labelangle=" + link.getLabelangle() + ",");
// }
// sb.append("labelangle=1,");
sb.append("];");
SvekUtils.println(sb);
}
public String rankSame() {
if (OptionFlags.HORIZONTAL_LINE_BETWEEN_DIFFERENT_PACKAGE_ALLOWED == false && link.getLength() == 1) {
return "{rank=same; " + getStartUid() + "; " + getEndUid() + "}";
}
return null;
}
public static void appendTable(StringBuilder sb, Dimension2D dim, int col) {
final int w = (int) dim.getWidth();
final int h = (int) dim.getHeight();
appendTable(sb, w, h, col);
}
public static void appendTable(StringBuilder sb, int w, int h, int col) {
sb.append("");
sb.append("");
sb.append("");
sb.append(" ");
sb.append(" ");
sb.append("
");
}
public final String getStartUid() {
if (startUid.endsWith(":h")) {
return startUid.substring(0, startUid.length() - 2);
}
return startUid;
}
public final String getEndUid() {
if (endUid.endsWith(":h")) {
return endUid.substring(0, endUid.length() - 2);
}
return endUid;
}
public UDrawable getExtremity(LinkHat hat, LinkDecor decor, PointListIterator pointListIterator) {
final ExtremityFactory extremityFactory = decor.getExtremityFactory();
if (extremityFactory != null) {
final List points = pointListIterator.next();
final Point2D p0 = points.get(0);
final Point2D p1 = points.get(1);
final Point2D p2 = points.get(2);
return extremityFactory.createUDrawable(p0, p1, p2);
} else if (decor != LinkDecor.NONE) {
final UShape sh = new UPolygon(pointListIterator.next());
return new UDrawable() {
public void drawU(UGraphic ug, double x, double y) {
ug.draw(x, y, sh);
}
};
}
return null;
}
public void solveLine(final String svg, final int fullHeight, MinFinder corner1) {
if (this.link.isInvis()) {
return;
}
int idx = getIndexFromColor(svg, this.lineColor);
idx = svg.indexOf("d=\"", idx);
if (idx == -1) {
throw new IllegalStateException();
}
final int end = svg.indexOf("\"", idx + 3);
final String path = svg.substring(idx + 3, end);
dotPath = new DotPath(path, fullHeight);
final PointListIterator pointListIterator = new PointListIterator(svg.substring(end), fullHeight);
this.extremity2 = getExtremity(link.getType().getHat2(), link.getType().getDecor2(), pointListIterator);
this.extremity1 = getExtremity(link.getType().getHat1(), link.getType().getDecor1(), pointListIterator);
if (this.noteLabelText != null) {
final Point2D pos = getXY(svg, this.noteLabelColor, fullHeight);
corner1.manage(pos);
this.noteLabelXY = TextBlockUtils.asPositionable(noteLabelText, stringBounder, pos);
}
if (this.startTailText != null) {
final Point2D pos = getXY(svg, this.startTailColor, fullHeight);
corner1.manage(pos);
this.startTailLabelXY = TextBlockUtils.asPositionable(startTailText, stringBounder, pos);
}
if (this.endHeadText != null) {
final Point2D pos = getXY(svg, this.endHeadColor, fullHeight);
corner1.manage(pos);
this.endHeadLabelXY = TextBlockUtils.asPositionable(endHeadText, stringBounder, pos);
}
if (isOpalisable() == false) {
setOpale(false);
}
}
private boolean isOpalisable() {
return dotPath.getBeziers().size() <= 1;
}
private Point2D.Double getXY(String svg, int color, int height) {
final int idx = getIndexFromColor(svg, color);
return SvekUtils.getMinXY(SvekUtils.extractPointsList(svg, idx, height));
}
private int getIndexFromColor(String svg, int color) {
String s = "stroke=\"" + StringUtils.getAsHtml(color).toLowerCase() + "\"";
int idx = svg.indexOf(s);
if (idx != -1) {
return idx;
}
s = ";stroke:" + StringUtils.getAsHtml(color).toLowerCase() + ";";
idx = svg.indexOf(s);
if (idx != -1) {
return idx;
}
s = "fill=\"" + StringUtils.getAsHtml(color).toLowerCase() + "\"";
idx = svg.indexOf(s);
if (idx != -1) {
return idx;
}
throw new IllegalStateException("color=" + color + " " + StringUtils.getAsHtml(color).toLowerCase());
}
public void drawU(UGraphic ug, double x, double y, HtmlColor color) {
if (opale) {
return;
}
final Url url = link.getUrl();
if (url != null) {
ug.startUrl(url);
}
if (link.isAutoLinkOfAGroup()) {
final Cluster cl = bibliotekon.getCluster((IGroup) link.getEntity1());
x += cl.getWidth();
x -= dotPath.getStartPoint().getX() - cl.getMinX();
}
x += dx;
y += dy;
if (link.isInvis()) {
return;
}
if (this.link.getSpecificColor() != null) {
color = this.link.getSpecificColor();
}
ug.getParam().setColor(color);
ug.getParam().setBackcolor(null);
ug.getParam().setStroke(link.getType().getStroke());
double moveStartX = 0;
double moveStartY = 0;
double moveEndX = 0;
double moveEndY = 0;
if (projectionCluster != null && link.getEntity1() == projectionCluster.getGroup()) {
final DotPath copy = new DotPath(dotPath);
final Point2D start = copy.getStartPoint();
final Point2D proj = projectionCluster.getClusterPosition().getProjectionOnFrontier(start);
moveStartX = proj.getX() - start.getX();
moveStartY = proj.getY() - start.getY();
copy.forceStartPoint(proj.getX(), proj.getY());
ug.draw(x, y, copy);
} else if (projectionCluster != null && link.getEntity2() == projectionCluster.getGroup()) {
final DotPath copy = new DotPath(dotPath);
final Point2D end = copy.getEndPoint();
final Point2D proj = projectionCluster.getClusterPosition().getProjectionOnFrontier(end);
moveEndX = proj.getX() - end.getX();
moveEndY = proj.getY() - end.getY();
copy.forceEndPoint(proj.getX(), proj.getY());
ug.draw(x, y, copy);
} else {
ug.draw(x, y, dotPath);
}
// if (picLine1 != null) {
// final ClusterPosition clusterPosition = picLine1.getClusterPosition();
// final PointDirected inters = dotPath.getIntersection(clusterPosition);
// ExtremityStateLine1 extr1 = new ExtremityStateLine1(inters.getAngle(), inters.getPoint2D());
// extr1.drawU(ug, x, y);
// } else if (picLine2 != null) {
// final ClusterPosition clusterPosition = picLine2.getClusterPosition();
// final PointDirected inters = dotPath.getIntersection(clusterPosition);
// ExtremityStateLine2 extr2 = new ExtremityStateLine2(inters.getAngle(), inters.getPoint2D());
// extr2.drawU(ug, x, y);
// }
ug.getParam().setStroke(new UStroke());
if (this.extremity1 != null) {
ug.getParam().setColor(color);
if (this.link.getType().getDecor1().isFill()) {
ug.getParam().setBackcolor(color);
} else {
ug.getParam().setBackcolor(null);
}
this.extremity1.drawU(ug, x + moveEndX, y + moveEndY);
}
if (this.extremity2 != null) {
ug.getParam().setColor(color);
if (this.link.getType().getDecor2().isFill()) {
ug.getParam().setBackcolor(color);
} else {
ug.getParam().setBackcolor(null);
}
this.extremity2.drawU(ug, x + moveStartX, y + moveStartY);
}
if (this.noteLabelText != null) {
this.noteLabelText.drawU(ug, x + this.noteLabelXY.getPosition().getX(), y
+ this.noteLabelXY.getPosition().getY());
}
if (this.startTailText != null) {
this.startTailText.drawU(ug, x + this.startTailLabelXY.getPosition().getX(), y
+ this.startTailLabelXY.getPosition().getY());
}
if (this.endHeadText != null) {
this.endHeadText.drawU(ug, x + this.endHeadLabelXY.getPosition().getX(), y
+ this.endHeadLabelXY.getPosition().getY());
}
if (link.getType().getMiddleDecor() != LinkMiddleDecor.NONE) {
ug.getParam().setColor(color);
final PointAndAngle middle = dotPath.getMiddle();
final double angleRad = middle.getAngle();
final double angleDeg = -angleRad * 180.0 / Math.PI;
final UDrawable mi = link.getType().getMiddleDecor().getMiddleFactory().createUDrawable(angleDeg - 45);
mi.drawU(ug, x + middle.getX(), y + middle.getY());
}
if (url != null) {
ug.closeAction();
}
}
public boolean isInverted() {
return link.isInverted();
}
private double getDecorDzeta() {
final int size1 = link.getType().getDecor1().getMargin();
final int size2 = link.getType().getDecor2().getMargin();
return size1 + size2;
}
public double getHorizontalDzeta(StringBounder stringBounder) {
if (startUid.equals(endUid)) {
return getDecorDzeta();
}
final ArithmeticStrategy strategy;
if (isHorizontal()) {
strategy = new ArithmeticStrategySum();
} else {
return 0;
}
if (noteLabelText != null) {
strategy.eat(noteLabelText.calculateDimension(stringBounder).getWidth());
}
if (startTailText != null) {
strategy.eat(startTailText.calculateDimension(stringBounder).getWidth());
}
if (endHeadText != null) {
strategy.eat(endHeadText.calculateDimension(stringBounder).getWidth());
}
return strategy.getResult() + getDecorDzeta();
}
private boolean isHorizontal() {
return link.getLength() == 1;
}
public double getVerticalDzeta(StringBounder stringBounder) {
if (startUid.equals(endUid)) {
return getDecorDzeta();
}
if (isHorizontal()) {
return 0;
}
final ArithmeticStrategy strategy = new ArithmeticStrategySum();
if (noteLabelText != null) {
strategy.eat(noteLabelText.calculateDimension(stringBounder).getHeight());
}
if (startTailText != null) {
strategy.eat(startTailText.calculateDimension(stringBounder).getHeight());
}
if (endHeadText != null) {
strategy.eat(endHeadText.calculateDimension(stringBounder).getHeight());
}
return strategy.getResult() + getDecorDzeta();
}
public void manageCollision(Collection allShapes) {
for (Shape sh : allShapes) {
final Positionable cl = PositionableUtils.addMargin(sh, 8, 8);
if (startTailText != null && PositionableUtils.intersect(cl, startTailLabelXY)) {
startTailLabelXY = PositionableUtils.moveAwayFrom(cl, startTailLabelXY);
}
if (endHeadText != null && PositionableUtils.intersect(cl, endHeadLabelXY)) {
endHeadLabelXY = PositionableUtils.moveAwayFrom(cl, endHeadLabelXY);
}
}
// final Positionable start = getStartTailPositionnable();
// if (start != null) {
// for (Shape sh : allShapes) {
// if (cut(start, sh)) {
// avoid(startTailLabelXY, start, sh);
// }
// }
// }
//
// final Positionable end = getEndHeadPositionnable();
// if (end != null) {
// for (Shape sh : allShapes) {
// if (cut(end, sh)) {
// avoid(endHeadLabelXY, end, sh);
// }
// }
// }
}
private void avoid(Point2D.Double move, Positionable pos, Shape sh) {
final Oscillator oscillator = new Oscillator();
final Point2D.Double orig = new Point2D.Double(move.x, move.y);
while (cut(pos, sh)) {
final Point2D.Double m = oscillator.nextPosition();
move.setLocation(orig.x + m.x, orig.y + m.y);
}
}
private boolean cut(Positionable pos, Shape sh) {
return BezierUtils.intersect(pos, sh) || tooClose(pos);
}
private boolean tooClose(Positionable pos) {
final double dist = dotPath.getMinDist(BezierUtils.getCenter(pos));
final Dimension2D dim = pos.getSize();
// Log.println("dist=" + dist);
return dist < (dim.getWidth() / 2 + 2) || dist < (dim.getHeight() / 2 + 2);
}
public void moveSvek(double deltaX, double deltaY) {
this.dx += deltaX;
this.dy += deltaY;
}
public final DotPath getDotPath() {
final DotPath result = new DotPath(dotPath);
result.moveSvek(dx, dy);
return result;
}
public int getLength() {
return link.getLength();
}
public void setOpale(boolean opale) {
this.link.setOpale(opale);
this.opale = opale;
}
public boolean isOpale() {
return opale;
}
public boolean isHorizontalSolitary() {
return link.isHorizontalSolitary();
}
public boolean isLinkFromOrToGroup(IEntity group) {
return link.getEntity1() == group || link.getEntity2() == group;
}
public boolean hasEntryPoint() {
return link.hasEntryPoint();
}
public void setProjectionCluster(Cluster cluster) {
this.projectionCluster = cluster;
}
public boolean isHidden() {
return link.isHidden();
}
public boolean sameConnections(Line other) {
return link.sameConnections(other.link);
}
private boolean isAutolink() {
return link.getEntity1() == link.getEntity2();
}
}