diff --git a/plugins/org.eclipse.elk.alg.knot/.classpath b/plugins/org.eclipse.elk.alg.knot/.classpath new file mode 100644 index 000000000..946fb3d34 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/org.eclipse.elk.alg.knot/.project b/plugins/org.eclipse.elk.alg.knot/.project new file mode 100644 index 000000000..296a80962 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/.project @@ -0,0 +1,34 @@ + + + org.eclipse.elk.alg.knot + + + + + + org.eclipse.xtext.ui.shared.xtextBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + org.eclipse.xtext.ui.shared.xtextNature + + diff --git a/plugins/org.eclipse.elk.alg.knot/.settings/org.eclipse.jdt.core.prefs b/plugins/org.eclipse.elk.alg.knot/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..62ef3488c --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/plugins/org.eclipse.elk.alg.knot/META-INF/MANIFEST.MF b/plugins/org.eclipse.elk.alg.knot/META-INF/MANIFEST.MF new file mode 100644 index 000000000..b2a029150 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/META-INF/MANIFEST.MF @@ -0,0 +1,12 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Knot +Bundle-SymbolicName: org.eclipse.elk.alg.knot +Bundle-Version: 1.0.0.qualifier +Require-Bundle: org.eclipse.elk.core, + org.eclipse.elk.graph, + org.eclipse.elk.alg.common, + com.google.guava, + org.eclipse.elk.alg.force +Automatic-Module-Name: org.eclipse.elk.alg.knot +Bundle-RequiredExecutionEnvironment: JavaSE-17 diff --git a/plugins/org.eclipse.elk.alg.knot/build.properties b/plugins/org.eclipse.elk.alg.knot/build.properties new file mode 100644 index 000000000..2b0d95b6b --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/build.properties @@ -0,0 +1,5 @@ +source.. = src/ +output.. = bin/ +bin.includes = plugin.xml,\ + META-INF/,\ + . diff --git a/plugins/org.eclipse.elk.alg.knot/src-gen/org/eclipse/elk/alg/knot/KnotMetadataProvider.java b/plugins/org.eclipse.elk.alg.knot/src-gen/org/eclipse/elk/alg/knot/KnotMetadataProvider.java new file mode 100644 index 000000000..25fe60246 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/src-gen/org/eclipse/elk/alg/knot/KnotMetadataProvider.java @@ -0,0 +1,339 @@ +package org.eclipse.elk.alg.knot; + +import java.util.EnumSet; +import org.eclipse.elk.core.data.ILayoutMetaDataProvider; +import org.eclipse.elk.core.data.LayoutOptionData; +import org.eclipse.elk.graph.properties.IProperty; +import org.eclipse.elk.graph.properties.Property; + +@SuppressWarnings("all") +public class KnotMetadataProvider implements ILayoutMetaDataProvider { + /** + * Default value for {@link #REVERSE_INPUT}. + */ + private static final boolean REVERSE_INPUT_DEFAULT = false; + + /** + * True if nodes should be placed in reverse order of their + * appearance in the graph. + */ + public static final IProperty REVERSE_INPUT = new Property( + "org.eclipse.elk.alg.knot.reverseInput", + REVERSE_INPUT_DEFAULT, + null, + null); + + /** + * Default value for {@link #NODE_RADIUS}. + */ + private static final double NODE_RADIUS_DEFAULT = 25; + + /** + * The distances of bend points around a node. A larger radius creates + * greater curves and therefore increasing the graph size. + * To small or to large sizes may cause problems. + */ + public static final IProperty NODE_RADIUS = new Property( + "org.eclipse.elk.alg.knot.nodeRadius", + NODE_RADIUS_DEFAULT, + null, + null); + + /** + * Default value for {@link #SHIFT_THRESHOLD}. + */ + private static final double SHIFT_THRESHOLD_DEFAULT = 420; + + /** + * Condition for the moving operation for nodes. Only nodes where the angles at + * their four bend points is in sum below that threshold are allowed to move. + * Keeping nodes from moving to much can improve the graph. Angle in degree. + */ + public static final IProperty SHIFT_THRESHOLD = new Property( + "org.eclipse.elk.alg.knot.shiftThreshold", + SHIFT_THRESHOLD_DEFAULT, + null, + null); + + /** + * Default value for {@link #DESIRED_NODE_DISTANCE}. + */ + private static final double DESIRED_NODE_DISTANCE_DEFAULT = 25; + + /** + * The distances between connected nodes that should be preserved. + * Should be at minimum the same as the node radius. + */ + public static final IProperty DESIRED_NODE_DISTANCE = new Property( + "org.eclipse.elk.alg.knot.desiredNodeDistance", + DESIRED_NODE_DISTANCE_DEFAULT, + null, + null); + + /** + * Default value for {@link #SEPARATE_AXIS_ROTATION}. + */ + private static final boolean SEPARATE_AXIS_ROTATION_DEFAULT = false; + + /** + * Whether the outgoing edges and the incoming edges can be rotated separately. + */ + public static final IProperty SEPARATE_AXIS_ROTATION = new Property( + "org.eclipse.elk.alg.knot.separateAxisRotation", + SEPARATE_AXIS_ROTATION_DEFAULT, + null, + null); + + /** + * Default value for {@link #ROTATION_VALUE}. + */ + private static final double ROTATION_VALUE_DEFAULT = 1; + + /** + * Weighted step size for node rotations. + */ + public static final IProperty ROTATION_VALUE = new Property( + "org.eclipse.elk.alg.knot.rotationValue", + ROTATION_VALUE_DEFAULT, + null, + null); + + /** + * Default value for {@link #SHIFT_VALUE}. + */ + private static final double SHIFT_VALUE_DEFAULT = 1; + + /** + * Weighted step size for node shift movements. + */ + public static final IProperty SHIFT_VALUE = new Property( + "org.eclipse.elk.alg.knot.shiftValue", + SHIFT_VALUE_DEFAULT, + null, + null); + + /** + * Default value for {@link #ENABLE_ADDITIONAL_BEND_POINTS}. + */ + private static final boolean ENABLE_ADDITIONAL_BEND_POINTS_DEFAULT = false; + + /** + * Whether the algorithm is allowed to create new bend points in the middle of edges + * and move them in order to relax angles at the outer bend points. + * Helps creating larger curves. + */ + public static final IProperty ENABLE_ADDITIONAL_BEND_POINTS = new Property( + "org.eclipse.elk.alg.knot.enableAdditionalBendPoints", + ENABLE_ADDITIONAL_BEND_POINTS_DEFAULT, + null, + null); + + /** + * Default value for {@link #ADDITIONAL_BEND_POINTS_THRESHOLD}. + */ + private static final double ADDITIONAL_BEND_POINTS_THRESHOLD_DEFAULT = 100; + + /** + * When the bend point angles of an edge are below this threshold the edge + * is considered overstretched. If additional bend points are enabled the algorithm + * will add further bend points in the middle of edges when the outer bend points have + * an angle below this threshold in degree . + */ + public static final IProperty ADDITIONAL_BEND_POINTS_THRESHOLD = new Property( + "org.eclipse.elk.alg.knot.additionalBendPointsThreshold", + ADDITIONAL_BEND_POINTS_THRESHOLD_DEFAULT, + null, + null); + + /** + * Default value for {@link #CURVE_HEIGHT}. + */ + private static final double CURVE_HEIGHT_DEFAULT = 30; + + /** + * Maximum distance that the additional bend points can distance themselves and therefore influences + * the altitude of the curve they create. + */ + public static final IProperty CURVE_HEIGHT = new Property( + "org.eclipse.elk.alg.knot.curveHeight", + CURVE_HEIGHT_DEFAULT, + null, + null); + + /** + * Default value for {@link #CURVE_WIDTH_FACTOR}. + */ + private static final double CURVE_WIDTH_FACTOR_DEFAULT = 0.2; + + /** + * Scale factor for the distance of helper bend points towards the additional bend points. It influences + * the width and pointedness of the curve they create. + */ + public static final IProperty CURVE_WIDTH_FACTOR = new Property( + "org.eclipse.elk.alg.knot.curveWidthFactor", + CURVE_WIDTH_FACTOR_DEFAULT, + null, + null); + + /** + * Default value for {@link #ITERATION_LIMIT}. + */ + private static final int ITERATION_LIMIT_DEFAULT = 200; + + /** + * Maximum number of iterations of each stress reducing process. + */ + public static final IProperty ITERATION_LIMIT = new Property( + "org.eclipse.elk.alg.knot.iterationLimit", + ITERATION_LIMIT_DEFAULT, + null, + null); + + public void apply(final org.eclipse.elk.core.data.ILayoutMetaDataProvider.Registry registry) { + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.reverseInput") + .group("") + .name("Reverse Input") + .description("True if nodes should be placed in reverse order of their appearance in the graph.") + .defaultValue(REVERSE_INPUT_DEFAULT) + .type(LayoutOptionData.Type.BOOLEAN) + .optionClass(Boolean.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.nodeRadius") + .group("") + .name("Node radius") + .description("The distances of bend points around a node. A larger radius creates greater curves and therefore increasing the graph size. To small or to large sizes may cause problems.") + .defaultValue(NODE_RADIUS_DEFAULT) + .type(LayoutOptionData.Type.DOUBLE) + .optionClass(Double.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.shiftThreshold") + .group("") + .name("Shift threshold") + .description("Condition for the moving operation for nodes. Only nodes where the angles at their four bend points is in sum below that threshold are allowed to move. Keeping nodes from moving to much can improve the graph. Angle in degree.") + .defaultValue(SHIFT_THRESHOLD_DEFAULT) + .type(LayoutOptionData.Type.DOUBLE) + .optionClass(Double.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.desiredNodeDistance") + .group("") + .name("Desired distance between nodes") + .description("The distances between connected nodes that should be preserved. Should be at minimum the same as the node radius.") + .defaultValue(DESIRED_NODE_DISTANCE_DEFAULT) + .type(LayoutOptionData.Type.DOUBLE) + .optionClass(Double.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.separateAxisRotation") + .group("") + .name("Allow separate axis rotation") + .description("Whether the outgoing edges and the incoming edges can be rotated separately.") + .defaultValue(SEPARATE_AXIS_ROTATION_DEFAULT) + .type(LayoutOptionData.Type.BOOLEAN) + .optionClass(Boolean.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.rotationValue") + .group("") + .name("Rotation step size") + .description("Weighted step size for node rotations.") + .defaultValue(ROTATION_VALUE_DEFAULT) + .type(LayoutOptionData.Type.DOUBLE) + .optionClass(Double.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.shiftValue") + .group("") + .name("Shift step size") + .description("Weighted step size for node shift movements.") + .defaultValue(SHIFT_VALUE_DEFAULT) + .type(LayoutOptionData.Type.DOUBLE) + .optionClass(Double.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.enableAdditionalBendPoints") + .group("") + .name("Enable additional bend points") + .description("Whether the algorithm is allowed to create new bend points in the middle of edges and move them in order to relax angles at the outer bend points. Helps creating larger curves.") + .defaultValue(ENABLE_ADDITIONAL_BEND_POINTS_DEFAULT) + .type(LayoutOptionData.Type.BOOLEAN) + .optionClass(Boolean.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.additionalBendPointsThreshold") + .group("") + .name("Threshold for additional bend points") + .description("When the bend point angles of an edge are below this threshold the edge is considered overstretched. If additional bend points are enabled the algorithm will add further bend points in the middle of edges when the outer bend points have an angle below this threshold in degree .") + .defaultValue(ADDITIONAL_BEND_POINTS_THRESHOLD_DEFAULT) + .type(LayoutOptionData.Type.DOUBLE) + .optionClass(Double.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.curveHeight") + .group("") + .name("Curve Height") + .description("Maximum distance that the additional bend points can distance themselves and therefore influences the altitude of the curve they create.") + .defaultValue(CURVE_HEIGHT_DEFAULT) + .type(LayoutOptionData.Type.DOUBLE) + .optionClass(Double.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.curveWidthFactor") + .group("") + .name("Curve Width") + .description("Scale factor for the distance of helper bend points towards the additional bend points. It influences the width and pointedness of the curve they create.") + .defaultValue(CURVE_WIDTH_FACTOR_DEFAULT) + .type(LayoutOptionData.Type.DOUBLE) + .optionClass(Double.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + registry.register(new LayoutOptionData.Builder() + .id("org.eclipse.elk.alg.knot.iterationLimit") + .group("") + .name("Iteration Limit") + .description("Maximum number of iterations of each stress reducing process.") + .defaultValue(ITERATION_LIMIT_DEFAULT) + .type(LayoutOptionData.Type.INT) + .optionClass(Integer.class) + .targets(EnumSet.of(LayoutOptionData.Target.PARENTS)) + .visibility(LayoutOptionData.Visibility.VISIBLE) + .create() + ); + new org.eclipse.elk.alg.knot.options.KnotOptions().apply(registry); + } +} diff --git a/plugins/org.eclipse.elk.alg.knot/src/META-INF/services/org.eclipse.elk.core.data.ILayoutMetaDataProvider b/plugins/org.eclipse.elk.alg.knot/src/META-INF/services/org.eclipse.elk.core.data.ILayoutMetaDataProvider new file mode 100644 index 000000000..959ba9489 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/src/META-INF/services/org.eclipse.elk.core.data.ILayoutMetaDataProvider @@ -0,0 +1 @@ +org.eclipse.elk.alg.knot.KnotMetadataProvider \ No newline at end of file diff --git a/plugins/org.eclipse.elk.alg.knot/src/org/eclipse/elk/alg/knot/Knot.melk b/plugins/org.eclipse.elk.alg.knot/src/org/eclipse/elk/alg/knot/Knot.melk new file mode 100644 index 000000000..55aa40e76 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/src/org/eclipse/elk/alg/knot/Knot.melk @@ -0,0 +1,149 @@ +package org.eclipse.elk.alg.knot + +import org.eclipse.elk.alg.knot.KnotLayoutProvider +import org.eclipse.elk.core.math.ElkPadding +import org.eclipse.elk.core.options.EdgeRouting + +bundle { + metadataClass KnotMetadataProvider + idPrefix org.eclipse.elk.alg.knot +} + +option reverseInput : boolean { + label "Reverse Input" + description + "True if nodes should be placed in reverse order of their + appearance in the graph." + default = false + targets parents +} + +algorithm Knot(KnotLayoutProvider) { + label "ELK Knot" + description + "Creating a knot diagram of a layout by using stress majorization. + Stress exists if the connected edges consist of sharp or if the distance + between a pair of nodes doesn't match their desired distance. + The method allows to specify individual sizes and distances." + metadataClass options.KnotOptions + supports reverseInput + supports org.eclipse.elk.padding = new ElkPadding(10) + supports org.eclipse.elk.spacing.edgeEdge = 5 + supports org.eclipse.elk.spacing.edgeNode = 10 + supports org.eclipse.elk.spacing.nodeNode = 10 + supports org.eclipse.elk.edgeRouting = EdgeRouting.SPLINES + supports nodeRadius + supports shiftThreshold + supports desiredNodeDistance + supports separateAxisRotation + supports rotationValue + supports shiftValue + supports enableAdditionalBendPoints + supports additionalBendPointsThreshold + supports curveHeight + supports curveWidthFactor + supports iterationLimit +} + + +option nodeRadius: double { + label "Node radius" + description + "The distances of bend points around a node. A larger radius creates + greater curves and therefore increasing the graph size. + To small or to large sizes may cause problems." + default = 25 + targets parents +} + +option shiftThreshold: double { + label "Shift threshold" + description + "Condition for the moving operation for nodes. Only nodes where the angles at + their four bend points is in sum below that threshold are allowed to move. + Keeping nodes from moving to much can improve the graph. Angle in degree." + default = 420 + targets parents +} + +option desiredNodeDistance: double { + label "Desired distance between nodes" + description + "The distances between connected nodes that should be preserved. + Should be at minimum the same as the node radius." + default = 25 + targets parents +} + +option separateAxisRotation: boolean { + label "Allow separate axis rotation" + description + "Whether the outgoing edges and the incoming edges can be rotated separately." + default = false + targets parents +} + +option rotationValue: double { + label "Rotation step size" + description + "Weighted step size for node rotations." + default = 1 + targets parents +} + +option shiftValue: double { + label "Shift step size" + description + "Weighted step size for node shift movements." + default = 1 + targets parents +} + +option enableAdditionalBendPoints: boolean { + label "Enable additional bend points" + description + "Whether the algorithm is allowed to create new bend points in the middle of edges + and move them in order to relax angles at the outer bend points. + Helps creating larger curves." + default = false + targets parents +} + +option additionalBendPointsThreshold: double { + label "Threshold for additional bend points" + description + "When the bend point angles of an edge are below this threshold the edge + is considered overstretched. If additional bend points are enabled the algorithm + will add further bend points in the middle of edges when the outer bend points have + an angle below this threshold in degree ." + default = 100 + targets parents +} + +option curveHeight: double { + label "Curve Height" + description + "Maximum distance that the additional bend points can distance themselves and therefore influences + the altitude of the curve they create." + default = 30 + targets parents +} + +option curveWidthFactor: double { + label "Curve Width" + description + "Scale factor for the distance of helper bend points towards the additional bend points. It influences + the width and pointedness of the curve they create." + default = 0.2 + targets parents +} + +option iterationLimit: int { + label "Iteration Limit" + description + "Maximum number of iterations of each stress reducing process." + default = 200 + targets parents +} + + diff --git a/plugins/org.eclipse.elk.alg.knot/src/org/eclipse/elk/alg/knot/KnotLayoutProvider.java b/plugins/org.eclipse.elk.alg.knot/src/org/eclipse/elk/alg/knot/KnotLayoutProvider.java new file mode 100644 index 000000000..48ec1ae13 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.knot/src/org/eclipse/elk/alg/knot/KnotLayoutProvider.java @@ -0,0 +1,902 @@ +/** + * @author Jean-Pierre Runge (stu224382) (Mat: 1150750) + */ +package org.eclipse.elk.alg.knot; + +import java.util.List; +import org.eclipse.elk.core.AbstractLayoutProvider; +import org.eclipse.elk.core.math.KVector; +import org.eclipse.elk.core.util.IElkProgressMonitor; +import org.eclipse.elk.graph.ElkBendPoint; +import org.eclipse.elk.graph.ElkEdge; +import org.eclipse.elk.graph.ElkNode; +import org.eclipse.elk.graph.util.ElkGraphUtil; +import org.eclipse.elk.alg.common.NodeMicroLayout; +import org.eclipse.elk.alg.force.ComponentsProcessor; +import org.eclipse.elk.alg.force.ElkGraphImporter; +import org.eclipse.elk.alg.force.ForceLayoutProvider; +import org.eclipse.elk.alg.force.IGraphImporter; +import org.eclipse.elk.alg.force.graph.FGraph; +import org.eclipse.elk.alg.force.options.StressOptions; +import org.eclipse.elk.alg.force.stress.StressMajorization; +import org.eclipse.elk.alg.knot.options.KnotOptions; + +/** + * Layout provider for the knot layout algorithm. This algorithm places nodes and routes edges in order to modify + * positions further via stress minimization. + */ +public class KnotLayoutProvider extends AbstractLayoutProvider { + + /** Connected components processor. */ + private ComponentsProcessor componentsProcessor = new ComponentsProcessor(); + /** Implementation of stress majorization for standard stress layout. */ + private StressMajorization stressMajorization = new StressMajorization(); + + /** The distance which bend points around nodes must preserve. */ + private double nodeRadius = 25; //25 got overall best results; + /** The sum of all four bend point angles on a node must be lower in order to perform a shift movement. */ + private double shiftThreshold = 420; + /** Desired distance between connected nodes (should at least be nodeRadius). */ + private double desiredNodeDistance = nodeRadius; + /** Whether the outgoing and incoming edges can be rotated separately. */ + private boolean separateAxisRotation = false; + /** Weighted step size for rotations. */ + private double rotationValue = 1; + /** Weighted step size for shift movements. */ + private double shiftValue = 1; + + /** Whether the algorithm is allowed to create new bend points to relax overstretched edges. */ + private boolean enableAdditionalBendPoints = true; + /** When the bend point angles of an edge are below this threshold the edge is considered overstretched. */ + private double additionalBendPointsThreshold = 100; + /** How much the additional bend points can distance themselves. Influences the curve height. */ + private double curveHeight = 50; + /** Factor on how near the helping bend points are positioned for spline visualization. Influences the curve width. */ + private double curveWidthFactor = 0.2; + /** Enable if the algorithm is allowed to change the bend point distance around nodes. */ + private boolean enableFlexibleNodeRadius = false; + + /** Maximum number of iterations of each stress reducing process. */ + private int iterationLimit = 200; + /** Epsilon for terminating the stress minimizing process. */ + //TODO: Try implementing stress depending termination criterion. + private double epsilon; + + + + @Override + public void layout(final ElkNode layoutGraph, final IElkProgressMonitor progressMonitor) { + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Initialization: + + // Start progress monitor + progressMonitor.begin("ELK Knot", 2); + progressMonitor.log("Knot algorithm start"); + + // calculate initial coordinates + if (!layoutGraph.getProperty(StressOptions.INTERACTIVE)) { + new ForceLayoutProvider().layout(layoutGraph, progressMonitor.subTask(1)); + } else { + // If requested, compute nodes's dimensions, place node labels, ports, port labels, etc. + // Note that for the non-interactive case (above) this will be taken care of by the force layout provider + if (!layoutGraph.getProperty(StressOptions.OMIT_NODE_MICRO_LAYOUT)) { + NodeMicroLayout.forGraph(layoutGraph) + .execute(); + } + } + + + // Check for correct amount of edges (2 outgoing + 2 incoming). + for (ElkNode node : layoutGraph.getChildren()) { + if (node.getOutgoingEdges().size() != 2 && node.getIncomingEdges().size() != 2) { + throw new IllegalArgumentException("Invalid amount of edges on node: " + node); + } + // Shrink node sizes to appear invisible. + node.setHeight(0.0000000000001); + node.setWidth(0.0000000000001); + } + + + // Apply options. + this.nodeRadius = layoutGraph.getProperty(KnotOptions.NODE_RADIUS); + this.shiftThreshold = layoutGraph.getProperty(KnotOptions.SHIFT_THRESHOLD); + this.desiredNodeDistance = layoutGraph.getProperty(KnotOptions.DESIRED_NODE_DISTANCE); + this.separateAxisRotation = layoutGraph.getProperty(KnotOptions.SEPARATE_AXIS_ROTATION); + this.rotationValue = layoutGraph.getProperty(KnotOptions.ROTATION_VALUE); + this.shiftValue = layoutGraph.getProperty(KnotOptions.SHIFT_VALUE); + this.enableAdditionalBendPoints = layoutGraph.getProperty(KnotOptions.ENABLE_ADDITIONAL_BEND_POINTS); + this.additionalBendPointsThreshold = layoutGraph.getProperty(KnotOptions.ADDITIONAL_BEND_POINTS_THRESHOLD); + this.curveHeight = layoutGraph.getProperty(KnotOptions.CURVE_HEIGHT); + this.curveWidthFactor = layoutGraph.getProperty(KnotOptions.CURVE_WIDTH_FACTOR); + this.iterationLimit = layoutGraph.getProperty(KnotOptions.ITERATION_LIMIT); + + epsilon = 10e-4; + + // -------- Use stress layout as initial layout. (reused section) -------- + // transform the input graph + IGraphImporter graphImporter = new ElkGraphImporter(); + FGraph fgraph = graphImporter.importGraph(layoutGraph); + + // split the input graph into components + List components = componentsProcessor.split(fgraph); + + // perform the actual layout + for (FGraph subGraph : components) { + if (subGraph.getNodes().size() <= 1) { + continue; + } + stressMajorization.initialize(subGraph); + stressMajorization.execute(); + + // Note that contrary to force itself, labels are not considered during stress layout. + // Hence, all we can do here is to place the labels at reasonable positions after layout has finished. + subGraph.getLabels().forEach(label -> label.refreshPosition()); + } + + // pack the components back into one graph + fgraph = componentsProcessor.recombine(components); + // apply the layout results to the original graph + graphImporter.applyLayout(fgraph); + + // ----------------------- Stress layout finished ----------------------- + + + // Create the 4 bend points around each node. + initalizeBendPoints(layoutGraph); + + progressMonitor.logGraph(layoutGraph, "Initialization"); + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Main process: + + // SETUP ROTATION: + + // First improvement that will already reduce the stress significantly. + for(ElkNode node : layoutGraph.getChildren()) { + stressMinimizingRotation(node); + } + progressMonitor.logGraph(layoutGraph, "Setup rotation"); + + + // STRESS MINIMIZATION: + + // The main sequence of moving and rotating nodes in order to reduce stress values and bringing + // the components into the right position. Multiple iterations improving the results + int count = 0; + do { + + // Every node in order. + for(ElkNode node : layoutGraph.getChildren()) { + // Rotation before shifting had better results. + stressMinimizingRotation(node); + + // If this node has many sharp angles, it could use a shift. + if (computeNodeAngles(node) < shiftThreshold) { + stressMinimizingShift(layoutGraph, node); + } + } + + progressMonitor.logGraph(layoutGraph, "Stress reduction on iteration " + count); + count++; + + // Can happen that in one iteration the stress gets worse, but over multiple iterations it gets better. + // Therefore no further termination criterion. + } while((count < iterationLimit)); + + // Closing rotation after the last shift movement: + for(ElkNode node : layoutGraph.getChildren()) { + stressMinimizingRotation(node); + } + + + + // FURTHER OPTIMIZATIONS: + + // Create additional bend points on edges when the existing bend points have a certain angle. + // Proceed to move the new bend point in order to relax those angles. + if (enableAdditionalBendPoints) { + for (ElkNode currNode : layoutGraph.getChildren()) { + // Consider only outgoing nodes so there are no duplicates. + for(ElkEdge oEdge : currNode.getOutgoingEdges()) { + addMiddleBendPoint(oEdge); + } + } + progressMonitor.logGraph(layoutGraph, "Additional middle bend points"); + } + + if(enableFlexibleNodeRadius) { + // TODO: Larger curves by growing nodeRadius until angles hitting a critical range. + // Individual for each node. + } + + progressMonitor.done(); /**/ + } + + + // TODO: Adjust the image size to the new coordinates. + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Stress computation functions: + + /** + * Computes the stress value of a given node that is created by sharp angles on the bend points of its + * corresponding edges. The outgoing edges as well as the incoming edges are taken into account. + * Sharper angles create more stress while angles of 180° would be perfect. + * + * @param node of which the angle stress value is desired. + * @return The stress value as a Double. + */ + private double computeAngleStress(ElkNode node) { + + double stress = 0; + double angle = 0; + + for (ElkEdge oEdge : node.getOutgoingEdges()) { + // Calculate angle between source node <- first bend point -> next bend point. + ElkBendPoint bp = oEdge.getSections().get(0).getBendPoints().get(0); + // An angle of 180° would cause no stress. + angle = 180 - calculateBendPointAngle(bp, oEdge); + // Sharper angles must not be allowed, therefore exponentially causing more stress. + stress = stress + Math.pow(angle,2); + + } + for (ElkEdge iEdge : node.getIncomingEdges()) { + // Calculate angle between previous bend point <- last bend point -> target node. + List bps = iEdge.getSections().get(0).getBendPoints(); + ElkBendPoint bp = bps.get(bps.size()-1); + angle = 180 - calculateBendPointAngle(bp, iEdge); + stress = stress + Math.pow(angle,2); + } + + return stress; + } + + + /** + * Computes the stress value for a given node that is created by sharp angles on the bend points of only + * its outgoing edges. + * Sharper angles create more stress while angles of 180° would be perfect. Additionally, the angle between + * this axis and the axis of incoming edges creates stress as well. + * + * @param node of which the angle stress value for the outgoing edges is desired. + * @return The stress value as a Double. + */ + private double computeOutAxisStress(ElkNode node) { + + double stress = 0; + double angle = 0; + + for (ElkEdge oEdge : node.getOutgoingEdges()) { + // Calculate angle between source node <- first bend point -> next bend point. + ElkBendPoint bp = oEdge.getSections().get(0).getBendPoints().get(0); + // An angle of 180° would cause no stress. + angle = 180 - calculateBendPointAngle(bp, oEdge); + // Sharper angles must not be allowed, therefore exponentially causing more stress. + stress = stress + Math.pow(angle,2); + + } + // Additional stress relative to other axis. + // An angle of 90° between the two axis would be perfect while an angle close to 0° is prohibited. + angle = 90 - calculateAxisAngle(node); + stress = stress + Math.pow(angle,2); + + return stress; + } + + + /** + * Computes the stress value for a given node that is created by sharp angles on the bend points of only + * its incoming edges. + * Sharper angles create more stress while angles of 180° would be perfect. Additionally, the angle between + * this axis and the axis of outgoing edges creates stress as well. + * + * @param node of which the angle stress value for the incoming edges is desired. + * @return The stress value as a Double. + */ + private double computeInAxisStress(ElkNode node) { + + double stress = 0; + double angle = 0; + + for (ElkEdge iEdge : node.getIncomingEdges()) { + // Calculate angle between previous bend point <- last bend point -> target node. + List bps = iEdge.getSections().get(0).getBendPoints(); + ElkBendPoint bp = bps.get(bps.size()-1); + // An angle of 180° would cause no stress. + angle = 180 - calculateBendPointAngle(bp, iEdge); + // Sharper angles must not be allowed, therefore exponentially causing more stress. + stress = stress + Math.pow(angle,2); + } + // Additional stress relative to other axis. + // An angle of 90° between the two axis would be perfect while an angle close to 0° is prohibited. + angle = 90 - calculateAxisAngle(node); + stress = stress + Math.pow(angle,2); + + return stress; + } + + + /** + * Computes the stress value of a given node that is created by distance towards other nodes. + * The range of the bend points around the nodes are taken into account. + * + * @param graph the whole graph. + * @param node node of which the distance stress value is desired. + * @return The stress value as a Double. + */ + private double computeDistanceStress(ElkNode graph, ElkNode node) { + + double stress = 0; + + for (ElkEdge oEdge : node.getOutgoingEdges()) { + + // Get neighbor node that are connected via the outgoing edges. + ElkNode target = (ElkNode) oEdge.getTargets().get(0); + double dx = target.getX() - node.getX(); + double dy = target.getY() - node.getY(); + + // Vector from this node to neighbor node. + KVector distanceVector = new KVector(dx, dy); + double distance = distanceVector.length(); + + // Exponentially more stress when node is further away from desired distance. + stress = stress + Math.pow(Math.abs(distance - desiredNodeDistance), 1.5); + } + + for (ElkEdge iEdge : node.getIncomingEdges()) { + + // Get neighbor node that are connected via the incoming edges. + ElkNode source = (ElkNode) iEdge.getSources().get(0); + double dx = source.getX() - node.getX(); + double dy = source.getY() - node.getY(); + + // Vector from this node to neighbor node. + KVector distanceVector = new KVector(dx, dy); + double distance = distanceVector.length(); + + // Exponentially more stress when node is further away from desired distance. + stress = stress + Math.pow(Math.abs(distance - desiredNodeDistance), 1.5); + } + return stress; + } + + + /** + * Calculates the angle at the given bend point between its previous and next points along the given edge. + * + * @param bendPoint of the desired angle. + * @param edge of the corresponding bend point. + * @return The angle at the given bend point in degree. + */ + private double calculateBendPointAngle(ElkBendPoint bendPoint, ElkEdge edge) { + + List bps = edge.getSections().get(0).getBendPoints(); + int i = bps.indexOf(bendPoint); + double x = bendPoint.getX(); + double y = bendPoint.getY(); + + // Vectors from the bend point (x,y) towards the previous and next point. + KVector prevPoint; + KVector nextPoint; + + // When it is the first bend point the previous point along the edge is the source node. + if(i == 0) { + ElkNode sNode = (ElkNode) edge.getSources().get(0); + prevPoint = new KVector(sNode.getX() - x, sNode.getY() - y); + nextPoint = new KVector(bps.get(i+1).getX() - x, bps.get(i+1).getY() - y); + + // When it is the last bend point the next point along the edge is the target node. + } else if (i == bps.size()-1) { + ElkNode tNode = (ElkNode) edge.getTargets().get(0); + prevPoint = new KVector(bps.get(i-1).getX() - x, bps.get(i-1).getY() - y); + nextPoint = new KVector(tNode.getX() - x, tNode.getY() - y); + + // Bend point is in between other bend points. + } else { + prevPoint = new KVector(bps.get(i-1).getX() - x,bps.get(i-1).getY() - y); + nextPoint = new KVector(bps.get(i+1).getX() - x,bps.get(i+1).getY() - y); + } + // Calculate angle between vectors. + return(Math.toDegrees(nextPoint.angle(prevPoint))); + } + + + /** + * Calculates the angle at the given node between its outgoing and incoming edges. + * + * @param node of which the angle between the incoming and outgoing edges is desired. + * @return The angle at the given node in degree. + */ + private double calculateAxisAngle(ElkNode node) { + + double x = node.getX(); + double y = node.getY(); + + // Vectors from the node (x,y) towards one bend point of each axis. + KVector prevPoint; + KVector nextPoint; + + // Bend point of the first outgoing edge. + ElkBendPoint bpo = node.getOutgoingEdges().get(0).getSections().get(0).getBendPoints().get(0); + // Bend point of the first incoming edge. + List bps = node.getIncomingEdges().get(0).getSections().get(0).getBendPoints(); + ElkBendPoint bpi = bps.get(bps.size()-1); + + prevPoint = new KVector(bpo.getX() - x, bpo.getY() - y); + nextPoint = new KVector(bpi.getX() - x, bpi.getY() - y); + + // Calculate angle between the two axis. + return(Math.toDegrees(nextPoint.angle(prevPoint))); + } + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Stress reducing functions: + + /** + * Attempt in minimizing the stress value of a given node by rotating its corresponding + * bend points around the node. Rotates the outgoing and incoming axis separately when enabled. + * + * @param node of which the stress value should be reduced by rotation. + */ + private void stressMinimizingRotation(ElkNode node) { + + int count = 0; + double angle = 0; + double prevStress = Double.MAX_VALUE; + + do { + // ROTATION: + + // Save previous stress for comparision after rotation. + prevStress = computeAngleStress(node); + // The more stress the greater the rotation should be. + angle = rotationValue*(prevStress / 2500); + rotateNode(node, angle); + + // When the new stress is larger, turn back. + if (prevStress < computeAngleStress(node)) { + // A greater rotation here allows adjustments in both directions. + rotateNode(node, - 2*angle); + } + + + // AXIS ROTATION when enabled: + + // Whole rotation + individual axis rotation works well (only axis not so well). + if (separateAxisRotation) { + + prevStress = computeOutAxisStress(node); + // The more stress the greater the rotation should be. + angle = rotationValue*(prevStress / 10000); + rotateOutgoingAxis(node, angle); + + // When the new stress is larger, turn back. + if (prevStress < computeOutAxisStress(node)) { + rotateOutgoingAxis(node, - 2*angle); + } + + // Stress of incoming axis depends on the freshly changed angle of the outgoing axis. + prevStress = computeInAxisStress(node); + angle = rotationValue*(prevStress / 10000); + rotateIncomingAxis(node, angle); + + // When the new stress is larger, turn back. + if (prevStress < computeInAxisStress(node)) { + rotateIncomingAxis(node, - 2*angle); + } + } + + count++; + } while(count < iterationLimit && epsilon < 2); + } + + + /** + * Attempt in minimizing the stress value of a given node by shifting its position and the corresponding + * bend points around the node. + * + * @param graph the whole graph. + * @param node of which the stress value should be reduced by shift movements. + */ + private void stressMinimizingShift(ElkNode graph, ElkNode node) { + + int count = 0; + double prevStress = Double.MAX_VALUE; + + do { + // MOVEMENT on x-Axis: + + // Stress depends on the angles of bend points + position relative to connected nodes. + prevStress = computeAngleStress(node) + computeDistanceStress(graph, node); + + moveNode(node, node.getX() + shiftValue, node.getY()); + + // When the new stress is larger, shift back. + if (prevStress < (computeAngleStress(node) + computeDistanceStress(graph, node))) { + // A greater movement here allows adjustments in both directions. + moveNode(node, node.getX() - 2*shiftValue, node.getY()); + } + + // MOVEMENT on y-Axis: + + // Compute stress anew with freshly changed position of the node. + prevStress = computeAngleStress(node) + computeDistanceStress(graph, node); + moveNode(node, node.getX(), node.getY() + shiftValue); + + // When the new stress is larger, shift back. + if (prevStress < (computeAngleStress(node) + computeDistanceStress(graph, node))) { + moveNode(node, node.getX(), node.getY() - 2*shiftValue); + } + + count++; + } while(count < iterationLimit); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Additional improvement functions: + + /** + * Adds an additional bend point on the middle of a given edge if the angle on the outer bend points + * are to sharp and below the 'additionalBendPointsThreshold'. + * + * @param edge to which an additional middle bend point is desired. + */ + private void addMiddleBendPoint(ElkEdge edge) { + + // Checks if the outer bend point angles are below the threshold. + if (needsMidPoint(edge)){ + + List bps = edge.getSections().get(0).getBendPoints(); + + double x1 = bps.get(0).getX(); + double y1 = bps.get(0).getY(); + double x2 = bps.get(bps.size()-1).getX(); + double y2 = bps.get(bps.size()-1).getY(); + + // Initial position is in the center of the edge. + double x = (x1 + x2)/2; + double y = (y1 + y2)/2; + + // Create the bend point and adds it to the list in between the others. + ElkBendPoint middleBp = ElkGraphUtil.createBendPoint(null, x, y); + bps.add(1, middleBp); + + // Shift the new bend point to relax angles of others but keep an equal distance to each side. + shiftBendPoint(bps.get(1), edge); + + + // Further create 2 bend points to help visualizing smooth splines. + + // After the creation of an additional middle bend point, the final visualization consist of + // 5 Points in total. For the creation of a bézier curve, these get split up into a 3-point and + // a 2-point bézier curve, resulting in having a smooth curve into a sharp edge. + // These 2 helper bend points will be opposite of each other next to the new middle point and + // therefore using the same technique to have a smooth edge transition like the nodes. + + ElkBendPoint helperBp1 = ElkGraphUtil.createBendPoint(null, x, y); + ElkBendPoint helperBp2 = ElkGraphUtil.createBendPoint(null, x, y); + + // Use vector to mimic the direction from first bp to last bp. + KVector directionVector = new KVector(x2 - x1, y2 - y1); + // Distance from the middle bend point to the helpers, large factor results in wider curves. + directionVector.scale(curveWidthFactor); + + // Position helper bend points opposite of each other next to the new middle bend point. + helperBp1.set(bps.get(1).getX() + directionVector.x, bps.get(1).getY() + directionVector.y); + directionVector.scale(-1); + helperBp2.set(bps.get(1).getX() + directionVector.x, bps.get(1).getY() + directionVector.y); + + // Adds them to the list in the right order. + bps.add(2, helperBp1); + bps.add(1, helperBp2); + } + } + + + /** + * Whether an given edge needs an additional bend point helping to relax the angles on the outer bend points. + * + * @param edge in question. + * @return If the given edge needs a middle bend point. + */ + private boolean needsMidPoint(ElkEdge edge) { + + List bps = edge.getSections().get(0).getBendPoints(); + + // Edge needs a middle bend point when the angle of the outer bend points is below a desire threshold. + ElkBendPoint FirstBp = bps.get(0); + ElkBendPoint LastBp = bps.get(bps.size()-1); + + return (calculateBendPointAngle(FirstBp, edge) <= additionalBendPointsThreshold + && calculateBendPointAngle(LastBp, edge) <= additionalBendPointsThreshold); + } + + + /** + * Perform a stress reducing shift for a bend point, depending on the angles and distances + * of the outer bend points. + * + * @param bp to shift. + * @param edge of corresponding bend point. + */ + private void shiftBendPoint(ElkBendPoint bp, ElkEdge edge) { + + List bps = edge.getSections().get(0).getBendPoints(); + int bpIndex = bps.indexOf(bp); + + // Only shifts bend point if it is not one of the outer ones. + if (bpIndex != 0 && bpIndex != bps.size()-1) { + + double x = bp.getX(); + double y = bp.getY(); + double prevStress = 0; + + // Remember start position calculate the height of the new bend point. + ElkBendPoint startPos = ElkGraphUtil.createBendPoint(null, x, y); + + // Shift middle bend point. + int count = 0; + do { + // MOVEMENT on x-Axis: + + // Stress depends on the angles of outer bend points + equal distance towards them. + prevStress = bendPointStress(bp, edge); + bp.setX(x+1); + + // When angle smaller or distance to other bps to great, shift back. + if (bendPointStress(bp, edge) > prevStress) { + bp.setX(x-2); + } + x = bp.getX(); + + + // MOVEMENT on y-Axis: + + // Compute stress anew with freshly changed position of the bend point. + prevStress = bendPointStress(bp, edge); + bp.setY(y+1); + + // When angle smaller or distance to other bps to great, shift back. + if (bendPointStress(bp, edge) > prevStress) { + bp.setY(y-2); + } + y = bp.getY(); + + count++; + + // How much the bend point is allowed distance itself (the bigger the curve gets). + // Otherwise it would relax the angles by keep shifting further away. + } while(count < iterationLimit && (calculateBendPointDistance(bp, startPos) <= curveHeight)); + + bp.setX(x); + bp.setY(y); + } + } + + + /** + * Stress of a single bend point depending on the angles and distances towards the previous and next bend point. + * + * @param bp the bend point of which the stress value is desired. + * @param edge of the given bend point. + * @return The stress value as a Double. + */ + private double bendPointStress(ElkBendPoint bp, ElkEdge edge) { + + double stress = 0; + List bps = edge.getSections().get(0).getBendPoints(); + int bpIndex = bps.indexOf(bp); + + // Only consider bend point that are in between of other bend points (not the outer ones). + if (bpIndex != 0 && bpIndex != bps.size()-1) { + // Get previous and next bend point in order. + ElkBendPoint prevBendPoint = bps.get(bpIndex - 1); + ElkBendPoint nextBendPoint = bps.get(bpIndex + 1); + + // Angles of the neighbor bend points. + double anglePrevBp = calculateBendPointAngle(prevBendPoint, edge); + double angleNextBp = calculateBendPointAngle(nextBendPoint, edge); + // Distances towards the neighbor bend points. + double distPrevBp = calculateBendPointDistance(bp, prevBendPoint); + double distNextBp = calculateBendPointDistance(bp, nextBendPoint); + + // Sharper angles must not be allowed, therefore exponentially causing more stress. + // Angle of 180° would be best, even if it is not achievable. + stress = Math.pow((180 - anglePrevBp), 2) + Math.pow((180 - angleNextBp), 2); + + // It's desired to have equal distances towards neighbor bend points. + stress = stress + Math.pow(Math.abs(distNextBp - distPrevBp), 3); + + } + return stress; + } + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Helping functions: + + /** + * Initialize bend points around a node for each of it's 4 edges. + * @param graph the node that contains the graph. + */ + private void initalizeBendPoints(ElkNode graph) { + + // Initialize bend points for outgoing edges around nodes. They need to get placed first. + for (ElkNode currNode : graph.getChildren()) { + // First bp is below node, second bp is above node. + for (int i = 0; i < 2; i++) { + + double d = nodeRadius * Math.pow(-1, i); + + // Outgoing edges share initially the same x coordinate as the node. + ElkEdge oEdge = currNode.getOutgoingEdges().get(i); + // We don't have hyperedges and therefore only one section. + ElkGraphUtil.createBendPoint(oEdge.getSections().get(0), currNode.getX(), currNode.getY()+d); + } + } + + // Initialize bend points for incoming edges around nodes. They need to get placed last. + for (ElkNode currNode : graph.getChildren()) { + // First bp is right of node, second bp is left of node + for (int i = 0; i < 2; i++) { + + double d = nodeRadius * Math.pow(-1, i); + + // Outgoing edges share initially the same y coordinate as the node. + ElkEdge iEdge = currNode.getIncomingEdges().get(i); + // We don't have hyperedges and therefore only one section. + ElkGraphUtil.createBendPoint(iEdge.getSections().get(0), currNode.getX()+d, currNode.getY()); + } + } + } + + + /** + * Rotates the 4 bend points around a given node by the angle provided in degrees, creating the illusion + * of actual rotating the whole node. Rotation is clockwise. + * + * @param node to rotate. + * @param angle in degree. + * */ + private void rotateNode(ElkNode node, double angle) { + // Perform rotation on both axis simultaneously. + rotateOutgoingAxis(node, angle); + rotateIncomingAxis(node, angle); + } + + + /** + * Rotates the axis of outgoing edges for the given node by the angle provided in degrees. Rotation is clockwise. + * @param node of which the outgoing axis will rotate. + * @param angle in degree. + * */ + private void rotateOutgoingAxis(ElkNode node, double angle) { + for (ElkEdge oEdge : node.getOutgoingEdges()) { + // For outgoing edges, the first bend point needs to rotate. + ElkBendPoint bp = oEdge.getSections().get(0).getBendPoints().get(0); + rotateBendPointAroundPoint(bp, node.getX(), node.getY(), angle); + } + } + + + /** + * Rotates the axis of incoming edges for the given node by the angle provided in degrees. Rotation is clockwise. + * @param node of which the incoming axis will rotate. + * @param angle in degree. + * */ + private void rotateIncomingAxis(ElkNode node, double angle) { + for (ElkEdge iEdge : node.getIncomingEdges()) { + // For incoming edges, the last bend point needs to rotate. + List bps = iEdge.getSections().get(0).getBendPoints(); + ElkBendPoint bp = bps.get(bps.size()-1); + rotateBendPointAroundPoint(bp, node.getX(), node.getY(), angle); + } + } + + + /** + * Rotates the given bend point around another point with given coordinates by the angle provided + * in degrees. Rotation is clockwise. + * + * @param bendPoint to rotate. + * @param x coordinate of the rotation point. + * @param y coordinate of the rotation point. + * @param angle in degree. + * */ + private void rotateBendPointAroundPoint(ElkBendPoint bendPoint, double x, double y, double angle) { + // Shift rotation point to origin. + double bpx = bendPoint.getX() - x; + double bpy = bendPoint.getY() - y; + + double sin = Math.sin(Math.toRadians(angle)); + double cos = Math.cos(Math.toRadians(angle)); + // Rotate bend point around origin, then shift back to new position. + bendPoint.setX((bpx * cos - bpy * sin) + x); + bendPoint.setY((bpy * cos + bpx * sin) + y); + } + + + /** + * Move the given node with its 4 bend points to the given coordinates. + * @param node to move. + * @param x the destined position on x-axis + * @param y the destined position on y-axis + */ + private void moveNode(ElkNode node, double x, double y) { + + // Distance towards the destined location. + double distX = x - node.getX(); + double distY = y - node.getY(); + + // Move bend points of outgoing edges. + for (ElkEdge oEdge : node.getOutgoingEdges()) { + ElkBendPoint bp = oEdge.getSections().get(0).getBendPoints().get(0); + bp.set(bp.getX() + distX, bp.getY() + distY); + } + + // Move bend points of incoming edges. + for (ElkEdge iEdge : node.getIncomingEdges()) { + List bps = iEdge.getSections().get(0).getBendPoints(); + ElkBendPoint bp = bps.get(bps.size()-1); + bp.set(bp.getX() + distX, bp.getY() + distY); + } + + // Move node. + node.setLocation(x, y); + } + + + /** + * Calculates the distance between two given bend points. + * @param first bend point. + * @param second bend point. + * @return The distance as a Double. + */ + private double calculateBendPointDistance(ElkBendPoint bp1, ElkBendPoint bp2) { + + // Difference between points. + double dx = bp2.getX() - bp1.getX(); + double dy = bp2.getY() - bp1.getY(); + + // Vector from this bp to other bp. + KVector distanceVector = new KVector(dx, dy); + return distanceVector.length(); + } + + + /** + * Sums up the angles at the four bend points around the given node. + * @param node of which the angles are desired. + * @return The sum of all four angles in degree as a Double. + */ + private double computeNodeAngles(ElkNode node) { + + double sum = 0; + + for (ElkEdge oEdge : node.getOutgoingEdges()) { + // Calculate angle between source node <- first bend point -> next bend point. + ElkBendPoint bp = oEdge.getSections().get(0).getBendPoints().get(0); + sum = sum + calculateBendPointAngle(bp, oEdge); + + } + for (ElkEdge iEdge : node.getIncomingEdges()) { + // Calculate angle between previous bend point <- last bend point -> target node. + List bps = iEdge.getSections().get(0).getBendPoints(); + ElkBendPoint bp = bps.get(bps.size()-1); + sum = sum + calculateBendPointAngle(bp, iEdge); + } + return sum; + } + +} + + +