From e2adc3adcdd1a62882bc6d29ece7e8542d3f5280 Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Wed, 10 Jul 2024 12:09:46 +0200 Subject: [PATCH 1/6] Adds a layout unzipping intermediate processor after crossing minmization. --- .../elk/alg/layered/GraphConfigurator.java | 8 + .../org/eclipse/elk/alg/layered/Layered.melk | 28 +++ .../eclipse/elk/alg/layered/graph/LNode.java | 6 +- .../IntermediateProcessorStrategy.java | 6 + .../unzipping/GeneralLayerUnzipper.java | 221 ++++++++++++++++++ .../options/LayerUnzippingStrategy.java | 23 ++ 6 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java create mode 100644 plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/LayerUnzippingStrategy.java diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/GraphConfigurator.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/GraphConfigurator.java index a773d42797..f6c99df03f 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/GraphConfigurator.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/GraphConfigurator.java @@ -297,6 +297,14 @@ private LayoutProcessorConfiguration getPhaseIndependentL : IntermediateProcessorStrategy.TWO_SIDED_GREEDY_SWITCH; configuration.addBefore(LayeredPhases.P4_NODE_PLACEMENT, internalGreedyType); } + + switch (lgraph.getProperty(LayeredOptions.CROSSING_MINIMIZATION_LAYER_UNZIPPING_STRATEGY)) { + case N_LAYERS: + configuration.addBefore(LayeredPhases.P4_NODE_PLACEMENT, IntermediateProcessorStrategy.LAYER_UNZIPPER); + break; + default: + break; + } // Wrapping of graphs switch (lgraph.getProperty(LayeredOptions.WRAPPING_STRATEGY)) { diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk index ec3c4a0b57..aa72e951e2 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk @@ -192,6 +192,8 @@ algorithm layered(LayeredLayoutProvider) { supports crossingMinimization.inLayerPredOf supports crossingMinimization.inLayerSuccOf supports layering.layerChoiceConstraint + supports crossingMinimization.layerUnzipping.strategy + supports crossingMinimization.layerUnzipping.layerSplit supports crossingMinimization.positionChoiceConstraint supports org.eclipse.elk.interactiveLayout supports layering.layerId @@ -368,6 +370,32 @@ group crossingMinimization { targets parents requires org.eclipse.elk.hierarchyHandling == HierarchyHandling.INCLUDE_CHILDREN } + + group layerUnzipping { + + option strategy: LayerUnzippingStrategy { + label "Layer Unzipping Strategy" + description + "The strategy to use for unzipping a layer into multiple sublayers while maintaining + the existing ordering of nodes and edges after crossing minimization. The default + value is 'NONE'." + default = LayerUnzippingStrategy.NONE + targets parents + } + + advanced option layerSplit: Integer { + label "Unzipping Layer Split" + description + "Defines the number of sublayers to split a layer into when using the N_LAYERS strategy. + The property can be set to the first node in a layer, which then applies the property + for the layer the node belongs to." + default = 2 + targets nodes + lowerBound = 1 + requires layerUnzipping.strategy == LayerUnzippingStrategy.N_LAYERS + } + } + group greedySwitch { diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/LNode.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/LNode.java index ea3fa7b73e..31d7a17ef1 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/LNode.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/LNode.java @@ -48,7 +48,11 @@ public static enum NodeType { /** a dummy node to represent a mid-label on an edge. */ LABEL, /** a dummy node representing a breaking point used to 'wrap' graphs. */ - BREAKING_POINT; + BREAKING_POINT, + /** a dummy node serving as a placeholder to reserve space*/ + PLACEHOLDER, + /** a placeholder node that can't be shifted */ + NONSHIFTING_PLACEHOLDER; /** * Return the color used when writing debug output graphs. The colors are given as strings of diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/IntermediateProcessorStrategy.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/IntermediateProcessorStrategy.java index 6bdee42b1f..e4947be281 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/IntermediateProcessorStrategy.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/IntermediateProcessorStrategy.java @@ -11,6 +11,7 @@ import org.eclipse.elk.alg.layered.graph.LGraph; import org.eclipse.elk.alg.layered.intermediate.compaction.HorizontalGraphCompactor; +import org.eclipse.elk.alg.layered.intermediate.unzipping.GeneralLayerUnzipper; import org.eclipse.elk.alg.layered.intermediate.wrapping.BreakingPointInserter; import org.eclipse.elk.alg.layered.intermediate.wrapping.BreakingPointProcessor; import org.eclipse.elk.alg.layered.intermediate.wrapping.BreakingPointRemover; @@ -95,6 +96,8 @@ public enum IntermediateProcessorStrategy implements ILayoutProcessorFactory create() { case SELF_LOOP_PORT_RESTORER: return new SelfLoopPortRestorer(); + + case LAYER_UNZIPPER: + return new GeneralLayerUnzipper(); case SELF_LOOP_POSTPROCESSOR: return new SelfLoopPostProcessor(); diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java new file mode 100644 index 0000000000..0661568192 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java @@ -0,0 +1,221 @@ +/******************************************************************************* + * Copyright (c) 2024 Kiel University and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.elk.alg.layered.intermediate.unzipping; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import org.eclipse.elk.alg.layered.graph.LEdge; +import org.eclipse.elk.alg.layered.graph.LGraph; +import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.graph.LNode.NodeType; +import org.eclipse.elk.alg.layered.graph.Layer; +import org.eclipse.elk.alg.layered.intermediate.LongEdgeSplitter; +import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.options.LayeredOptions; +import org.eclipse.elk.core.alg.ILayoutProcessor; +import org.eclipse.elk.core.options.PortConstraints; +import org.eclipse.elk.core.util.IElkProgressMonitor; +import org.eclipse.elk.core.util.Pair; + +import com.google.common.collect.Lists; + +/** + * Divides nodes up between layers to create a more compact final layout. + * Reads the property of each layer to determine how many sub-layers it + * should be split into. + * + *
+ *
Preconditions:
+ *
A layered graph whose node order has been decided.
+ *
Postconditions:
+ *
Layers are split up into multiple layers with the nodes alternating between them.
+ *
Slots:
+ *
Before phase 4.
+ *
Same-slot dependencies:
+ *
None
+ *
+ * + */ +public class GeneralLayerUnzipper implements ILayoutProcessor { + + @Override + public void process (LGraph graph, IElkProgressMonitor progressMonitor) { + + processLayerSplitProperty(graph); + + int insertionLayerOffset = 1; + List> newLayers = new ArrayList<>(); + for (int i = 0; i < graph.getLayers().size(); i++) { + + int N = graph.getLayers().get(i).getProperty(LayeredOptions.CROSSING_MINIMIZATION_LAYER_UNZIPPING_LAYER_SPLIT); + + // only split if there are more nodes than the resulting sub-layers + // an alternative would be to reduce N for this layer, this may or may + // not be desirable + if (graph.getLayers().get(i).getNodes().size() > N) { + + List subLayers = new ArrayList<>(); + // add current layer as first sub-layer + subLayers.add(graph.getLayers().get(i)); + for (int j = 0; j < N - 1; j++) { + Layer newLayer = new Layer(graph); + newLayers.add(new Pair<>(newLayer, i+j+insertionLayerOffset)); + subLayers.add(newLayer); + } + insertionLayerOffset += N - 1; + + int nodesInLayer = subLayers.get(0).getNodes().size(); + for (int j = 0, nodeIndex = 0; j < nodesInLayer; j++, nodeIndex++) { + LNode node = subLayers.get(0).getNodes().get(nodeIndex); + if (node.getType() != NodeType.NONSHIFTING_PLACEHOLDER) { + nodeIndex += shiftNode(graph, subLayers, j % N, nodeIndex); + } else { + j -= 1; + } + + } + } + } + for (Pair newLayer : newLayers) { + graph.getLayers().add(newLayer.getSecond(), newLayer.getFirst()); + } + + // remove unconnected placeholder nodes + for (Layer layer : graph.getLayers()) { + ListIterator nodeIterator = layer.getNodes().listIterator(); + while (nodeIterator.hasNext()) { + LNode node = nodeIterator.next(); + if (node.getType() == NodeType.PLACEHOLDER || node.getType() == NodeType.NONSHIFTING_PLACEHOLDER) { + nodeIterator.remove(); + } + } + } + + + } + + /** + * checks the layer split property of the first node in a layer and copies the property to the layer + * @param graph + */ + private void processLayerSplitProperty(LGraph graph) { + for (Layer layer : graph.getLayers()) { + layer.setProperty(LayeredOptions.CROSSING_MINIMIZATION_LAYER_UNZIPPING_LAYER_SPLIT, + layer.getNodes().get(0).getProperty(LayeredOptions.CROSSING_MINIMIZATION_LAYER_UNZIPPING_LAYER_SPLIT)); + } + + } + + /** + * Shifts a node from one layer to another and adds dummy nodes for the long edges this introduces. + * @param graph + * @param subLayers + * @param targetLayer + * @param nodeIndex + * + * @return the number new nodes in the original layer + */ + private int shiftNode(LGraph graph, List subLayers, int targetLayer, int nodeIndex) { + LNode node = subLayers.get(0).getNodes().get(nodeIndex); + if (targetLayer > 0){ + node.setLayer(subLayers.get(targetLayer)); + } + // handle incoming edges and preceding layers + int edgeCount = 0; + // If there are no incoming edges, the nodeindex will have to be decreased by one + boolean noIncomingEdges = true; + List reversedIncomingEdges = Lists.reverse(Lists.newArrayList(node.getIncomingEdges())); + for (LEdge incomingEdge : reversedIncomingEdges) { // backwards? + noIncomingEdges = false; + LEdge nextEdgeToSplit = incomingEdge; + for (int layerIndex = 0; layerIndex < targetLayer; layerIndex++) { + LNode dummyNode = createDummyNode(graph, nextEdgeToSplit); + if (nodeIndex + edgeCount > subLayers.get(layerIndex).getNodes().size()) { + dummyNode.setLayer(subLayers.get(layerIndex)); + } else { + dummyNode.setLayer(nodeIndex + edgeCount, subLayers.get(layerIndex)); + } + nextEdgeToSplit = LongEdgeSplitter.splitEdge(nextEdgeToSplit, dummyNode); + } + if (targetLayer > 0) { + edgeCount += 1; + } + } + + // create unconnected dummy nodes to fill the layers if there are no incoming edges + if (noIncomingEdges) { + for (int layerIndex = 0; layerIndex < targetLayer; layerIndex++) { + LNode dummyNode = new LNode(graph); + dummyNode.setType(NodeType.PLACEHOLDER); + if (nodeIndex + edgeCount > subLayers.get(layerIndex).getNodes().size()) { + dummyNode.setLayer(subLayers.get(layerIndex)); + } else { + dummyNode.setLayer(nodeIndex + edgeCount, subLayers.get(layerIndex)); + } + } + if (targetLayer > 0) { + edgeCount += 1; + } + } + + // handle outgoing edges and following layers + boolean extraEdge = false; + for (LEdge outgoingEdge : node.getOutgoingEdges()) { + LEdge nextEdgeToSplit = outgoingEdge; + for (int layerIndex = targetLayer + 1; layerIndex < subLayers.size(); layerIndex++) { + LNode dummyNode = createDummyNode(graph, nextEdgeToSplit); + dummyNode.setLayer(subLayers.get(layerIndex)); + nextEdgeToSplit = LongEdgeSplitter.splitEdge(nextEdgeToSplit, dummyNode); + } + + for (int layerIndex = 0; layerIndex <= targetLayer; layerIndex++) { + if (extraEdge) { + // add a placeholder beneath node's old position so that later + LNode placeholder = new LNode(graph); + placeholder.setType(NodeType.NONSHIFTING_PLACEHOLDER); + + if (nodeIndex + 1 > subLayers.get(layerIndex).getNodes().size()) { + placeholder.setLayer(subLayers.get(layerIndex)); + } else { + placeholder.setLayer(nodeIndex + 1, subLayers.get(layerIndex)); + } + } + } + + if (extraEdge) { + edgeCount += 1; + } + + extraEdge = true; + } + + if (edgeCount > 0) { + return edgeCount - 1; + } else { + return 0; + } + } + + /** + * @param graph + * @param nextEdgeToSplit + * @return + */ + private LNode createDummyNode(LGraph graph, LEdge nextEdgeToSplit) { + LNode dummyNode = new LNode(graph); + dummyNode.setType(NodeType.LONG_EDGE); + dummyNode.setProperty(InternalProperties.ORIGIN, nextEdgeToSplit); + dummyNode.setProperty(LayeredOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_POS); + return dummyNode; + } + +} diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/LayerUnzippingStrategy.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/LayerUnzippingStrategy.java new file mode 100644 index 0000000000..ab5c148091 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/LayerUnzippingStrategy.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2024 Kiel University and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.elk.alg.layered.options; + +/** + * Strategies for unzipping layers i.e. splitting up nodes into multiple layers to + * create more compact drawings. + * + */ +public enum LayerUnzippingStrategy { + + NONE, + /** Splits all layers with more than two nodes into two layers. */ + N_LAYERS; + +} From e1e9e4c6b26e0df512528d1a066d9c225a5f71e1 Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Wed, 10 Jul 2024 12:33:32 +0200 Subject: [PATCH 2/6] remove old comment --- .../layered/intermediate/unzipping/GeneralLayerUnzipper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java index 0661568192..da389bb092 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java @@ -134,7 +134,7 @@ private int shiftNode(LGraph graph, List subLayers, int targetLayer, int // If there are no incoming edges, the nodeindex will have to be decreased by one boolean noIncomingEdges = true; List reversedIncomingEdges = Lists.reverse(Lists.newArrayList(node.getIncomingEdges())); - for (LEdge incomingEdge : reversedIncomingEdges) { // backwards? + for (LEdge incomingEdge : reversedIncomingEdges) { noIncomingEdges = false; LEdge nextEdgeToSplit = incomingEdge; for (int layerIndex = 0; layerIndex < targetLayer; layerIndex++) { From f3330096d0695f68ce5c1d3c1bc44a81bcb7887d Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Thu, 11 Jul 2024 12:42:56 +0200 Subject: [PATCH 3/6] Properties of layer unzipping Add property to control whether the alternating pattern resets after long edges. Search for any node in a layer that sets a property and apply the first found to the layer of that node. Move Properties from crossing minimization group to their own group. --- .../elk/alg/layered/GraphConfigurator.java | 2 +- .../org/eclipse/elk/alg/layered/Layered.melk | 73 ++++++++++++------- .../unzipping/GeneralLayerUnzipper.java | 32 ++++++-- 3 files changed, 73 insertions(+), 34 deletions(-) diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/GraphConfigurator.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/GraphConfigurator.java index f6c99df03f..3f861f8d4f 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/GraphConfigurator.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/GraphConfigurator.java @@ -298,7 +298,7 @@ private LayoutProcessorConfiguration getPhaseIndependentL configuration.addBefore(LayeredPhases.P4_NODE_PLACEMENT, internalGreedyType); } - switch (lgraph.getProperty(LayeredOptions.CROSSING_MINIMIZATION_LAYER_UNZIPPING_STRATEGY)) { + switch (lgraph.getProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY)) { case N_LAYERS: configuration.addBefore(LayeredPhases.P4_NODE_PLACEMENT, IntermediateProcessorStrategy.LAYER_UNZIPPER); break; diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk index aa72e951e2..2a62ff3980 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk @@ -90,6 +90,11 @@ algorithm layered(LayeredLayoutProvider) { supports wrapping.multiEdge.distancePenalty supports wrapping.multiEdge.improveWrappedEdges + // layer unzipping + supports layerUnzipping.strategy + supports layerUnzipping.layerSplit + supports layerUnzipping.resetOnLongEdges + // flexible nodes during node placement supports nodePlacement.networkSimplex.nodeFlexibility supports nodePlacement.networkSimplex.nodeFlexibility.^default @@ -192,8 +197,6 @@ algorithm layered(LayeredLayoutProvider) { supports crossingMinimization.inLayerPredOf supports crossingMinimization.inLayerSuccOf supports layering.layerChoiceConstraint - supports crossingMinimization.layerUnzipping.strategy - supports crossingMinimization.layerUnzipping.layerSplit supports crossingMinimization.positionChoiceConstraint supports org.eclipse.elk.interactiveLayout supports layering.layerId @@ -370,32 +373,6 @@ group crossingMinimization { targets parents requires org.eclipse.elk.hierarchyHandling == HierarchyHandling.INCLUDE_CHILDREN } - - group layerUnzipping { - - option strategy: LayerUnzippingStrategy { - label "Layer Unzipping Strategy" - description - "The strategy to use for unzipping a layer into multiple sublayers while maintaining - the existing ordering of nodes and edges after crossing minimization. The default - value is 'NONE'." - default = LayerUnzippingStrategy.NONE - targets parents - } - - advanced option layerSplit: Integer { - label "Unzipping Layer Split" - description - "Defines the number of sublayers to split a layer into when using the N_LAYERS strategy. - The property can be set to the first node in a layer, which then applies the property - for the layer the node belongs to." - default = 2 - targets nodes - lowerBound = 1 - requires layerUnzipping.strategy == LayerUnzippingStrategy.N_LAYERS - } - } - group greedySwitch { @@ -965,6 +942,46 @@ group wrapping { } +/* ------------------------ + * Layer Unzipping + * ------------------------*/ +group layerUnzipping { + + option strategy: LayerUnzippingStrategy { + label "Layer Unzipping Strategy" + description + "The strategy to use for unzipping a layer into multiple sublayers while maintaining + the existing ordering of nodes and edges after crossing minimization. The default + value is 'NONE'." + default = LayerUnzippingStrategy.NONE + targets parents + } + + advanced option layerSplit: Integer { + label "Unzipping Layer Split" + description + "Defines the number of sublayers to split a layer into when using the N_LAYERS strategy. + The property can be set to the first node in a layer, which then applies the property + for the layer the node belongs to." + default = 2 + targets nodes + lowerBound = 1 + requires layerUnzipping.strategy == LayerUnzippingStrategy.N_LAYERS + } + + option resetOnLongEdges: Boolean { + label "Reset Alternation on Long Edges" + description + "If set to true, nodes will always be placed in the first sublayer after a long edge. + Otherwise long edge dummies are treated the same as regular nodes. The default value is true. + The property can be set to the first node in a layer, which then applies the property + for the layer the node belongs to." + default = true + targets nodes + requires layerUnzipping.strategy == LayerUnzippingStrategy.N_LAYERS + } + } + /* ------------------------ * edgeLabels * ------------------------*/ diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java index da389bb092..2ab0021309 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java @@ -56,7 +56,8 @@ public void process (LGraph graph, IElkProgressMonitor progressMonitor) { List> newLayers = new ArrayList<>(); for (int i = 0; i < graph.getLayers().size(); i++) { - int N = graph.getLayers().get(i).getProperty(LayeredOptions.CROSSING_MINIMIZATION_LAYER_UNZIPPING_LAYER_SPLIT); + int N = graph.getLayers().get(i).getProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT); + boolean resetOnLongEdges = graph.getLayers().get(i).getProperty(LayeredOptions.LAYER_UNZIPPING_RESET_ON_LONG_EDGES); // only split if there are more nodes than the resulting sub-layers // an alternative would be to reduce N for this layer, this may or may @@ -74,12 +75,17 @@ public void process (LGraph graph, IElkProgressMonitor progressMonitor) { insertionLayerOffset += N - 1; int nodesInLayer = subLayers.get(0).getNodes().size(); - for (int j = 0, nodeIndex = 0; j < nodesInLayer; j++, nodeIndex++) { + for (int j = 0, nodeIndex = 0, targetLayer = 0; j < nodesInLayer; j++, nodeIndex++, targetLayer++) { LNode node = subLayers.get(0).getNodes().get(nodeIndex); if (node.getType() != NodeType.NONSHIFTING_PLACEHOLDER) { - nodeIndex += shiftNode(graph, subLayers, j % N, nodeIndex); + nodeIndex += shiftNode(graph, subLayers, targetLayer % N, nodeIndex); } else { j -= 1; + targetLayer -= 1; + } + if (resetOnLongEdges && node.getType() == NodeType.LONG_EDGE) { + // reset next iterations target layer to 0 + targetLayer = -1; } } @@ -109,8 +115,24 @@ public void process (LGraph graph, IElkProgressMonitor progressMonitor) { */ private void processLayerSplitProperty(LGraph graph) { for (Layer layer : graph.getLayers()) { - layer.setProperty(LayeredOptions.CROSSING_MINIMIZATION_LAYER_UNZIPPING_LAYER_SPLIT, - layer.getNodes().get(0).getProperty(LayeredOptions.CROSSING_MINIMIZATION_LAYER_UNZIPPING_LAYER_SPLIT)); + boolean setLayerSplit = false; + boolean setResetOnLongEdges = false; + for (LNode node : layer.getNodes()) { + if (!setLayerSplit && node.hasProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT)) { + layer.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, + node.getProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT)); + setLayerSplit = true; + } + if (!setResetOnLongEdges && node.hasProperty(LayeredOptions.LAYER_UNZIPPING_RESET_ON_LONG_EDGES)) { + layer.setProperty(LayeredOptions.LAYER_UNZIPPING_RESET_ON_LONG_EDGES, + node.getProperty(LayeredOptions.LAYER_UNZIPPING_RESET_ON_LONG_EDGES)); + setResetOnLongEdges = true; + } + if (setLayerSplit && setResetOnLongEdges) { + // all options have been set and we can skip the remaining nodes of the layer + break; + } + } } } From 5066349c8b81b899a6509033df66adedca61aab4 Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Thu, 11 Jul 2024 13:48:11 +0200 Subject: [PATCH 4/6] Improve documentation of layer unzipper. --- .../src/org/eclipse/elk/alg/layered/graph/LNode.java | 5 +++-- .../layered/intermediate/unzipping/GeneralLayerUnzipper.java | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/LNode.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/LNode.java index 31d7a17ef1..5baf86f26b 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/LNode.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/LNode.java @@ -49,9 +49,10 @@ public static enum NodeType { LABEL, /** a dummy node representing a breaking point used to 'wrap' graphs. */ BREAKING_POINT, - /** a dummy node serving as a placeholder to reserve space*/ + /** a dummy node serving as a placeholder to reserve space when 'unzipping' graphs. + * this is used when there are no edges. */ PLACEHOLDER, - /** a placeholder node that can't be shifted */ + /** a placeholder node that can't be shifted when 'unzipping' graphs. this is used in front of extra edges. */ NONSHIFTING_PLACEHOLDER; /** diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java index 2ab0021309..a6eb8d8058 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java @@ -37,7 +37,9 @@ *
Preconditions:
*
A layered graph whose node order has been decided.
*
Postconditions:
- *
Layers are split up into multiple layers with the nodes alternating between them.
+ *
Layers are split up into multiple layers with the nodes alternating between them. For example, if layerSplit + * is set to 3 and there are 5 nodes in a layer, then node 1 is placed in sublayer 1, node 2 in sublayer 2, node 3 in + * sublayer 3, node 4 in sublayer 1 and node 5 in sublayer 2.
*
Slots:
*
Before phase 4.
*
Same-slot dependencies:
From 1200047ef2eb773faf2e1ce1b2b47153ad866c7c Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Mon, 15 Jul 2024 16:46:38 +0200 Subject: [PATCH 5/6] Add tests for n-layer unzipper. --- .../unzipping/GeneralLayerUnzipper.java | 2 +- .../GeneralLayerUnzipperTest.java | 728 ++++++++++++++++++ 2 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 test/org.eclipse.elk.alg.layered.test/src/org/eclipse/elk/alg/layered/intermediate/GeneralLayerUnzipperTest.java diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java index a6eb8d8058..a8390d10f6 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java @@ -132,7 +132,7 @@ private void processLayerSplitProperty(LGraph graph) { } if (setLayerSplit && setResetOnLongEdges) { // all options have been set and we can skip the remaining nodes of the layer - break; + return; } } } diff --git a/test/org.eclipse.elk.alg.layered.test/src/org/eclipse/elk/alg/layered/intermediate/GeneralLayerUnzipperTest.java b/test/org.eclipse.elk.alg.layered.test/src/org/eclipse/elk/alg/layered/intermediate/GeneralLayerUnzipperTest.java new file mode 100644 index 0000000000..7531066abc --- /dev/null +++ b/test/org.eclipse.elk.alg.layered.test/src/org/eclipse/elk/alg/layered/intermediate/GeneralLayerUnzipperTest.java @@ -0,0 +1,728 @@ +/******************************************************************************* + * Copyright (c) 2024 Kiel University and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.elk.alg.layered.intermediate; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.eclipse.elk.alg.layered.LayeredLayoutProvider; +import org.eclipse.elk.alg.layered.options.LayerUnzippingStrategy; +import org.eclipse.elk.alg.layered.options.LayeredOptions; +import org.eclipse.elk.alg.layered.options.OrderingStrategy; +import org.eclipse.elk.alg.test.PlainJavaInitialization; +import org.eclipse.elk.core.util.NullElkProgressMonitor; +import org.eclipse.elk.graph.ElkNode; +import org.eclipse.elk.graph.util.ElkGraphUtil; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests the general n-way layer unzipper. + * + */ +public class GeneralLayerUnzipperTest { + + LayeredLayoutProvider layeredLayout; + + @Before + public void setUp() { + PlainJavaInitialization.initializePlainJavaLayout(); + layeredLayout = new LayeredLayoutProvider(); + } + + /** + * Tests splitting a layer of three nodes into two layers in an A-B-A pattern. + */ + @Test + public void simpleTwoSplit() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode nodeFinal = ElkGraphUtil.createNode(graph); + nodeFinal.setWidth(30); + nodeFinal.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node4); + ElkGraphUtil.createSimpleEdge(node2, nodeFinal); + ElkGraphUtil.createSimpleEdge(node3, nodeFinal); + ElkGraphUtil.createSimpleEdge(node4, nodeFinal); + + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + assertTrue(node2.getX() < node3.getX()); + assertTrue(node4.getX() < node3.getX()); + assertTrue(node3.getX() < nodeFinal.getX()); + } + + /** + * Tests splitting a layer of 4 nodes into 3 layers in an A-B-C-A pattern. + */ + @Test + public void simpleThreeSplit() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode node5 = ElkGraphUtil.createNode(graph); + node5.setWidth(30); + node5.setHeight(30); + + ElkNode nodeFinal = ElkGraphUtil.createNode(graph); + nodeFinal.setWidth(30); + nodeFinal.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node4); + ElkGraphUtil.createSimpleEdge(node1, node5); + ElkGraphUtil.createSimpleEdge(node2, nodeFinal); + ElkGraphUtil.createSimpleEdge(node3, nodeFinal); + ElkGraphUtil.createSimpleEdge(node4, nodeFinal); + ElkGraphUtil.createSimpleEdge(node5, nodeFinal); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 3); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node5.getX()); + assertTrue(node2.getX() < node3.getX()); + assertTrue(node3.getX() < node4.getX()); + assertTrue(node5.getX() < node4.getX()); + assertTrue(node4.getX() < nodeFinal.getX()); + } + + /** + * Tests the case that the nodes of the split layer have no further outgoing edges i.e. there is no further + * connected layer after them. + */ + @Test + public void danglingOutgoing() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode node5 = ElkGraphUtil.createNode(graph); + node5.setWidth(30); + node5.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node4); + ElkGraphUtil.createSimpleEdge(node1, node5); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + assertTrue(node2.getX() < node3.getX()); + assertTrue(node2.getX() < node5.getX()); + assertTrue(node4.getX() < node3.getX()); + assertTrue(node4.getX() < node5.getX()); + } + + /** + * Tests the case that there is no connected layer leading into the layer that is split. + */ + @Test + public void danglingIncoming() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode node5 = ElkGraphUtil.createNode(graph); + node5.setWidth(30); + node5.setHeight(30); + + ElkNode nodeFinal = ElkGraphUtil.createNode(graph); + nodeFinal.setWidth(30); + nodeFinal.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, nodeFinal); + ElkGraphUtil.createSimpleEdge(node2, nodeFinal); + ElkGraphUtil.createSimpleEdge(node3, nodeFinal); + ElkGraphUtil.createSimpleEdge(node4, nodeFinal); + ElkGraphUtil.createSimpleEdge(node5, nodeFinal); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + assertTrue(node3.getX() < node2.getX()); + assertTrue(node3.getX() < node4.getX()); + assertTrue(node5.getX() < node2.getX()); + assertTrue(node5.getX() < node4.getX()); + + assertTrue(node2.getX() < nodeFinal.getX()); + assertTrue(node4.getX() < nodeFinal.getX()); + } + + /** + * Tests a two layer graph being split into a four layer graph. + */ + @Test + public void multipleLayersSplit() { + ElkNode graph = ElkGraphUtil.createGraph(); + + // LAYER 1 + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + // LAYER 2 + ElkNode node21 = ElkGraphUtil.createNode(graph); + node21.setWidth(30); + node21.setHeight(30); + + ElkNode node22 = ElkGraphUtil.createNode(graph); + node22.setWidth(30); + node22.setHeight(30); + + ElkNode node23 = ElkGraphUtil.createNode(graph); + node23.setWidth(30); + node23.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node21); + + ElkGraphUtil.createSimpleEdge(node2, node21); + ElkGraphUtil.createSimpleEdge(node2, node22); + + ElkGraphUtil.createSimpleEdge(node3, node21); + ElkGraphUtil.createSimpleEdge(node3, node22); + ElkGraphUtil.createSimpleEdge(node4, node22); + ElkGraphUtil.createSimpleEdge(node4, node23); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node1.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + node21.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + assertTrue(node3.getX() < node2.getX()); + assertTrue(node3.getX() < node4.getX()); + + assertTrue(node2.getX() < node21.getX()); + assertTrue(node2.getX() < node23.getX()); + assertTrue(node4.getX() < node21.getX()); + assertTrue(node4.getX() < node23.getX()); + + assertTrue(node21.getX() < node22.getX()); + assertTrue(node23.getX() < node22.getX()); + } + + /** + * Tests the case that there are multiple incoming edges connecting to a node that is in a layer being split. + */ + @Test + public void multipleIncomingEdges() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode nodeFinal = ElkGraphUtil.createNode(graph); + nodeFinal.setWidth(30); + nodeFinal.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node4); + ElkGraphUtil.createSimpleEdge(node2, nodeFinal); + ElkGraphUtil.createSimpleEdge(node3, nodeFinal); + ElkGraphUtil.createSimpleEdge(node4, nodeFinal); + + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + assertTrue(node2.getX() < node3.getX()); + assertTrue(node4.getX() < node3.getX()); + assertTrue(node3.getX() < nodeFinal.getX()); + } + + /** + * Tests the case that there are multiple outgoing edges connecting to a node that is in a layer being split. + */ + @Test + public void multipleOutgoingEdges() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode nodeFinal = ElkGraphUtil.createNode(graph); + nodeFinal.setWidth(30); + nodeFinal.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node4); + ElkGraphUtil.createSimpleEdge(node2, nodeFinal); + ElkGraphUtil.createSimpleEdge(node2, nodeFinal); + ElkGraphUtil.createSimpleEdge(node2, nodeFinal); + ElkGraphUtil.createSimpleEdge(node2, nodeFinal); + ElkGraphUtil.createSimpleEdge(node3, nodeFinal); + ElkGraphUtil.createSimpleEdge(node4, nodeFinal); + ElkGraphUtil.createSimpleEdge(node4, nodeFinal); + ElkGraphUtil.createSimpleEdge(node4, nodeFinal); + ElkGraphUtil.createSimpleEdge(node4, nodeFinal); + + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + assertTrue(node2.getX() < node3.getX()); + assertTrue(node4.getX() < node3.getX()); + assertTrue(node3.getX() < nodeFinal.getX()); + } + + /** + * Tests the case that some nodes are not connected to a preceding layer and some are. + */ + @Test + public void mixedDanglingIncoming() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode node5 = ElkGraphUtil.createNode(graph); + node5.setWidth(30); + node5.setHeight(30); + + ElkNode node6 = ElkGraphUtil.createNode(graph); + node6.setWidth(30); + node6.setHeight(30); + + ElkNode node7 = ElkGraphUtil.createNode(graph); + node7.setWidth(30); + node7.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node4); + + ElkGraphUtil.createSimpleEdge(node2, node5); + ElkGraphUtil.createSimpleEdge(node3, node5); + ElkGraphUtil.createSimpleEdge(node3, node6); + ElkGraphUtil.createSimpleEdge(node4, node7); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + node5.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + + assertTrue(node2.getX() < node3.getX()); + assertTrue(node4.getX() < node3.getX()); + + assertTrue(node3.getX() < node5.getX()); + assertTrue(node3.getX() < node7.getX()); + + assertTrue(node5.getX() < node6.getX()); + assertTrue(node7.getX() < node6.getX()); + } + + /** + * Tests the case that some nodes are not connected to a following layer and some are. + */ + @Test + public void mixedDanglingOutgoing() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode node5 = ElkGraphUtil.createNode(graph); + node5.setWidth(30); + node5.setHeight(30); + + ElkNode node6 = ElkGraphUtil.createNode(graph); + node6.setWidth(30); + node6.setHeight(30); + + ElkNode node7 = ElkGraphUtil.createNode(graph); + node7.setWidth(30); + node7.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node3); + ElkGraphUtil.createSimpleEdge(node1, node4); + + ElkGraphUtil.createSimpleEdge(node3, node5); + ElkGraphUtil.createSimpleEdge(node3, node6); + ElkGraphUtil.createSimpleEdge(node4, node7); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + node5.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + + assertTrue(node2.getX() < node3.getX()); + assertTrue(node4.getX() < node3.getX()); + + assertTrue(node3.getX() < node5.getX()); + assertTrue(node3.getX() < node7.getX()); + + assertTrue(node5.getX() < node6.getX()); + assertTrue(node7.getX() < node6.getX()); + } + + /** + * Tests the case that one layer does a 2-split and another layer does a 3-split. + */ + @Test + public void mixedTwoThreeLayerSplit() { + ElkNode graph = ElkGraphUtil.createGraph(); + + // LAYER 1 + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + // LAYER 2 + ElkNode node21 = ElkGraphUtil.createNode(graph); + node21.setWidth(30); + node21.setHeight(30); + + ElkNode node22 = ElkGraphUtil.createNode(graph); + node22.setWidth(30); + node22.setHeight(30); + + ElkNode node23 = ElkGraphUtil.createNode(graph); + node23.setWidth(30); + node23.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node21); + + ElkGraphUtil.createSimpleEdge(node2, node21); + ElkGraphUtil.createSimpleEdge(node2, node22); + + ElkGraphUtil.createSimpleEdge(node3, node21); + ElkGraphUtil.createSimpleEdge(node3, node22); + ElkGraphUtil.createSimpleEdge(node4, node22); + ElkGraphUtil.createSimpleEdge(node4, node23); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node1.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 3); + node21.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node2.getX() < node3.getX()); + assertTrue(node4.getX() < node2.getX()); + + assertTrue(node3.getX() < node21.getX()); + assertTrue(node3.getX() < node23.getX()); + + assertTrue(node21.getX() < node22.getX()); + assertTrue(node23.getX() < node22.getX()); + } + + /** + * Tests that the staggered placement of the nodes resets when long edge crosses the layer. + */ + @Test + public void resetOnLongEdges() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode node5 = ElkGraphUtil.createNode(graph); + node5.setWidth(30); + node5.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node3); + // long edge + ElkGraphUtil.createSimpleEdge(node1, node5); + ElkGraphUtil.createSimpleEdge(node1, node4); + + ElkGraphUtil.createSimpleEdge(node4, node5); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_RESET_ON_LONG_EDGES, true); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node4.getX()); + assertTrue(node2.getX() < node3.getX()); + assertTrue(node3.getX() < node5.getX()); + assertTrue(node4.getX() < node3.getX()); + } + + /** + * Tests the case that the resetOnLongEdges option is disabled. In this case the staggering should not be reset. + */ + @Test + public void noResetOnLongEdges() { + ElkNode graph = ElkGraphUtil.createGraph(); + + ElkNode node1 = ElkGraphUtil.createNode(graph); + node1.setWidth(30); + node1.setHeight(30); + + + ElkNode node2 = ElkGraphUtil.createNode(graph); + node2.setWidth(30); + node2.setHeight(30); + + ElkNode node3 = ElkGraphUtil.createNode(graph); + node3.setWidth(30); + node3.setHeight(30); + + ElkNode node4 = ElkGraphUtil.createNode(graph); + node4.setWidth(30); + node4.setHeight(30); + + ElkNode node5 = ElkGraphUtil.createNode(graph); + node5.setWidth(30); + node5.setHeight(30); + + ElkNode node6 = ElkGraphUtil.createNode(graph); + node6.setWidth(30); + node6.setHeight(30); + + ElkGraphUtil.createSimpleEdge(node1, node2); + ElkGraphUtil.createSimpleEdge(node1, node3); + // long edge + ElkGraphUtil.createSimpleEdge(node1, node5); + ElkGraphUtil.createSimpleEdge(node1, node4); + + ElkGraphUtil.createSimpleEdge(node4, node5); + ElkGraphUtil.createSimpleEdge(node1, node6); + + graph.setProperty(LayeredOptions.LAYER_UNZIPPING_STRATEGY, LayerUnzippingStrategy.N_LAYERS); + graph.setProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy.PREFER_EDGES); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_LAYER_SPLIT, 2); + node2.setProperty(LayeredOptions.LAYER_UNZIPPING_RESET_ON_LONG_EDGES, false); + + layeredLayout.layout(graph, new NullElkProgressMonitor()); + + assertTrue(node1.getX() < node2.getX()); + assertTrue(node1.getX() < node6.getX()); + assertTrue(node2.getX() < node3.getX()); + assertTrue(node2.getX() < node4.getX()); + assertTrue(node3.getX() < node5.getX()); + assertTrue(node4.getX() < node5.getX()); + assertTrue(node6.getX() < node3.getX()); + assertTrue(node6.getX() < node4.getX()); + } +} From c4c6206c7a6b2c93dfdc4cc165a3286edd67d9b9 Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Tue, 16 Jul 2024 17:33:11 +0200 Subject: [PATCH 6/6] Update GeneralLayerUnzipper.java --- .../alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java index a8390d10f6..a054153719 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/unzipping/GeneralLayerUnzipper.java @@ -230,6 +230,7 @@ private int shiftNode(LGraph graph, List subLayers, int targetLayer, int } /** + * Creates a dummy node for an edge that should be split into a long edge. * @param graph * @param nextEdgeToSplit * @return