mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-27 14:54:52 +02:00
8343705: IGV: Interactive Node Moving in Hierarchical Layout
Reviewed-by: chagedorn, thartmann, rcastanedalo
This commit is contained in:
parent
4da7c35484
commit
28b0f3eaa5
9 changed files with 1298 additions and 15 deletions
|
@ -33,7 +33,7 @@ import java.util.*;
|
|||
*
|
||||
* @author Thomas Wuerthinger
|
||||
*/
|
||||
public class HierarchicalLayoutManager extends LayoutManager {
|
||||
public class HierarchicalLayoutManager extends LayoutManager implements LayoutMover {
|
||||
|
||||
int maxLayerLength;
|
||||
private LayoutGraph graph;
|
||||
|
@ -77,6 +77,62 @@ public class HierarchicalLayoutManager extends LayoutManager {
|
|||
graph = layoutGraph;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveLink(Point linkPos, int shiftX) {
|
||||
int layerNr = graph.findLayer(linkPos.y);
|
||||
for (LayoutNode node : graph.getLayer(layerNr)) {
|
||||
if (node.isDummy() && linkPos.x == node.getX()) {
|
||||
LayoutLayer layer = graph.getLayer(layerNr);
|
||||
if (layer.contains(node)) {
|
||||
node.setX(linkPos.x + shiftX);
|
||||
layer.sortNodesByX();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
writeBack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveVertices(Set<? extends Vertex> movedVertices) {
|
||||
for (Vertex vertex : movedVertices) {
|
||||
moveVertex(vertex);
|
||||
}
|
||||
writeBack();
|
||||
}
|
||||
|
||||
private void writeBack() {
|
||||
graph.optimizeBackEdgeCrossings();
|
||||
graph.updateLayerMinXSpacing();
|
||||
graph.straightenEdges();
|
||||
WriteResult.apply(graph);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveVertex(Vertex movedVertex) {
|
||||
Point newLoc = movedVertex.getPosition();
|
||||
LayoutNode movedNode = graph.getLayoutNode(movedVertex);
|
||||
assert !movedNode.isDummy();
|
||||
|
||||
int layerNr = graph.findLayer(newLoc.y + movedNode.getOuterHeight() / 2);
|
||||
if (movedNode.getLayer() == layerNr) { // we move the node in the same layer
|
||||
LayoutLayer layer = graph.getLayer(layerNr);
|
||||
if (layer.contains(movedNode)) {
|
||||
movedNode.setX(newLoc.x);
|
||||
layer.sortNodesByX();
|
||||
}
|
||||
} else { // only remove edges if we moved the node to a new layer
|
||||
if (maxLayerLength > 0) return; // TODO: not implemented
|
||||
graph.removeNodeAndEdges(movedNode);
|
||||
layerNr = graph.insertNewLayerIfNeeded(movedNode, layerNr);
|
||||
graph.addNodeToLayer(movedNode, layerNr);
|
||||
movedNode.setX(newLoc.x);
|
||||
graph.getLayer(layerNr).sortNodesByX();
|
||||
graph.removeEmptyLayers();
|
||||
graph.addEdges(movedNode, maxLayerLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes self-edges from nodes in the graph. If self-edges are to be included in the layout
|
||||
* (`layoutSelfEdges` is true), it stores them in the node for later processing and marks the graph
|
||||
|
|
|
@ -169,6 +169,24 @@ public class LayoutEdge {
|
|||
this.to = to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the absolute x-coordinate of the source node's connection point for this edge.
|
||||
*
|
||||
* @return The x-coordinate of the source node's connection point.
|
||||
*/
|
||||
public int getFromX() {
|
||||
return from.getX() + getRelativeFromX();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the absolute x-coordinate of the target node's connection point for this edge.
|
||||
*
|
||||
* @return The x-coordinate of the target node's connection point.
|
||||
*/
|
||||
public int getToX() {
|
||||
return to.getX() + getRelativeToX();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the relative horizontal position from the source node's left boundary to the edge's starting point.
|
||||
*
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
*/
|
||||
package com.sun.hotspot.igv.hierarchicallayout;
|
||||
|
||||
import static com.sun.hotspot.igv.hierarchicallayout.LayoutNode.NODE_POS_COMPARATOR;
|
||||
import com.sun.hotspot.igv.layout.Link;
|
||||
import com.sun.hotspot.igv.layout.Port;
|
||||
import com.sun.hotspot.igv.layout.Vertex;
|
||||
|
@ -52,6 +53,8 @@ public class LayoutGraph {
|
|||
private final Set<Link> links;
|
||||
private final SortedSet<Vertex> vertices;
|
||||
private final LinkedHashMap<Vertex, Set<Port>> inputPorts;
|
||||
private final LinkedHashMap<Vertex, Set<Port>> outputPorts;
|
||||
private final LinkedHashMap<Port, Set<Link>> portLinks;
|
||||
|
||||
// Layout Management: LayoutNodes and LayoutLayers
|
||||
private final LinkedHashMap<Vertex, LayoutNode> layoutNodes;
|
||||
|
@ -69,9 +72,9 @@ public class LayoutGraph {
|
|||
public LayoutGraph(Collection<? extends Link> links, Collection<? extends Vertex> additionalVertices) {
|
||||
this.links = new HashSet<>(links);
|
||||
vertices = new TreeSet<>(additionalVertices);
|
||||
LinkedHashMap<Port, Set<Link>> portLinks = new LinkedHashMap<>(links.size());
|
||||
portLinks = new LinkedHashMap<>(links.size());
|
||||
inputPorts = new LinkedHashMap<>(links.size());
|
||||
LinkedHashMap<Vertex, Set<Port>> outputPorts = new LinkedHashMap<>(links.size());
|
||||
outputPorts = new LinkedHashMap<>(links.size());
|
||||
|
||||
for (Link link : links) {
|
||||
assert link.getFrom() != null;
|
||||
|
@ -175,6 +178,114 @@ public class LayoutGraph {
|
|||
return Collections.unmodifiableList(allNodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new layer at the specified index in the layers list.
|
||||
* Adjusts the layer numbers of existing nodes in layers below the inserted layer.
|
||||
*
|
||||
* @param layerNr The index at which to insert the new layer.
|
||||
* @return The newly created LayoutLayer.
|
||||
*/
|
||||
private LayoutLayer createNewLayer(int layerNr) {
|
||||
LayoutLayer layer = new LayoutLayer();
|
||||
layers.add(layerNr, layer);
|
||||
|
||||
// update layer field in nodes below layerNr
|
||||
for (int l = layerNr + 1; l < getLayerCount(); l++) {
|
||||
for (LayoutNode layoutNode : getLayer(l)) {
|
||||
layoutNode.setLayer(l);
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the layer at the specified index.
|
||||
* Adjusts the layer numbers of existing nodes in layers below the deleted layer.
|
||||
*
|
||||
* @param layerNr The index of the layer to delete.
|
||||
*/
|
||||
private void deleteLayer(int layerNr) {
|
||||
layers.remove(layerNr);
|
||||
|
||||
// Update the layer field in nodes below the deleted layer
|
||||
for (int l = layerNr; l < getLayerCount(); l++) {
|
||||
for (LayoutNode layoutNode : getLayer(l)) {
|
||||
layoutNode.setLayer(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that no neighboring nodes of the specified node are in the same layer.
|
||||
* If any neighbor is found in the specified layer, inserts a new layer to avoid conflicts.
|
||||
* Returns the adjusted layer number where the node can be safely inserted.
|
||||
*
|
||||
* @param node The LayoutNode to check and possibly reposition.
|
||||
* @param layerNr The proposed layer number for the node.
|
||||
* @return The layer number where the node can be safely inserted after adjustments.
|
||||
*/
|
||||
public int insertNewLayerIfNeeded(LayoutNode node, int layerNr) {
|
||||
for (Link inputLink : getInputLinks(node.getVertex())) {
|
||||
if (inputLink.getFrom().getVertex() == inputLink.getTo().getVertex()) continue;
|
||||
LayoutNode fromNode = getLayoutNode(inputLink.getFrom().getVertex());
|
||||
if (fromNode.getLayer() == layerNr) {
|
||||
moveExpandLayerDown(layerNr + 1);
|
||||
return layerNr + 1;
|
||||
}
|
||||
}
|
||||
for (Link outputLink : getOutputLinks(node.getVertex())) {
|
||||
if (outputLink.getFrom().getVertex() == outputLink.getTo().getVertex()) continue;
|
||||
LayoutNode toNode = getLayoutNode(outputLink.getTo().getVertex());
|
||||
if (toNode.getLayer() == layerNr) {
|
||||
moveExpandLayerDown(layerNr);
|
||||
return layerNr;
|
||||
}
|
||||
}
|
||||
return layerNr;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new layer at the specified index and adjusts nodes and edges accordingly.
|
||||
* Moves existing nodes and their successors down to accommodate the new layer.
|
||||
*
|
||||
* @param layerNr The index at which to insert the new layer.
|
||||
*/
|
||||
private void moveExpandLayerDown(int layerNr) {
|
||||
LayoutLayer newLayer = createNewLayer(layerNr);
|
||||
|
||||
if (layerNr == 0) return;
|
||||
LayoutLayer layerAbove = getLayer(layerNr - 1);
|
||||
|
||||
for (LayoutNode fromNode : layerAbove) {
|
||||
int fromX = fromNode.getX();
|
||||
Map<Integer, List<LayoutEdge>> successorsByX = fromNode.groupSuccessorsByX();
|
||||
fromNode.clearSuccessors();
|
||||
|
||||
for (Map.Entry<Integer, List<LayoutEdge>> entry : successorsByX.entrySet()) {
|
||||
Integer relativeFromX = entry.getKey();
|
||||
List<LayoutEdge> edges = entry.getValue();
|
||||
LayoutNode dummyNode = new LayoutNode();
|
||||
dummyNode.setX(fromX + relativeFromX);
|
||||
dummyNode.setLayer(layerNr);
|
||||
for (LayoutEdge edge : edges) {
|
||||
dummyNode.addSuccessor(edge);
|
||||
}
|
||||
LayoutEdge dummyEdge = new LayoutEdge(fromNode, dummyNode, relativeFromX, 0, edges.get(0).getLink());
|
||||
if (edges.get(0).isReversed()) dummyEdge.reverse();
|
||||
|
||||
fromNode.addSuccessor(dummyEdge);
|
||||
dummyNode.addPredecessor(dummyEdge);
|
||||
for (LayoutEdge edge : edges) {
|
||||
edge.setFrom(dummyNode);
|
||||
}
|
||||
addDummyToLayer(dummyNode, layerNr);
|
||||
}
|
||||
}
|
||||
|
||||
newLayer.sortNodesByX();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an unmodifiable list of all layers in the graph.
|
||||
*
|
||||
|
@ -193,6 +304,31 @@ public class LayoutGraph {
|
|||
return layers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the LayoutNode associated with the specified Vertex.
|
||||
*
|
||||
* @param vertex The vertex whose LayoutNode is to be retrieved.
|
||||
* @return The LayoutNode corresponding to the given vertex, or null if not found.
|
||||
*/
|
||||
public LayoutNode getLayoutNode(Vertex vertex) {
|
||||
return layoutNodes.get(vertex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a LayoutNode to the specified layer and registers it in the graph.
|
||||
*
|
||||
* @param node The LayoutNode to add to the layer.
|
||||
* @param layerNumber The index of the layer to which the node will be added.
|
||||
*/
|
||||
public void addNodeToLayer(LayoutNode node, int layerNumber) {
|
||||
assert !node.isDummy();
|
||||
node.setLayer(layerNumber);
|
||||
getLayer(layerNumber).add(node);
|
||||
if (!layoutNodes.containsKey(node.getVertex())) {
|
||||
layoutNodes.put(node.getVertex(), node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a LayoutNode to the specified layer and registers it in the graph.
|
||||
*
|
||||
|
@ -283,6 +419,148 @@ public class LayoutGraph {
|
|||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all incoming links to the specified vertex.
|
||||
*
|
||||
* @param vertex The vertex whose incoming links are to be retrieved.
|
||||
* @return A set of links that are incoming to the vertex.
|
||||
*/
|
||||
public List<Link> getInputLinks(Vertex vertex) {
|
||||
List<Link> inputLinks = new ArrayList<>();
|
||||
for (Port inputPort : inputPorts.getOrDefault(vertex, Collections.emptySet())) {
|
||||
inputLinks.addAll(portLinks.getOrDefault(inputPort, Collections.emptySet()));
|
||||
}
|
||||
return inputLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all outgoing links from the specified vertex.
|
||||
*
|
||||
* @param vertex The vertex whose outgoing links are to be retrieved.
|
||||
* @return A set of links that are outgoing from the vertex.
|
||||
*/
|
||||
public List<Link> getOutputLinks(Vertex vertex) {
|
||||
List<Link> outputLinks = new ArrayList<>();
|
||||
for (Port outputPort : outputPorts.getOrDefault(vertex, Collections.emptySet())) {
|
||||
outputLinks.addAll(portLinks.getOrDefault(outputPort, Collections.emptySet()));
|
||||
}
|
||||
return outputLinks;
|
||||
}
|
||||
|
||||
public List<Link> getAllLinks(Vertex vertex) {
|
||||
List<Link> allLinks = new ArrayList<>();
|
||||
|
||||
for (Port inputPort : inputPorts.getOrDefault(vertex, Collections.emptySet())) {
|
||||
allLinks.addAll(portLinks.getOrDefault(inputPort, Collections.emptySet()));
|
||||
}
|
||||
|
||||
for (Port outputPort : outputPorts.getOrDefault(vertex, Collections.emptySet())) {
|
||||
allLinks.addAll(portLinks.getOrDefault(outputPort, Collections.emptySet()));
|
||||
}
|
||||
|
||||
return allLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified LayoutNode and all its connected edges from the graph.
|
||||
*
|
||||
* @param node The LayoutNode to remove along with its edges.
|
||||
*/
|
||||
public void removeNodeAndEdges(LayoutNode node) {
|
||||
assert !node.isDummy();
|
||||
removeEdges(node); // a node can only be removed together with its edges
|
||||
int layer = node.getLayer();
|
||||
layers.get(layer).remove(node);
|
||||
layers.get(layer).updateNodeIndices();
|
||||
layoutNodes.remove(node.getVertex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all edges connected to the specified LayoutNode.
|
||||
* Handles the removal of associated dummy nodes if they are no longer needed.
|
||||
* Updates the graph structure accordingly after node movement.
|
||||
*
|
||||
* @param node The LayoutNode whose connected edges are to be removed.
|
||||
*/
|
||||
public void removeEdges(LayoutNode node) {
|
||||
assert !node.isDummy();
|
||||
for (Link link : getAllLinks(node.getVertex())) {
|
||||
removeEdge(link);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeEdge(Link link) {
|
||||
Vertex from = link.getFrom().getVertex();
|
||||
Vertex to = link.getTo().getVertex();
|
||||
LayoutNode toNode = getLayoutNode(to);
|
||||
LayoutNode fromNode = getLayoutNode(from);
|
||||
|
||||
if (toNode.getLayer() < fromNode.getLayer()) {
|
||||
// Reversed edge
|
||||
toNode = fromNode;
|
||||
}
|
||||
|
||||
// Remove preds-edges bottom up, starting at "to" node
|
||||
// Cannot start from "from" node since there might be joint edges
|
||||
List<LayoutEdge> toNodePredsEdges = List.copyOf(toNode.getPredecessors());
|
||||
for (LayoutEdge edge : toNodePredsEdges) {
|
||||
LayoutNode predNode = edge.getFrom();
|
||||
LayoutEdge edgeToRemove;
|
||||
|
||||
if (edge.getLink() != null && edge.getLink().equals(link)) {
|
||||
toNode.removePredecessor(edge);
|
||||
edgeToRemove = edge;
|
||||
} else {
|
||||
// Wrong edge, look at next
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!predNode.isDummy() && predNode.getVertex().equals(from)) {
|
||||
// No dummy nodes inbetween 'from' and 'to' vertex
|
||||
predNode.removeSuccessor(edgeToRemove);
|
||||
break;
|
||||
} else {
|
||||
// Must remove edges between dummy nodes
|
||||
boolean found = true;
|
||||
LayoutNode succNode = toNode;
|
||||
while (predNode.isDummy() && found) {
|
||||
found = false;
|
||||
|
||||
if (predNode.getSuccessors().size() <= 1 && predNode.getPredecessors().size() <= 1) {
|
||||
// Dummy node used only for this link, remove if not already removed
|
||||
assert predNode.isDummy();
|
||||
int layer = predNode.getLayer();
|
||||
layers.get(layer).remove(predNode);
|
||||
layers.get(layer).updateNodeIndices();
|
||||
dummyNodes.remove(predNode);
|
||||
} else {
|
||||
// anchor node, should not be removed
|
||||
break;
|
||||
}
|
||||
|
||||
if (predNode.getPredecessors().size() == 1) {
|
||||
predNode.removeSuccessor(edgeToRemove);
|
||||
succNode = predNode;
|
||||
edgeToRemove = predNode.getPredecessors().get(0);
|
||||
predNode = edgeToRemove.getFrom();
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
predNode.removeSuccessor(edgeToRemove);
|
||||
succNode.removePredecessor(edgeToRemove);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (fromNode.getReversedLinkStartPoints().containsKey(link)) {
|
||||
fromNode.computeReversedLinkPoints(false);
|
||||
}
|
||||
if (toNode.getReversedLinkStartPoints().containsKey(link)) {
|
||||
toNode.computeReversedLinkPoints(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the LayoutLayer at the specified index.
|
||||
*
|
||||
|
@ -293,6 +571,30 @@ public class LayoutGraph {
|
|||
return layers.get(layerNr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the layer closest to the given y-coordinate.
|
||||
*
|
||||
* @param y the y-coordinate to check
|
||||
* @return the index of the optimal layer, or -1 if no layers are found
|
||||
*/
|
||||
public int findLayer(int y) {
|
||||
int optimalLayer = -1;
|
||||
int minDistance = Integer.MAX_VALUE;
|
||||
for (int l = 0; l < getLayerCount(); l++) {
|
||||
// Check if y is within this layer's bounds
|
||||
if (y >= getLayer(l).getTop() && y <= getLayer(l).getBottom()) {
|
||||
return l;
|
||||
}
|
||||
|
||||
int distance = Math.abs(getLayer(l).getCenter() - y);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
optimalLayer = l;
|
||||
}
|
||||
}
|
||||
return optimalLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the layers vertically, calculating their heights and setting their positions.
|
||||
* Centers the nodes within each layer vertically.
|
||||
|
@ -314,6 +616,380 @@ public class LayoutGraph {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimizes routing of reversed (back) edges to reduce crossings.
|
||||
*/
|
||||
public void optimizeBackEdgeCrossings() {
|
||||
for (LayoutNode node : getLayoutNodes()) {
|
||||
node.optimizeBackEdgeCrossing();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes empty layers from the graph.
|
||||
* Iteratively checks for and removes layers that contain only dummy nodes.
|
||||
*/
|
||||
public void removeEmptyLayers() {
|
||||
int i = 0;
|
||||
while (i < getLayerCount()) {
|
||||
LayoutLayer layer = getLayer(i);
|
||||
if (layer.containsOnlyDummyNodes()) {
|
||||
removeEmptyLayer(i);
|
||||
} else {
|
||||
i++; // Move to the next layer only if no removal occurred
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the layer at the specified index if it is empty or contains only dummy nodes.
|
||||
* Adjusts the positions of nodes and edges accordingly.
|
||||
*
|
||||
* @param layerNr The index of the layer to remove.
|
||||
*/
|
||||
private void removeEmptyLayer(int layerNr) {
|
||||
LayoutLayer layer = getLayer(layerNr);
|
||||
if (!layer.containsOnlyDummyNodes()) return;
|
||||
|
||||
for (LayoutNode dummyNode : layer) {
|
||||
if (dummyNode.getSuccessors().isEmpty()) {
|
||||
dummyNode.setLayer(layerNr + 1);
|
||||
getLayer(layerNr + 1).add(dummyNode);
|
||||
dummyNode.setX(dummyNode.calculateOptimalXFromPredecessors(true));
|
||||
getLayer(layerNr + 1).sortNodesByX();
|
||||
continue;
|
||||
} else if (dummyNode.getPredecessors().isEmpty()) {
|
||||
dummyNode.setLayer(layerNr - 1);
|
||||
dummyNode.setX(dummyNode.calculateOptimalXFromSuccessors(true));
|
||||
getLayer(layerNr - 1).add(dummyNode);
|
||||
getLayer(layerNr - 1).sortNodesByX();
|
||||
continue;
|
||||
}
|
||||
LayoutEdge layoutEdge = dummyNode.getPredecessors().get(0);
|
||||
|
||||
// remove the layoutEdge
|
||||
LayoutNode fromNode = layoutEdge.getFrom();
|
||||
fromNode.removeSuccessor(layoutEdge);
|
||||
|
||||
List<LayoutEdge> successorEdges = dummyNode.getSuccessors();
|
||||
for (LayoutEdge successorEdge : successorEdges) {
|
||||
successorEdge.setRelativeFromX(layoutEdge.getRelativeFromX());
|
||||
successorEdge.setFrom(fromNode);
|
||||
fromNode.addSuccessor(successorEdge);
|
||||
}
|
||||
dummyNode.clearPredecessors();
|
||||
dummyNode.clearSuccessors();
|
||||
dummyNodes.remove(dummyNode);
|
||||
}
|
||||
|
||||
deleteLayer(layerNr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Repositions the specified LayoutNode horizontally within its layer to the new x-coordinate.
|
||||
* Ensures no overlap with adjacent nodes and maintains minimum spacing.
|
||||
*
|
||||
* @param layoutNode The LayoutNode to reposition.
|
||||
* @param newX The new x-coordinate to set for the node.
|
||||
*/
|
||||
private void repositionLayoutNodeX(LayoutNode layoutNode, int newX) {
|
||||
int currentX = layoutNode.getX();
|
||||
|
||||
// Early exit if the desired position is the same as the current position
|
||||
if (newX == currentX) {
|
||||
return;
|
||||
}
|
||||
|
||||
LayoutLayer layer = getLayer(layoutNode.getLayer());
|
||||
if (newX > currentX) {
|
||||
layer.tryShiftNodeRight(layoutNode, newX);
|
||||
} else {
|
||||
layer.tryShiftNodeLeft(layoutNode, newX);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns the x-coordinate of a single dummy successor node for the given LayoutNode.
|
||||
* If the node has exactly one successor and that successor is a dummy node,
|
||||
* sets the dummy node's x-coordinate to align with the current node or the edge's starting point.
|
||||
*
|
||||
* @param node The LayoutNode whose dummy successor is to be aligned.
|
||||
*/
|
||||
private void alignSingleSuccessorDummyNodeX(LayoutNode node) {
|
||||
// Retrieve the list of successor edges
|
||||
List<LayoutEdge> successors = node.getSuccessors();
|
||||
|
||||
// Proceed only if there is exactly one successor
|
||||
if (successors.size() != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
LayoutEdge successorEdge = successors.get(0);
|
||||
LayoutNode successorNode = successorEdge.getTo();
|
||||
|
||||
// Proceed only if the successor node is a dummy node
|
||||
if (!successorNode.isDummy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the target x-coordinate based on whether the current node is a dummy
|
||||
int targetX = node.isDummy() ? node.getX() : successorEdge.getStartX();
|
||||
|
||||
// Align the successor dummy node to the target x-coordinate
|
||||
repositionLayoutNodeX(successorNode, targetX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns the x-coordinates of dummy successor nodes within the specified layer.
|
||||
* Performs alignment in both forward and backward directions to ensure consistency.
|
||||
*
|
||||
* @param layer The LayoutLayer whose nodes' dummy successors need alignment.
|
||||
*/
|
||||
private void alignLayerDummySuccessors(LayoutLayer layer) {
|
||||
// Forward pass: Align dummy successors from the first node to the last.
|
||||
for (LayoutNode node : layer) {
|
||||
alignSingleSuccessorDummyNodeX(node);
|
||||
}
|
||||
|
||||
// Backward pass: Align dummy successors from the last node to the first.
|
||||
for (int i = layer.size() - 1; i >= 0; i--) {
|
||||
LayoutNode node = layer.get(i);
|
||||
alignSingleSuccessorDummyNodeX(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Straightens edges in the graph by aligning dummy nodes to reduce bends.
|
||||
* Processes all layers to align dummy successor nodes.
|
||||
*/
|
||||
public void straightenEdges() {
|
||||
// Forward pass: Align dummy successors from the first layer to the last.
|
||||
for (int i = 0; i < getLayerCount(); i++) {
|
||||
alignLayerDummySuccessors(getLayer(i));
|
||||
}
|
||||
|
||||
// Backward pass: Align dummy successors from the last layer to the first.
|
||||
for (int i = getLayerCount() - 1; i >= 0; i--) {
|
||||
alignLayerDummySuccessors(getLayer(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the minimum X spacing for all layers in the graph.
|
||||
*/
|
||||
public void updateLayerMinXSpacing() {
|
||||
for (LayoutLayer layer : this.getLayers()) {
|
||||
layer.updateMinXSpacing(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the optimal horizontal position (index) for the specified node within the given layer,
|
||||
* aiming to minimize the number of edge crossings.
|
||||
*
|
||||
* @param node The node to position.
|
||||
* @param layerNr The index of the layer in which to position the node.
|
||||
* @return The optimal position index within the layer for the node.
|
||||
*/
|
||||
private int optimalPosition(LayoutNode node, int layerNr) {
|
||||
getLayer(layerNr).sort(NODE_POS_COMPARATOR);
|
||||
int edgeCrossings = Integer.MAX_VALUE;
|
||||
int optimalPos = -1;
|
||||
|
||||
// Try each possible position in the layerNr
|
||||
for (int i = 0; i < getLayer(layerNr).size() + 1; i++) {
|
||||
int xCoord;
|
||||
if (i == 0) {
|
||||
xCoord = getLayer(layerNr).get(i).getX() - node.getWidth() - 1;
|
||||
} else {
|
||||
xCoord = getLayer(layerNr).get(i - 1).getX() + getLayer(layerNr).get(i - 1).getWidth() + 1;
|
||||
}
|
||||
|
||||
int currentCrossings = 0;
|
||||
|
||||
if (0 <= layerNr - 1) {
|
||||
// For each link with an end point in vertex, check how many edges cross it
|
||||
for (LayoutEdge edge : node.getPredecessors()) {
|
||||
if (edge.getFrom().getLayer() == layerNr - 1) {
|
||||
int fromNodeXCoord = edge.getFromX();
|
||||
int toNodeXCoord = xCoord;
|
||||
if (!node.isDummy()) {
|
||||
toNodeXCoord += edge.getRelativeToX();
|
||||
}
|
||||
for (LayoutNode n : getLayer(layerNr - 1)) {
|
||||
for (LayoutEdge e : n.getSuccessors()) {
|
||||
if (e.getTo() == null) {
|
||||
continue;
|
||||
}
|
||||
int compFromXCoord = e.getFromX();
|
||||
int compToXCoord = e.getToX();
|
||||
if ((fromNodeXCoord > compFromXCoord && toNodeXCoord < compToXCoord)
|
||||
|| (fromNodeXCoord < compFromXCoord
|
||||
&& toNodeXCoord > compToXCoord)) {
|
||||
currentCrossings += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Edge crossings across current layerNr and layerNr below
|
||||
if (layerNr + 1 < getLayerCount()) {
|
||||
// For each link with an end point in vertex, check how many edges cross it
|
||||
for (LayoutEdge edge : node.getSuccessors()) {
|
||||
if (edge.getTo().getLayer() == layerNr + 1) {
|
||||
int toNodeXCoord = edge.getToX();
|
||||
int fromNodeXCoord = xCoord;
|
||||
if (!node.isDummy()) {
|
||||
fromNodeXCoord += edge.getRelativeFromX();
|
||||
}
|
||||
for (LayoutNode n : getLayer(layerNr + 1)) {
|
||||
for (LayoutEdge e : n.getPredecessors()) {
|
||||
if (e.getFrom() == null) {
|
||||
continue;
|
||||
}
|
||||
int compFromXCoord = e.getFromX();
|
||||
int compToXCoord = e.getToX();
|
||||
if ((fromNodeXCoord > compFromXCoord && toNodeXCoord < compToXCoord)
|
||||
|| (fromNodeXCoord < compFromXCoord
|
||||
&& toNodeXCoord > compToXCoord)) {
|
||||
currentCrossings += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentCrossings <= edgeCrossings) {
|
||||
edgeCrossings = currentCrossings;
|
||||
optimalPos = i;
|
||||
}
|
||||
}
|
||||
return optimalPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates layout edges for the specified node and reverses edges as needed.
|
||||
* Reverses edges that go from lower to higher layers to maintain proper layering.
|
||||
*
|
||||
* @param node The LayoutNode for which to create and reverse edges.
|
||||
*/
|
||||
public void createAndReverseLayoutEdges(LayoutNode node) {
|
||||
List<Link> nodeLinks = new ArrayList<>(getInputLinks(node.getVertex()));
|
||||
nodeLinks.addAll(getOutputLinks(node.getVertex()));
|
||||
nodeLinks.sort(LINK_COMPARATOR);
|
||||
|
||||
List<LayoutNode> reversedLayoutNodes = new ArrayList<>();
|
||||
for (Link link : nodeLinks) {
|
||||
if (link.getFrom().getVertex() == link.getTo().getVertex()) continue;
|
||||
LayoutEdge layoutEdge = createLayoutEdge(link);
|
||||
|
||||
LayoutNode fromNode = layoutEdge.getFrom();
|
||||
LayoutNode toNode = layoutEdge.getTo();
|
||||
|
||||
if (fromNode.getLayer() > toNode.getLayer()) {
|
||||
HierarchicalLayoutManager.ReverseEdges.reverseEdge(layoutEdge);
|
||||
reversedLayoutNodes.add(fromNode);
|
||||
reversedLayoutNodes.add(toNode);
|
||||
}
|
||||
}
|
||||
|
||||
// ReverseEdges
|
||||
for (LayoutNode layoutNode : reversedLayoutNodes) {
|
||||
layoutNode.computeReversedLinkPoints(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts dummy nodes along the edges from predecessors of the specified node,
|
||||
* for edges that span more than one layer.
|
||||
*
|
||||
* @param layoutNode The node for which to create predecessor dummy nodes.
|
||||
*/
|
||||
public void createDummiesForNodePredecessor(LayoutNode layoutNode) {
|
||||
for (LayoutEdge predEdge : layoutNode.getPredecessors()) {
|
||||
LayoutNode fromNode = predEdge.getFrom();
|
||||
LayoutNode toNode = predEdge.getTo();
|
||||
if (Math.abs(toNode.getLayer() - fromNode.getLayer()) <= 1) continue;
|
||||
|
||||
boolean hasEdgeFromSamePort = false;
|
||||
LayoutEdge edgeFromSamePort = new LayoutEdge(fromNode, toNode, predEdge.getLink());
|
||||
if (predEdge.isReversed()) edgeFromSamePort.reverse();
|
||||
|
||||
for (LayoutEdge succEdge : fromNode.getSuccessors()) {
|
||||
if (succEdge.getRelativeFromX() == predEdge.getRelativeFromX() && succEdge.getTo().isDummy()) {
|
||||
edgeFromSamePort = succEdge;
|
||||
hasEdgeFromSamePort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEdgeFromSamePort) {
|
||||
LayoutEdge curEdge = edgeFromSamePort;
|
||||
boolean newEdge = true;
|
||||
while (curEdge.getTo().getLayer() < toNode.getLayer() - 1 && curEdge.getTo().isDummy() && newEdge) {
|
||||
// Traverse down the chain of dummy nodes linking together the edges originating
|
||||
// from the same port
|
||||
newEdge = false;
|
||||
if (curEdge.getTo().getSuccessors().size() == 1) {
|
||||
curEdge = curEdge.getTo().getSuccessors().get(0);
|
||||
newEdge = true;
|
||||
} else {
|
||||
for (LayoutEdge e : curEdge.getTo().getSuccessors()) {
|
||||
if (e.getTo().isDummy()) {
|
||||
curEdge = e;
|
||||
newEdge = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutNode prevDummy;
|
||||
if (!curEdge.getTo().isDummy()) {
|
||||
prevDummy = curEdge.getFrom();
|
||||
} else {
|
||||
prevDummy = curEdge.getTo();
|
||||
}
|
||||
|
||||
predEdge.setFrom(prevDummy);
|
||||
predEdge.setRelativeFromX(prevDummy.getWidth() / 2);
|
||||
fromNode.removeSuccessor(predEdge);
|
||||
prevDummy.addSuccessor(predEdge);
|
||||
}
|
||||
|
||||
LayoutNode layoutNode1 = predEdge.getTo();
|
||||
if (predEdge.getTo().getLayer() - 1 > predEdge.getFrom().getLayer()) {
|
||||
LayoutEdge prevEdge = predEdge;
|
||||
for (int l = layoutNode1.getLayer() - 1; l > prevEdge.getFrom().getLayer(); l--) {
|
||||
LayoutNode dummyNode = new LayoutNode();
|
||||
dummyNode.addSuccessor(prevEdge);
|
||||
LayoutEdge result = new LayoutEdge(prevEdge.getFrom(), dummyNode, prevEdge.getRelativeFromX(), 0, prevEdge.getLink());
|
||||
if (prevEdge.isReversed()) result.reverse();
|
||||
dummyNode.addPredecessor(result);
|
||||
prevEdge.setRelativeFromX(0);
|
||||
prevEdge.getFrom().removeSuccessor(prevEdge);
|
||||
prevEdge.getFrom().addSuccessor(result);
|
||||
prevEdge.setFrom(dummyNode);
|
||||
dummyNode.setLayer(l);
|
||||
List<LayoutNode> layerNodes = getLayer(l);
|
||||
if (layerNodes.isEmpty()) {
|
||||
dummyNode.setPos(0);
|
||||
} else {
|
||||
dummyNode.setPos(optimalPosition(dummyNode, l));
|
||||
}
|
||||
for (LayoutNode n : layerNodes) {
|
||||
if (n.getPos() >= dummyNode.getPos()) {
|
||||
n.setPos(n.getPos() + 1);
|
||||
}
|
||||
}
|
||||
addDummyToLayer(dummyNode, l);
|
||||
prevEdge = dummyNode.getPredecessors().get(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts dummy nodes along the edges to successors of the specified node,
|
||||
* for edges that span more than one layer.
|
||||
|
@ -430,4 +1106,20 @@ public class LayoutGraph {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds edges connected to the specified node, including any necessary dummy nodes.
|
||||
* Handles edge reversal, dummy node insertion for both predecessors and successors,
|
||||
* and updates node positions accordingly.
|
||||
*
|
||||
* @param node The LayoutNode to which edges will be added.
|
||||
* @param maxLayerLength The maximum number of layers an edge can span without splitting it
|
||||
*/
|
||||
public void addEdges(LayoutNode node, int maxLayerLength) {
|
||||
assert !node.isDummy();
|
||||
createAndReverseLayoutEdges(node);
|
||||
createDummiesForNodeSuccessor(node, maxLayerLength);
|
||||
createDummiesForNodePredecessor(node);
|
||||
updatePositions();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
|
@ -154,6 +154,10 @@ public class LayoutLayer extends ArrayList<LayoutNode> {
|
|||
y = top;
|
||||
}
|
||||
|
||||
public int getCenter() {
|
||||
return y + height / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bottom Y-coordinate of this layer.
|
||||
*
|
||||
|
@ -181,6 +185,53 @@ public class LayoutLayer extends ArrayList<LayoutNode> {
|
|||
this.height = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this layer contains only dummy nodes.
|
||||
*
|
||||
* @return true if all nodes in the layer are dummy nodes; false otherwise.
|
||||
*/
|
||||
public boolean containsOnlyDummyNodes() {
|
||||
for (LayoutNode node : this) {
|
||||
if (!node.isDummy()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the nodes in this layer by their X-coordinate in increasing order.
|
||||
* Assigns position indices to nodes based on the sorted order.
|
||||
* Adjusts the X-coordinates of nodes to ensure minimum spacing between them.
|
||||
*/
|
||||
public void sortNodesByX() {
|
||||
if (isEmpty()) return;
|
||||
|
||||
sort(NODE_X_COMPARATOR); // Sort nodes in the layer increasingly by x
|
||||
|
||||
updateNodeIndices();
|
||||
updateMinXSpacing(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures nodes have minimum horizontal spacing by adjusting their X positions.
|
||||
*
|
||||
* @param startFromZero if true, starts positioning from X = 0; otherwise, uses the first node's current X.
|
||||
*/
|
||||
public void updateMinXSpacing(boolean startFromZero) {
|
||||
if (isEmpty()) {
|
||||
return; // No nodes to adjust.
|
||||
}
|
||||
|
||||
int minX = startFromZero ? 0 : this.get(0).getX();
|
||||
|
||||
for (LayoutNode node : this) {
|
||||
int x = Math.max(node.getX(), minX);
|
||||
node.setX(x);
|
||||
minX = x + node.getOuterWidth() + NODE_OFFSET;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes nodes' X positions with spacing.
|
||||
*/
|
||||
|
@ -203,4 +254,60 @@ public class LayoutLayer extends ArrayList<LayoutNode> {
|
|||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to move the specified node to the right within the layer to the given X-coordinate.
|
||||
* Ensures that the node does not overlap with its right neighbor by checking required spacing.
|
||||
* If movement is possible without causing overlap, the node's X-coordinate is updated.
|
||||
*
|
||||
* @param layoutNode The node to move.
|
||||
* @param newX The desired new X-coordinate for the node.
|
||||
*/
|
||||
public void tryShiftNodeRight(LayoutNode layoutNode, int newX) {
|
||||
int currentX = layoutNode.getX();
|
||||
int shiftAmount = newX - currentX;
|
||||
int rightPos = layoutNode.getPos() + 1;
|
||||
|
||||
if (rightPos < size()) {
|
||||
// There is a right neighbor
|
||||
LayoutNode rightNeighbor = get(rightPos);
|
||||
int proposedRightEdge = layoutNode.getRight() + shiftAmount;
|
||||
int requiredLeftEdge = rightNeighbor.getOuterLeft() - NODE_OFFSET;
|
||||
|
||||
if (proposedRightEdge <= requiredLeftEdge) {
|
||||
layoutNode.setX(newX);
|
||||
}
|
||||
} else {
|
||||
// No right neighbor; safe to move freely to the right
|
||||
layoutNode.setX(newX);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to move the specified node to the left within the layer to the given X-coordinate.
|
||||
* Ensures that the node does not overlap with its left neighbor by checking required spacing.
|
||||
* If movement is possible without causing overlap, the node's X-coordinate is updated.
|
||||
*
|
||||
* @param layoutNode The node to move.
|
||||
* @param newX The desired new X-coordinate for the node.
|
||||
*/
|
||||
public void tryShiftNodeLeft(LayoutNode layoutNode, int newX) {
|
||||
int currentX = layoutNode.getX();
|
||||
int shiftAmount = currentX - newX;
|
||||
int leftPos = layoutNode.getPos() - 1;
|
||||
|
||||
if (leftPos >= 0) {
|
||||
// There is a left neighbor
|
||||
LayoutNode leftNeighbor = get(leftPos);
|
||||
int proposedLeftEdge = layoutNode.getLeft() - shiftAmount;
|
||||
int requiredRightEdge = leftNeighbor.getOuterRight() + NODE_OFFSET;
|
||||
|
||||
if (requiredRightEdge <= proposedLeftEdge) {
|
||||
layoutNode.setX(newX);
|
||||
}
|
||||
} else {
|
||||
// No left neighbor; safe to move freely to the left
|
||||
layoutNode.setX(newX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code 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
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*
|
||||
*/
|
||||
package com.sun.hotspot.igv.hierarchicallayout;
|
||||
|
||||
import com.sun.hotspot.igv.layout.Vertex;
|
||||
import java.awt.Point;
|
||||
import java.util.Set;
|
||||
|
||||
public interface LayoutMover {
|
||||
/**
|
||||
* Moves a link by shifting its position along the X-axis.
|
||||
*
|
||||
* @param linkPos The current position of the link.
|
||||
* @param shiftX The amount to shift the link along the X-axis.
|
||||
*/
|
||||
void moveLink(Point linkPos, int shiftX);
|
||||
|
||||
/**
|
||||
* Moves a set of vertices.
|
||||
*
|
||||
* @param movedVertices A set of vertices to be moved.
|
||||
*/
|
||||
void moveVertices(Set<? extends Vertex> movedVertices);
|
||||
|
||||
/**
|
||||
* Moves a single vertex.
|
||||
*
|
||||
* @param movedVertex The vertex to be moved.
|
||||
*/
|
||||
void moveVertex(Vertex movedVertex);
|
||||
}
|
||||
|
|
@ -69,6 +69,7 @@ public class LayoutNode {
|
|||
private int rightMargin;
|
||||
private int leftMargin;
|
||||
private int pos = -1; // Position within its layer
|
||||
private boolean reverseLeft = false;
|
||||
private int crossingNumber = 0;
|
||||
|
||||
public boolean hasSelfEdge() {
|
||||
|
@ -250,6 +251,15 @@ public class LayoutNode {
|
|||
return x + leftMargin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the outer left boundary (including left margin) of the node.
|
||||
*
|
||||
* @return The x-coordinate of the outer left boundary.
|
||||
*/
|
||||
public int getOuterLeft() {
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total width of the node, including left and right margins.
|
||||
*
|
||||
|
@ -272,6 +282,15 @@ public class LayoutNode {
|
|||
return height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the right boundary (excluding right margin) of the node.
|
||||
*
|
||||
* @return The x-coordinate of the right boundary.
|
||||
*/
|
||||
public int getRight() {
|
||||
return x + leftMargin + width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the outer right boundary (including right margin) of the node.
|
||||
*
|
||||
|
@ -436,6 +455,50 @@ public class LayoutNode {
|
|||
this.pos = pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the successor edges by their relative x-coordinate from the current node.
|
||||
*
|
||||
* @return A map of relative x-coordinate to list of successor edges.
|
||||
*/
|
||||
public Map<Integer, List<LayoutEdge>> groupSuccessorsByX() {
|
||||
Map<Integer, List<LayoutEdge>> result = new HashMap<>();
|
||||
for (LayoutEdge succEdge : succs) {
|
||||
result.computeIfAbsent(succEdge.getRelativeFromX(), k -> new ArrayList<>()).add(succEdge);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int getBackedgeCrossingScore() {
|
||||
int score = 0;
|
||||
for (LayoutEdge predEdge : preds) {
|
||||
if (predEdge.isReversed()) {
|
||||
List<Point> points = reversedLinkEndPoints.get(predEdge.getLink());
|
||||
if (points != null) {
|
||||
int x0 = points.get(points.size() - 1).x;
|
||||
int xn = points.get(0).x;
|
||||
int startPoint = predEdge.getStartX();
|
||||
int endPoint = predEdge.getEndX();
|
||||
int win = (x0 < xn) ? (startPoint - endPoint) : (endPoint - startPoint);
|
||||
score += win;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (LayoutEdge succEdge : succs) {
|
||||
if (succEdge.isReversed()) {
|
||||
List<Point> points = reversedLinkStartPoints.get(succEdge.getLink());
|
||||
if (points != null) {
|
||||
int x0 = points.get(points.size() - 1).x;
|
||||
int xn = points.get(0).x;
|
||||
int startPoint = succEdge.getStartX();
|
||||
int endPoint = succEdge.getEndX();
|
||||
int win = (x0 > xn) ? (startPoint - endPoint) : (endPoint - startPoint);
|
||||
score += win;
|
||||
}
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
private boolean computeReversedStartPoints(boolean left) {
|
||||
TreeMap<Integer, ArrayList<LayoutEdge>> sortedDownMap = left ? new TreeMap<>() : new TreeMap<>(Collections.reverseOrder());
|
||||
for (LayoutEdge succEdge : succs) {
|
||||
|
@ -518,12 +581,28 @@ public class LayoutNode {
|
|||
}
|
||||
|
||||
public void computeReversedLinkPoints(boolean reverseLeft) {
|
||||
this.reverseLeft = reverseLeft;
|
||||
|
||||
initSize();
|
||||
reversedLinkStartPoints.clear();
|
||||
reversedLinkEndPoints.clear();
|
||||
|
||||
boolean hasReversedDown = computeReversedStartPoints(reverseLeft);
|
||||
computeReversedEndPoints(hasReversedDown != reverseLeft);
|
||||
boolean hasReversedUP = computeReversedEndPoints(hasReversedDown != reverseLeft);
|
||||
}
|
||||
|
||||
public boolean isReverseRight() {
|
||||
return !reverseLeft;
|
||||
}
|
||||
|
||||
public void optimizeBackEdgeCrossing() {
|
||||
if (reversedLinkStartPoints.isEmpty() && reversedLinkEndPoints.isEmpty()) return;
|
||||
int orig_score = getBackedgeCrossingScore();
|
||||
computeReversedLinkPoints(isReverseRight());
|
||||
int reverse_score = getBackedgeCrossingScore();
|
||||
if (orig_score > reverse_score) {
|
||||
computeReversedLinkPoints(isReverseRight());
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<Point> getSelfEdgePoints() {
|
||||
|
|
|
@ -81,13 +81,17 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
private final LayerWidget mainLayer;
|
||||
private final LayerWidget blockLayer;
|
||||
private final LayerWidget connectionLayer;
|
||||
private final Widget shadowWidget;
|
||||
private final Widget pointerWidget;
|
||||
private final DiagramViewModel model;
|
||||
private ModelState modelState;
|
||||
private boolean rebuilding;
|
||||
|
||||
private final Map<OutputSlot, Set<LineWidget>> outputSlotToLineWidget = new HashMap<>();
|
||||
private final Map<InputSlot, Set<LineWidget>> inputSlotToLineWidget = new HashMap<>();
|
||||
private final HierarchicalStableLayoutManager hierarchicalStableLayoutManager;
|
||||
private HierarchicalLayoutManager seaLayoutManager;
|
||||
|
||||
private LayoutMover layoutMover;
|
||||
|
||||
/**
|
||||
* The alpha level of partially visible figures.
|
||||
|
@ -341,6 +345,12 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
mainLayer.setBorder(emptyBorder);
|
||||
addChild(mainLayer);
|
||||
|
||||
pointerWidget = new Widget(DiagramScene.this);
|
||||
addChild(pointerWidget);
|
||||
|
||||
shadowWidget = new Widget(DiagramScene.this);
|
||||
addChild(shadowWidget);
|
||||
|
||||
setLayout(LayoutFactory.createAbsoluteLayout());
|
||||
getActions().addAction(mouseZoomAction);
|
||||
getActions().addAction(ActionFactory.createPopupMenuAction((widget, localLocation) -> createPopupMenu()));
|
||||
|
@ -596,6 +606,163 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
}
|
||||
}
|
||||
|
||||
private MoveProvider getFigureMoveProvider() {
|
||||
return new MoveProvider() {
|
||||
|
||||
private boolean hasMoved = false; // Flag to track movement
|
||||
private int startLayerY;
|
||||
|
||||
private void setFigureShadow(Figure f) {
|
||||
FigureWidget fw = getWidget(f);
|
||||
Color c = f.getColor();
|
||||
Border border = new FigureWidget.RoundedBorder(new Color(0,0,0, 50), 1);
|
||||
shadowWidget.setBorder(border);
|
||||
shadowWidget.setBackground(new Color(c.getRed(), c.getGreen(), c.getBlue(), 50));
|
||||
shadowWidget.setPreferredLocation(fw.getPreferredLocation());
|
||||
shadowWidget.setPreferredSize(f.getSize());
|
||||
shadowWidget.setVisible(true);
|
||||
shadowWidget.setOpaque(true);
|
||||
shadowWidget.revalidate();
|
||||
shadowWidget.repaint();
|
||||
}
|
||||
|
||||
private void setMovePointer(Figure f) {
|
||||
Border border = new FigureWidget.RoundedBorder(Color.RED, 1);
|
||||
pointerWidget.setBorder(border);
|
||||
pointerWidget.setBackground(Color.RED);
|
||||
pointerWidget.setPreferredBounds(new Rectangle(0, 0, 3, f.getSize().height));
|
||||
pointerWidget.setVisible(false);
|
||||
pointerWidget.setOpaque(true);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void movementStarted(Widget widget) {
|
||||
if (layoutMover == null) return; // Do nothing if layoutMover is not available
|
||||
|
||||
widget.bringToFront();
|
||||
startLayerY = widget.getLocation().y;
|
||||
hasMoved = false; // Reset the movement flag
|
||||
Set<Figure> selectedFigures = model.getSelectedFigures();
|
||||
if (selectedFigures.size() == 1) {
|
||||
Figure selectedFigure = selectedFigures.iterator().next();
|
||||
setFigureShadow(selectedFigure);
|
||||
setMovePointer(selectedFigure);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void movementFinished(Widget widget) {
|
||||
shadowWidget.setVisible(false);
|
||||
pointerWidget.setVisible(false);
|
||||
if (layoutMover == null || !hasMoved) return; // Do nothing if layoutMover is not available or no movement occurred
|
||||
rebuilding = true;
|
||||
|
||||
Set<Figure> movedFigures = new HashSet<>(model.getSelectedFigures());
|
||||
for (Figure figure : movedFigures) {
|
||||
FigureWidget fw = getWidget(figure);
|
||||
figure.setPosition(new Point(fw.getLocation().x, fw.getLocation().y));
|
||||
}
|
||||
|
||||
layoutMover.moveVertices(movedFigures);
|
||||
rebuildConnectionLayer();
|
||||
|
||||
for (FigureWidget fw : getVisibleFigureWidgets()) {
|
||||
fw.updatePosition();
|
||||
}
|
||||
|
||||
validateAll();
|
||||
addUndo();
|
||||
rebuilding = false;
|
||||
}
|
||||
|
||||
private static final int MAGNET_SIZE = 5;
|
||||
|
||||
private int magnetToStartLayerY(Widget widget, Point location) {
|
||||
int shiftY = location.y - widget.getLocation().y;
|
||||
if (Math.abs(location.y - startLayerY) <= MAGNET_SIZE) {
|
||||
if (Math.abs(widget.getLocation().y - startLayerY) > MAGNET_SIZE) {
|
||||
shiftY = startLayerY - widget.getLocation().y;
|
||||
} else {
|
||||
shiftY = 0;
|
||||
}
|
||||
}
|
||||
return shiftY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point getOriginalLocation(Widget widget) {
|
||||
if (layoutMover == null) return widget.getLocation(); // default behavior
|
||||
return ActionFactory.createDefaultMoveProvider().getOriginalLocation(widget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNewLocation(Widget widget, Point location) {
|
||||
if (layoutMover == null) return; // Do nothing if layoutMover is not available
|
||||
hasMoved = true; // Mark that a movement occurred
|
||||
|
||||
int shiftX = location.x - widget.getLocation().x;
|
||||
int shiftY = magnetToStartLayerY(widget, location);
|
||||
|
||||
List<Figure> selectedFigures = new ArrayList<>( model.getSelectedFigures());
|
||||
selectedFigures.sort(Comparator.comparingInt(f -> f.getPosition().x));
|
||||
for (Figure figure : selectedFigures) {
|
||||
FigureWidget fw = getWidget(figure);
|
||||
for (InputSlot inputSlot : figure.getInputSlots()) {
|
||||
assert inputSlot != null;
|
||||
if (inputSlotToLineWidget.containsKey(inputSlot)) {
|
||||
for (LineWidget lw : inputSlotToLineWidget.get(inputSlot)) {
|
||||
assert lw != null;
|
||||
Point toPt = lw.getTo();
|
||||
Point fromPt = lw.getFrom();
|
||||
if (toPt != null && fromPt != null) {
|
||||
int xTo = toPt.x + shiftX;
|
||||
int yTo = toPt.y + shiftY;
|
||||
lw.setTo(new Point(xTo, yTo));
|
||||
lw.setFrom(new Point(fromPt.x + shiftX, fromPt.y));
|
||||
LineWidget pred = lw.getPredecessor();
|
||||
pred.setTo(new Point(pred.getTo().x + shiftX, pred.getTo().y));
|
||||
pred.revalidate();
|
||||
lw.revalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (OutputSlot outputSlot : figure.getOutputSlots()) {
|
||||
assert outputSlot != null;
|
||||
if (outputSlotToLineWidget.containsKey(outputSlot)) {
|
||||
for (LineWidget lw : outputSlotToLineWidget.get(outputSlot)) {
|
||||
assert lw != null;
|
||||
Point fromPt = lw.getFrom();
|
||||
Point toPt = lw.getTo();
|
||||
if (toPt != null && fromPt != null) {
|
||||
int xFrom = fromPt.x + shiftX;
|
||||
int yFrom = fromPt.y + shiftY;
|
||||
lw.setFrom(new Point(xFrom, yFrom));
|
||||
lw.setTo(new Point(toPt.x + shiftX, toPt.y));
|
||||
for (LineWidget succ : lw.getSuccessors()) {
|
||||
succ.setFrom(new Point(succ.getFrom().x + shiftX, succ.getFrom().y));
|
||||
succ.revalidate();
|
||||
}
|
||||
lw.revalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Point newLocation = new Point(fw.getLocation().x + shiftX, fw.getLocation().y + shiftY);
|
||||
ActionFactory.createDefaultMoveProvider().setNewLocation(fw, newLocation);
|
||||
}
|
||||
|
||||
FigureWidget fw = getWidget(selectedFigures.iterator().next());
|
||||
pointerWidget.setVisible(true);
|
||||
Point newLocation = new Point(fw.getLocation().x + shiftX -3, fw.getLocation().y + shiftY);
|
||||
ActionFactory.createDefaultMoveProvider().setNewLocation(pointerWidget, newLocation);
|
||||
connectionLayer.revalidate();
|
||||
connectionLayer.repaint();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void rebuildMainLayer() {
|
||||
mainLayer.removeChildren();
|
||||
for (Figure figure : getModel().getDiagram().getFigures()) {
|
||||
|
@ -604,6 +771,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
figureWidget.getActions().addAction(ActionFactory.createPopupMenuAction(figureWidget));
|
||||
figureWidget.getActions().addAction(selectAction);
|
||||
figureWidget.getActions().addAction(hoverAction);
|
||||
figureWidget.getActions().addAction(ActionFactory.createMoveAction(null, getFigureMoveProvider()));
|
||||
addObject(figure, figureWidget);
|
||||
mainLayer.addChild(figureWidget);
|
||||
|
||||
|
@ -737,6 +905,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
}
|
||||
|
||||
private void doStableSeaLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
|
||||
layoutMover = null;
|
||||
boolean enable = model.getCutEdges();
|
||||
boolean previous = hierarchicalStableLayoutManager.getCutEdges();
|
||||
hierarchicalStableLayoutManager.setCutEdges(enable);
|
||||
|
@ -749,17 +918,20 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
|
||||
private void doSeaLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
|
||||
seaLayoutManager = new HierarchicalLayoutManager();
|
||||
layoutMover = seaLayoutManager;
|
||||
seaLayoutManager.setCutEdges(model.getCutEdges());
|
||||
seaLayoutManager.doLayout(new LayoutGraph(visibleConnections, visibleFigures));
|
||||
}
|
||||
|
||||
private void doClusteredLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
|
||||
layoutMover = null;
|
||||
HierarchicalClusterLayoutManager clusterLayoutManager = new HierarchicalClusterLayoutManager();
|
||||
clusterLayoutManager.setCutEdges(model.getCutEdges());
|
||||
clusterLayoutManager.doLayout(new LayoutGraph(visibleConnections, visibleFigures));
|
||||
}
|
||||
|
||||
private void doCFGLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
|
||||
layoutMover = null;
|
||||
HierarchicalCFGLayoutManager cfgLayoutManager = new HierarchicalCFGLayoutManager(getVisibleBlockConnections(), getVisibleBlocks());
|
||||
cfgLayoutManager.setCutEdges(model.getCutEdges());
|
||||
cfgLayoutManager.doLayout(new LayoutGraph(visibleConnections, visibleFigures));
|
||||
|
@ -777,6 +949,84 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
|
||||
private final Point specialNullPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
|
||||
private MoveProvider getFigureConnectionMoveProvider() {
|
||||
return new MoveProvider() {
|
||||
|
||||
Point startLocation;
|
||||
Point originalPosition;
|
||||
|
||||
@Override
|
||||
public void movementStarted(Widget widget) {
|
||||
if (layoutMover == null) return; // Do nothing if layoutMover is not available
|
||||
LineWidget lw = (LineWidget) widget;
|
||||
startLocation = lw.getClientAreaLocation();
|
||||
originalPosition = lw.getFrom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void movementFinished(Widget widget) {
|
||||
if (layoutMover == null) return; // Do nothing if layoutMover is not available
|
||||
LineWidget lineWidget = (LineWidget) widget;
|
||||
if (lineWidget.getPredecessor() == null) return;
|
||||
if (lineWidget.getSuccessors().isEmpty()) return;
|
||||
if (lineWidget.getFrom().x != lineWidget.getTo().x) return;
|
||||
|
||||
int shiftX = lineWidget.getClientAreaLocation().x - startLocation.x;
|
||||
if (shiftX == 0) return;
|
||||
|
||||
rebuilding = true;
|
||||
layoutMover.moveLink(originalPosition, shiftX);
|
||||
rebuildConnectionLayer();
|
||||
for (FigureWidget fw : getVisibleFigureWidgets()) {
|
||||
fw.updatePosition();
|
||||
}
|
||||
validateAll();
|
||||
addUndo();
|
||||
rebuilding = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point getOriginalLocation(Widget widget) {
|
||||
if (layoutMover == null) return widget.getLocation(); // default behavior
|
||||
LineWidget lineWidget = (LineWidget) widget;
|
||||
return lineWidget.getClientAreaLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNewLocation(Widget widget, Point location) {
|
||||
if (layoutMover == null) return; // Do nothing if layoutMover is not available
|
||||
LineWidget lineWidget = (LineWidget) widget;
|
||||
if (lineWidget.getPredecessor() == null) return;
|
||||
if (lineWidget.getSuccessors().isEmpty()) return;
|
||||
if (lineWidget.getFrom().x != lineWidget.getTo().x) return;
|
||||
|
||||
int shiftX = location.x - lineWidget.getClientAreaLocation().x;
|
||||
if (shiftX == 0) return;
|
||||
|
||||
Point oldFrom = lineWidget.getFrom();
|
||||
Point newFrom = new Point(oldFrom.x + shiftX, oldFrom.y);
|
||||
|
||||
Point oldTo = lineWidget.getTo();
|
||||
Point newTo = new Point(oldTo.x + shiftX, oldTo.y);
|
||||
|
||||
lineWidget.setTo(newTo);
|
||||
lineWidget.setFrom(newFrom);
|
||||
lineWidget.revalidate();
|
||||
|
||||
LineWidget predecessor = lineWidget.getPredecessor();
|
||||
Point toPt = predecessor.getTo();
|
||||
predecessor.setTo(new Point(toPt.x + shiftX, toPt.y));
|
||||
predecessor.revalidate();
|
||||
|
||||
for (LineWidget successor : lineWidget.getSuccessors()) {
|
||||
Point fromPt = successor.getFrom();
|
||||
successor.setFrom(new Point(fromPt.x + shiftX, fromPt.y));
|
||||
successor.revalidate();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void processOutputSlot(OutputSlot outputSlot, List<FigureConnection> connections, int controlPointIndex, Point lastPoint, LineWidget predecessor) {
|
||||
Map<Point, List<FigureConnection>> pointMap = new HashMap<>(connections.size());
|
||||
|
||||
|
@ -824,6 +1074,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
newPredecessor.setVisible(isVisible);
|
||||
|
||||
if (predecessor == null) {
|
||||
assert outputSlot != null;
|
||||
if (outputSlotToLineWidget.containsKey(outputSlot)) {
|
||||
outputSlotToLineWidget.get(outputSlot).add(newPredecessor);
|
||||
} else {
|
||||
|
@ -834,6 +1085,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
newWidgets.add(newPredecessor);
|
||||
addObject(new ConnectionSet(connectionList), newPredecessor);
|
||||
newPredecessor.getActions().addAction(hoverAction);
|
||||
newPredecessor.getActions().addAction(ActionFactory.createMoveAction(null, getFigureConnectionMoveProvider()));
|
||||
}
|
||||
|
||||
processOutputSlot(outputSlot, connectionList, controlPointIndex + 1, currentPoint, newPredecessor);
|
||||
|
@ -843,6 +1095,8 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
for (FigureConnection connection : connections) {
|
||||
if (isVisibleFigureConnection(connection)) {
|
||||
InputSlot inputSlot = connection.getInputSlot();
|
||||
if (predecessor != null) {
|
||||
assert inputSlot != null;
|
||||
if (inputSlotToLineWidget.containsKey(inputSlot)) {
|
||||
inputSlotToLineWidget.get(inputSlot).add(predecessor);
|
||||
} else {
|
||||
|
@ -852,6 +1106,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processBlockConnection(BlockConnection blockConnection) {
|
||||
boolean isDashed = blockConnection.getStyle() == Connection.ConnectionStyle.DASHED;
|
||||
|
@ -1212,9 +1467,6 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
|
|||
}
|
||||
}
|
||||
|
||||
Map<OutputSlot, Set<LineWidget>> outputSlotToLineWidget = new HashMap<>();
|
||||
Map<InputSlot, Set<LineWidget>> inputSlotToLineWidget = new HashMap<>();
|
||||
|
||||
public JPopupMenu createPopupMenu() {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
|
||||
|
|
|
@ -185,6 +185,10 @@ public class FigureWidget extends Widget implements Properties.Provider, PopupMe
|
|||
this.setToolTipText(PropertiesConverter.convertToHTML(f.getProperties()));
|
||||
}
|
||||
|
||||
public void updatePosition() {
|
||||
setPreferredLocation(figure.getPosition());
|
||||
}
|
||||
|
||||
public int getFigureHeight() {
|
||||
return middleWidget.getPreferredBounds().height;
|
||||
}
|
||||
|
|
|
@ -60,8 +60,8 @@ public class LineWidget extends Widget implements PopupMenuProvider {
|
|||
private final OutputSlot outputSlot;
|
||||
private final DiagramScene scene;
|
||||
private final List<? extends Connection> connections;
|
||||
private final Point from;
|
||||
private final Point to;
|
||||
private Point from;
|
||||
private Point to;
|
||||
private Rectangle clientArea;
|
||||
private final LineWidget predecessor;
|
||||
private final List<LineWidget> successors;
|
||||
|
@ -125,6 +125,10 @@ public class LineWidget extends Widget implements PopupMenuProvider {
|
|||
}));
|
||||
}
|
||||
|
||||
public Point getClientAreaLocation() {
|
||||
return clientArea.getLocation();
|
||||
}
|
||||
|
||||
private void computeClientArea() {
|
||||
int minX = from.x;
|
||||
int minY = from.y;
|
||||
|
@ -158,6 +162,16 @@ public class LineWidget extends Widget implements PopupMenuProvider {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
public void setFrom(Point from) {
|
||||
this.from = from;
|
||||
computeClientArea();
|
||||
}
|
||||
|
||||
public void setTo(Point to) {
|
||||
this.to= to;
|
||||
computeClientArea();
|
||||
}
|
||||
|
||||
public Point getFrom() {
|
||||
return from;
|
||||
}
|
||||
|
@ -166,6 +180,14 @@ public class LineWidget extends Widget implements PopupMenuProvider {
|
|||
return to;
|
||||
}
|
||||
|
||||
public LineWidget getPredecessor() {
|
||||
return predecessor;
|
||||
}
|
||||
|
||||
public List<LineWidget> getSuccessors() {
|
||||
return Collections.unmodifiableList(successors);
|
||||
}
|
||||
|
||||
private void addSuccessor(LineWidget widget) {
|
||||
this.successors.add(widget);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue