From d36e6e21e0bee56beb9f7bba1b75661607836881 Mon Sep 17 00:00:00 2001 From: Dwitry Date: Wed, 14 Aug 2019 13:56:26 +0300 Subject: [PATCH] Show multiple relations in Graph view Resolves #59 --- .../renderers/CustomEdgeRenderer.java | 131 +++++--- .../visualization/util/TestVisualization.java | 285 ++++++++++++++++++ 2 files changed, 373 insertions(+), 43 deletions(-) create mode 100644 ui/visualization/src/test/java/com/neueda/jetbrains/plugin/graphdb/visualization/util/TestVisualization.java diff --git a/ui/visualization/src/main/java/com/neueda/jetbrains/plugin/graphdb/visualization/renderers/CustomEdgeRenderer.java b/ui/visualization/src/main/java/com/neueda/jetbrains/plugin/graphdb/visualization/renderers/CustomEdgeRenderer.java index 182eac37..1702193f 100644 --- a/ui/visualization/src/main/java/com/neueda/jetbrains/plugin/graphdb/visualization/renderers/CustomEdgeRenderer.java +++ b/ui/visualization/src/main/java/com/neueda/jetbrains/plugin/graphdb/visualization/renderers/CustomEdgeRenderer.java @@ -1,24 +1,30 @@ package com.neueda.jetbrains.plugin.graphdb.visualization.renderers; -import com.neueda.jetbrains.plugin.graphdb.platform.ShouldNeverHappenException; -import com.neueda.jetbrains.plugin.graphdb.visualization.util.IntersectionUtil; import com.neueda.jetbrains.plugin.graphdb.visualization.util.RenderingUtil; import prefuse.Constants; import prefuse.render.EdgeRenderer; import prefuse.visual.EdgeItem; import prefuse.visual.VisualItem; +import prefuse.visual.tuple.TableNodeItem; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.util.List; +import java.util.Optional; +import java.util.Spliterator; import static com.neueda.jetbrains.plugin.graphdb.visualization.constants.VisualizationParameters.EDGE_THICKNESS; import static com.neueda.jetbrains.plugin.graphdb.visualization.constants.VisualizationParameters.NODE_DIAMETER; +import static java.lang.Math.*; +import static java.util.Spliterators.spliteratorUnknownSize; +import static java.util.stream.Collectors.toList; +import static java.util.stream.StreamSupport.stream; public class CustomEdgeRenderer extends EdgeRenderer { private static final double RADIUS = (NODE_DIAMETER + EDGE_THICKNESS) / 2; + private static final double RIGHT_ANGLE_IN_RADS = 1.5708; public CustomEdgeRenderer(int edgeTypeLine) { super(edgeTypeLine); @@ -30,59 +36,89 @@ protected Shape getRawShape(VisualItem item) { VisualItem item1 = edge.getSourceItem(); VisualItem item2 = edge.getTargetItem(); - getAlignedPoint(m_tmpPoints[0], item1.getBounds(), m_xAlign1, m_yAlign1); - getAlignedPoint(m_tmpPoints[1], item2.getBounds(), m_xAlign2, m_yAlign2); - m_curWidth = (float) (m_width * getLineWidth(item)); + boolean isDirected = m_edgeArrow != Constants.EDGE_ARROW_NONE; + boolean isLoopNode = item1 == item2; + double shift = getRelationShift(edge, item1, item2); - // TODO decide on the angle here for loop arrow - double angle = 0.261799 * 3; + double x1 = item1.getBounds().getCenterX(); + double y1 = item1.getBounds().getCenterY(); + double x2 = item2.getBounds().getCenterX(); + double y2 = item2.getBounds().getCenterY(); - boolean isLoopNode = item1 == item2; - if (!isLoopNode && edge.isDirected() && m_edgeArrow != Constants.EDGE_ARROW_NONE) { - boolean forward = (m_edgeArrow == Constants.EDGE_ARROW_FORWARD); - Point2D start = m_tmpPoints[forward ? 0 : 1]; - Point2D end = m_tmpPoints[forward ? 1 : 0]; + if (isLoopNode) { + double angle = 0.261799 * 3 * shift; - VisualItem dest = forward ? edge.getTargetItem() : edge.getSourceItem(); - Point2D center = new Point2D.Double(dest.getBounds().getCenterX(), dest.getBounds().getCenterY()); - List intersections = IntersectionUtil.getCircleLineIntersectionPoint(start, end, center, dest.getBounds().getWidth() / 2); + getAlignedPoint(m_tmpPoints[0], item1.getBounds(), m_xAlign1, m_yAlign1); + getAlignedPoint(m_tmpPoints[1], item2.getBounds(), m_xAlign2, m_yAlign2); + m_curWidth = (float) (m_width * getLineWidth(item)); - if (intersections.size() == 0) { - throw new ShouldNeverHappenException("Andrew Naydyonock", "edge always intersect a node"); + if (isDirected) { + double x = m_tmpPoints[0].getX(); + double y = m_tmpPoints[0].getY(); + + Point.Double[] arrowPoints = RenderingUtil.arrow(angle, RADIUS, x, y, m_arrowHeight + 8); + AffineTransform at = getArrowTrans(arrowPoints[0], arrowPoints[1], m_curWidth); + m_curArrow = at.createTransformedShape(m_arrowHead); } - end = intersections.get(0); + return RenderingUtil.loopArrow(angle, RADIUS, x1, y1, m_arrowHeight); + } else { + double angle = RIGHT_ANGLE_IN_RADS - atan2(y2 - y1, x2 - x1); - AffineTransform at = getArrowTrans(start, end, m_curWidth); - m_curArrow = at.createTransformedShape(m_arrowHead); + double shiftInRad = toRadians(shift * 20); - Point2D lineEnd = m_tmpPoints[forward ? 1 : 0]; - lineEnd.setLocation(0, -m_arrowHeight); - at.transform(lineEnd, lineEnd); - } else if (isLoopNode && edge.isDirected() && m_edgeArrow != Constants.EDGE_ARROW_NONE) { - double x = m_tmpPoints[0].getX(); - double y = m_tmpPoints[0].getY(); + double lineX1 = x1 + RADIUS * sin(angle + shiftInRad); + double lineY1 = y1 + RADIUS * cos(angle + shiftInRad); - Point.Double[] arrowPoints = RenderingUtil.arrow(angle, RADIUS, x, y, m_arrowHeight + 8); - AffineTransform at = getArrowTrans(arrowPoints[0], arrowPoints[1], m_curWidth); - m_curArrow = at.createTransformedShape(m_arrowHead); - } else { - m_curArrow = null; - } + double arrowX = x2 - RADIUS * sin(angle - shiftInRad); + double arrowY = y2 - RADIUS * cos(angle - shiftInRad); - Shape shape; - double n1x = m_tmpPoints[0].getX(); - double n1y = m_tmpPoints[0].getY(); - double n2x = m_tmpPoints[1].getX(); - double n2y = m_tmpPoints[1].getY(); - if (isLoopNode) { - shape = RenderingUtil.loopArrow(angle, RADIUS, n1x, n1y, m_arrowHeight); - } else { - m_line.setLine(n1x, n1y, n2x, n2y); - shape = m_line; + if (isDirected) { + AffineTransform at = getArrowTrans(new Point2D.Double(lineX1, lineY1), new Point2D.Double(arrowX, arrowY), m_curWidth); + m_curArrow = at.createTransformedShape(m_arrowHead); + + double lineX2 = arrowX - m_arrowWidth * sin(angle); + double lineY2 = arrowY - m_arrowWidth * cos(angle); + + m_line.setLine(lineX1, lineY1, lineX2, lineY2); + } else { + m_line.setLine(lineX1, lineY1, arrowX, arrowY); + } + + return m_line; } + } - return shape; + private double getRelationShift(EdgeItem edge, VisualItem node1, VisualItem node2) { + boolean reverse = node1.getRow() > node2.getRow(); + VisualItem node = reverse ? node1 : node2; + + return cast(node, TableNodeItem.class) + .map(n -> { + Spliterator iterator = spliteratorUnknownSize(n.edges(), 0); + List edges = stream(iterator, false) + .map(e -> cast(e, EdgeItem.class)) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(e -> + (e.getSourceItem() == node1 && e.getTargetItem() == node2) || + (e.getSourceItem() == node2 && e.getTargetItem() == node1) + ) + .collect(toList()); + + if (edges.size() > 1) { + int relNumber = edges.indexOf(edge); + double relPos = relNumber - edges.size() / 2; + if (edges.size() % 2 == 0) { + relPos = relPos + 0.5; + } + + return reverse ? relPos : relPos * -1; + } else { + return 0d; + } + }) + .orElse(0d); } @Override @@ -97,4 +133,13 @@ public boolean locatePoint(Point2D p, VisualItem item) { || (m_curArrow != null && m_curArrow.contains(p.getX(), p.getY())); } } + + @SuppressWarnings("unchecked") + private Optional cast(Object o, Class clazz) { + if (clazz.isInstance(o)) { + return Optional.of((T) o); + } else { + return Optional.empty(); + } + } } diff --git a/ui/visualization/src/test/java/com/neueda/jetbrains/plugin/graphdb/visualization/util/TestVisualization.java b/ui/visualization/src/test/java/com/neueda/jetbrains/plugin/graphdb/visualization/util/TestVisualization.java new file mode 100644 index 00000000..fd9984ff --- /dev/null +++ b/ui/visualization/src/test/java/com/neueda/jetbrains/plugin/graphdb/visualization/util/TestVisualization.java @@ -0,0 +1,285 @@ +package com.neueda.jetbrains.plugin.graphdb.visualization.util; + + +import com.intellij.ui.JBColor; +import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphNode; +import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphPropertyContainer; +import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphRelationship; +import com.neueda.jetbrains.plugin.graphdb.visualization.PrefuseVisualization; +import com.neueda.jetbrains.plugin.graphdb.visualization.services.LookAndFeelService; +import org.junit.AfterClass; +import org.junit.Ignore; +import org.junit.Test; + +import javax.swing.*; +import java.awt.*; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Shows Graph Visualization component without launching full IDE + * Prepopulated with different combinations of nodes and relationships + * Useful for experimentation when working on visualization + */ +@Ignore("For development. Nothing to test.") +public class TestVisualization { + private static AtomicInteger counter = new AtomicInteger(); + private static PrefuseVisualization vis = new PrefuseVisualization(mockLook()); + + @Test + public void showNoRels() { + addNode("A single"); + addNode("B single"); + } + + @Test + public void showSimple() { + addRel(addNode("A"), addNode("B")); + } + + @Test + public void show2Rel() { + GraphNode r2a = addNode("A 2rel"); + GraphNode r2b = addNode("B 2rel"); + addRel(r2a, r2b); + addRel(r2b, r2a); + } + + @Test + public void show3Nodes() { + GraphNode tr2a = addNode("A 2rel"); + GraphNode tr2b = addNode("B 2rel"); + GraphNode tr2c = addNode("C 2rel"); + addRel(tr2a, tr2b); + addRel(tr2b, tr2a); + addRel(tr2b, tr2c); + addRel(tr2c, tr2b); + } + + @Test + public void show4Rel() { + GraphNode r4a = addNode("A 4rel"); + GraphNode r4b = addNode("B 4rel"); + addRel(r4a, r4b); + addRel(r4a, r4b); + addRel(r4a, r4b); + addRel(r4b, r4a); + } + + @Test + public void showLoop() { + GraphNode loop = addNode("selfLoop"); + addRel(loop, loop); + } + + @Test + public void showMultiLoop() { + GraphNode loop3 = addNode("selfLoop x3"); + addRel(loop3, loop3); + addRel(loop3, loop3); + addRel(loop3, loop3); + } + + @Test + public void show2SameDirection() { + GraphNode sda = addNode("A sd"); + GraphNode sdb = addNode("B sd"); + addRel(sdb, sda); + addRel(sdb, sda); + } + + @Test + public void show5Rel() { + GraphNode node3 = addNode("A 5rel"); + GraphNode node4 = addNode("B 5rel"); + addRel(node4, node3); + addRel(node3, node4); + addRel(node4, node3); + addRel(node3, node4); + addRel(node3, node4); + } + + @Test + public void show15Rel() { + GraphNode a = addNode("A 15rel"); + GraphNode b = addNode("B 15rel"); + for (int i = 0; i < 15; i++) { + addRel(a, b); + } + } + + @AfterClass + public static void show() throws IOException { + vis.paint(); + JComponent com = vis.getCanvas(); + JFrame.setDefaultLookAndFeelDecorated(true); + JFrame frame = new JFrame("JComponent Example"); + frame.setSize(1500, 1500); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(com); + frame.setVisible(true); + + System.in.read(); + } + + private static GraphRelationship addRel(final GraphNode startNode, final GraphNode endNode) { + String id = "" + counter.incrementAndGet(); + return addRel(id, startNode, endNode, "relType", "name", new HashMap<>()); + } + + private static GraphRelationship addRel(String id, + final GraphNode startNode, + final GraphNode endNode, + String type, + final String name, + Map properties) { + GraphRelationship newRelationship = new GraphRelationship() { + @Override + public boolean hasStartAndEndNode() { + return true; + } + + @Override + public String getStartNodeId() { + return startNode.getId(); + } + + @Override + public String getEndNodeId() { + return endNode.getId(); + } + + @Override + public GraphNode getStartNode() { + return startNode; + } + + @Override + public GraphNode getEndNode() { + return endNode; + } + + @Override + public String getId() { + return id; + } + + @Override + public GraphPropertyContainer getPropertyContainer() { + return () -> properties; + } + + @Override + public List getTypes() { + return Collections.singletonList(type); + } + + @Override + public String getTypesName() { + return name; + } + + @Override + public boolean isTypesSingle() { + return true; + } + }; + + vis.addRelation(newRelationship); + return newRelationship; + } + + private static GraphNode addNode(String name) { + String id = "" + counter.incrementAndGet(); + return addNode(id, "nodeType", "name", Collections.singletonMap("name", name)); + } + + private static GraphNode addNode(String id, final String type, final String name, final Map properties) { + + GraphNode newNode = new GraphNode() { + @Override + public String getId() { + return id; + } + + @Override + public GraphPropertyContainer getPropertyContainer() { + return () -> properties; + } + + @Override + public List getTypes() { + return Collections.singletonList(type); + } + + @Override + public String getTypesName() { + return name; + } + + @Override + public boolean isTypesSingle() { + return false; + } + }; + vis.addNode(newNode); + return newNode; + } + + + + private static LookAndFeelService mockLook() { + return new LookAndFeelService() { + @Override + public Color getBackgroundColor() { + return JBColor.BLACK; + } + + @Override + public Color getBorderColor() { + return JBColor.RED; + } + + @Override + public Color getNodeStrokeColor() { + return JBColor.GREEN; + } + + @Override + public Color getNodeStrokeHoverColor() { + return JBColor.ORANGE; + } + + @Override + public Color getNodeFillColor() { + return JBColor.CYAN; + } + + @Override + public Color getNodeFillHoverColor() { + return JBColor.DARK_GRAY; + } + + @Override + public Color getEdgeStrokeColor() { + return JBColor.LIGHT_GRAY; + } + + @Override + public Color getEdgeFillColor() { + return JBColor.MAGENTA; + } + + @Override + public Color getTextColor() { + return JBColor.WHITE; + } + }; + } + + +}