Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a layout unzipping intermediate processor after crossing minimization #1052

Merged
merged 6 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,14 @@ private LayoutProcessorConfiguration<LayeredPhases, LGraph> getPhaseIndependentL
: IntermediateProcessorStrategy.TWO_SIDED_GREEDY_SWITCH;
configuration.addBefore(LayeredPhases.P4_NODE_PLACEMENT, internalGreedyType);
}

switch (lgraph.getProperty(LayeredOptions.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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -937,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
* ------------------------*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ 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 when 'unzipping' graphs.
* this is used when there are no edges. */
PLACEHOLDER,
/** a placeholder node that can't be shifted when 'unzipping' graphs. this is used in front of extra edges. */
NONSHIFTING_PLACEHOLDER;

/**
* Return the color used when writing debug output graphs. The colors are given as strings of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -95,6 +96,8 @@ public enum IntermediateProcessorStrategy implements ILayoutProcessorFactory<LGr
TWO_SIDED_GREEDY_SWITCH,
/** Position self loops after phase 3. */
SELF_LOOP_PORT_RESTORER,
/** Unzips layers for compaction. */
LAYER_UNZIPPER,
/** Wraps graphs such that they better fit a given drawing area, allowing only a single edge per cut. */
SINGLE_EDGE_GRAPH_WRAPPER,
/** Makes sure that in-layer constraints are handled. */
Expand Down Expand Up @@ -327,6 +330,9 @@ public ILayoutProcessor<LGraph> create() {

case SELF_LOOP_PORT_RESTORER:
return new SelfLoopPortRestorer();

case LAYER_UNZIPPER:
return new GeneralLayerUnzipper();

case SELF_LOOP_POSTPROCESSOR:
return new SelfLoopPostProcessor();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*******************************************************************************
* 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.
*
* <dl>
* <dt>Preconditions:</dt>
* <dd>A layered graph whose node order has been decided.</dd>
* <dt>Postconditions:</dt>
* <dd>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.</dd>
* <dt>Slots:</dt>
* <dd>Before phase 4.</dd>
* <dt>Same-slot dependencies:</dt>
* <dd>None</dd>
* </dl>
*
*/
public class GeneralLayerUnzipper implements ILayoutProcessor<LGraph> {

@Override
public void process (LGraph graph, IElkProgressMonitor progressMonitor) {

processLayerSplitProperty(graph);

int insertionLayerOffset = 1;
List<Pair<Layer, Integer>> newLayers = new ArrayList<>();
for (int i = 0; i < graph.getLayers().size(); i++) {

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
// not be desirable
if (graph.getLayers().get(i).getNodes().size() > N) {

List<Layer> 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, 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, targetLayer % N, nodeIndex);
} else {
j -= 1;
targetLayer -= 1;
}
if (resetOnLongEdges && node.getType() == NodeType.LONG_EDGE) {
// reset next iterations target layer to 0
targetLayer = -1;
}

}
}
}
for (Pair<Layer, Integer> newLayer : newLayers) {
graph.getLayers().add(newLayer.getSecond(), newLayer.getFirst());
}

// remove unconnected placeholder nodes
for (Layer layer : graph.getLayers()) {
ListIterator<LNode> 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()) {
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
return;
}
}
}

}

/**
* 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<Layer> 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<LEdge> reversedIncomingEdges = Lists.reverse(Lists.newArrayList(node.getIncomingEdges()));
for (LEdge incomingEdge : reversedIncomingEdges) {
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;
}
}

/**
soerendomroes marked this conversation as resolved.
Show resolved Hide resolved
* Creates a dummy node for an edge that should be split into a long edge.
* @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;
}

}
Original file line number Diff line number Diff line change
@@ -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;

}
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
Loading