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

Radial layout, rotation extensions. #945

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -38,6 +38,10 @@ algorithm radial(RadialLayoutProvider) {
supports org.eclipse.elk.portLabels.placement
supports compactionStepSize
supports compactor
supports rotator
supports targetAngle
supports computeAdditionalWedgeSpace
supports outgoingEdgeAngles
supports optimizationCriteria
supports orderId
supports radius
Expand Down Expand Up @@ -72,6 +76,45 @@ option radius: double {
targets parents
}

// Rotation
option rotator: RotationStrategy {
label "Rotation"
description
"The rotator option determines how a rotation of the layout should be performed."
targets parents
default = RotationStrategy.NONE
}

option targetAngle: double {
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
label "Target Angle"
description
"The angle in radians that the layout should be rotated to after layout. Used by
{@link RotationStrategy.ROTATE_TO_ANGLE}."
targets parents
default = 0
requires rotator == RotationStrategy.ROTATE_TO_ANGLE
}

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 rotator == RotationStrategy.ROTATE_TO_ANGLE
}

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
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,15 @@
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;
import org.eclipse.elk.alg.radial.options.RotationStrategy;
import org.eclipse.elk.core.AbstractLayoutProvider;
import org.eclipse.elk.core.alg.AlgorithmAssembler;
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 +87,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.ROTATOR) != RotationStrategy.NONE) {
configuration.addBefore(RadialLayoutPhases.P2_EDGE_ROUTING, IntermediateProcessorStrategy.ROTATION);
}

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

if (layoutGraph.getProperty(RadialOptions.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,47 @@
/*******************************************************************************
* 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> {

/* (non-Javadoc)
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
* @see org.eclipse.elk.core.alg.ILayoutProcessor#process(java.lang.Object, org.eclipse.elk.core.util.IElkProgressMonitor)
*/
@Override
public void process(ElkNode graph, IElkProgressMonitor progressMonitor) {

ElkNode root = graph.getProperty(InternalProperties.ROOT_NODE);
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
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 = end.add(start.scale(-1, -1));
double angle = Math.atan2(edgeVector.y, edgeVector.x);

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

}

}
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
@@ -0,0 +1,84 @@
/*******************************************************************************
* 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 {

/* (non-Javadoc)
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
* @see org.eclipse.elk.alg.radial.intermediate.rotation.IRadialRotator#rotate(org.eclipse.elk.graph.ElkNode)
*/
@Override
public void rotate(ElkNode graph) {
double targetAngle = graph.getProperty(RadialOptions.TARGET_ANGLE);

if (graph.getProperty(RadialOptions.COMPUTE_ADDITIONAL_WEDGE_SPACE)) {
// additionally we want to get half the angle of the first wedge to leave space
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
// take first two nodes and compute angle between their incident edges
ElkNode root = graph.getProperty(InternalProperties.ROOT_NODE);
if (root.getOutgoingEdges().size() <= 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest using more wordy comments here to describe what you are doing. Maybe it would be helpful to add the formula you are using to the comments.

// leave angle untouched
if (targetAngle < 0) {
targetAngle += 2*Math.PI;
}
ElkNode firstNode = (ElkNode) root.getOutgoingEdges().get(0).getTargets().get(0);
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
KVector firstVector = new KVector(firstNode.getX() + firstNode.getWidth() / 2, firstNode.getY() + firstNode.getHeight() / 2);

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

targetAngle = Math.PI - (alignmentAngle - targetAngle + Math.PI);
} else {
ElkNode firstNode = (ElkNode) root.getOutgoingEdges().get(0).getTargets().get(0);
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
ElkNode secondNode = (ElkNode) root.getOutgoingEdges().get(1).getTargets().get(0);
KVector firstVector = new KVector(firstNode.getX() + firstNode.getWidth() / 2, firstNode.getY() + firstNode.getHeight() / 2);
KVector secondVector = new KVector(secondNode.getX() + secondNode.getWidth() / 2, secondNode.getY() + secondNode.getHeight() / 2);
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved

double alpha = targetAngle;
if (alpha < 0) {
alpha += 2*Math.PI;
}

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

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

targetAngle = Math.PI - (alignmentAngle - alpha + wedgeAngle / 2);

}
}

// rotate all nodes around the origin, because the root node is positioned at the origin
soerendomroes marked this conversation as resolved.
Show resolved Hide resolved
// 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,32 @@
/*******************************************************************************
* 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.options.RadialOptions;
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.
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
*
*/
public class GeneralRotator implements ILayoutProcessor<ElkNode> {

@Override
public void process(final ElkNode graph, final IElkProgressMonitor progressMonitor) {
progressMonitor.begin("General 'Rotator", 1);
progressMonitor.logGraph(graph, "Before");
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
IRadialRotator rotator = graph.getProperty(RadialOptions.ROTATOR).create();
rotator.rotate(graph);
progressMonitor.logGraph(graph, "After");
}

}
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
@@ -0,0 +1,40 @@
/*******************************************************************************
* 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.options;

import org.eclipse.elk.alg.radial.intermediate.rotation.AngleRotation;
import org.eclipse.elk.alg.radial.intermediate.rotation.IRadialRotator;

/**
* The list of selectable rotation algorithms.
*
*/
public enum RotationStrategy {
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved

/** No rotation. **/
NONE,
/** Rotate to a given angle. */
ROTATE_TO_ANGLE;

/**
* Instantiate the chosen rotation strategy.
*
* @return A rotator.
*/
public IRadialRotator create() {
switch (this) {
case ROTATE_TO_ANGLE:
return new AngleRotation();
default:
throw new IllegalArgumentException(
"No implementation is available for the layout option " + this.toString());
}
}
}
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);
Eddykasp marked this conversation as resolved.
Show resolved Hide resolved
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