fix: comprehensive ElkDiagramRenderer cleanup and Y-offset fix
Based on thorough code review, fixes all identified issues: 1. **Y-offset root cause**: Added post-layout normalization that shifts all positioned nodes and edges so the bounding box starts at (0,0). ELK can place nodes at arbitrary positions within its root graph; normalizing compensates regardless of what ELK computes internally. 2. **Bounding box**: Compute from recursively flattened node tree + edge point bounds. Removes double-counting of compound children (children have absolute coords, not relative to parent). 3. **SVG double-drawing**: Compound children were drawn both inside drawCompoundContainer and again in the allNodes loop. Now collects compound child IDs and skips them in the second pass. 4. **findNode**: Now recurses into children for nested compound lookup. 5. **colorForType**: Removed redundant double-check on EIP_TYPES. 6. **Dead code removed**: routeNodeMap/indexNodeRecursive (populated but never read), MIN_NODE_WIDTH/CHAR_WIDTH/LABEL_PADDING (unused). 7. **Static initialization**: LayoutMetaDataProvider registration moved from constructor to static block (runs once, not per instance). 8. **Debug logging removed**: Removed diagnostic System.out.println. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,12 +9,12 @@ import com.cameleer3.server.core.diagram.DiagramRenderer;
|
|||||||
import com.cameleer3.server.core.diagram.PositionedEdge;
|
import com.cameleer3.server.core.diagram.PositionedEdge;
|
||||||
import com.cameleer3.server.core.diagram.PositionedNode;
|
import com.cameleer3.server.core.diagram.PositionedNode;
|
||||||
import org.eclipse.elk.alg.layered.options.LayeredMetaDataProvider;
|
import org.eclipse.elk.alg.layered.options.LayeredMetaDataProvider;
|
||||||
|
import org.eclipse.elk.alg.layered.options.NodePlacementStrategy;
|
||||||
import org.eclipse.elk.core.RecursiveGraphLayoutEngine;
|
import org.eclipse.elk.core.RecursiveGraphLayoutEngine;
|
||||||
import org.eclipse.elk.core.options.CoreOptions;
|
import org.eclipse.elk.core.options.CoreOptions;
|
||||||
import org.eclipse.elk.core.options.Direction;
|
import org.eclipse.elk.core.options.Direction;
|
||||||
import org.eclipse.elk.core.options.EdgeRouting;
|
import org.eclipse.elk.core.options.EdgeRouting;
|
||||||
import org.eclipse.elk.core.options.HierarchyHandling;
|
import org.eclipse.elk.core.options.HierarchyHandling;
|
||||||
import org.eclipse.elk.alg.layered.options.NodePlacementStrategy;
|
|
||||||
import org.eclipse.elk.core.util.BasicProgressMonitor;
|
import org.eclipse.elk.core.util.BasicProgressMonitor;
|
||||||
import org.eclipse.elk.graph.ElkBendPoint;
|
import org.eclipse.elk.graph.ElkBendPoint;
|
||||||
import org.eclipse.elk.graph.ElkEdge;
|
import org.eclipse.elk.graph.ElkEdge;
|
||||||
@@ -40,17 +40,20 @@ import java.util.Set;
|
|||||||
/**
|
/**
|
||||||
* ELK + JFreeSVG implementation of {@link DiagramRenderer}.
|
* ELK + JFreeSVG implementation of {@link DiagramRenderer}.
|
||||||
* <p>
|
* <p>
|
||||||
* Uses Eclipse ELK layered algorithm for top-to-bottom layout computation
|
* Uses Eclipse ELK layered algorithm for layout computation
|
||||||
* and JFreeSVG for SVG document generation with color-coded nodes.
|
* and JFreeSVG for SVG document generation with color-coded nodes.
|
||||||
*/
|
*/
|
||||||
public class ElkDiagramRenderer implements DiagramRenderer {
|
public class ElkDiagramRenderer implements DiagramRenderer {
|
||||||
|
|
||||||
|
// Register ELK provider once (not per-instance)
|
||||||
|
static {
|
||||||
|
org.eclipse.elk.core.data.LayoutMetaDataService.getInstance()
|
||||||
|
.registerLayoutMetaDataProviders(new LayeredMetaDataProvider());
|
||||||
|
}
|
||||||
|
|
||||||
private static final int PADDING = 20;
|
private static final int PADDING = 20;
|
||||||
private static final int NODE_HEIGHT = 40;
|
private static final int NODE_HEIGHT = 40;
|
||||||
private static final int NODE_WIDTH = 160;
|
private static final int NODE_WIDTH = 160;
|
||||||
private static final int MIN_NODE_WIDTH = 80;
|
|
||||||
private static final int CHAR_WIDTH = 8;
|
|
||||||
private static final int LABEL_PADDING = 32;
|
|
||||||
private static final int COMPOUND_TOP_PADDING = 30;
|
private static final int COMPOUND_TOP_PADDING = 30;
|
||||||
private static final int COMPOUND_SIDE_PADDING = 10;
|
private static final int COMPOUND_SIDE_PADDING = 10;
|
||||||
private static final int CORNER_RADIUS = 8;
|
private static final int CORNER_RADIUS = 8;
|
||||||
@@ -108,20 +111,11 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
NodeType.ON_COMPLETION
|
NodeType.ON_COMPLETION
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Top-level handler types that are laid out in their own separate ELK graph
|
/** Top-level handler types laid out in their own separate ELK graph. */
|
||||||
* to prevent them from affecting the main flow's node positioning. */
|
|
||||||
private static final Set<NodeType> HANDLER_SECTION_TYPES = EnumSet.of(
|
private static final Set<NodeType> HANDLER_SECTION_TYPES = EnumSet.of(
|
||||||
NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER, NodeType.ON_COMPLETION
|
NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER, NodeType.ON_COMPLETION
|
||||||
);
|
);
|
||||||
|
|
||||||
public ElkDiagramRenderer() {
|
|
||||||
// Ensure the layered algorithm meta data provider is registered.
|
|
||||||
// LayoutMetaDataService uses ServiceLoader, but explicit registration
|
|
||||||
// guarantees availability regardless of classpath ordering.
|
|
||||||
org.eclipse.elk.core.data.LayoutMetaDataService.getInstance()
|
|
||||||
.registerLayoutMetaDataProviders(new LayeredMetaDataProvider());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String renderSvg(RouteGraph graph) {
|
public String renderSvg(RouteGraph graph) {
|
||||||
LayoutResult result = computeLayout(graph, Direction.DOWN);
|
LayoutResult result = computeLayout(graph, Direction.DOWN);
|
||||||
@@ -144,7 +138,17 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
Font labelFont = new Font("SansSerif", Font.PLAIN, 12);
|
Font labelFont = new Font("SansSerif", Font.PLAIN, 12);
|
||||||
g2.setFont(labelFont);
|
g2.setFont(labelFont);
|
||||||
|
|
||||||
// Draw compound containers first (background)
|
// Collect IDs of nodes drawn inside compounds (to avoid double-drawing)
|
||||||
|
Set<String> compoundChildIds = new HashSet<>();
|
||||||
|
for (PositionedNode node : allNodes(layout.nodes())) {
|
||||||
|
if (result.compoundInfos.containsKey(node.id()) && node.children() != null) {
|
||||||
|
for (PositionedNode child : node.children()) {
|
||||||
|
collectAllIds(child, compoundChildIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw compound containers first (background + children inside)
|
||||||
for (Map.Entry<String, CompoundInfo> entry : result.compoundInfos.entrySet()) {
|
for (Map.Entry<String, CompoundInfo> entry : result.compoundInfos.entrySet()) {
|
||||||
CompoundInfo ci = entry.getValue();
|
CompoundInfo ci = entry.getValue();
|
||||||
PositionedNode pn = findNode(layout.nodes(), ci.nodeId);
|
PositionedNode pn = findNode(layout.nodes(), ci.nodeId);
|
||||||
@@ -153,8 +157,9 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw leaf nodes
|
// Draw leaf nodes (skip compounds and their children — already drawn above)
|
||||||
for (PositionedNode node : allNodes(layout.nodes())) {
|
for (PositionedNode node : allNodes(layout.nodes())) {
|
||||||
|
if (compoundChildIds.contains(node.id())) continue;
|
||||||
if (!result.compoundInfos.containsKey(node.id()) || node.children().isEmpty()) {
|
if (!result.compoundInfos.containsKey(node.id()) || node.children().isEmpty()) {
|
||||||
drawNode(g2, node, result.nodeColors.getOrDefault(node.id(), PURPLE));
|
drawNode(g2, node, result.nodeColors.getOrDefault(node.id(), PURPLE));
|
||||||
}
|
}
|
||||||
@@ -181,7 +186,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
|
private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
|
||||||
ElkGraphFactory factory = ElkGraphFactory.eINSTANCE;
|
ElkGraphFactory factory = ElkGraphFactory.eINSTANCE;
|
||||||
|
|
||||||
// Create root node
|
// Create root node for main flow
|
||||||
ElkNode rootNode = factory.createElkNode();
|
ElkNode rootNode = factory.createElkNode();
|
||||||
rootNode.setIdentifier("root");
|
rootNode.setIdentifier("root");
|
||||||
rootNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
rootNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||||
@@ -193,14 +198,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
rootNode.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
rootNode.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
||||||
NodePlacementStrategy.LINEAR_SEGMENTS);
|
NodePlacementStrategy.LINEAR_SEGMENTS);
|
||||||
|
|
||||||
// Build index of all RouteNodes (flat list from graph + recursive children)
|
|
||||||
Map<String, RouteNode> routeNodeMap = new HashMap<>();
|
|
||||||
if (graph.getNodes() != null) {
|
|
||||||
for (RouteNode rn : graph.getNodes()) {
|
|
||||||
indexNodeRecursive(rn, routeNodeMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track which nodes are children of a compound (at any depth)
|
// Track which nodes are children of a compound (at any depth)
|
||||||
Set<String> childNodeIds = new HashSet<>();
|
Set<String> childNodeIds = new HashSet<>();
|
||||||
|
|
||||||
@@ -209,9 +206,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
Map<String, Color> nodeColors = new HashMap<>();
|
Map<String, Color> nodeColors = new HashMap<>();
|
||||||
Set<String> compoundNodeIds = new HashSet<>();
|
Set<String> compoundNodeIds = new HashSet<>();
|
||||||
|
|
||||||
// Separate handler sections (ON_EXCEPTION, ON_COMPLETION, ERROR_HANDLER)
|
// Separate handler sections from main flow nodes
|
||||||
// from main flow nodes. Handler sections are laid out in their own ELK
|
|
||||||
// graphs to prevent them from affecting the main flow's Y positioning.
|
|
||||||
List<RouteNode> mainNodes = new ArrayList<>();
|
List<RouteNode> mainNodes = new ArrayList<>();
|
||||||
List<RouteNode> handlerNodes = new ArrayList<>();
|
List<RouteNode> handlerNodes = new ArrayList<>();
|
||||||
if (graph.getNodes() != null) {
|
if (graph.getNodes() != null) {
|
||||||
@@ -233,9 +228,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process handler sections into their OWN separate ELK root graphs.
|
// Process handler sections into their OWN separate ELK root graphs
|
||||||
// This prevents them from affecting the main flow's Y positioning.
|
|
||||||
// Each handler gets its own independent layout.
|
|
||||||
List<ElkNode> handlerRoots = new ArrayList<>();
|
List<ElkNode> handlerRoots = new ArrayList<>();
|
||||||
for (RouteNode rn : handlerNodes) {
|
for (RouteNode rn : handlerNodes) {
|
||||||
ElkNode handlerRoot = factory.createElkNode();
|
ElkNode handlerRoot = factory.createElkNode();
|
||||||
@@ -245,6 +238,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
handlerRoot.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
|
handlerRoot.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
|
||||||
handlerRoot.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
|
handlerRoot.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
|
||||||
handlerRoot.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
|
handlerRoot.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
|
||||||
|
handlerRoot.setProperty(CoreOptions.EDGE_ROUTING, EdgeRouting.POLYLINE);
|
||||||
handlerRoot.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
handlerRoot.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
||||||
NodePlacementStrategy.LINEAR_SEGMENTS);
|
NodePlacementStrategy.LINEAR_SEGMENTS);
|
||||||
|
|
||||||
@@ -254,7 +248,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create ELK edges — skip edges that cross between different ELK root graphs
|
// Create ELK edges — skip edges that cross between different ELK root graphs
|
||||||
// (e.g., main flow → handler section). These cannot be laid out by ELK.
|
|
||||||
if (graph.getEdges() != null) {
|
if (graph.getEdges() != null) {
|
||||||
for (RouteEdge re : graph.getEdges()) {
|
for (RouteEdge re : graph.getEdges()) {
|
||||||
ElkNode sourceElk = elkNodeMap.get(re.getSource());
|
ElkNode sourceElk = elkNodeMap.get(re.getSource());
|
||||||
@@ -270,9 +263,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the containing node for the edge
|
|
||||||
ElkNode containingNode = findCommonParent(sourceElk, targetElk);
|
ElkNode containingNode = findCommonParent(sourceElk, targetElk);
|
||||||
|
|
||||||
ElkEdge elkEdge = factory.createElkEdge();
|
ElkEdge elkEdge = factory.createElkEdge();
|
||||||
elkEdge.setContainingNode(containingNode);
|
elkEdge.setContainingNode(containingNode);
|
||||||
elkEdge.getSources().add(sourceElk);
|
elkEdge.getSources().add(sourceElk);
|
||||||
@@ -280,46 +271,32 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run layout — main flow
|
// Run layout
|
||||||
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
|
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
|
||||||
engine.layout(rootNode, new BasicProgressMonitor());
|
engine.layout(rootNode, new BasicProgressMonitor());
|
||||||
|
|
||||||
// Debug: log root dimensions and children
|
|
||||||
System.out.println("[ELK DEBUG] rootNode: " + rootNode.getWidth() + "x" + rootNode.getHeight()
|
|
||||||
+ " children=" + rootNode.getChildren().size());
|
|
||||||
for (ElkNode child : rootNode.getChildren()) {
|
|
||||||
System.out.println("[ELK DEBUG] child " + child.getIdentifier()
|
|
||||||
+ " x=" + child.getX() + " y=" + child.getY()
|
|
||||||
+ " w=" + child.getWidth() + " h=" + child.getHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run layout — each handler section independently
|
|
||||||
for (ElkNode handlerRoot : handlerRoots) {
|
for (ElkNode handlerRoot : handlerRoots) {
|
||||||
engine.layout(handlerRoot, new BasicProgressMonitor());
|
engine.layout(handlerRoot, new BasicProgressMonitor());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract results — only top-level nodes (children collected recursively)
|
// Extract positioned nodes
|
||||||
List<PositionedNode> positionedNodes = new ArrayList<>();
|
List<PositionedNode> positionedNodes = new ArrayList<>();
|
||||||
Map<String, CompoundInfo> compoundInfos = new HashMap<>();
|
Map<String, CompoundInfo> compoundInfos = new HashMap<>();
|
||||||
|
|
||||||
if (graph.getNodes() != null) {
|
if (graph.getNodes() != null) {
|
||||||
for (RouteNode rn : graph.getNodes()) {
|
for (RouteNode rn : graph.getNodes()) {
|
||||||
if (childNodeIds.contains(rn.getId())) {
|
if (childNodeIds.contains(rn.getId())) {
|
||||||
// Skip — collected under its parent compound
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ElkNode elkNode = elkNodeMap.get(rn.getId());
|
ElkNode elkNode = elkNodeMap.get(rn.getId());
|
||||||
if (elkNode == null) continue;
|
if (elkNode == null) continue;
|
||||||
|
|
||||||
// Use the correct root for coordinate calculation:
|
|
||||||
// handler nodes use their handler root, main flow uses rootNode
|
|
||||||
ElkNode coordRoot = getElkRoot(elkNode);
|
ElkNode coordRoot = getElkRoot(elkNode);
|
||||||
positionedNodes.add(extractPositionedNode(rn, elkNode, elkNodeMap,
|
positionedNodes.add(extractPositionedNode(rn, elkNode, elkNodeMap,
|
||||||
compoundNodeIds, compoundInfos, coordRoot));
|
compoundNodeIds, compoundInfos, coordRoot));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract edges from main root + all handler roots
|
// Extract edges from main root + handler roots
|
||||||
List<PositionedEdge> positionedEdges = new ArrayList<>();
|
List<PositionedEdge> positionedEdges = new ArrayList<>();
|
||||||
List<ElkEdge> allEdges = collectAllEdges(rootNode);
|
List<ElkEdge> allEdges = collectAllEdges(rootNode);
|
||||||
for (ElkNode hr : handlerRoots) {
|
for (ElkNode hr : handlerRoots) {
|
||||||
@@ -329,7 +306,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
|
String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
|
||||||
String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
|
String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
|
||||||
|
|
||||||
// Determine which root this edge belongs to for coordinate calculation
|
|
||||||
ElkNode edgeRoot = getElkRoot(elkEdge.getContainingNode());
|
ElkNode edgeRoot = getElkRoot(elkEdge.getContainingNode());
|
||||||
|
|
||||||
List<double[]> points = new ArrayList<>();
|
List<double[]> points = new ArrayList<>();
|
||||||
@@ -350,7 +326,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find label from original edge
|
|
||||||
String label = "";
|
String label = "";
|
||||||
if (graph.getEdges() != null) {
|
if (graph.getEdges() != null) {
|
||||||
for (RouteEdge re : graph.getEdges()) {
|
for (RouteEdge re : graph.getEdges()) {
|
||||||
@@ -364,23 +339,24 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points));
|
positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute bounding box from actual positioned node coordinates
|
// Normalize: shift all nodes and edges so bounding box starts at (0, 0).
|
||||||
// (not rootNode dimensions, which ignore handler roots)
|
// ELK can place nodes at arbitrary positions within the root graph;
|
||||||
|
// normalizing ensures the output starts at the origin regardless.
|
||||||
|
normalizePositions(positionedNodes, positionedEdges);
|
||||||
|
|
||||||
|
// Compute bounding box from all positioned nodes and edges
|
||||||
double totalWidth = 0;
|
double totalWidth = 0;
|
||||||
double totalHeight = 0;
|
double totalHeight = 0;
|
||||||
for (PositionedNode pn : positionedNodes) {
|
for (PositionedNode pn : allNodes(positionedNodes)) {
|
||||||
double right = pn.x() + pn.width();
|
double right = pn.x() + pn.width();
|
||||||
double bottom = pn.y() + pn.height();
|
double bottom = pn.y() + pn.height();
|
||||||
if (right > totalWidth) totalWidth = right;
|
if (right > totalWidth) totalWidth = right;
|
||||||
if (bottom > totalHeight) totalHeight = bottom;
|
if (bottom > totalHeight) totalHeight = bottom;
|
||||||
// Also check children bounds for compounds
|
|
||||||
if (pn.children() != null) {
|
|
||||||
for (PositionedNode child : pn.children()) {
|
|
||||||
double cr = pn.x() + child.x() + child.width();
|
|
||||||
double cb = pn.y() + child.y() + child.height();
|
|
||||||
if (cr > totalWidth) totalWidth = cr;
|
|
||||||
if (cb > totalHeight) totalHeight = cb;
|
|
||||||
}
|
}
|
||||||
|
for (PositionedEdge pe : positionedEdges) {
|
||||||
|
for (double[] pt : pe.points()) {
|
||||||
|
if (pt[0] > totalWidth) totalWidth = pt[0];
|
||||||
|
if (pt[1] > totalHeight) totalHeight = pt[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +364,61 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
return new LayoutResult(layout, nodeColors, compoundInfos);
|
return new LayoutResult(layout, nodeColors, compoundInfos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shift all positioned nodes and edge points so the bounding box starts at (0, 0).
|
||||||
|
* This compensates for ELK placing nodes at non-zero origins within its root graph.
|
||||||
|
*/
|
||||||
|
private void normalizePositions(List<PositionedNode> nodes, List<PositionedEdge> edges) {
|
||||||
|
// Find minimum x/y across all nodes (recursively including children)
|
||||||
|
double minX = Double.MAX_VALUE;
|
||||||
|
double minY = Double.MAX_VALUE;
|
||||||
|
for (PositionedNode pn : allNodes(nodes)) {
|
||||||
|
if (pn.x() < minX) minX = pn.x();
|
||||||
|
if (pn.y() < minY) minY = pn.y();
|
||||||
|
}
|
||||||
|
for (PositionedEdge pe : edges) {
|
||||||
|
for (double[] pt : pe.points()) {
|
||||||
|
if (pt[0] < minX) minX = pt[0];
|
||||||
|
if (pt[1] < minY) minY = pt[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minX <= 0 && minY <= 0) return; // Already at or past origin
|
||||||
|
|
||||||
|
// Shift nodes — PositionedNode is a record, so we must rebuild
|
||||||
|
double shiftX = minX > 0 ? minX : 0;
|
||||||
|
double shiftY = minY > 0 ? minY : 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < nodes.size(); i++) {
|
||||||
|
nodes.set(i, shiftNode(nodes.get(i), shiftX, shiftY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift edge points in-place
|
||||||
|
for (PositionedEdge pe : edges) {
|
||||||
|
for (double[] pt : pe.points()) {
|
||||||
|
pt[0] -= shiftX;
|
||||||
|
pt[1] -= shiftY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recursively shift a PositionedNode and its children by (dx, dy). */
|
||||||
|
private PositionedNode shiftNode(PositionedNode pn, double dx, double dy) {
|
||||||
|
List<PositionedNode> shiftedChildren = List.of();
|
||||||
|
if (pn.children() != null && !pn.children().isEmpty()) {
|
||||||
|
shiftedChildren = new ArrayList<>();
|
||||||
|
for (PositionedNode child : pn.children()) {
|
||||||
|
shiftedChildren.add(shiftNode(child, dx, dy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new PositionedNode(
|
||||||
|
pn.id(), pn.label(), pn.type(),
|
||||||
|
pn.x() - dx, pn.y() - dy,
|
||||||
|
pn.width(), pn.height(),
|
||||||
|
shiftedChildren
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// SVG drawing helpers
|
// SVG drawing helpers
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -410,7 +441,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
|
|
||||||
private void drawCompoundContainer(SVGGraphics2D g2, PositionedNode node, Color color) {
|
private void drawCompoundContainer(SVGGraphics2D g2, PositionedNode node, Color color) {
|
||||||
// Semi-transparent background
|
// Semi-transparent background
|
||||||
Color bg = new Color(color.getRed(), color.getGreen(), color.getBlue(), 38); // ~15% alpha
|
Color bg = new Color(color.getRed(), color.getGreen(), color.getBlue(), 38);
|
||||||
g2.setColor(bg);
|
g2.setColor(bg);
|
||||||
g2.fill(new RoundRectangle2D.Double(
|
g2.fill(new RoundRectangle2D.Double(
|
||||||
node.x(), node.y(), node.width(), node.height(),
|
node.x(), node.y(), node.width(), node.height(),
|
||||||
@@ -425,7 +456,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
|
|
||||||
// Label at top
|
// Label at top
|
||||||
g2.setColor(color);
|
g2.setColor(color);
|
||||||
FontMetrics fm = g2.getFontMetrics();
|
|
||||||
float labelX = (float) (node.x() + COMPOUND_SIDE_PADDING);
|
float labelX = (float) (node.x() + COMPOUND_SIDE_PADDING);
|
||||||
float labelY = (float) (node.y() + 18);
|
float labelY = (float) (node.y() + 18);
|
||||||
g2.drawString(node.label(), labelX, labelY);
|
g2.drawString(node.label(), labelX, labelY);
|
||||||
@@ -479,7 +509,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
if (ENDPOINT_TYPES.contains(type)) return BLUE;
|
if (ENDPOINT_TYPES.contains(type)) return BLUE;
|
||||||
if (PROCESSOR_TYPES.contains(type)) return GREEN;
|
if (PROCESSOR_TYPES.contains(type)) return GREEN;
|
||||||
if (ERROR_TYPES.contains(type)) return RED;
|
if (ERROR_TYPES.contains(type)) return RED;
|
||||||
if (EIP_TYPES.contains(type)) return EIP_TYPES.contains(type) ? PURPLE : PURPLE;
|
if (EIP_TYPES.contains(type)) return PURPLE;
|
||||||
if (CROSS_ROUTE_TYPES.contains(type)) return CYAN;
|
if (CROSS_ROUTE_TYPES.contains(type)) return CYAN;
|
||||||
return PURPLE;
|
return PURPLE;
|
||||||
}
|
}
|
||||||
@@ -497,16 +527,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
// Recursive node building
|
// Recursive node building
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
/** Index a RouteNode and all its descendants into the map. */
|
|
||||||
private void indexNodeRecursive(RouteNode node, Map<String, RouteNode> map) {
|
|
||||||
map.put(node.getId(), node);
|
|
||||||
if (node.getChildren() != null) {
|
|
||||||
for (RouteNode child : node.getChildren()) {
|
|
||||||
indexNodeRecursive(child, map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively create ELK nodes. Compound nodes become ELK containers
|
* Recursively create ELK nodes. Compound nodes become ELK containers
|
||||||
* with their children nested inside. Non-compound nodes become leaf nodes.
|
* with their children nested inside. Non-compound nodes become leaf nodes.
|
||||||
@@ -535,7 +555,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING,
|
new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING,
|
||||||
COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
|
COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
|
||||||
|
|
||||||
// Recursively create children inside this compound
|
|
||||||
for (RouteNode child : rn.getChildren()) {
|
for (RouteNode child : rn.getChildren()) {
|
||||||
childNodeIds.add(child.getId());
|
childNodeIds.add(child.getId());
|
||||||
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
|
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
|
||||||
@@ -552,7 +571,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively extract a PositionedNode from the ELK layout result.
|
* Recursively extract a PositionedNode from the ELK layout result.
|
||||||
* Compound nodes include their children with absolute coordinates.
|
* All coordinates are absolute (relative to the ELK root).
|
||||||
*/
|
*/
|
||||||
private PositionedNode extractPositionedNode(
|
private PositionedNode extractPositionedNode(
|
||||||
RouteNode rn, ElkNode elkNode, Map<String, ElkNode> elkNodeMap,
|
RouteNode rn, ElkNode elkNode, Map<String, ElkNode> elkNodeMap,
|
||||||
@@ -600,14 +619,12 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
|
|
||||||
/** Proper lowest common ancestor of two ELK nodes. */
|
/** Proper lowest common ancestor of two ELK nodes. */
|
||||||
private ElkNode findCommonParent(ElkNode a, ElkNode b) {
|
private ElkNode findCommonParent(ElkNode a, ElkNode b) {
|
||||||
// Collect all ancestors of 'a' (including a itself)
|
|
||||||
Set<ElkNode> ancestorsOfA = new HashSet<>();
|
Set<ElkNode> ancestorsOfA = new HashSet<>();
|
||||||
ElkNode current = a;
|
ElkNode current = a;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
ancestorsOfA.add(current);
|
ancestorsOfA.add(current);
|
||||||
current = current.getParent();
|
current = current.getParent();
|
||||||
}
|
}
|
||||||
// Walk up from 'b' until we find a common ancestor
|
|
||||||
current = b;
|
current = b;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
if (ancestorsOfA.contains(current)) {
|
if (ancestorsOfA.contains(current)) {
|
||||||
@@ -615,7 +632,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
}
|
}
|
||||||
current = current.getParent();
|
current = current.getParent();
|
||||||
}
|
}
|
||||||
// Fallback: root of 'a'
|
|
||||||
return getElkRoot(a);
|
return getElkRoot(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,13 +663,19 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
return edges;
|
return edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recursively find a PositionedNode by ID. */
|
||||||
private PositionedNode findNode(List<PositionedNode> nodes, String id) {
|
private PositionedNode findNode(List<PositionedNode> nodes, String id) {
|
||||||
for (PositionedNode n : nodes) {
|
for (PositionedNode n : nodes) {
|
||||||
if (n.id().equals(id)) return n;
|
if (n.id().equals(id)) return n;
|
||||||
|
if (n.children() != null) {
|
||||||
|
PositionedNode found = findNode(n.children(), id);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recursively flatten a PositionedNode tree. */
|
||||||
private List<PositionedNode> allNodes(List<PositionedNode> nodes) {
|
private List<PositionedNode> allNodes(List<PositionedNode> nodes) {
|
||||||
List<PositionedNode> all = new ArrayList<>();
|
List<PositionedNode> all = new ArrayList<>();
|
||||||
for (PositionedNode n : nodes) {
|
for (PositionedNode n : nodes) {
|
||||||
@@ -665,6 +687,16 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recursively collect all node IDs from a tree. */
|
||||||
|
private void collectAllIds(PositionedNode node, Set<String> ids) {
|
||||||
|
ids.add(node.id());
|
||||||
|
if (node.children() != null) {
|
||||||
|
for (PositionedNode child : node.children()) {
|
||||||
|
collectAllIds(child, ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Internal data classes
|
// Internal data classes
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user