From 83898117b3bbd4c95d27d2baf16eae2dc7b0df27 Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Mon, 14 Aug 2023 14:00:56 +0200 Subject: [PATCH] Radial layout, rotation extensions. (#945) * Adds options to control the rotation of radial layouts. Particularly with top-down layout. * Updated documentation and refactoring * update copyright dates --- .../org/eclipse/elk/alg/radial/Radial.melk | 45 +++++++++++ .../elk/alg/radial/RadialLayoutProvider.java | 14 +++- .../intermediate/EdgeAngleCalculator.java | 50 +++++++++++++ .../IntermediateProcessorStrategy.java | 16 +++- .../compaction/GeneralCompactor.java | 14 +++- .../intermediate/rotation/AngleRotation.java | 75 +++++++++++++++++++ .../intermediate/rotation/GeneralRotator.java | 39 ++++++++++ .../intermediate/rotation/IRadialRotator.java | 28 +++++++ .../org/eclipse/elk/core/math/KVector.java | 14 +++- 9 files changed, 284 insertions(+), 11 deletions(-) create mode 100644 plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/EdgeAngleCalculator.java create mode 100644 plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/AngleRotation.java create mode 100644 plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/GeneralRotator.java create mode 100644 plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/IRadialRotator.java diff --git a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/Radial.melk b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/Radial.melk index 28088112a7..afef45d06f 100644 --- a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/Radial.melk +++ b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/Radial.melk @@ -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 @@ -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" diff --git a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/RadialLayoutProvider.java b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/RadialLayoutProvider.java index 8e64145634..a2c7f34780 100644 --- a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/RadialLayoutProvider.java +++ b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/RadialLayoutProvider.java @@ -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 @@ -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; @@ -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; /** @@ -89,8 +86,17 @@ private List> 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); diff --git a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/EdgeAngleCalculator.java b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/EdgeAngleCalculator.java new file mode 100644 index 0000000000..d272bea223 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/EdgeAngleCalculator.java @@ -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 { + + /** + * 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); + } + + } + +} diff --git a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/IntermediateProcessorStrategy.java b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/IntermediateProcessorStrategy.java index 039e0e0aa7..9a9ae7ee9a 100644 --- a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/IntermediateProcessorStrategy.java +++ b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/IntermediateProcessorStrategy.java @@ -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 @@ -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; @@ -27,8 +28,15 @@ public enum IntermediateProcessorStrategy implements ILayoutProcessorFactory create() { @@ -37,8 +45,12 @@ public ILayoutProcessor 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()); diff --git a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/compaction/GeneralCompactor.java b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/compaction/GeneralCompactor.java index 65262d6f13..37ccdc4026 100644 --- a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/compaction/GeneralCompactor.java +++ b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/compaction/GeneralCompactor.java @@ -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 @@ -22,9 +22,17 @@ public class GeneralCompactor implements ILayoutProcessor { @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 } } \ No newline at end of file diff --git a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/AngleRotation.java b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/AngleRotation.java new file mode 100644 index 0000000000..797b95d9d0 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/AngleRotation.java @@ -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); + } + + } + +} diff --git a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/GeneralRotator.java b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/GeneralRotator.java new file mode 100644 index 0000000000..4b8c2a9881 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/GeneralRotator.java @@ -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 { + + @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 + } + +} diff --git a/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/IRadialRotator.java b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/IRadialRotator.java new file mode 100644 index 0000000000..e02af67c30 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.radial/src/org/eclipse/elk/alg/radial/intermediate/rotation/IRadialRotator.java @@ -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); + +} \ No newline at end of file diff --git a/plugins/org.eclipse.elk.core/src/org/eclipse/elk/core/math/KVector.java b/plugins/org.eclipse.elk.core/src/org/eclipse/elk/core/math/KVector.java index 1f35375674..edfb99529a 100644 --- a/plugins/org.eclipse.elk.core/src/org/eclipse/elk/core/math/KVector.java +++ b/plugins/org.eclipse.elk.core/src/org/eclipse/elk/core/math/KVector.java @@ -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.