org.eclipse.elk.graph.util.ElkGraphUtil Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2016 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.graph.util;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.eclipse.elk.graph.EMapPropertyHolder;
import org.eclipse.elk.graph.ElkBendPoint;
import org.eclipse.elk.graph.ElkConnectableShape;
import org.eclipse.elk.graph.ElkEdge;
import org.eclipse.elk.graph.ElkEdgeSection;
import org.eclipse.elk.graph.ElkGraphElement;
import org.eclipse.elk.graph.ElkGraphFactory;
import org.eclipse.elk.graph.ElkGraphPackage;
import org.eclipse.elk.graph.ElkLabel;
import org.eclipse.elk.graph.ElkNode;
import org.eclipse.elk.graph.ElkPort;
import org.eclipse.elk.graph.properties.AdvancedPropertyValue;
import org.eclipse.elk.graph.properties.ExperimentalPropertyValue;
import org.eclipse.emf.common.util.AbstractTreeIterator;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EContentsEList.FeatureFilter;
import org.eclipse.emf.ecore.util.EContentsEList.FeatureIteratorImpl;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
/**
* Utility methods that make using the ElkGraph data structure a bit easier and thereby make ELK great again! The
* class offers different types of methods described below.
*
*
* Factory Methods
*
* While {@link ElkGraphFactory} offers methods that simply create new model objects, the factory methods
* offered by this class are designed to make creating a graph programmatically as easy as possible. This
* includes automatically setting containments and may at some point also include applying sensible defaults,
* where applicable. The latter is why using these methods instead of factory methods is usually a good
* idea.
*
*
* Convenience Methods
*
* This class offers quite a few convenience methods, including easy iteration over end points of an edge
* or edges incident to a node, a way to find the node a connectable shape represents, as well as ways to
* interact with a graph's structure.
*
* TODO: More documentation about what's in here.
*/
public final class ElkGraphUtil {
///////////////////////////////////////////////////////////////////////////////////////////////////
// Creation
/**
* Creates a new root node that will represent a graph.
*
* @return the root node.
*/
public static ElkNode createGraph() {
return createNode(null);
}
/**
* Creates a new node in the graph represented by the given parent node.
*
* @param parent the parent node. May be {@code null}, in which case the new node is not added to anything.
* @return the new node.
*/
public static ElkNode createNode(final ElkNode parent) {
ElkNode node = ElkGraphFactory.eINSTANCE.createElkNode();
if (parent != null) {
node.setParent(parent);
}
return node;
}
/**
* Creates a new port for the given parent node.
*
* @param parent the parent node. May be {@code null}, in which case the new port is not added to anything.
* @return the new port.
*/
public static ElkPort createPort(final ElkNode parent) {
ElkPort port = ElkGraphFactory.eINSTANCE.createElkPort();
if (parent != null) {
port.setParent(parent);
}
return port;
}
/**
* Creates a new label for the given graph element.
*
* @param parent the parent element. May be {@code null}, in which case the new label is not added to anything.
* @return the new label.
*/
public static ElkLabel createLabel(final ElkGraphElement parent) {
ElkLabel label = ElkGraphFactory.eINSTANCE.createElkLabel();
if (parent != null) {
label.setParent(parent);
}
return label;
}
/**
* Creates a new label with the given text for the given graph element.
*
* @param text the label's text.
* @param parent the parent element. May be {@code null}, in which case the new label is not added to anything.
* @return the new label.
*/
public static ElkLabel createLabel(final String text, final ElkGraphElement parent) {
ElkLabel label = createLabel(parent);
label.setText(text);
return label;
}
/**
* Creates a new edge contained in the given node, but not connecting anything yet. Note that the containing
* node defines the coordinate system for the edge's routes and is not straightforward to select. One way to get
* around this issue is to create an edge without a containing node, setup its sources and targets, and call
* {@link #updateContainment(ElkEdge)} afterwards.
*
* @param containingNode the edge's containing node. May be {@code null}, in which case the new edge is not added
* to anything.
* @return the new edge.
*/
public static ElkEdge createEdge(final ElkNode containingNode) {
ElkEdge edge = ElkGraphFactory.eINSTANCE.createElkEdge();
if (containingNode != null) {
edge.setContainingNode(containingNode);
}
return edge;
}
/**
* Creates an edge that connects the given source to the given target and sets the containing node accordingly.
* This requires the source and target to be in the same graph model.
*
* @param source the edge's source.
* @param target the edge's target.
* @return the new edge.
* @throws NullPointerException if {@code source} or {@code target} is {@code null}.
*/
public static ElkEdge createSimpleEdge(final ElkConnectableShape source, final ElkConnectableShape target) {
Objects.requireNonNull(source, "source cannot be null");
Objects.requireNonNull(target, "target cannot be null");
ElkEdge edge = createEdge(null);
edge.getSources().add(source);
edge.getTargets().add(target);
updateContainment(edge);
return edge;
}
/**
* Creates a hyperedge that connects the given sources to the given targets and sets the containing node
* accordingly. This requires the sources and targets to be in the same graph model.
*
* @param sources the edge's sources.
* @param targets the edge's targets.
* @return the new edge.
* @throws NullPointerException if {@code sources} or {@code targets} is {@code null}.
*/
public static ElkEdge createHyperedge(final Iterable sources,
final Iterable targets) {
Objects.requireNonNull(sources, "sources cannot be null");
Objects.requireNonNull(targets, "targets cannot be null");
ElkEdge edge = createEdge(null);
List edgeSources = edge.getSources();
for (ElkConnectableShape source : sources) {
edgeSources.add(source);
}
List edgeTargets = edge.getTargets();
for (ElkConnectableShape target : targets) {
edgeTargets.add(target);
}
updateContainment(edge);
return edge;
}
/**
* Creates a new edge section and adds it to the given edge.
*
* @param edge the edge to add the new section to. May be {@code null}, in which case the new edge section is
* not added to anything.
* @return the new edge section.
*/
public static ElkEdgeSection createEdgeSection(final ElkEdge edge) {
ElkEdgeSection section = ElkGraphFactory.eINSTANCE.createElkEdgeSection();
if (edge != null) {
edge.getSections().add(section);
}
return section;
}
/**
* Returns the edge's first edge section or creates one if there is none.
*
* @param edge the edge whose first edge section to retrieve.
* @param resetSection {@code true} if all bend points should be removed and the location reset on the section
* before it is returned.
* @param removeOtherSections {@code true} if all the other edge sections, if any, should be removed.
* @return the first edge section.
*/
public static ElkEdgeSection firstEdgeSection(final ElkEdge edge, final boolean resetSection,
final boolean removeOtherSections) {
if (edge.getSections().isEmpty()) {
// Create and return a new section
return createEdgeSection(edge);
} else {
// Retrieve the first section
ElkEdgeSection section = edge.getSections().get(0);
if (resetSection) {
section.getBendPoints().clear();
section.setStartLocation(0, 0);
section.setEndLocation(0, 0);
}
if (removeOtherSections) {
List sections = edge.getSections();
while (sections.size() > 1) {
sections.remove(sections.size() - 1);
}
}
return section;
}
}
/**
* Creates a new bend point and adds it to the given edge section.
*
* @param edgeSection the edge section to add the bend point to. May be {@code null}, in which case the new
* bend point is not added to anything.
* @return the new bend point.
*/
public static ElkBendPoint createBendPoint(final ElkEdgeSection edgeSection) {
return createBendPoint(edgeSection, 0, 0);
}
/**
* Creates a new bend point with the given coordinates and adds it to the given edge section.
*
* @param edgeSection the edge section to add the bend point to. May be {@code null}, in which case the new
* bend point is not added to anything.
* @param x the bend point's x coordinate.
* @param y the bend point's y coordinate.
* @return the new bend point.
*/
public static ElkBendPoint createBendPoint(final ElkEdgeSection edgeSection, final double x, final double y) {
ElkBendPoint bendPoint = ElkGraphFactory.eINSTANCE.createElkBendPoint();
bendPoint.set(x, y);
if (edgeSection != null) {
edgeSection.getBendPoints().add(bendPoint);
}
return bendPoint;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Edge Containment
/**
* Changes an edge's containment to the one returned by {@link #findBestEdgeContainment(ElkEdge)}.
*
* @param edge the edge to update the containment of.
* @throws NullPointerException if {@code edge} is {@code null}.
* @throws IllegalArgumentException if {@code edge} does not have at least one source or target.
*/
public static void updateContainment(final ElkEdge edge) {
Objects.requireNonNull(edge, "edge cannot be null");
edge.setContainingNode(findBestEdgeContainment(edge));
}
/**
* Finds the node the given edge should best be contained in given the edge's sources and targets. This is usually
* the first common ancestor of all sources and targets. Finding this containment requires all sources and targets
* to be contained in the same graph model. If that is not the case, this method will return {@code null}. If the
* edge is not connected to anything, an exception is thrown. If the edge is connected to only one shape, that
* shape's parent is returned.
*
* @param edge the edge to find the best containment for.
* @return the best containing node, or {@code null} if none could be found.
* @throws NullPointerException if {@code edge} is {@code null}.
* @throws IllegalArgumentException if {@code edge} does not have at least one source or target.
*/
public static ElkNode findBestEdgeContainment(final ElkEdge edge) {
Objects.requireNonNull(edge, "edge cannot be null");
/* We start with corner cases: an edge which is not connected to anything and an edge which is connected to
* just one shape.
*/
switch (edge.getSources().size() + edge.getTargets().size()) {
case 0:
// We need at least one to work with
throw new IllegalArgumentException("The edge must have at least one source or target.");
case 1:
// Return the parent of the only incident thing
if (edge.getSources().isEmpty()) {
return connectableShapeToNode(edge.getTargets().get(0)).getParent();
} else {
return connectableShapeToNode(edge.getSources().get(0)).getParent();
}
}
/* From now on we know that the edge has at least two incident shapes. We now check the simple common cases
* first before moving on to the more complex general case. The most common cases are that the edge is not a
* hyperedge and one of the following is true:
* 1. The edge's source and target have the same parent node. In this case, the containment is that parent
* node.
* 2. The source is the target's parent (or the other way around). In that case, the source or the target
* is the containment, respectively.
*/
if (edge.getSources().size() == 1 && edge.getTargets().size() == 1) {
ElkNode sourceNode = connectableShapeToNode(edge.getSources().get(0));
ElkNode targetNode = connectableShapeToNode(edge.getTargets().get(0));
if (sourceNode.getParent() == targetNode.getParent()) {
return sourceNode.getParent();
} else if (sourceNode == targetNode.getParent()) {
return sourceNode;
} else if (targetNode == sourceNode.getParent()) {
return targetNode;
}
}
/* Finally, the most general case. We go through all incident shapes and keep track of the highest
* common ancestor we have found so far. For each new node we process, we distinguish three cases:
* 1. It is the common ancestor itself or one of the common ancestor's descendants. In this case,
* nothing needs to be done.
* 2. It is a sibling of the common ancestor. In this case, the common ancestor is the new node's
* parent.
* 3. The common ancestor is either a descendant of the new shape or the two are not related in
* any way. In this case, we simply traverse down the ancestor hierarchy of both nodes and
* set the common ancestor to the lowest common ancestor of the two that we find.
*/
Iterator incidentShapes = allIncidentShapes(edge).iterator();
ElkNode commonAncestor = connectableShapeToNode(incidentShapes.next());
while (incidentShapes.hasNext()) {
ElkNode incidentNode = connectableShapeToNode(incidentShapes.next());
// Check if the current common ancestor is not an ancestor to the new node, in which case we need to act
if (incidentNode != commonAncestor && !isDescendant(incidentNode, commonAncestor)) {
if (incidentNode.getParent() == commonAncestor.getParent()) {
// The two nodes are siblings, the common ancestor is their parent
commonAncestor = incidentNode.getParent();
} else {
commonAncestor = findLowestCommonAncestor(commonAncestor, incidentNode);
if (commonAncestor == null) {
// The nodes are not part of the same graph. Abort.
return null;
}
}
}
}
return commonAncestor;
}
/**
* Returns the lowest common ancestor of the given two nodes. If the two nodes are not part of the same graph
* (that is, their root nodes differ), there is no common ancestor.
*
* @param node1 the first node.
* @param node2 the second node.
* @return the lowest common ancestor or {@code null} if there is none.
*/
public static ElkNode findLowestCommonAncestor(final ElkNode node1, final ElkNode node2) {
// Retrieve iterators over the node ancestors
List ancestors1 = Lists.newArrayList(new AncestorIterator(node1, true));
ListIterator iterator1 = ancestors1.listIterator(ancestors1.size());
List ancestors2 = Lists.newArrayList(new AncestorIterator(node2, true));
ListIterator iterator2 = ancestors2.listIterator(ancestors2.size());
// Traverse the ancestor hierarchies from the end as longs as the elements we find are the same
ElkNode commonAncestor = null;
while (iterator1.hasPrevious() && iterator2.hasPrevious()) {
ElkNode ancestor1 = iterator1.previous();
ElkNode ancestor2 = iterator2.previous();
if (ancestor1 == ancestor2) {
commonAncestor = ancestor1;
} else {
// The ancestral lines differ; no need to continue
break;
}
}
return commonAncestor;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Convenience
/**
* Returns an iterable which contains all edges incoming to a node, whether directly connected or
* through a port.
*
* @param node the node whose incoming edges to gather.
* @return iterable with all incoming edges.
*/
public static Iterable allIncomingEdges(final ElkNode node) {
List> incomingEdgeIterables = Lists.newArrayListWithCapacity(1 + node.getPorts().size());
incomingEdgeIterables.add(node.getIncomingEdges());
for (ElkPort port : node.getPorts()) {
incomingEdgeIterables.add(port.getIncomingEdges());
}
return Iterables.concat(incomingEdgeIterables);
}
/**
* Returns an iterable which contains all edges outgoing from a node, whether directly connected or
* through a port.
*
* @param node the node whose outgoing edges to gather.
* @return iterable with all outgoing edges.
*/
public static Iterable allOutgoingEdges(final ElkNode node) {
List> outgoingEdgeIterables = Lists.newArrayListWithCapacity(1 + node.getPorts().size());
outgoingEdgeIterables.add(node.getOutgoingEdges());
for (ElkPort port : node.getPorts()) {
outgoingEdgeIterables.add(port.getOutgoingEdges());
}
return Iterables.concat(outgoingEdgeIterables);
}
/**
* Returns an iterable which contains all edges the given connectable shape is incident to.
*
* @param shape the connectable shape whose incident edges to return.
* @return iterable with all incident edges.
*/
public static Iterable allIncidentEdges(final ElkConnectableShape shape) {
return Iterables.concat(shape.getIncomingEdges(), shape.getOutgoingEdges());
}
/**
* Returns an iterable which contains all edges incident to a node, whether directly connected or
* through a port.
*
* @param node the node whose incident edges to gather.
* @return iterable with all incident edges.
*/
public static Iterable allIncidentEdges(final ElkNode node) {
return Iterables.concat(allOutgoingEdges(node), allIncomingEdges(node));
}
/**
* Returns an iterable which contains all connectable shapes the given edge is incident to.
*
* @param edge the edge whose end points to return.
* @return iterable with all incident shapes.
*/
public static Iterable allIncidentShapes(final ElkEdge edge) {
return Iterables.concat(edge.getSources(), edge.getTargets());
}
/**
* Gathers all edge sections incident to a given edge section, regardless of whether the sections are
* incoming or outgoing.
*
* @param section the section whose incident sections to gather.
* @return iterable over all incident sections.
*/
public static Iterable allIncidentSections(final ElkEdgeSection section) {
return Iterables.concat(section.getIncomingSections(), section.getOutgoingSections());
}
/**
* Determines whether the given child node is a descendant of the given ancestor. This method is not reflexive (a
* node is not its own descendant).
*
* @param child a child node.
* @param ancestor a prospective ancestory node.
* @return {@code true} if {@code child} is a direct or indirect child of {@code ancestor}.
*/
public static boolean isDescendant(final ElkNode child, final ElkNode ancestor) {
// Go up the hierarchy and see if we find the ancestor
ElkNode current = child;
while (current.getParent() != null) {
current = current.getParent();
if (current == ancestor) {
return true;
}
}
// Reached the root node without finding the ancestor
return false;
}
/**
* Returns the node that represents the graph the given element is part of. The method can return {@code null}.
* This either indicates that the given element is itself the root node of a graph, or that the element is part
* of an invalid graph structure.
*
* @param element the element whose graph to return.
* @return the graph that contains the element, or {@code null} if such a graph could not be found.
*/
public static ElkNode containingGraph(final ElkGraphElement element) {
ElkGraphElement current = element;
while (current != null) {
if (current instanceof ElkEdge) {
// Edges have a direct reference to their containing graph
return ((ElkEdge) current).getContainingNode();
} else if (current instanceof ElkNode) {
// A node's container is its graph
return ((ElkNode) current).getParent();
} else if (current.eContainer() instanceof ElkGraphElement) {
// Walk up the hierarchy
current = (ElkGraphElement) current.eContainer();
} else {
// Something's wrong
return null;
}
}
// --> current is null
return null;
}
/**
* Returns the node that belongs to the given connectable shape. That is, if the shape is a node, that itself is
* returned. If it is a port, the port's parent node is returned. This method may well return {@code null} if the
* connectable shape is a port that does not belong to a node.
*
* @param connectableShape the shape whose node to return.
* @return the node that belongs to the shape.
* @throws NullPointerException if {@code connectableShape} is {@code null}.
*/
public static ElkNode connectableShapeToNode(final ElkConnectableShape connectableShape) {
if (connectableShape instanceof ElkNode) {
return (ElkNode) connectableShape;
} else if (connectableShape instanceof ElkPort) {
return ((ElkPort) connectableShape).getParent();
} else if (connectableShape == null) {
throw new NullPointerException("connectableShape cannot be null");
} else {
// In case the meta model is changed in the distant future...
throw new UnsupportedOperationException("Only support nodes and ports.");
}
}
/**
* Returns the port that belongs to the given connectable shape, if any. That is, if the shape is a port, that
* itself is returned. If it is not, {@code null} is returned.
*
* @param connectableShape the shape whose port to return.
* @return the port that belongs to the shape or {@code null}.
* @throws NullPointerException if {@code connectableShape} is {@code null}.
*/
public static ElkPort connectableShapeToPort(final ElkConnectableShape connectableShape) {
if (connectableShape instanceof ElkPort) {
return (ElkPort) connectableShape;
} else if (connectableShape == null) {
throw new NullPointerException("connectableShape cannot be null");
} else {
return null;
}
}
/**
* Returns the source node of the passed simple edge. That is, the passed edge must have
* exactly one source and one target.
*
* @param simpleEdge the edge whose source node to return.
* @return the source node of the edge, regardless whether the actual source is a port or a node.
* @throws IllegalArgumentException if {@code simpleEdge} is not 'simple'.
*/
public static ElkNode getSourceNode(final ElkEdge simpleEdge) {
if (simpleEdge.getSources().size() != 1 || simpleEdge.getTargets().size() != 1) {
throw new IllegalArgumentException("Passed edge is not 'simple'.");
}
return connectableShapeToNode(simpleEdge.getSources().get(0));
}
/**
* Returns the target node of the passed simple edge. That is, the passed edge must have
* exactly one source and one target.
*
* @param simpleEdge the edge whose target node to return.
* @return the source node of the edge, regardless whether the actual source is a port or a node.
* @throws IllegalArgumentException if {@code simpleEdge} is not 'simple'.
*/
public static ElkNode getTargetNode(final ElkEdge simpleEdge) {
if (simpleEdge.getSources().size() != 1 || simpleEdge.getTargets().size() != 1) {
throw new IllegalArgumentException("Passed edge is not 'simple'.");
}
return connectableShapeToNode(simpleEdge.getTargets().get(0));
}
/**
* Returns the source port of the passed simple edge. That is, the passed edge must have
* exactly one source and one target.
*
* @param simpleEdge the edge whose source port to return.
* @return the source port of the edge. If the edge connects directly to a node, {@code null} is returned.
* @throws IllegalArgumentException if {@code simpleEdge} is not 'simple'.
*/
public static ElkPort getSourcePort(final ElkEdge simpleEdge) {
if (simpleEdge.getSources().size() != 1 || simpleEdge.getTargets().size() != 1) {
throw new IllegalArgumentException("Passed edge is not 'simple'.");
}
return connectableShapeToPort(simpleEdge.getSources().get(0));
}
/**
* Returns the target port of the passed simple edge. That is, the passed edge must have
* exactly one source and one target.
*
* @param simpleEdge the edge whose target port to return.
* @return the source node of the edge. If the edge connects directly to a node, {@code null} is returned.
* @throws IllegalArgumentException if {@code simpleEdge} is not 'simple'.
*/
public static ElkPort getTargetPort(final ElkEdge simpleEdge) {
if (simpleEdge.getSources().size() != 1 || simpleEdge.getTargets().size() != 1) {
throw new IllegalArgumentException("Passed edge is not 'simple'.");
}
return connectableShapeToPort(simpleEdge.getTargets().get(0));
}
/**
* Finds the non-label element that the given label labels. Since labels can be nested, we traverse up the
* containment hierarchy until we find a non-label element.
*
* @param label
* the label whose labeled element to find.
* @return first non-label ancestor or {@code null} if there is none.
*/
public static ElkGraphElement elementLabeledBy(final ElkLabel label) {
EObject element = label.getParent();
while (element instanceof ElkLabel) {
element = element.eContainer();
}
// Ensure that the element we've found is actually an ElkGraphElement
return element instanceof ElkGraphElement
? (ElkGraphElement) element
: null;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Iteration
/**
* A tree iterator that skips properties of {@link EMapPropertyHolder}s.
*/
private static class PropertiesSkippingTreeIterator extends AbstractTreeIterator {
/** Bogus serial version ID. */
private static final long serialVersionUID = 1L;
PropertiesSkippingTreeIterator(final Object object, final boolean includeRoot) {
super(object, includeRoot);
}
@Override
protected Iterator getChildren(final Object object) {
// We know that the object is an EObject; get an iterator over its content
Iterator iterator = ((EObject) object).eContents().iterator();
// The iterator will usually be a FeatureIteratorImpl that we can set a feature filter on
if (iterator instanceof FeatureIteratorImpl) {
((FeatureIteratorImpl) iterator).filter(new FeatureFilter() {
public boolean isIncluded(final EStructuralFeature eStructuralFeature) {
// We include everything but properties (layout options)
if (eStructuralFeature.getContainerClass().equals(EMapPropertyHolder.class)) {
return eStructuralFeature.getFeatureID()
!= ElkGraphPackage.EMAP_PROPERTY_HOLDER__PROPERTIES;
} else {
return true;
}
}
});
}
return iterator;
}
}
/**
* Returns an iterator over the EMF tree rooted at the given object which skips properties. This is usually useful
* for iterating over an ELK graph while modifying the properties of elements, which would otherwise be prone to
* throwing {@link ConcurrentModificationException}s.
*
* @param root the EMF tree's root.
* @param includeRoot {@code true} if the first returned element should be the tree's root itself.
* @return the requested iterator.
*/
public static Iterator propertiesSkippingIteratorFor(final EObject root, final boolean includeRoot) {
return new PropertiesSkippingTreeIterator(root, includeRoot);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Property values
/**
* Returns whether an enumeration value, usually the value of layout option, is considered advanced.
* The user should have extended knowledge about the effects and underlying methods that are
* activated when using the layout option value.
*
* @param enumValue an enumeration value, usually representing the value of a layout option.
* @return whether the value is annotated as {@link AdvancedPropertyValue}.
* @see AdvancedPropertyValue
*/
public static boolean isAdvancedPropertyValue(final Enum> enumValue) {
// elkjs-exclude-start
if (enumValue != null) {
try {
Annotation[] annotations =
enumValue.getClass().getField(enumValue.name()).getAnnotations();
return Arrays.stream(annotations)
.anyMatch(a -> a instanceof AdvancedPropertyValue);
} catch (NoSuchFieldException | SecurityException e) {
return false;
}
}
// elkjs-exclude-end
return false;
}
/**
* Returns whether an enumeration value, usually the value of layout option, is considered experimental.
* Experimental layout options may result in unexpected behavior, for instance because their implementation has not
* been thoroughly tested yet.
*
* @param enumValue
* an enumeration value, usually representing the value of a layout option.
* @return whether the value is annotated as {@link ExperimentalPropertyValue}.
* @see ExperimentalPropertyValue
*/
public static boolean isExperimentalPropertyValue(final Enum> enumValue) {
// elkjs-exclude-start
if (enumValue != null) {
try {
Annotation[] annotations =
enumValue.getClass().getField(enumValue.name()).getAnnotations();
return Arrays.stream(annotations)
.anyMatch(a -> a instanceof ExperimentalPropertyValue);
} catch (NoSuchFieldException | SecurityException e) {
return false;
}
}
// elkjs-exclude-end
return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Privates
/**
* Private constructor, don't call.
*/
private ElkGraphUtil() {
// Nothing to do here...
}
/**
* An iterator which, given a starting node, walks up the ancestor tree. The iteration can either start with the
* node itself or with its parent.
*/
private static class AncestorIterator implements Iterator {
/** The next node we will return. */
private ElkNode nextNode;
/**
* Creates a new iterator.
*
* @param startNode the node whose ancestors we want to travel along.
* @param includeNode {@code true} if {@code startNode} should be the first thing we return, {@code false}
* if its parent should be the first thing.
*/
AncestorIterator(final ElkNode startNode, final boolean includeNode) {
nextNode = includeNode ? startNode : startNode.getParent();
}
/* (non-Javadoc)
* @see java.util.Iterator#hasNext()
*/
@Override
public boolean hasNext() {
return nextNode != null;
}
/* (non-Javadoc)
* @see java.util.Iterator#next()
*/
@Override
public ElkNode next() {
if (nextNode == null) {
throw new NoSuchElementException("There is no more element.");
}
ElkNode next = nextNode;
nextNode = nextNode.getParent();
return next;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy