refactor: simplify ElkDiagramRenderer layout code
- Introduce LayoutContext to bundle 8 accumulator params into 1 object - Extract computeLayout (261 lines) into 6 focused sub-methods: buildNodeIndex, partitionNodes, createElkRoot, createElkEdges, postProcessDoTrySections, extractLayout - Consolidate duplicated DO_TRY handler iteration via orderedHandlerChildren - De-duplicate ELK root configuration (main + handler roots) - Add DO_TRY test cases for section ordering and uniform width - Clean up orphaned Javadoc comments No behavioral changes. 882 → 841 lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,48 +185,66 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
|
||||
ElkGraphFactory factory = ElkGraphFactory.eINSTANCE;
|
||||
LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE);
|
||||
|
||||
// Create root node for main flow
|
||||
ElkNode rootNode = factory.createElkNode();
|
||||
rootNode.setIdentifier("root");
|
||||
rootNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||
rootNode.setProperty(CoreOptions.DIRECTION, rootDirection);
|
||||
rootNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING);
|
||||
rootNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING);
|
||||
rootNode.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
|
||||
rootNode.setProperty(CoreOptions.EDGE_ROUTING, EdgeRouting.POLYLINE);
|
||||
rootNode.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
||||
NodePlacementStrategy.LINEAR_SEGMENTS);
|
||||
// 1. Partition graph nodes into main flow vs handler sections
|
||||
Map<String, RouteNode> nodeById = buildNodeIndex(graph);
|
||||
List<RouteNode> mainNodes = new ArrayList<>();
|
||||
List<RouteNode> handlerNodes = new ArrayList<>();
|
||||
partitionNodes(graph, nodeById, mainNodes, handlerNodes);
|
||||
|
||||
// Track which nodes are children of a compound (at any depth)
|
||||
Set<String> childNodeIds = new HashSet<>();
|
||||
// 2. Build ELK graphs — main flow + separate handler roots
|
||||
ElkNode rootNode = createElkRoot("root", rootDirection, 1.0, ctx);
|
||||
for (RouteNode rn : mainNodes) {
|
||||
if (!ctx.elkNodeMap.containsKey(rn.getId())) {
|
||||
createElkNodeRecursive(rn, rootNode, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Create ELK nodes recursively — compounds contain their children
|
||||
Map<String, ElkNode> elkNodeMap = new HashMap<>();
|
||||
Map<String, Color> nodeColors = new HashMap<>();
|
||||
Set<String> compoundNodeIds = new HashSet<>();
|
||||
List<ElkNode> handlerRoots = new ArrayList<>();
|
||||
for (RouteNode rn : handlerNodes) {
|
||||
ElkNode hr = createElkRoot("handler-root-" + rn.getId(), rootDirection, 0.5, ctx);
|
||||
createElkNodeRecursive(rn, hr, ctx);
|
||||
handlerRoots.add(hr);
|
||||
}
|
||||
|
||||
// Build a lookup from the flat nodes list (graph.getNodes() contains
|
||||
// duplicates — children appear both nested AND top-level). Use this for
|
||||
// resolving edge endpoints to RouteNode objects.
|
||||
// 3. Create ELK edges (filtering DO_TRY internals and cross-root edges)
|
||||
createElkEdges(graph, ctx);
|
||||
|
||||
// 4. Run layout engine
|
||||
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
|
||||
engine.layout(rootNode, new BasicProgressMonitor());
|
||||
for (ElkNode hr : handlerRoots) {
|
||||
engine.layout(hr, new BasicProgressMonitor());
|
||||
}
|
||||
|
||||
// 5. Post-process: fix DO_TRY section ordering and widths
|
||||
postProcessDoTrySections(ctx);
|
||||
|
||||
// 6. Extract positioned result
|
||||
return extractLayout(graph, rootNode, handlerRoots, ctx);
|
||||
}
|
||||
|
||||
/** Build a lookup from the flat nodes list (deduplicates nested + top-level entries). */
|
||||
private Map<String, RouteNode> buildNodeIndex(RouteGraph graph) {
|
||||
Map<String, RouteNode> nodeById = new HashMap<>();
|
||||
if (graph.getNodes() != null) {
|
||||
for (RouteNode rn : graph.getNodes()) {
|
||||
nodeById.put(rn.getId(), rn);
|
||||
}
|
||||
}
|
||||
return nodeById;
|
||||
}
|
||||
|
||||
// Separate main flow from handler sections using graph structure:
|
||||
// - Start from graph.getRoot() and walk FLOW edges for main flow
|
||||
// - Handler sections are top-level nodes of HANDLER_SECTION_TYPES
|
||||
// This avoids the problem of the flat nodes list containing duplicates.
|
||||
/**
|
||||
* Separate main flow nodes from handler sections by walking FLOW edges
|
||||
* from the graph root. Handler sections (onException, onCompletion, etc.)
|
||||
* are placed in their own list for independent layout.
|
||||
*/
|
||||
private void partitionNodes(RouteGraph graph, Map<String, RouteNode> nodeById,
|
||||
List<RouteNode> mainNodes, List<RouteNode> handlerNodes) {
|
||||
Set<String> mainNodeIds = new HashSet<>();
|
||||
List<RouteNode> mainNodes = new ArrayList<>();
|
||||
List<RouteNode> handlerNodes = new ArrayList<>();
|
||||
|
||||
// Collect main flow node IDs by walking from root along non-ERROR edges.
|
||||
// graph.getRoot() provides the entry point; ERROR edges lead to handler sections.
|
||||
if (graph.getRoot() != null && graph.getEdges() != null) {
|
||||
mainNodeIds.add(graph.getRoot().getId());
|
||||
boolean changed = true;
|
||||
@@ -235,7 +253,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
for (RouteEdge re : graph.getEdges()) {
|
||||
if (re.getEdgeType() == RouteEdge.EdgeType.ERROR) continue;
|
||||
if (mainNodeIds.contains(re.getSource()) && !mainNodeIds.contains(re.getTarget())) {
|
||||
// Don't follow edges INTO handler compounds
|
||||
RouteNode target = nodeById.get(re.getTarget());
|
||||
if (target != null && target.getType() != null
|
||||
&& HANDLER_SECTION_TYPES.contains(target.getType())) {
|
||||
@@ -248,7 +265,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Build node lists — deduplicate (flat list has duplicates)
|
||||
Set<String> seen = new HashSet<>();
|
||||
if (graph.getNodes() != null) {
|
||||
for (RouteNode rn : graph.getNodes()) {
|
||||
@@ -258,124 +274,91 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
&& rn.getChildren() != null && !rn.getChildren().isEmpty()) {
|
||||
handlerNodes.add(rn);
|
||||
} else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) {
|
||||
// When no root is available (legacy/test graphs), include all
|
||||
// non-handler nodes as main flow (fallback to old behavior)
|
||||
mainNodes.add(rn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process main flow nodes into the root ELK graph
|
||||
Set<String> doTryNodeIds = new HashSet<>();
|
||||
Map<String, List<String>> doTrySectionOrder = new LinkedHashMap<>();
|
||||
for (RouteNode rn : mainNodes) {
|
||||
if (!elkNodeMap.containsKey(rn.getId())) {
|
||||
createElkNodeRecursive(rn, rootNode, factory, elkNodeMap, nodeColors,
|
||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
||||
/** Create a configured ELK root node for the layered algorithm. */
|
||||
private ElkNode createElkRoot(String identifier, Direction direction,
|
||||
double spacingScale, LayoutContext ctx) {
|
||||
ElkNode root = ctx.factory.createElkNode();
|
||||
root.setIdentifier(identifier);
|
||||
root.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||
root.setProperty(CoreOptions.DIRECTION, direction);
|
||||
root.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * spacingScale);
|
||||
root.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * spacingScale);
|
||||
root.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
|
||||
root.setProperty(CoreOptions.EDGE_ROUTING, EdgeRouting.POLYLINE);
|
||||
root.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
||||
NodePlacementStrategy.LINEAR_SEGMENTS);
|
||||
return root;
|
||||
}
|
||||
|
||||
/** Create ELK edges, skipping DO_TRY-to-descendant and cross-root edges. */
|
||||
private void createElkEdges(RouteGraph graph, LayoutContext ctx) {
|
||||
if (graph.getEdges() == null) return;
|
||||
for (RouteEdge re : graph.getEdges()) {
|
||||
ElkNode sourceElk = ctx.elkNodeMap.get(re.getSource());
|
||||
ElkNode targetElk = ctx.elkNodeMap.get(re.getTarget());
|
||||
if (sourceElk == null || targetElk == null) continue;
|
||||
|
||||
// Skip edges from DO_TRY to its own children (keep continuation edges)
|
||||
if (ctx.doTryNodeIds.contains(re.getSource()) && isDescendantOf(targetElk, sourceElk)) {
|
||||
continue;
|
||||
}
|
||||
// Skip edges that cross ELK root boundaries
|
||||
if (getElkRoot(sourceElk) != getElkRoot(targetElk)) continue;
|
||||
|
||||
ElkEdge elkEdge = ctx.factory.createElkEdge();
|
||||
elkEdge.setContainingNode(findCommonParent(sourceElk, targetElk));
|
||||
elkEdge.getSources().add(sourceElk);
|
||||
elkEdge.getTargets().add(targetElk);
|
||||
}
|
||||
}
|
||||
|
||||
// Process handler sections into their OWN separate ELK root graphs
|
||||
List<ElkNode> handlerRoots = new ArrayList<>();
|
||||
for (RouteNode rn : handlerNodes) {
|
||||
ElkNode handlerRoot = factory.createElkNode();
|
||||
handlerRoot.setIdentifier("handler-root-" + rn.getId());
|
||||
handlerRoot.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||
handlerRoot.setProperty(CoreOptions.DIRECTION, rootDirection);
|
||||
handlerRoot.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
|
||||
handlerRoot.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
|
||||
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,
|
||||
NodePlacementStrategy.LINEAR_SEGMENTS);
|
||||
|
||||
createElkNodeRecursive(rn, handlerRoot, factory, elkNodeMap, nodeColors,
|
||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
||||
handlerRoots.add(handlerRoot);
|
||||
}
|
||||
|
||||
// Create ELK edges — skip edges that cross between different ELK root graphs
|
||||
if (graph.getEdges() != null) {
|
||||
for (RouteEdge re : graph.getEdges()) {
|
||||
ElkNode sourceElk = elkNodeMap.get(re.getSource());
|
||||
ElkNode targetElk = elkNodeMap.get(re.getTarget());
|
||||
if (sourceElk == null || targetElk == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip edges from DO_TRY to its own children (entry + handler edges).
|
||||
// Keep edges from DO_TRY to nodes OUTSIDE it (continuation edges).
|
||||
if (doTryNodeIds.contains(re.getSource())) {
|
||||
if (isDescendantOf(targetElk, sourceElk)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip edges that cross ELK root boundaries
|
||||
ElkNode sourceRoot = getElkRoot(sourceElk);
|
||||
ElkNode targetRoot = getElkRoot(targetElk);
|
||||
if (sourceRoot != targetRoot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ElkNode containingNode = findCommonParent(sourceElk, targetElk);
|
||||
ElkEdge elkEdge = factory.createElkEdge();
|
||||
elkEdge.setContainingNode(containingNode);
|
||||
elkEdge.getSources().add(sourceElk);
|
||||
elkEdge.getTargets().add(targetElk);
|
||||
}
|
||||
}
|
||||
|
||||
// Run layout
|
||||
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
|
||||
engine.layout(rootNode, new BasicProgressMonitor());
|
||||
for (ElkNode handlerRoot : handlerRoots) {
|
||||
engine.layout(handlerRoot, new BasicProgressMonitor());
|
||||
}
|
||||
|
||||
// Post-process DO_TRY compounds: re-stack sections in correct order
|
||||
// and stretch all sections to the same width
|
||||
// (ELK doesn't reliably order disconnected children within a compound)
|
||||
for (Map.Entry<String, List<String>> entry : doTrySectionOrder.entrySet()) {
|
||||
/**
|
||||
* Post-process DO_TRY compounds after layout: re-stack sections in correct
|
||||
* vertical order (try_body → doFinally → doCatch) and stretch to uniform width.
|
||||
* ELK doesn't reliably order disconnected children within a compound.
|
||||
*/
|
||||
private void postProcessDoTrySections(LayoutContext ctx) {
|
||||
for (Map.Entry<String, List<String>> entry : ctx.doTrySectionOrder.entrySet()) {
|
||||
List<String> orderedIds = entry.getValue();
|
||||
List<ElkNode> sections = new ArrayList<>();
|
||||
for (String id : orderedIds) {
|
||||
ElkNode section = elkNodeMap.get(id);
|
||||
ElkNode section = ctx.elkNodeMap.get(id);
|
||||
if (section != null) sections.add(section);
|
||||
}
|
||||
if (sections.size() < 2) continue;
|
||||
|
||||
// Re-stack in correct vertical order
|
||||
double startY = sections.stream().mapToDouble(ElkNode::getY).min().orElse(0);
|
||||
double spacing = NODE_SPACING * 0.4; // matches DO_TRY spacing
|
||||
double spacing = NODE_SPACING * 0.4;
|
||||
double currentY = startY;
|
||||
for (ElkNode section : sections) {
|
||||
section.setY(currentY);
|
||||
currentY += section.getHeight() + spacing;
|
||||
}
|
||||
|
||||
// Stretch all sections to the widest one's width
|
||||
double maxWidth = sections.stream().mapToDouble(ElkNode::getWidth).max().orElse(0);
|
||||
for (ElkNode section : sections) {
|
||||
section.setWidth(maxWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract positioned nodes, edges, and bounding box from the ELK layout result. */
|
||||
private LayoutResult extractLayout(RouteGraph graph, ElkNode rootNode,
|
||||
List<ElkNode> handlerRoots, LayoutContext ctx) {
|
||||
// Extract positioned nodes
|
||||
List<PositionedNode> positionedNodes = new ArrayList<>();
|
||||
Map<String, CompoundInfo> compoundInfos = new HashMap<>();
|
||||
|
||||
if (graph.getNodes() != null) {
|
||||
for (RouteNode rn : graph.getNodes()) {
|
||||
if (childNodeIds.contains(rn.getId())) {
|
||||
continue;
|
||||
}
|
||||
ElkNode elkNode = elkNodeMap.get(rn.getId());
|
||||
if (ctx.childNodeIds.contains(rn.getId())) continue;
|
||||
ElkNode elkNode = ctx.elkNodeMap.get(rn.getId());
|
||||
if (elkNode == null) continue;
|
||||
|
||||
ElkNode coordRoot = getElkRoot(elkNode);
|
||||
positionedNodes.add(extractPositionedNode(rn, elkNode, elkNodeMap,
|
||||
compoundNodeIds, compoundInfos, coordRoot));
|
||||
positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,25 +371,17 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
for (ElkEdge elkEdge : allEdges) {
|
||||
String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
|
||||
String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
|
||||
|
||||
ElkNode edgeRoot = getElkRoot(elkEdge.getContainingNode());
|
||||
|
||||
List<double[]> points = new ArrayList<>();
|
||||
for (ElkEdgeSection section : elkEdge.getSections()) {
|
||||
points.add(new double[]{
|
||||
section.getStartX() + getAbsoluteX(elkEdge.getContainingNode(), edgeRoot),
|
||||
section.getStartY() + getAbsoluteY(elkEdge.getContainingNode(), edgeRoot)
|
||||
});
|
||||
double cx = getAbsoluteX(elkEdge.getContainingNode(), edgeRoot);
|
||||
double cy = getAbsoluteY(elkEdge.getContainingNode(), edgeRoot);
|
||||
points.add(new double[]{section.getStartX() + cx, section.getStartY() + cy});
|
||||
for (ElkBendPoint bp : section.getBendPoints()) {
|
||||
points.add(new double[]{
|
||||
bp.getX() + getAbsoluteX(elkEdge.getContainingNode(), edgeRoot),
|
||||
bp.getY() + getAbsoluteY(elkEdge.getContainingNode(), edgeRoot)
|
||||
});
|
||||
points.add(new double[]{bp.getX() + cx, bp.getY() + cy});
|
||||
}
|
||||
points.add(new double[]{
|
||||
section.getEndX() + getAbsoluteX(elkEdge.getContainingNode(), edgeRoot),
|
||||
section.getEndY() + getAbsoluteY(elkEdge.getContainingNode(), edgeRoot)
|
||||
});
|
||||
points.add(new double[]{section.getEndX() + cx, section.getEndY() + cy});
|
||||
}
|
||||
|
||||
String label = "";
|
||||
@@ -418,32 +393,24 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points));
|
||||
}
|
||||
|
||||
// Note: normalization (shifting to 0,0 origin) is handled per-section
|
||||
// in the frontend's useDiagramData.ts, which separates main flow from
|
||||
// handler sections and normalizes each independently.
|
||||
|
||||
// Compute bounding box from all positioned nodes and edges
|
||||
double totalWidth = 0;
|
||||
double totalHeight = 0;
|
||||
// Compute bounding box
|
||||
double totalWidth = 0, totalHeight = 0;
|
||||
for (PositionedNode pn : allNodes(positionedNodes)) {
|
||||
double right = pn.x() + pn.width();
|
||||
double bottom = pn.y() + pn.height();
|
||||
if (right > totalWidth) totalWidth = right;
|
||||
if (bottom > totalHeight) totalHeight = bottom;
|
||||
totalWidth = Math.max(totalWidth, pn.x() + pn.width());
|
||||
totalHeight = Math.max(totalHeight, pn.y() + pn.height());
|
||||
}
|
||||
for (PositionedEdge pe : positionedEdges) {
|
||||
for (double[] pt : pe.points()) {
|
||||
if (pt[0] > totalWidth) totalWidth = pt[0];
|
||||
if (pt[1] > totalHeight) totalHeight = pt[1];
|
||||
totalWidth = Math.max(totalWidth, pt[0]);
|
||||
totalHeight = Math.max(totalHeight, pt[1]);
|
||||
}
|
||||
}
|
||||
|
||||
DiagramLayout layout = new DiagramLayout(totalWidth, totalHeight, positionedNodes, positionedEdges);
|
||||
return new LayoutResult(layout, nodeColors, compoundInfos);
|
||||
return new LayoutResult(layout, ctx.nodeColors, ctx.compoundInfos);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
@@ -559,23 +526,20 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
* with their children nested inside. Non-compound nodes become leaf nodes.
|
||||
*/
|
||||
private void createElkNodeRecursive(
|
||||
RouteNode rn, ElkNode parentElk, ElkGraphFactory factory,
|
||||
Map<String, ElkNode> elkNodeMap, Map<String, Color> nodeColors,
|
||||
Set<String> compoundNodeIds, Set<String> childNodeIds,
|
||||
Set<String> doTryNodeIds, Map<String, List<String>> doTrySectionOrder) {
|
||||
RouteNode rn, ElkNode parentElk, LayoutContext ctx) {
|
||||
|
||||
boolean isCompound = rn.getType() != null && COMPOUND_TYPES.contains(rn.getType())
|
||||
&& rn.getChildren() != null && !rn.getChildren().isEmpty();
|
||||
|
||||
ElkNode elkNode = factory.createElkNode();
|
||||
ElkNode elkNode = ctx.factory.createElkNode();
|
||||
elkNode.setIdentifier(rn.getId());
|
||||
elkNode.setParent(parentElk);
|
||||
|
||||
if (isCompound && rn.getType() == NodeType.DO_TRY) {
|
||||
// DO_TRY: vertical container with a virtual _TRY_BODY wrapper for the try body
|
||||
// and DO_CATCH/DO_FINALLY as separate children below
|
||||
doTryNodeIds.add(rn.getId());
|
||||
compoundNodeIds.add(rn.getId());
|
||||
ctx.doTryNodeIds.add(rn.getId());
|
||||
ctx.compoundNodeIds.add(rn.getId());
|
||||
elkNode.setWidth(200);
|
||||
elkNode.setHeight(100);
|
||||
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||
@@ -606,7 +570,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
// Virtual _TRY_BODY wrapper
|
||||
if (!tryBodyChildren.isEmpty()) {
|
||||
String wrapperId = rn.getId() + "._try_body";
|
||||
ElkNode wrapper = factory.createElkNode();
|
||||
ElkNode wrapper = ctx.factory.createElkNode();
|
||||
wrapper.setIdentifier(wrapperId);
|
||||
wrapper.setParent(elkNode);
|
||||
wrapper.setWidth(200);
|
||||
@@ -617,39 +581,26 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
wrapper.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
|
||||
wrapper.setProperty(CoreOptions.PADDING,
|
||||
new org.eclipse.elk.core.math.ElkPadding(8, 8, 8, 8));
|
||||
compoundNodeIds.add(wrapperId);
|
||||
elkNodeMap.put(wrapperId, wrapper);
|
||||
ctx.compoundNodeIds.add(wrapperId);
|
||||
ctx.elkNodeMap.put(wrapperId, wrapper);
|
||||
sectionOrder.add(wrapperId);
|
||||
|
||||
for (RouteNode child : tryBodyChildren) {
|
||||
childNodeIds.add(child.getId());
|
||||
createElkNodeRecursive(child, wrapper, factory, elkNodeMap, nodeColors,
|
||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
||||
ctx.childNodeIds.add(child.getId());
|
||||
createElkNodeRecursive(child, wrapper, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// DO_FINALLY sections (middle)
|
||||
for (RouteNode child : handlerChildren) {
|
||||
if (child.getType() == NodeType.DO_FINALLY) {
|
||||
childNodeIds.add(child.getId());
|
||||
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
|
||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
||||
sectionOrder.add(child.getId());
|
||||
}
|
||||
}
|
||||
// DO_CATCH sections (bottom)
|
||||
for (RouteNode child : handlerChildren) {
|
||||
if (child.getType() == NodeType.DO_CATCH) {
|
||||
childNodeIds.add(child.getId());
|
||||
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
|
||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
||||
sectionOrder.add(child.getId());
|
||||
}
|
||||
// Handler sections in order: DO_FINALLY (middle), then DO_CATCH (bottom)
|
||||
for (RouteNode handler : orderedHandlerChildren(handlerChildren)) {
|
||||
ctx.childNodeIds.add(handler.getId());
|
||||
createElkNodeRecursive(handler, elkNode, ctx);
|
||||
sectionOrder.add(handler.getId());
|
||||
}
|
||||
|
||||
doTrySectionOrder.put(rn.getId(), sectionOrder);
|
||||
ctx.doTrySectionOrder.put(rn.getId(), sectionOrder);
|
||||
} else if (isCompound) {
|
||||
compoundNodeIds.add(rn.getId());
|
||||
ctx.compoundNodeIds.add(rn.getId());
|
||||
elkNode.setWidth(200);
|
||||
elkNode.setHeight(100);
|
||||
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||
@@ -661,17 +612,16 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
|
||||
|
||||
for (RouteNode child : rn.getChildren()) {
|
||||
childNodeIds.add(child.getId());
|
||||
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
|
||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
||||
ctx.childNodeIds.add(child.getId());
|
||||
createElkNodeRecursive(child, elkNode, ctx);
|
||||
}
|
||||
} else {
|
||||
elkNode.setWidth(NODE_WIDTH);
|
||||
elkNode.setHeight(NODE_HEIGHT);
|
||||
}
|
||||
|
||||
elkNodeMap.put(rn.getId(), elkNode);
|
||||
nodeColors.put(rn.getId(), colorForType(rn.getType()));
|
||||
ctx.elkNodeMap.put(rn.getId(), elkNode);
|
||||
ctx.nodeColors.put(rn.getId(), colorForType(rn.getType()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -679,29 +629,26 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
* All coordinates are absolute (relative to the ELK root).
|
||||
*/
|
||||
private PositionedNode extractPositionedNode(
|
||||
RouteNode rn, ElkNode elkNode, Map<String, ElkNode> elkNodeMap,
|
||||
Set<String> compoundNodeIds, Map<String, CompoundInfo> compoundInfos,
|
||||
ElkNode rootNode) {
|
||||
RouteNode rn, ElkNode elkNode, ElkNode rootNode, LayoutContext ctx) {
|
||||
|
||||
double absX = getAbsoluteX(elkNode, rootNode);
|
||||
double absY = getAbsoluteY(elkNode, rootNode);
|
||||
|
||||
List<PositionedNode> children = List.of();
|
||||
if (compoundNodeIds.contains(rn.getId()) && rn.getChildren() != null) {
|
||||
if (ctx.compoundNodeIds.contains(rn.getId()) && rn.getChildren() != null) {
|
||||
children = new ArrayList<>();
|
||||
|
||||
if (rn.getType() == NodeType.DO_TRY) {
|
||||
// DO_TRY: extract virtual _TRY_BODY wrapper first, then handler children
|
||||
String wrapperId = rn.getId() + "._try_body";
|
||||
ElkNode wrapperElk = elkNodeMap.get(wrapperId);
|
||||
ElkNode wrapperElk = ctx.elkNodeMap.get(wrapperId);
|
||||
if (wrapperElk != null) {
|
||||
List<PositionedNode> wrapperChildren = new ArrayList<>();
|
||||
for (RouteNode child : rn.getChildren()) {
|
||||
if (child.getType() != NodeType.DO_CATCH && child.getType() != NodeType.DO_FINALLY) {
|
||||
ElkNode childElk = elkNodeMap.get(child.getId());
|
||||
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
|
||||
if (childElk != null) {
|
||||
wrapperChildren.add(extractPositionedNode(child, childElk, elkNodeMap,
|
||||
compoundNodeIds, compoundInfos, rootNode));
|
||||
wrapperChildren.add(extractPositionedNode(child, childElk, rootNode, ctx));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,37 +658,24 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
getAbsoluteY(wrapperElk, rootNode),
|
||||
wrapperElk.getWidth(), wrapperElk.getHeight(),
|
||||
wrapperChildren));
|
||||
compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE));
|
||||
ctx.compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE));
|
||||
}
|
||||
// Handler children: DO_FINALLY first, then DO_CATCH
|
||||
for (RouteNode child : rn.getChildren()) {
|
||||
if (child.getType() == NodeType.DO_FINALLY) {
|
||||
ElkNode childElk = elkNodeMap.get(child.getId());
|
||||
if (childElk != null) {
|
||||
children.add(extractPositionedNode(child, childElk, elkNodeMap,
|
||||
compoundNodeIds, compoundInfos, rootNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (RouteNode child : rn.getChildren()) {
|
||||
if (child.getType() == NodeType.DO_CATCH) {
|
||||
ElkNode childElk = elkNodeMap.get(child.getId());
|
||||
if (childElk != null) {
|
||||
children.add(extractPositionedNode(child, childElk, elkNodeMap,
|
||||
compoundNodeIds, compoundInfos, rootNode));
|
||||
}
|
||||
// Handler children in order: DO_FINALLY first, then DO_CATCH
|
||||
for (RouteNode handler : orderedHandlerChildren(rn.getChildren())) {
|
||||
ElkNode childElk = ctx.elkNodeMap.get(handler.getId());
|
||||
if (childElk != null) {
|
||||
children.add(extractPositionedNode(handler, childElk, rootNode, ctx));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (RouteNode child : rn.getChildren()) {
|
||||
ElkNode childElk = elkNodeMap.get(child.getId());
|
||||
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
|
||||
if (childElk != null) {
|
||||
children.add(extractPositionedNode(child, childElk, elkNodeMap,
|
||||
compoundNodeIds, compoundInfos, rootNode));
|
||||
children.add(extractPositionedNode(child, childElk, rootNode, ctx));
|
||||
}
|
||||
}
|
||||
}
|
||||
compoundInfos.put(rn.getId(), new CompoundInfo(rn.getId(), colorForType(rn.getType())));
|
||||
ctx.compoundInfos.put(rn.getId(), new CompoundInfo(rn.getId(), colorForType(rn.getType())));
|
||||
}
|
||||
|
||||
return new PositionedNode(
|
||||
@@ -758,7 +692,18 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
// ELK graph helpers
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/** Walk up to the top-level root of an ELK node's hierarchy. */
|
||||
/** Return handler children in section order: DO_FINALLY first, then DO_CATCH. */
|
||||
private static List<RouteNode> orderedHandlerChildren(List<RouteNode> children) {
|
||||
List<RouteNode> ordered = new ArrayList<>();
|
||||
for (RouteNode c : children) {
|
||||
if (c.getType() == NodeType.DO_FINALLY) ordered.add(c);
|
||||
}
|
||||
for (RouteNode c : children) {
|
||||
if (c.getType() == NodeType.DO_CATCH) ordered.add(c);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/** Check if 'child' is a descendant of 'ancestor' in the ELK node hierarchy. */
|
||||
private boolean isDescendantOf(ElkNode child, ElkNode ancestor) {
|
||||
ElkNode current = child.getParent();
|
||||
@@ -847,7 +792,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
return all;
|
||||
}
|
||||
|
||||
/** Recursively collect all node IDs from a tree. */
|
||||
/** Collect IDs of all RouteNode descendants (for handler separation). */
|
||||
private void collectDescendantIds(List<RouteNode> nodes, Set<String> ids) {
|
||||
for (RouteNode n : nodes) {
|
||||
@@ -878,4 +822,20 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
||||
) {}
|
||||
|
||||
private record CompoundInfo(String nodeId, Color color) {}
|
||||
|
||||
/** Mutable state accumulated during ELK graph construction and extraction. */
|
||||
private static class LayoutContext {
|
||||
final ElkGraphFactory factory;
|
||||
final Map<String, ElkNode> elkNodeMap = new HashMap<>();
|
||||
final Map<String, Color> nodeColors = new HashMap<>();
|
||||
final Set<String> compoundNodeIds = new HashSet<>();
|
||||
final Set<String> childNodeIds = new HashSet<>();
|
||||
final Set<String> doTryNodeIds = new HashSet<>();
|
||||
final Map<String, List<String>> doTrySectionOrder = new LinkedHashMap<>();
|
||||
final Map<String, CompoundInfo> compoundInfos = new HashMap<>();
|
||||
|
||||
LayoutContext(ElkGraphFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,98 @@ class ElkDiagramRendererTest {
|
||||
assertEquals(2, choiceNode.children().size(), "Choice node should have 2 children (when, otherwise)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a DO_TRY graph: from -> doTry(try: [process, log], doFinally: [cleanup], doCatch: [errorLog]) -> to
|
||||
*/
|
||||
private RouteGraph buildDoTryGraph() {
|
||||
RouteGraph graph = new RouteGraph("try-catch-route");
|
||||
graph.setExtractedAt(Instant.now());
|
||||
graph.setVersion(1);
|
||||
|
||||
RouteNode from = new RouteNode("node-1", NodeType.ENDPOINT, "timer:tick");
|
||||
RouteNode doTry = new RouteNode("node-2", NodeType.DO_TRY, "doTry");
|
||||
RouteNode process = new RouteNode("node-3", NodeType.PROCESSOR, "process");
|
||||
RouteNode log1 = new RouteNode("node-4", NodeType.LOG, "log:tryBody");
|
||||
RouteNode doFinally = new RouteNode("node-5", NodeType.DO_FINALLY, "doFinally");
|
||||
RouteNode cleanup = new RouteNode("node-6", NodeType.LOG, "log:cleanup");
|
||||
RouteNode doCatch = new RouteNode("node-7", NodeType.DO_CATCH, "doCatch");
|
||||
RouteNode errorLog = new RouteNode("node-8", NodeType.LOG, "log:error");
|
||||
RouteNode to = new RouteNode("node-9", NodeType.TO, "log:done");
|
||||
|
||||
doFinally.setChildren(List.of(cleanup));
|
||||
doCatch.setChildren(List.of(errorLog));
|
||||
doTry.setChildren(List.of(process, log1, doFinally, doCatch));
|
||||
|
||||
graph.setRoot(from);
|
||||
graph.setNodes(List.of(from, doTry, process, log1, doFinally, cleanup, doCatch, errorLog, to));
|
||||
graph.setEdges(List.of(
|
||||
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-3", "node-4", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-5", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-5", "node-6", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-7", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-7", "node-8", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-9", RouteEdge.EdgeType.FLOW)
|
||||
));
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
@Test
|
||||
void layoutJson_doTryGraph_sectionsInCorrectOrder() {
|
||||
DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());
|
||||
|
||||
assertNotNull(layout);
|
||||
|
||||
// Find the DO_TRY compound node
|
||||
PositionedNode doTryNode = layout.nodes().stream()
|
||||
.filter(n -> "node-2".equals(n.id()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("DO_TRY node not found"));
|
||||
|
||||
assertNotNull(doTryNode.children(), "DO_TRY should have children");
|
||||
assertFalse(doTryNode.children().isEmpty(), "DO_TRY should have non-empty children");
|
||||
|
||||
// Find sections by ID pattern
|
||||
PositionedNode tryBody = doTryNode.children().stream()
|
||||
.filter(n -> n.id() != null && n.id().contains("._try_body"))
|
||||
.findFirst().orElse(null);
|
||||
PositionedNode finallySection = doTryNode.children().stream()
|
||||
.filter(n -> "node-5".equals(n.id()))
|
||||
.findFirst().orElse(null);
|
||||
PositionedNode catchSection = doTryNode.children().stream()
|
||||
.filter(n -> "node-7".equals(n.id()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
assertNotNull(tryBody, "Try body wrapper should exist");
|
||||
assertNotNull(finallySection, "doFinally section should exist");
|
||||
assertNotNull(catchSection, "doCatch section should exist");
|
||||
|
||||
// Verify vertical order: tryBody.y < doFinally.y < doCatch.y
|
||||
assertTrue(tryBody.y() < finallySection.y(),
|
||||
"Try body (y=" + tryBody.y() + ") should be above doFinally (y=" + finallySection.y() + ")");
|
||||
assertTrue(finallySection.y() < catchSection.y(),
|
||||
"doFinally (y=" + finallySection.y() + ") should be above doCatch (y=" + catchSection.y() + ")");
|
||||
}
|
||||
|
||||
@Test
|
||||
void layoutJson_doTryGraph_sectionsHaveSameWidth() {
|
||||
DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());
|
||||
|
||||
PositionedNode doTryNode = layout.nodes().stream()
|
||||
.filter(n -> "node-2".equals(n.id()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("DO_TRY node not found"));
|
||||
|
||||
List<PositionedNode> sections = doTryNode.children();
|
||||
double firstWidth = sections.get(0).width();
|
||||
for (PositionedNode section : sections) {
|
||||
assertEquals(firstWidth, section.width(), 0.1,
|
||||
"All sections should have the same width, but " + section.id() + " differs");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void renderSvg_compoundGraph_producesValidSvg() {
|
||||
String svg = renderer.renderSvg(buildCompoundGraph());
|
||||
|
||||
Reference in New Issue
Block a user