ElkGraphBuilder.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
 * All Rights Reserved.
 *
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 *
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 *
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 *
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.ge.internal.diagram.runtime.layout;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.elk.core.math.ElkPadding;
import org.eclipse.elk.core.math.KVector;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.elk.core.options.Direction;
import org.eclipse.elk.core.options.HierarchyHandling;
import org.eclipse.elk.core.options.NodeLabelPlacement;
import org.eclipse.elk.core.options.PortConstraints;
import org.eclipse.elk.core.options.PortSide;
import org.eclipse.elk.core.options.SizeConstraint;
import org.eclipse.elk.core.options.SizeOptions;
import org.eclipse.elk.core.service.LayoutMapping;
import org.eclipse.elk.graph.ElkConnectableShape;
import org.eclipse.elk.graph.ElkEdge;
import org.eclipse.elk.graph.ElkGraphElement;
import org.eclipse.elk.graph.ElkLabel;
import org.eclipse.elk.graph.ElkNode;
import org.eclipse.elk.graph.ElkPort;
import org.eclipse.elk.graph.util.ElkGraphUtil;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.osate.ge.DockingPosition;
import org.osate.ge.graphics.Dimension;
import org.osate.ge.graphics.Style;
import org.osate.ge.graphics.internal.AgeConnection;
import org.osate.ge.graphics.internal.AgeShape;
import org.osate.ge.graphics.internal.Label;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramElementPredicates;
import org.osate.ge.internal.diagram.runtime.DiagramNode;
import org.osate.ge.internal.diagram.runtime.DockArea;
import org.osate.ge.internal.diagram.runtime.styling.StyleProvider;
import org.osate.ge.internal.util.DiagramElementUtil;

/**
 * Converts a branch to an ELK graph.
 *
 */
class ElkGraphBuilder {
	// Filter for diagram elements for which ELK ports will be created.
	private final Predicate<DiagramElement> dockedShapeFilter = de -> de.getGraphic() instanceof AgeShape
			&& !(de.getGraphic() instanceof Label) && de.getDockArea() != null;

	private final double paddingSize = 10;
	private final double portAndContentsPadding = 12.0; // Padding between ports and other contents

	private final StyleProvider styleProvider;
	private final LayoutInfoProvider layoutInfoProvider;
	private final LayoutOptions options;
	private final boolean omitNestedPorts;
	private final FixedPortPositionProvider fixedPortPositionProvider;
	private final boolean layoutConnectionLabels;

	/**
	 * Provides information used to position ports when fixed port positions are being used. If null is returned, the graph builder will assign a default position.
	 * Fixed port positions are used when nested ports are included in the graph. If the position of a port is provided, then the provider must provide the position for all
	 * sibling ports.
	 */
	public static interface FixedPortPositionProvider {
		/**
		 * Gets the side for the diagram element
		 * @param de the diagram element for which to get the side
		 * @return the side for the element element. A null value indicates that a fixed side is not provided.
		 */
		PortSide getPortSide(final DiagramElement de);

		/**
		 * Gets position along axis
		 * @param de the diagram element for which to get the position
		 * @return the position along the axis
		 */
		Double getPortPosition(final DiagramElement de);

		/**
		 * Implementation that returns null for all values
		 */
		public static final FixedPortPositionProvider NO_OP = new FixedPortPositionProvider() {
			@Override
			public PortSide getPortSide(DiagramElement de) {
				return null;
			}

			@Override
			public Double getPortPosition(DiagramElement de) {
				return null;
			}
		};
	}

	private ElkGraphBuilder(final StyleProvider styleProvider, final LayoutInfoProvider layoutInfoProvider,
			final LayoutOptions options, final boolean omitNestedPorts,
			final FixedPortPositionProvider fixedPortPositionProvider) {
		this.styleProvider = Objects.requireNonNull(styleProvider, "styleProvider must not be null");
		this.layoutInfoProvider = Objects.requireNonNull(layoutInfoProvider, "layoutInfoProvider must not be null");
		this.options = Objects.requireNonNull(options, "options must not be null");
		this.omitNestedPorts = omitNestedPorts;
		this.fixedPortPositionProvider = Objects.requireNonNull(fixedPortPositionProvider,
				"fixedPortPositionProvider must not be null");
		// The previous workaround only disabled layout of connection labels in specific cases. Unfortunately, the error
		// still occurs in some cases. This completely disables layout of connection labels until the issue is resolved.
		// See https://github.com/eclipse/elk/issues/763
		this.layoutConnectionLabels = false;
	}

	/**
	 * Builds an ELK graph for a node
	 * @param rootDiagramNode the root of the diagram branch for which to build the graph
	 * @param styleProvider is a style provider which provides the style for the diagram elements. The style provider is expected to return a final style. The style must not contain null values.
	 * @param layoutInfoProvider is the layout info provider which is used to determine label sizes.
	 * @param options the layout options
	 * @param omitNestedPorts must be false if layout ports on default sides is true.
	 * @param portPlacementInfoProvider provider which determines fixed port positions
	 * @return the layout mapping which contains the graph
	 */
	public static LayoutMapping buildLayoutGraph(final DiagramNode rootDiagramNode, final StyleProvider styleProvider,
			final LayoutInfoProvider layoutInfoProvider, final LayoutOptions options, final boolean omitNestedPorts,
			final FixedPortPositionProvider portPlacementInfoProvider) {
		// This case would indicate that we are using a multi-pass layout even though fixed position ports are being used.
		// Fixed side port positioning is not reliable because exceptions can be thrown in cases involving edges between nodes.
		if (omitNestedPorts && options.layoutPortsOnDefaultSides) {
			throw new IllegalArgumentException(
					"Omitting nested ports while position ports on default sides is not supported");
		}

		final ElkGraphBuilder graphBuilder = new ElkGraphBuilder(styleProvider, layoutInfoProvider, options,
				omitNestedPorts, portPlacementInfoProvider);
		return graphBuilder.buildLayoutGraph(rootDiagramNode);
	}

	private LayoutMapping buildLayoutGraph(final DiagramNode rootDiagramNode) {
		// Create the graph
		final LayoutMapping mapping = new LayoutMapping(null);
		final ElkNode rootNode = ElkGraphUtil.createGraph();
		rootNode.setProperty(CoreOptions.DIRECTION, Direction.RIGHT);
		mapping.setLayoutGraph(rootNode);

		// As of 2020-04-06, INCLUDE_CHILDREN causes layout issues. In particular, labels can overlap with children
		// https://github.com/eclipse/elk/issues/316
		// https://github.com/eclipse/elk/issues/412
		rootNode.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.SEPARATE_CHILDREN);

		if (rootDiagramNode instanceof AgeDiagram) {
			final ElkNode diagramElkNode = ElkGraphUtil.createNode(rootNode);
			mapping.getGraphMap().put(diagramElkNode, rootDiagramNode);
			createElkGraphElementsForNonLabelChildShapes(rootDiagramNode, diagramElkNode, mapping);
		} else if (rootDiagramNode instanceof DiagramElement) {
			createElkGraphElementsForElements(Collections.singleton((DiagramElement) rootDiagramNode), rootNode,
					mapping);
		}

		createElkGraphElementsForConnections(rootDiagramNode, mapping);

		return mapping;
	}

	private void createElkGraphElementsForNonLabelChildShapes(final DiagramNode parentNode, final ElkNode parent,
			final LayoutMapping mapping) {
		createElkGraphElementsForElements(parentNode.getChildren(), parent, mapping);
	}

	private void createElkGraphElementsForElements(final Collection<DiagramElement> elements, final ElkNode parent,
			final LayoutMapping mapping) {
		elements.stream()
		.filter(de -> de.getGraphic() instanceof AgeShape && !(de.getGraphic() instanceof Label)
				&& de.getDockArea() == null)
		.forEachOrdered(de -> createElkGraphElementForNonLabelAndUndockedShape(de, parent, mapping));

		// Create ports
		createElkPortsForElements(elements, parent, mapping);
	}

	/**
	 * Before calling this method, all labels for the parent node should have already been created and the node labels placement property must be set for the parent.
	 * @param elements
	 * @param parent
	 * @param mapping
	 */
	private void createElkPortsForElements(final Collection<DiagramElement> elements, final ElkNode parent,
			final LayoutMapping mapping) {

		final EnumSet<NodeLabelPlacement> nodeLabelPlacement = parent.getProperty(CoreOptions.NODE_LABELS_PLACEMENT);
		final boolean labelsAtTop = nodeLabelPlacement != null && nodeLabelPlacement.contains(NodeLabelPlacement.V_TOP);
		final double topPadding = labelsAtTop
				? parent.getLabels().stream().mapToDouble(l -> l.getY() + l.getHeight()).sum()
						: 0.0;

		// Group children by the port side to which they should be assigned.
		final List<DiagramElement> dockedShapes = elements.stream()
				.filter(dockedShapeFilter)
				.collect(Collectors.toList());
		final boolean diagramElementIncludesNestedPorts = dockedShapes.stream()
				.flatMap(de -> de.getChildren().stream())
				.anyMatch(dockedShapeFilter);

		// Set the flag to indicate that there are nested ports which will not be included in the final layout graph
		if (omitNestedPorts && diagramElementIncludesNestedPorts) {
			mapping.getLayoutGraph().setProperty(AgeLayoutOptions.NESTED_PORTS_WERE_OMITTED, true);
		}

		// Set port constraints and graph hierarchy handling of the parent based on whether the diagram element actually has nested ports.
		final boolean hasNestedPorts = !omitNestedPorts && diagramElementIncludesNestedPorts;
		PortConstraints portConstraints;
		if (dockedShapes.size() == 0) {
			// Don't constrain ports if there aren't any. As of 2017-10-11, some other values can affect the layout even if the node does not contain ports.
			portConstraints = PortConstraints.FREE;
		} else {
			if (hasNestedPorts || options.layoutPortsOnDefaultSides) {
				portConstraints = PortConstraints.FIXED_POS;
			} else {
				portConstraints = PortConstraints.FREE;
			}
		}
		parent.setProperty(CoreOptions.PORT_CONSTRAINTS, portConstraints);

		final Map<PortSide, List<DiagramElement>> groupedDockedElements = dockedShapes.stream()
				.collect(Collectors.groupingBy(de -> getPortSide(de, hasNestedPorts), HashMap::new,
						Collectors.toCollection(ArrayList::new)));

		// Determine padding
		// Need to pad both left and right sides equally if ELK is determining the side of ports. Otherwise, the space for the
		// port may overlap with shapes. This is likely caused by adjusting the border offset of ports
		// to lay out ports within the bounds of the containing shape
		final boolean padOppositeSides = !portConstraints.isSideFixed();
		final ElkPadding parentPadding = new ElkPadding(
				parent.getParent() == null || parent.getParent().getParent() == null ? 0.0 : portAndContentsPadding);
		for (final Entry<PortSide, List<DiagramElement>> entry : groupedDockedElements.entrySet()) {
			final PortSide side = entry.getKey();

			double maxSize = 0;
			for (final DiagramElement de : entry.getValue()) {
				maxSize = Math.max(maxSize, getOrthogonalSize(de, side));
			}

			// Update padding for the side
			final double sidePadding = maxSize + portAndContentsPadding;
			switch (side) {
			case NORTH:
				parentPadding.top = Math.max(parentPadding.top, sidePadding);
				break;

			case SOUTH:
				parentPadding.bottom = Math.max(parentPadding.bottom, sidePadding);
				break;

			case EAST:
				parentPadding.right = Math.max(parentPadding.right, sidePadding);
				if (padOppositeSides) {
					parentPadding.left = Math.max(parentPadding.left, sidePadding);
				}
				break;

			case WEST:
				parentPadding.left = Math.max(parentPadding.left, sidePadding);
				if (padOppositeSides) {
					parentPadding.right = Math.max(parentPadding.right, sidePadding);
				}

				break;

			default:
				// Ignore
				break;
			}
		}

		// Create and position the ports
		for (final Entry<PortSide, List<DiagramElement>> portSideToElementsEntry : groupedDockedElements.entrySet()) {
			final PortSide side = portSideToElementsEntry.getKey();
			final double additionalPadding;
			if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
				additionalPadding = Math.max(parentPadding.left, parentPadding.right);
			} else {
				additionalPadding = topPadding;
			}
			createAndPositionPorts(parent, portSideToElementsEntry.getValue(), portSideToElementsEntry.getKey(),
					additionalPadding, mapping, hasNestedPorts);
		}

		// Set the padding
		parent.setProperty(CoreOptions.PADDING, parentPadding);
	}

	// Create and position ports for an elk node
	private void createAndPositionPorts(final ElkNode parentNode, final List<DiagramElement> dockedDiagramElements,
			final PortSide side, final double additionalPadding, final LayoutMapping mapping,
			final boolean parentHasNestedPorts) {
		// Create and position ports
		double position = paddingSize + additionalPadding;
		double maxPosition = position;
		for (final DiagramElement dockedElement : dockedDiagramElements) {
			final ElkPort newPort = createPort(parentNode, side, dockedElement, 0, mapping);

			// Determine if the position has been provided by the fixed port position provider
			if (parentHasNestedPorts) {
				final Double overridePosition = fixedPortPositionProvider.getPortPosition(dockedElement);
				if (overridePosition != null) {
					position = overridePosition;
				}
			}

			setPositionAlongSide(newPort, side, position);
			addPositionOffsets((DiagramElement) mapping.getGraphMap().get(newPort), 0.0, side, mapping);

			position += getSize(newPort, side) + paddingSize;

			// The max position may not always be increasing because the fixed port position provider may override the position.
			maxPosition = Math.max(maxPosition, position);
		}

		maxPosition += 5; // Additional padding

		if (parentHasNestedPorts) {
			// Create a dummy port so that the last port on the side will have the minimum padding
			final ElkPort newPort = ElkGraphUtil.createPort(parentNode);
			newPort.setProperty(CoreOptions.PORT_SIDE, side);
			newPort.setY(maxPosition);
			newPort.setWidth(0);
			newPort.setHeight(0);
		}
	}

	/**
	 *
	 * @param parent
	 * @param side
	 * @param dockedElement
	 * @param portBorderOffset additional offset from the border. Used to set the ELK port border offset for nested ports.
	 * @param mapping
	 * @return
	 */
	private ElkPort createPort(final ElkNode parent, final PortSide side, final DiagramElement dockedElement,
			final double portBorderOffset, final LayoutMapping mapping) {
		final List<DiagramElement> dockedChildren = getDockedChildren(dockedElement);

		final Dimension untransformedGraphicSize = layoutInfoProvider.getPortGraphicSize(dockedElement);
		final Dimension transformedGraphicSize = transformDimension(untransformedGraphicSize, side);

		final Dimension untransformedLabelsSize = layoutInfoProvider.getDockedElementLabelsSize(dockedElement);
		final Dimension transformedLabelsSize = transformDimension(untransformedLabelsSize, side);

		// Create child ports and sort them by the size of the dimension parallel to the docked side
		final List<ElkPort> childPorts = dockedChildren.stream()
				.map(ce -> createPort(parent, side, ce, getOrthogonalSize(transformedGraphicSize, side), mapping))
				.sorted((p1, p2) -> Double.compare(getSize(p1, side), getSize(p1, side)))
				.collect(Collectors.toCollection(ArrayList::new));

		// If the port has child ports, then we assume it is a feature group.
		// The child ports are split into two bins and positioned such that they do not overlap the center of the feature group.
		// The parent port will be sized accordingly.
		final double maxChildBinSize;
		if (childPorts.size() > 0) {
			// Split the ports into two lists which have roughly equal heights/widths(depending on the docked side)
			double[] binSize = { 0.0, 0.0 };
			@SuppressWarnings("unchecked")
			final List<ElkPort>[] binLists = new ArrayList[] { new ArrayList<>(), new ArrayList<>() };

			for (final ElkPort childPort : childPorts) {
				final double size = getSize(childPort, side);
				final int binIndex = binSize[0] <= binSize[1] ? 0 : 1;
				binLists[binIndex].add(childPort);
				binSize[binIndex] += size + paddingSize;
			}

			// Determine the total size of the feature
			maxChildBinSize = Math.max(binSize[0], binSize[1]);

			// Set the position of the port relative to its parent because the size and position of the parent will be selected after its children are sized.
			for (int i = 0; i < 2; i++) {
				double childPosition = transformedLabelsSize.height
						+ i * (maxChildBinSize + transformedGraphicSize.height);
				for (final ElkPort childPort : binLists[i]) {
					setPositionAlongSide(childPort, side, childPosition);
					childPosition += getSize(childPort, side) + paddingSize;
				}
			}
		} else {
			maxChildBinSize = 0.0;
		}

		// Size the parent port based on the bin size.
		final double newGraphicSize = 2.0 * maxChildBinSize + transformedGraphicSize.height;
		final double totalSize = transformedLabelsSize.height + newGraphicSize;

		final ElkPort newPort = ElkGraphUtil.createPort(parent);
		mapping.getGraphMap().put(newPort, dockedElement);
		newPort.setProperty(CoreOptions.PORT_SIDE, side);

		// Determine max orthogonal size of children
		double maxChildOrthogonalSize = 0;
		for (final ElkPort childPort : childPorts) {
			maxChildOrthogonalSize = Math.max(maxChildOrthogonalSize, getOrthogonalSize(childPort, side));
		}

		// Determine size and position
		final Dimension newSize;
		if (DiagramElementPredicates.isResizeable(dockedElement)) {
			newSize = new Dimension(Math.max(untransformedLabelsSize.width,
					maxChildOrthogonalSize + getOrthogonalSize(transformedGraphicSize, side)), totalSize);
		} else {
			newSize = new Dimension(Math.max(untransformedLabelsSize.width, untransformedGraphicSize.width),
					transformedLabelsSize.height + untransformedGraphicSize.height);
		}

		// If omitting nested ports, remove any child ports. They still need to be created as part of the sizing process, but are not supported for the
		// final graph.
		if (omitNestedPorts) {
			for (final ElkPort childPort : childPorts) {
				mapping.getGraphMap().remove(childPort);
				EcoreUtil.remove(childPort);
			}
		}

		final Dimension transformedNewSize = transformDimension(newSize, side);
		newPort.setWidth(transformedNewSize.width);
		newPort.setHeight(transformedNewSize.height);

		// Set port border offset
		if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
			newPort.setProperty(CoreOptions.PORT_BORDER_OFFSET, -newPort.getHeight() - portBorderOffset);
		} else {
			newPort.setProperty(CoreOptions.PORT_BORDER_OFFSET, -newPort.getWidth() - portBorderOffset);
		}

		// Set the port anchor based on where the actual graphic will be.
		final Dimension transformedPortAnchor = transformDimension(new Dimension(untransformedGraphicSize.width / 2,
				transformedLabelsSize.height + maxChildBinSize + untransformedGraphicSize.height / 2.0), side);
		newPort.setProperty(CoreOptions.PORT_ANCHOR,
				new KVector(transformedPortAnchor.width, transformedPortAnchor.height));

		return newPort;
	}

	private PortSide getPortSide(final DiagramElement de, final boolean usingFixedPortPositions) {
		// If the container has nested ports, attempt to determine the port position from the fixed port position provider
		if (usingFixedPortPositions) {
			PortSide portSide = fixedPortPositionProvider.getPortSide(de);
			if (portSide != null) {
				return portSide;
			}
		}

		final DockingPosition defaultDockingPosition = de.getGraphicalConfiguration().getDefaultDockingPosition();
		return PortSideUtil.getPortSideForNonGroupDockArea(
				(defaultDockingPosition == DockingPosition.ANY && de.getDockArea() != null) ? de.getDockArea()
						: DockArea.fromDockingPosition(defaultDockingPosition));

	}

	private List<DiagramElement> getDockedChildren(final DiagramElement de) {
		return de.getChildren()
				.stream()
				.filter(child -> child.getGraphic() instanceof AgeShape && !(child.getGraphic() instanceof Label)
						&& child.getDockArea() != null)
				.collect(Collectors.toCollection(ArrayList::new));
	}

	private Optional<ElkGraphElement> createElkGraphElementForNonLabelAndUndockedShape(final DiagramElement de,
			final ElkNode layoutParent, final LayoutMapping mapping) {
		final ElkNode newNode = ElkGraphUtil.createNode(layoutParent);
		mapping.getGraphMap().put(newNode, de);

		if (de.hasPosition()) {
			newNode.setLocation(de.getX(), de.getY());
		}

		// Setting the size is disabled because setting it causes shapes which have a flow path(along with corresponding ports) to grow.
//		if (de.hasSize()) {
//			newNode.setDimensions(de.getWidth(), de.getHeight());
//		}

		final EnumSet<SizeConstraint> nodeSizeConstraints = EnumSet.of(SizeConstraint.PORTS,
				SizeConstraint.MINIMUM_SIZE, SizeConstraint.NODE_LABELS);
		newNode.setProperty(CoreOptions.NODE_SIZE_CONSTRAINTS, nodeSizeConstraints);
		newNode.setProperty(CoreOptions.INSIDE_SELF_LOOPS_ACTIVATE, true);

		newNode.setProperty(CoreOptions.NODE_SIZE_OPTIONS,
				EnumSet.of(SizeOptions.DEFAULT_MINIMUM_SIZE, SizeOptions.ASYMMETRICAL));

		// Create labels
		createElkLabels(de, newNode, mapping);

		// Create Children
		createElkGraphElementsForNonLabelChildShapes(de, newNode, mapping);

		return Optional.of(newNode);
	}

	private void createElkLabels(final DiagramElement parentElement, final ElkGraphElement parentLayoutElement,
			final LayoutMapping mapping) {
		// Don't create labels for ElkPort. The bounds of the port contain their labels.
		if (parentLayoutElement instanceof ElkPort) {
			return;
		}

		final boolean isConnection = parentElement.getGraphic() instanceof AgeConnection;

		final Style style = styleProvider.getStyle(parentElement);
		if (style.getPrimaryLabelVisible()) {
			// Create Primary Label
			if (parentElement.getLabelName() != null) {
				final ElkLabel elkLabel = createElkLabel(parentLayoutElement, parentElement.getLabelName(),
						layoutInfoProvider.getPrimaryLabelSize(parentElement));
				if (isConnection) {
					if (!layoutConnectionLabels) {
						elkLabel.setProperty(CoreOptions.NO_LAYOUT, true);
					}

					mapping.getGraphMap().put(elkLabel, new PrimaryConnectionLabelReference(parentElement));
				}
			}
		}

		// Create label for annotations which are part of the graphic configuration. These are only supported by non-connections.
		if (!isConnection && parentElement.getGraphicalConfiguration().getAnnotation() != null) {
			createElkLabel(parentLayoutElement, parentElement.getGraphicalConfiguration().getAnnotation(),
					layoutInfoProvider.getAnnotationLabelSize(parentElement));
		}

		// Create Secondary Labels
		parentElement.getChildren()
		.stream()
		.filter(c -> c.getGraphic() instanceof Label)
		.forEachOrdered(labelElement -> {
			final ElkLabel elkLabel = createElkLabel(parentLayoutElement, labelElement.getLabelName(),
					layoutInfoProvider.getPrimaryLabelSize(labelElement));
			if (isConnection) {
				if (!layoutConnectionLabels) {
					elkLabel.setProperty(CoreOptions.NO_LAYOUT, true);
				}

				mapping.getGraphMap().put(elkLabel, new SecondaryConnectionLabelReference(labelElement));
			}
		});

		if (parentLayoutElement instanceof ElkNode) {
			parentLayoutElement.setProperty(CoreOptions.NODE_LABELS_PLACEMENT, getNodeLabelPlacement(style));
		}
	}

	private static EnumSet<NodeLabelPlacement> getNodeLabelPlacement(final Style s) {
		// Determine horizontal node label placement
		NodeLabelPlacement horizontalNodeLabelPlacement = NodeLabelPlacement.H_CENTER;
		if (s.getHorizontalLabelPosition() != null) {
			switch (s.getHorizontalLabelPosition()) {
			case BEFORE_GRAPHIC:
			case GRAPHIC_BEGINNING:
				horizontalNodeLabelPlacement = NodeLabelPlacement.H_LEFT;
				break;
			case GRAPHIC_CENTER:
				horizontalNodeLabelPlacement = NodeLabelPlacement.H_CENTER;
				break;
			case GRAPHIC_END:
			case AFTER_GRAPHIC:
				horizontalNodeLabelPlacement = NodeLabelPlacement.H_RIGHT;
				break;
			}
		}

		// Determine vertical node label placement
		NodeLabelPlacement verticalNodeLabelPlacement = NodeLabelPlacement.V_CENTER;
		if (s.getVerticalLabelPosition() != null) {
			switch (s.getVerticalLabelPosition()) {
			case BEFORE_GRAPHIC:
			case GRAPHIC_BEGINNING:
				verticalNodeLabelPlacement = NodeLabelPlacement.V_TOP;
				break;
			case GRAPHIC_CENTER:
				verticalNodeLabelPlacement = NodeLabelPlacement.V_CENTER;
				break;
			case GRAPHIC_END:
			case AFTER_GRAPHIC:
				verticalNodeLabelPlacement = NodeLabelPlacement.V_BOTTOM;
				break;
			}
		}

		// Build the node label placement set
		// Assume the placement of the nodes is inside because outside labels are only supported for docked shapes.
		// However, the ELK graph we build does not contain labels for ports, such labels are considered part of the port itself.
		return EnumSet.of(horizontalNodeLabelPlacement, verticalNodeLabelPlacement, NodeLabelPlacement.INSIDE);
	}

	/**
	 * Adds position offset along axis for the specified port, graphic port, and children
	 * @param de
	 * @param offset
	 * @param side
	 * @param mapping
	 */
	private void addPositionOffsets(final DiagramElement de, final double offset, final PortSide side,
			final LayoutMapping mapping) {
		final ElkPort childPort = (ElkPort) mapping.getGraphMap().inverse().get(de);

		final double newPosition = addPositionOffset(childPort, side, offset);

		// Only attempt to update child ports if nested ports are not being omitted.
		if (!omitNestedPorts) {
			de.getChildren()
			.stream()
			.filter(child -> child.getGraphic() instanceof AgeShape && !(child.getGraphic() instanceof Label)
					&& child.getDockArea() != null)
			.forEach(
					childDiagramElement -> addPositionOffsets(childDiagramElement, newPosition, side, mapping));
		}
	}

	/**
	 * Adds an offset to the position along the side's axis and returns the position along the axis.
	 * @param port
	 * @param side
	 * @param offset
	 * @return
	 */
	private static double addPositionOffset(final ElkPort port, final PortSide side, final double offset) {
		if (PortSide.SIDES_EAST_WEST.contains(side)) {
			final double newPosition = port.getY() + offset;
			port.setY(newPosition);
			return newPosition;
		} else if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
			final double newPosition = port.getX() + offset;
			port.setX(newPosition);
			return newPosition;
		} else {
			throw new IllegalArgumentException("Unexpected side: " + side);
		}
	}

	private static ElkLabel createElkLabel(final ElkGraphElement parentLayoutElement, final String txt,
			final Dimension labelSize) {
		final ElkLabel newLabel = ElkGraphUtil.createLabel(parentLayoutElement);
		newLabel.setText(txt);

		if (labelSize != null) {
			newLabel.setWidth(labelSize.width);
			newLabel.setHeight(labelSize.height);
		}

		return newLabel;
	}

	/**
	 * Creates ELK edges for connection diagram nodes which are descendants of the specified node.
	 * Even though the results of the ELK edge routing are not used, it is still important because it affects the placements of shapes.
	 */
	private void createElkGraphElementsForConnections(final DiagramNode dn, final LayoutMapping mapping) {
		for (final DiagramElement de : dn.getChildren()) {
			if (de.getGraphic() instanceof AgeConnection) {
				final AgeConnection connection = (AgeConnection) de.getGraphic();

				// Flow indicators are represented by a node in the container which has a port and an edge connecting that port to the starting element
				final ElkConnectableShape edgeStart = getConnectableShape(de.getStartElement(), mapping);
				ElkConnectableShape edgeEnd = null;
				if (connection.isFlowIndicator) {
					// Find the first undocked ancestor for the flow indicator
					final DiagramElement undockedContainer = DiagramElementUtil
							.getUndockedDiagramElement(de.getParent());
					if (undockedContainer == null) {
						// Ignore the flow indicator if unable to find a containing element which isn't docked.
						continue;
					}

					// Find the ELK shape for the ancestor
					final ElkConnectableShape endContainer = getConnectableShape(undockedContainer, mapping);
					if (!(endContainer instanceof ElkNode)) {
						// Ignore the flow indicator if the container isn't a node.
						continue;
					}

					// Create the node for the end of the flow indicator
					final ElkNode endNode = ElkGraphUtil.createNode((ElkNode) endContainer);
					endContainer.setDimensions(0, 0);
					endNode.setProperty(CoreOptions.NODE_SIZE_CONSTRAINTS, EnumSet.noneOf(SizeConstraint.class));
					endNode.setProperty(CoreOptions.NODE_SIZE_OPTIONS, EnumSet.noneOf(SizeOptions.class));

					// Create port
					final ElkPort endPort = ElkGraphUtil.createPort(endNode);
					endPort.setProperty(CoreOptions.PORT_SIDE, PortSide.WEST);
					endPort.setX(0);
					endPort.setY(0);
					endPort.setWidth(0);
					endPort.setHeight(0);
					edgeEnd = endPort;
				} else {
					edgeEnd = getConnectableShape(de.getEndElement(), mapping);
				}

				if (edgeStart != null && edgeEnd != null) {
					final ElkConnectableShape start = edgeStart;
					final ElkConnectableShape end = edgeEnd;

					boolean insideSelfLoopsYo = true;

					// If the start and end of the edge is the same element, route the edge outside of the element.
					// An example of this sort of edge is a steady state state transition in the EMV2
					if (start == end) {
						insideSelfLoopsYo = false;
					}

					final ElkEdge newEdge = ElkGraphUtil.createSimpleEdge(start, end);

					// Ignore edges that have the same start and end port. These do not layout as intended.
					// See https://github.com/eclipse/elk/issues/532.
					// Allow edges with the same start and end shape because they layout as intended.
					if (start == end && start instanceof ElkPort) {
						continue;
					}

					// Ensure the edge has at least one section. Fixes NPE that can occur when laying out connections
					// with the same source and destination port.
					ElkGraphUtil.createEdgeSection(newEdge);

					newEdge.setProperty(CoreOptions.INSIDE_SELF_LOOPS_YO, insideSelfLoopsYo);
					mapping.getGraphMap().put(newEdge, de);

					createElkLabels(de, newEdge, mapping);

					// Create a dummy label. Ensures the edge is at least a minimal size and improves visibility when it is routed
					// along with other edges
					if (connection.isFlowIndicator && newEdge.getLabels().isEmpty()) {
						final ElkLabel spacingLabel = createElkLabel(newEdge, "<Spacing>", new Dimension(10, 10));
						if (!layoutConnectionLabels) {
							spacingLabel.setProperty(CoreOptions.NO_LAYOUT, true);
						}
					}
				}
			}

			createElkGraphElementsForConnections(de, mapping);
		}
	}

	private ElkConnectableShape getConnectableShape(final DiagramElement de, final LayoutMapping mapping) {
		if (de == null) {
			return null;
		}

		final ElkGraphElement elkElement = mapping.getGraphMap().inverse().get(de);
		if (elkElement instanceof ElkConnectableShape) {
			return (ElkConnectableShape) elkElement;
		}

		// If omitting nested ports, use the first non-nested port for connection edges.
		// This is intended to encourage ELK to position ports on a more appropriate location.
		if (omitNestedPorts) {
			if (de.getDockArea() == DockArea.GROUP && de.getParent() instanceof DiagramElement) {
				return getConnectableShape((DiagramElement) de.getParent(), mapping);
			}
		}

		return null;
	}

	// These helper functions are useful for working with the size and position of an element with respect to a single axis as determined by a port side.

	/**
	 * Returns the size orthogonal to the specified side. For example for a WEST side, this method would return the width of the element.
	 * @param de
	 * @param side
	 * @return
	 */
	private static double getOrthogonalSize(final DiagramElement de, final PortSide side) {
		if (PortSide.SIDES_EAST_WEST.contains(side)) {
			return de.getWidth();
		} else if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
			return de.getHeight();
		} else {
			throw new IllegalArgumentException("Unexpected side: " + side);
		}
	}

	private static double getOrthogonalSize(final ElkPort port, final PortSide side) {
		if (PortSide.SIDES_EAST_WEST.contains(side)) {
			return port.getWidth();
		} else if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
			return port.getHeight();
		} else {
			throw new IllegalArgumentException("Unexpected side: " + side);
		}
	}

	private static double getOrthogonalSize(final Dimension dim, final PortSide side) {
		if (PortSide.SIDES_EAST_WEST.contains(side)) {
			return dim.width;
		} else if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
			return dim.height;
		} else {
			throw new IllegalArgumentException("Unexpected side: " + side);
		}
	}

	private static double getSize(final ElkPort port, final PortSide side) {
		if (PortSide.SIDES_EAST_WEST.contains(side)) {
			return port.getHeight();
		} else if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
			return port.getWidth();
		} else {
			throw new IllegalArgumentException("Unexpected side: " + side);
		}
	}

	/**
	 * Set the position of a port along a specified side. For example, if the side is a west side, it sets the Y coordinate.
	 * @param port
	 * @param side
	 * @param position
	 */
	private static void setPositionAlongSide(final ElkPort port, final PortSide side, final double position) {
		if (PortSide.SIDES_EAST_WEST.contains(side)) {
			port.setY(position);
		} else if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
			port.setX(position);
		} else {
			throw new IllegalArgumentException("Unexpected side: " + side);
		}
	}

	/**
	 * Rotates the specified dimension based on the side.
	 * @param dim is the width and height specified as if it was along the east/west side
	 * @param side
	 * @return
	 */
	private static Dimension transformDimension(final Dimension dim, final PortSide side) {
		if (PortSide.SIDES_EAST_WEST.contains(side)) {
			return dim;
		} else if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
			return new Dimension(dim.height, dim.width);
		} else {
			throw new IllegalArgumentException("Unexpected side: " + side);
		}
	}
}