Skip to content

Commit 36ecefc

Browse files
authored
Show multiple relations in Graph view (#125)
Resolves #59
1 parent e5d6b23 commit 36ecefc

File tree

2 files changed

+373
-43
lines changed

2 files changed

+373
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
package com.neueda.jetbrains.plugin.graphdb.visualization.renderers;
22

3-
import com.neueda.jetbrains.plugin.graphdb.platform.ShouldNeverHappenException;
4-
import com.neueda.jetbrains.plugin.graphdb.visualization.util.IntersectionUtil;
53
import com.neueda.jetbrains.plugin.graphdb.visualization.util.RenderingUtil;
64
import prefuse.Constants;
75
import prefuse.render.EdgeRenderer;
86
import prefuse.visual.EdgeItem;
97
import prefuse.visual.VisualItem;
8+
import prefuse.visual.tuple.TableNodeItem;
109

1110
import java.awt.*;
1211
import java.awt.geom.AffineTransform;
1312
import java.awt.geom.Point2D;
1413
import java.util.List;
14+
import java.util.Optional;
15+
import java.util.Spliterator;
1516

1617
import static com.neueda.jetbrains.plugin.graphdb.visualization.constants.VisualizationParameters.EDGE_THICKNESS;
1718
import static com.neueda.jetbrains.plugin.graphdb.visualization.constants.VisualizationParameters.NODE_DIAMETER;
19+
import static java.lang.Math.*;
20+
import static java.util.Spliterators.spliteratorUnknownSize;
21+
import static java.util.stream.Collectors.toList;
22+
import static java.util.stream.StreamSupport.stream;
1823

1924
public class CustomEdgeRenderer extends EdgeRenderer {
2025

2126
private static final double RADIUS = (NODE_DIAMETER + EDGE_THICKNESS) / 2;
27+
private static final double RIGHT_ANGLE_IN_RADS = 1.5708;
2228

2329
public CustomEdgeRenderer(int edgeTypeLine) {
2430
super(edgeTypeLine);
@@ -30,59 +36,89 @@ protected Shape getRawShape(VisualItem item) {
3036
VisualItem item1 = edge.getSourceItem();
3137
VisualItem item2 = edge.getTargetItem();
3238

33-
getAlignedPoint(m_tmpPoints[0], item1.getBounds(), m_xAlign1, m_yAlign1);
34-
getAlignedPoint(m_tmpPoints[1], item2.getBounds(), m_xAlign2, m_yAlign2);
35-
m_curWidth = (float) (m_width * getLineWidth(item));
39+
boolean isDirected = m_edgeArrow != Constants.EDGE_ARROW_NONE;
40+
boolean isLoopNode = item1 == item2;
41+
double shift = getRelationShift(edge, item1, item2);
3642

37-
// TODO decide on the angle here for loop arrow
38-
double angle = 0.261799 * 3;
43+
double x1 = item1.getBounds().getCenterX();
44+
double y1 = item1.getBounds().getCenterY();
45+
double x2 = item2.getBounds().getCenterX();
46+
double y2 = item2.getBounds().getCenterY();
3947

40-
boolean isLoopNode = item1 == item2;
41-
if (!isLoopNode && edge.isDirected() && m_edgeArrow != Constants.EDGE_ARROW_NONE) {
42-
boolean forward = (m_edgeArrow == Constants.EDGE_ARROW_FORWARD);
43-
Point2D start = m_tmpPoints[forward ? 0 : 1];
44-
Point2D end = m_tmpPoints[forward ? 1 : 0];
48+
if (isLoopNode) {
49+
double angle = 0.261799 * 3 * shift;
4550

46-
VisualItem dest = forward ? edge.getTargetItem() : edge.getSourceItem();
47-
Point2D center = new Point2D.Double(dest.getBounds().getCenterX(), dest.getBounds().getCenterY());
48-
List<Point2D> intersections = IntersectionUtil.getCircleLineIntersectionPoint(start, end, center, dest.getBounds().getWidth() / 2);
51+
getAlignedPoint(m_tmpPoints[0], item1.getBounds(), m_xAlign1, m_yAlign1);
52+
getAlignedPoint(m_tmpPoints[1], item2.getBounds(), m_xAlign2, m_yAlign2);
53+
m_curWidth = (float) (m_width * getLineWidth(item));
4954

50-
if (intersections.size() == 0) {
51-
throw new ShouldNeverHappenException("Andrew Naydyonock", "edge always intersect a node");
55+
if (isDirected) {
56+
double x = m_tmpPoints[0].getX();
57+
double y = m_tmpPoints[0].getY();
58+
59+
Point.Double[] arrowPoints = RenderingUtil.arrow(angle, RADIUS, x, y, m_arrowHeight + 8);
60+
AffineTransform at = getArrowTrans(arrowPoints[0], arrowPoints[1], m_curWidth);
61+
m_curArrow = at.createTransformedShape(m_arrowHead);
5262
}
5363

54-
end = intersections.get(0);
64+
return RenderingUtil.loopArrow(angle, RADIUS, x1, y1, m_arrowHeight);
65+
} else {
66+
double angle = RIGHT_ANGLE_IN_RADS - atan2(y2 - y1, x2 - x1);
5567

56-
AffineTransform at = getArrowTrans(start, end, m_curWidth);
57-
m_curArrow = at.createTransformedShape(m_arrowHead);
68+
double shiftInRad = toRadians(shift * 20);
5869

59-
Point2D lineEnd = m_tmpPoints[forward ? 1 : 0];
60-
lineEnd.setLocation(0, -m_arrowHeight);
61-
at.transform(lineEnd, lineEnd);
62-
} else if (isLoopNode && edge.isDirected() && m_edgeArrow != Constants.EDGE_ARROW_NONE) {
63-
double x = m_tmpPoints[0].getX();
64-
double y = m_tmpPoints[0].getY();
70+
double lineX1 = x1 + RADIUS * sin(angle + shiftInRad);
71+
double lineY1 = y1 + RADIUS * cos(angle + shiftInRad);
6572

66-
Point.Double[] arrowPoints = RenderingUtil.arrow(angle, RADIUS, x, y, m_arrowHeight + 8);
67-
AffineTransform at = getArrowTrans(arrowPoints[0], arrowPoints[1], m_curWidth);
68-
m_curArrow = at.createTransformedShape(m_arrowHead);
69-
} else {
70-
m_curArrow = null;
71-
}
73+
double arrowX = x2 - RADIUS * sin(angle - shiftInRad);
74+
double arrowY = y2 - RADIUS * cos(angle - shiftInRad);
7275

73-
Shape shape;
74-
double n1x = m_tmpPoints[0].getX();
75-
double n1y = m_tmpPoints[0].getY();
76-
double n2x = m_tmpPoints[1].getX();
77-
double n2y = m_tmpPoints[1].getY();
78-
if (isLoopNode) {
79-
shape = RenderingUtil.loopArrow(angle, RADIUS, n1x, n1y, m_arrowHeight);
80-
} else {
81-
m_line.setLine(n1x, n1y, n2x, n2y);
82-
shape = m_line;
76+
if (isDirected) {
77+
AffineTransform at = getArrowTrans(new Point2D.Double(lineX1, lineY1), new Point2D.Double(arrowX, arrowY), m_curWidth);
78+
m_curArrow = at.createTransformedShape(m_arrowHead);
79+
80+
double lineX2 = arrowX - m_arrowWidth * sin(angle);
81+
double lineY2 = arrowY - m_arrowWidth * cos(angle);
82+
83+
m_line.setLine(lineX1, lineY1, lineX2, lineY2);
84+
} else {
85+
m_line.setLine(lineX1, lineY1, arrowX, arrowY);
86+
}
87+
88+
return m_line;
8389
}
90+
}
8491

85-
return shape;
92+
private double getRelationShift(EdgeItem edge, VisualItem node1, VisualItem node2) {
93+
boolean reverse = node1.getRow() > node2.getRow();
94+
VisualItem node = reverse ? node1 : node2;
95+
96+
return cast(node, TableNodeItem.class)
97+
.map(n -> {
98+
Spliterator<?> iterator = spliteratorUnknownSize(n.edges(), 0);
99+
List<EdgeItem> edges = stream(iterator, false)
100+
.map(e -> cast(e, EdgeItem.class))
101+
.filter(Optional::isPresent)
102+
.map(Optional::get)
103+
.filter(e ->
104+
(e.getSourceItem() == node1 && e.getTargetItem() == node2) ||
105+
(e.getSourceItem() == node2 && e.getTargetItem() == node1)
106+
)
107+
.collect(toList());
108+
109+
if (edges.size() > 1) {
110+
int relNumber = edges.indexOf(edge);
111+
double relPos = relNumber - edges.size() / 2;
112+
if (edges.size() % 2 == 0) {
113+
relPos = relPos + 0.5;
114+
}
115+
116+
return reverse ? relPos : relPos * -1;
117+
} else {
118+
return 0d;
119+
}
120+
})
121+
.orElse(0d);
86122
}
87123

88124
@Override
@@ -97,4 +133,13 @@ public boolean locatePoint(Point2D p, VisualItem item) {
97133
|| (m_curArrow != null && m_curArrow.contains(p.getX(), p.getY()));
98134
}
99135
}
136+
137+
@SuppressWarnings("unchecked")
138+
private <T> Optional<T> cast(Object o, Class<T> clazz) {
139+
if (clazz.isInstance(o)) {
140+
return Optional.of((T) o);
141+
} else {
142+
return Optional.empty();
143+
}
144+
}
100145
}

0 commit comments

Comments
 (0)