Skip to content

Commit

Permalink
Radial layout, rotation extensions. (#945)
Browse files Browse the repository at this point in the history
* Adds options to control the rotation of radial layouts. Particularly
with top-down layout.

* Updated documentation and refactoring

* update copyright dates
  • Loading branch information
Eddykasp authored Aug 14, 2023
1 parent 59c66a0 commit 8389811
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ algorithm radial(RadialLayoutProvider) {
supports org.eclipse.elk.portLabels.placement
supports compactionStepSize
supports compactor
supports rotate
supports rotation.targetAngle
supports rotation.computeAdditionalWedgeSpace
supports rotation.outgoingEdgeAngles
supports optimizationCriteria
supports orderId
supports radius
Expand Down Expand Up @@ -72,6 +76,47 @@ option radius: double {
targets parents
}

// Rotation
option rotate: boolean {
label "Rotate"
description
"The rotate option determines whether a rotation of the layout should be performed."
targets parents
default = false
}

group rotation {
option targetAngle: double {
label "Target Angle"
description
"The angle in radians that the layout should be rotated to after layout."
targets parents
default = 0
requires rotate
}

advanced option computeAdditionalWedgeSpace: boolean {
label "Additional Wedge Space"
description
"If set to true, modifies the target angle by rotating further such that space is left
for an edge to pass in between the nodes. This option should only be used in conjunction
with top-down layout."
targets parents
default = false
requires rotate
}

advanced option outgoingEdgeAngles: boolean {
label "Outgoing Edge Angles"
description
"Calculate the required angle of connected nodes to leave space for an incoming edge. This
option should only be used in conjunction with top-down layout."
targets parents
default = false
}
}


//Compaction
option compactor: CompactionStrategy {
label "Compaction"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017, 2020 Kiel University and others.
* Copyright (c) 2017 - 2023 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
Expand All @@ -12,7 +12,6 @@
import java.util.List;

import org.eclipse.elk.alg.common.NodeMicroLayout;
import org.eclipse.elk.alg.common.nodespacing.NodeDimensionCalculation;
import org.eclipse.elk.alg.radial.intermediate.IntermediateProcessorStrategy;
import org.eclipse.elk.alg.radial.options.CompactionStrategy;
import org.eclipse.elk.alg.radial.options.RadialOptions;
Expand All @@ -21,8 +20,6 @@
import org.eclipse.elk.core.alg.ILayoutProcessor;
import org.eclipse.elk.core.alg.LayoutProcessorConfiguration;
import org.eclipse.elk.core.util.IElkProgressMonitor;
import org.eclipse.elk.core.util.adapters.ElkGraphAdapters;
import org.eclipse.elk.core.util.adapters.ElkGraphAdapters.ElkGraphAdapter;
import org.eclipse.elk.graph.ElkNode;

/**
Expand Down Expand Up @@ -89,8 +86,17 @@ private List<ILayoutProcessor<ElkNode>> assembleAlgorithm(final ElkNode layoutGr
if (layoutGraph.getProperty(RadialOptions.COMPACTOR) != CompactionStrategy.NONE) {
configuration.addBefore(RadialLayoutPhases.P2_EDGE_ROUTING, IntermediateProcessorStrategy.COMPACTION);
}

if (layoutGraph.getProperty(RadialOptions.ROTATE)) {
configuration.addBefore(RadialLayoutPhases.P2_EDGE_ROUTING, IntermediateProcessorStrategy.ROTATION);
}

configuration.addBefore(RadialLayoutPhases.P2_EDGE_ROUTING,
IntermediateProcessorStrategy.GRAPH_SIZE_CALCULATION);

if (layoutGraph.getProperty(RadialOptions.ROTATION_OUTGOING_EDGE_ANGLES)) {
configuration.addAfter(RadialLayoutPhases.P2_EDGE_ROUTING, IntermediateProcessorStrategy.OUTGOING_EDGE_ANGLES);
}

algorithmAssembler.addProcessorConfiguration(configuration);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*******************************************************************************
* Copyright (c) 2023 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.radial.intermediate;

import org.eclipse.elk.alg.radial.InternalProperties;
import org.eclipse.elk.alg.radial.options.RadialOptions;
import org.eclipse.elk.core.alg.ILayoutProcessor;
import org.eclipse.elk.core.math.KVector;
import org.eclipse.elk.core.util.IElkProgressMonitor;
import org.eclipse.elk.graph.ElkEdge;
import org.eclipse.elk.graph.ElkNode;

/**
* Calculates the angles of outgoing edges so that they can be used as an input by subsequent child
* layouts. Only makes sense when used in a top-down layout.
*
*/
public class EdgeAngleCalculator implements ILayoutProcessor<ElkNode> {

/**
* For each of edges connected to the root node we calculate its angle and store that information on the
* connected target node. This node can then later use that information as basis to align its own layout
* to the incoming edge. Because this sets an option on child nodes, this is only useful when laying the
* graph out in a top-down manner (or possibly in multiple layout runs).
*/
@Override
public void process(ElkNode graph, IElkProgressMonitor progressMonitor) {

ElkNode root = graph.getProperty(InternalProperties.ROOT_NODE);
for (ElkEdge edge : root.getOutgoingEdges()) {

KVector start = new KVector(edge.getSections().get(0).getStartX(), edge.getSections().get(0).getStartY());
KVector end = new KVector(edge.getSections().get(0).getEndX(), edge.getSections().get(0).getEndY());

KVector edgeVector = KVector.diff(end, start);
double angle = Math.atan2(edgeVector.y, edgeVector.x);

edge.getTargets().get(0).setProperty(RadialOptions.ROTATION_TARGET_ANGLE, angle);
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Kiel University and others.
* Copyright (c) 2017, 2023 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
Expand All @@ -11,6 +11,7 @@

import org.eclipse.elk.alg.radial.intermediate.compaction.GeneralCompactor;
import org.eclipse.elk.alg.radial.intermediate.overlaps.RadiusExtensionOverlapRemoval;
import org.eclipse.elk.alg.radial.intermediate.rotation.GeneralRotator;
import org.eclipse.elk.core.alg.ILayoutProcessor;
import org.eclipse.elk.core.alg.ILayoutProcessorFactory;
import org.eclipse.elk.graph.ElkNode;
Expand All @@ -27,8 +28,15 @@ public enum IntermediateProcessorStrategy implements ILayoutProcessorFactory<Elk
COMPACTION,

// Before phase 2
/** Rotate the final layout. */
ROTATION,
/** Calculate the graph size to the new values. */
GRAPH_SIZE_CALCULATION;
GRAPH_SIZE_CALCULATION,

// After phase 2
/** Store a target angle on child nodes to leave space for an edge coming from the root to the center of the child node. */
OUTGOING_EDGE_ANGLES;


@Override
public ILayoutProcessor<ElkNode> create() {
Expand All @@ -37,8 +45,12 @@ public ILayoutProcessor<ElkNode> create() {
return new RadiusExtensionOverlapRemoval();
case COMPACTION:
return new GeneralCompactor();
case ROTATION:
return new GeneralRotator();
case GRAPH_SIZE_CALCULATION:
return new CalculateGraphSize();
case OUTGOING_EDGE_ANGLES:
return new EdgeAngleCalculator();
default:
throw new IllegalArgumentException(
"No implementation is available for the layout processor " + this.toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Kiel University and others.
* Copyright (c) 2017, 2023 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
Expand All @@ -22,9 +22,17 @@ public class GeneralCompactor implements ILayoutProcessor<ElkNode> {
@Override
public void process(final ElkNode graph, final IElkProgressMonitor progressMonitor) {
progressMonitor.begin("General Compactor", 1);
progressMonitor.logGraph(graph, "Before");
// elkjs-exclude-start
if (progressMonitor.isLoggingEnabled()) {
progressMonitor.logGraph(graph, "Before");
}
// elkjs-exclude-end
IRadialCompactor compactor = graph.getProperty(RadialOptions.COMPACTOR).create();
compactor.compact(graph);
progressMonitor.logGraph(graph, "After");
// elkjs-exclude-start
if (progressMonitor.isLoggingEnabled()) {
progressMonitor.logGraph(graph, "After");
}
// elkjs-exclude-end
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*******************************************************************************
* Copyright (c) 2023 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.radial.intermediate.rotation;

import org.eclipse.elk.alg.radial.InternalProperties;
import org.eclipse.elk.alg.radial.options.RadialOptions;
import org.eclipse.elk.core.math.KVector;
import org.eclipse.elk.graph.ElkNode;

/**
* Rotates the entire layout around the origin to a set target angle.
*
*/
public class AngleRotation implements IRadialRotator {

@Override
public void rotate(ElkNode graph) {
double targetAngle = graph.getProperty(RadialOptions.ROTATION_TARGET_ANGLE);

if (graph.getProperty(RadialOptions.ROTATION_COMPUTE_ADDITIONAL_WEDGE_SPACE)) {
// Using the target angle as our base alignment we want to further rotate the layout such that a line
// following the target angle runs directly through the middle of the wedge between the first and last node.
ElkNode root = graph.getProperty(InternalProperties.ROOT_NODE);

ElkNode lastNode = (ElkNode) root.getOutgoingEdges().get(root.getOutgoingEdges().size() - 1).getTargets().get(0);
ElkNode firstNode = (ElkNode) root.getOutgoingEdges().get(0).getTargets().get(0);
KVector lastVector = new KVector(lastNode.getX() + lastNode.getWidth() / 2, lastNode.getY() + lastNode.getHeight() / 2);
KVector firstVector = new KVector(firstNode.getX() + firstNode.getWidth() / 2, firstNode.getY() + firstNode.getHeight() / 2);

// we shift all angles into the range (0,pi] to avoid dealing with negative angles.
double alpha = targetAngle;
if (alpha <= 0) {
alpha += 2*Math.PI;
}

double wedgeAngle = lastVector.angle(firstVector);
if (wedgeAngle <= 0) {
wedgeAngle += 2*Math.PI;
}

double alignmentAngle = Math.atan2(lastVector.y, lastVector.x);
if (alignmentAngle <= 0) {
alignmentAngle += 2*Math.PI;
}

// alpha (originally targetAngle) is the angle of the incoming edge that we wish to align ourselves with.
// wedgeAngle is the angle between the first and last nodes of our own layout. For the case of a single
// node this is 360 degrees.
// alignmentAngle is the angle of the vector pointing to the last node i.e. the end part of the segment
// we rotate the entire layout by subtracting the incoming angle alpha and we add half the wedge angle back
// to make the alignment go through the center of the wedge. Finally, we need to do a transformation to
// make all this work in our downward facing coordinate system. So everything is inverted and we need subtract
// the result from 180 degrees.
targetAngle = Math.PI - (alignmentAngle - alpha + wedgeAngle / 2);

}

// rotate all nodes around the origin, because the root node is positioned at the origin
// nodes are positioned with their center on the radius so use that for the rotation
for (ElkNode node : graph.getChildren()) {
KVector pos = new KVector(node.getX() + node.getWidth() / 2, node.getY() + node.getHeight() / 2);
pos.rotate(targetAngle);
node.setLocation(pos.x - node.getWidth() / 2, pos.y - node.getHeight() / 2);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*******************************************************************************
* Copyright (c) 2023 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.radial.intermediate.rotation;

import org.eclipse.elk.core.alg.ILayoutProcessor;
import org.eclipse.elk.core.util.IElkProgressMonitor;
import org.eclipse.elk.graph.ElkNode;

/**
* The layout processor for rotation. Sets up logging and calls the angle rotation implementation.
*
*/
public class GeneralRotator implements ILayoutProcessor<ElkNode> {

@Override
public void process(final ElkNode graph, final IElkProgressMonitor progressMonitor) {
progressMonitor.begin("General 'Rotator", 1);
// elkjs-exclude-start
if (progressMonitor.isLoggingEnabled()) {
progressMonitor.logGraph(graph, "Before");
}
// elkjs-exclude-end
IRadialRotator rotator = new AngleRotation();
rotator.rotate(graph);
// elkjs-exclude-start
if (progressMonitor.isLoggingEnabled()) {
progressMonitor.logGraph(graph, "After");
}
// elkjs-exclude-end
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*******************************************************************************
* Copyright (c) 2023 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.radial.intermediate.rotation;

import org.eclipse.elk.graph.ElkNode;

/**
* An interface for rotating the radial layout.
*
*/
public interface IRadialRotator {

/**
* Rotate the graph.
*
* @param graph
* The graph which is already radial.
*/
void rotate(ElkNode graph);

}
Original file line number Diff line number Diff line change
Expand Up @@ -435,10 +435,20 @@ public static double crossProduct(final KVector v, final KVector w) {
* @return the rotated vector
*/
public KVector rotate(final double angle) {
this.x = this.x * Math.cos(angle) - this.y * Math.sin(angle);
this.y = this.y * Math.sin(angle) + this.y * Math.cos(angle);
double newX = this.x * Math.cos(angle) - this.y * Math.sin(angle);
this.y = this.x * Math.sin(angle) + this.y * Math.cos(angle);
this.x = newX;
return this;
}

/**
* Returns the angle between this vector and another given vector in radians.
* @param other
* @return angle between vectors
*/
public double angle(KVector other) {
return Math.acos(this.dotProduct(other) / (this.length() * other.length()));
}

/**
* Apply the given bounds to this vector.
Expand Down

0 comments on commit 8389811

Please sign in to comment.