DiagramElementLayoutUtil.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.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.elk.core.RecursiveGraphLayoutEngine;
import org.eclipse.elk.core.math.KVector;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.elk.core.options.NodeLabelPlacement;
import org.eclipse.elk.core.options.PortSide;
import org.eclipse.elk.core.options.SizeConstraint;
import org.eclipse.elk.core.service.LayoutMapping;
import org.eclipse.elk.core.util.BasicProgressMonitor;
import org.eclipse.elk.core.util.ElkUtil;
import org.eclipse.elk.core.util.IGraphElementVisitor;
import org.eclipse.elk.graph.ElkEdge;
import org.eclipse.elk.graph.ElkEdgeSection;
import org.eclipse.elk.graph.ElkGraphElement;
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.ElkShape;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.statushandlers.StatusManager;
import org.osate.ge.BusinessObjectContext;
import org.osate.ge.DockingPosition;
import org.osate.ge.graphics.Dimension;
import org.osate.ge.graphics.Graphic;
import org.osate.ge.graphics.Point;
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.graphics.internal.ModeGraphic;
import org.osate.ge.internal.Activator;
import org.osate.ge.internal.GraphicalEditorException;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.AgeDiagramUtil;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramElementPredicates;
import org.osate.ge.internal.diagram.runtime.DiagramModification;
import org.osate.ge.internal.diagram.runtime.DiagramNode;
import org.osate.ge.internal.diagram.runtime.DiagramNodePredicates;
import org.osate.ge.internal.diagram.runtime.DockArea;
import org.osate.ge.internal.diagram.runtime.styling.StyleCalculator;
import org.osate.ge.internal.diagram.runtime.styling.StyleProvider;
import org.osate.ge.internal.ui.editor.InternalDiagramEditor;
import org.osate.ge.internal.util.DiagramElementUtil;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

/**
 * Utility class which provides functions for laying out diagrams.
 * <p>The methods in this class will catch error exceptions, display the error, and suppress them.</p>
 */
public class DiagramElementLayoutUtil {
	private DiagramElementLayoutUtil() {
	}

	private static final String INCREMENTAL_LAYOUT_LABEL = "Incremental Layout";
	private static final String LAYOUT_ALGORITHM = "org.eclipse.elk.layered";
	private static final double PORT_WIDTH_PADDING = 40.0;
	private static final double START_AND_END_BENDPOINT_DISTANCE = 4.0;

	/**
	 * Offset to the initial flow indicator in the primary axis from the start element. The primary axis is the X axis for start elements
	 * docked to the left or right sides. Used for incremental flow indicator layout.
	 */
	private static final double INCREMENTAL_FLOW_INDICATOR_PRIMARY_OFFSET = 50.0;

	/**
	 * Amount to increment the position of the flow indicator in the secondary axis. The secondary axis is the Y axis for start elements
	 * docked to the top or bottom. Used for incremental flow indicator layout.
	 */
	private static final double INCREMENTAL_FLOW_INDICATOR_SECONDARY_INCREMENT = 20.0;

	/**
	 * Amount to use to scale the primary offset when determining where to place bendpoint for incremental flow indicator layout
	 */
	private static final double INCREMENTAL_FLOW_INDICATOR_BENDPOINT_OFFSET_SCALING = 0.8;

	/**
	 * Performs a layout of the specified diagram nodes
	 * @param actionLabel description of the action
	 * @param editor the editor containing the diagram
	 * @param diagramNodes the nodes to layout. If null, the entire diagram will be laid out
	 * @param options the layout options
	 */
	public static void layout(final String actionLabel, final IEditorPart editor,
			final Collection<? extends DiagramNode> diagramNodes, final LayoutOptions options) {
		if (!(editor instanceof InternalDiagramEditor)) {
			throw new GraphicalEditorException("Editor must be an " + InternalDiagramEditor.class.getName());
		}

		final InternalDiagramEditor diagramEditor = ((InternalDiagramEditor) editor);
		final LayoutInfoProvider layoutInfoProvider = Adapters.adapt(diagramEditor, LayoutInfoProvider.class);
		layout(actionLabel, diagramEditor.getDiagram(), diagramNodes, layoutInfoProvider, options);
	}

	/**
	 * Perform a full diagram layout
	 * @param actionLabel description of the action
	 * @param diagram the diagram to layout
	 * @param layoutInfoProvider the layout info provider which provides additional information required for laying out the diagram
	 * @param options the layout options
	 */
	public static void layout(final String actionLabel, final AgeDiagram diagram, final LayoutInfoProvider layoutInfoProvider,
			final LayoutOptions options) {
		layout(actionLabel, diagram, null, layoutInfoProvider, options);
	}

	/**
	 * Performs a layout of the specified diagram nodes
	 * @param actionLabel description of the action
	 * @param diagram the diagram to layout. The specified nodes must be part of the specified diagram.
	 * @param diagramNodes the nodes to layout. If null, the entire diagram will be laid out
	 * @param layoutInfoProvider the layout info provider which provides additional information required for performing the layout
	 * @param options the layout options
	 */
	public static void layout(final String actionLabel, final AgeDiagram diagram,
			final Collection<? extends DiagramNode> diagramNodes, final LayoutInfoProvider layoutInfoProvider,
			final LayoutOptions options) {
		Objects.requireNonNull(actionLabel, "label must not be null");
		Objects.requireNonNull(diagram, "diagram must not be null");
		Objects.requireNonNull(layoutInfoProvider, "layoutInfoProvider must not be null");
		Objects.requireNonNull(options, "options must not be null");

		// Determine the diagram nodes to layout
		final Collection<DiagramNode> nodesToLayout;
		if (diagramNodes == null) {
			nodesToLayout = Collections.singletonList(diagram);
		} else {
			// Only layout shapes. Also filter out any descendants of specified diagram elements
			nodesToLayout = filterUnnecessaryNodes(diagramNodes, true);
		}

		if (nodesToLayout.isEmpty()) {
			return;
		}

		diagram.modify(actionLabel, m -> layout(m, nodesToLayout,
				new StyleCalculator(diagram.getConfiguration(), StyleProvider.EMPTY), layoutInfoProvider, options));
	}

	private static void layout(final DiagramModification m, final Collection<? extends DiagramNode> nodesToLayout,
			final StyleProvider styleProvider, final LayoutInfoProvider layoutInfoProvider,
			final LayoutOptions options) {
		Objects.requireNonNull(nodesToLayout, "nodesToLayout must not be null");

		try {
			// Layout the nodes
			final RecursiveGraphLayoutEngine layoutEngine = new RecursiveGraphLayoutEngine();
			for (final DiagramNode dn : nodesToLayout) {
				LayoutMapping mapping;
				ElkNode layoutGraph;

				// Perform the first layout. This layout will not include nested ports. This will allow ELK additional flexibility when determining port
				// placement.
				mapping = ElkGraphBuilder.buildLayoutGraph(dn, styleProvider, layoutInfoProvider, options,
						!options.layoutPortsOnDefaultSides, ElkGraphBuilder.FixedPortPositionProvider.NO_OP);
				layoutGraph = mapping.getLayoutGraph();
				layoutGraph.setProperty(CoreOptions.ALGORITHM, LAYOUT_ALGORITHM);
				applyProperties(dn, mapping);
				LayoutDebugUtil.saveElkGraphToDebugProject(layoutGraph, "pass1");
				layoutEngine.layout(layoutGraph, new BasicProgressMonitor());

				// If the layout omitted any nested ports, then perform a second layout which will include nested ports. Since nested ports are not supported by
				// ELK, fixed port positions will be used for nodes which contain nested ports. Provide the results from the previous layout to the graph
				// builder so
				// that it can assign the position of the top level ports of such nodes based on the results of the previous layout.
				//
				// Doing a second layout is usually better than assigning all port positions. Ideally, the second pass would just include assigning positions to
				// the
				// nested ports and performing edge routing.
				if (layoutGraph.getProperty(AgeLayoutOptions.NESTED_PORTS_WERE_OMITTED)) {
					final LayoutMapping initialLayoutMapping = mapping;

					mapping = ElkGraphBuilder.buildLayoutGraph(dn, styleProvider, layoutInfoProvider, options, false,
							new ElkGraphBuilder.FixedPortPositionProvider() {

						@Override
						public PortSide getPortSide(final DiagramElement de) {
							final ElkGraphElement ge = initialLayoutMapping.getGraphMap().inverse().get(de);
							if (ge instanceof ElkPort) {
								return ge.getProperty(CoreOptions.PORT_SIDE);
							}
							return null;
						}

						@Override
						public Double getPortPosition(final DiagramElement de) {
							final ElkGraphElement ge = initialLayoutMapping.getGraphMap().inverse().get(de);
							if (ge instanceof ElkPort) {
								final ElkPort port = (ElkPort) ge;
								final PortSide ps = port.getProperty(CoreOptions.PORT_SIDE);
								if (PortSide.SIDES_EAST_WEST.contains(ps)) {
									return port.getY();
								} else {
									return port.getX();
								}
							}

							return null;
						}
					});
					layoutGraph = mapping.getLayoutGraph();
					layoutGraph.setProperty(CoreOptions.ALGORITHM, LAYOUT_ALGORITHM);
					applyProperties(dn, mapping);
					LayoutDebugUtil.saveElkGraphToDebugProject(layoutGraph, "pass2");
					layoutEngine.layout(layoutGraph, new BasicProgressMonitor());
				}
				LayoutDebugUtil.saveElkGraphToDebugProject(layoutGraph, "final");

				applyShapeLayout(mapping, m);
				applyConnectionLayout(mapping, m);

				// Layout feature self loop connections. These are omitted from the ELK based layout.
				dn.getAllDiagramNodes()
				.filter(DiagramElementLayoutUtil::isFeatureSelfLoopConnection)
				.map(DiagramElement.class::cast)
				.forEachOrdered(de -> layoutFeatureSelfLoopConnection(de, m, layoutInfoProvider));
			}
		} catch (final RuntimeException ex) {
			// If a layout error occurs, display the exception but do not rethrow. This is so that the operation that attempted to do the layout will continue.
			// This is important because otherwise simple operations such a adding elements to the diagram will completely fail. Suppressing the error will
			// degrade performance but allow the user to keep working and should ensure things stay in a valid state.
			// It would be best for other parts of the code to handle exceptions properly to avoid entering into an invalid state but this is the best
			// workaround.
			final Status status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, "A layout error occured.", ex);
			StatusManager.getManager().handle(status, StatusManager.SHOW | StatusManager.LOG);
		}
	}

	/**
	 * Performs layout on elements in the specified diagram which have not been laid out.
	 * @param diagram the diagram for which to perform the incremental layout
	 * @param mod the modification to use to modify the diagram
	 * @param layoutInfoProvider the layout info provider which provides additional information required for laying out the diagram
	 */
	public static void layoutIncrementally(final AgeDiagram diagram, final DiagramModification mod,
			final LayoutInfoProvider layoutInfoProvider) {
		Objects.requireNonNull(diagram, "diagram must not be null");
		Objects.requireNonNull(mod, "mod must not be null");
		Objects.requireNonNull(layoutInfoProvider, "layoutInfoProvider must not be null");

		final IncrementalLayoutMode currentLayoutMode = LayoutPreferences.getCurrentIncrementalLayoutMode();

		// Get all the nodes that need to be layed out.
		final Set<DiagramNode> unfilteredNodesToLayout = getNodesToLayoutIncrementally(diagram, currentLayoutMode,
				new HashSet<>());
		if (unfilteredNodesToLayout.size() == 0) {
			return;
		}

		// Lay our flow indicators. In the container is eventually layed out, this will be replaced but in cases where that is not the case,
		// we provide a default layout. Flow indicators are connections and as such will be filtered in the next step.
		layoutFlowIndicators(mod,
				unfilteredNodesToLayout.stream()
				.filter(DiagramNodePredicates::isFlowIndicator)
				.map(DiagramElement.class::cast),
				layoutInfoProvider);

		final Collection<DiagramNode> nodesToLayout = DiagramElementLayoutUtil.filterUnnecessaryNodes(
				unfilteredNodesToLayout, currentLayoutMode == IncrementalLayoutMode.LAYOUT_DIAGRAM);

		if (nodesToLayout.size() == 0) {
			// If the filtered node list is empty then the unfiltered list still contain feature self loop connections that need to be layed out.
			unfilteredNodesToLayout.stream()
			.filter(DiagramElementLayoutUtil::isFeatureSelfLoopConnection)
			.map(DiagramElement.class::cast)
			.forEachOrdered(de -> layoutFeatureSelfLoopConnection(de, mod, layoutInfoProvider));
			return;
		}

		final LayoutOptions layoutOptions = LayoutOptions.createFromPreferences();
		if (currentLayoutMode == IncrementalLayoutMode.LAYOUT_DIAGRAM) {
			layout(INCREMENTAL_LAYOUT_LABEL, diagram, layoutInfoProvider, layoutOptions);
		} else {
			layout(mod, nodesToLayout, new StyleCalculator(diagram.getConfiguration(), StyleProvider.EMPTY),
					layoutInfoProvider, layoutOptions);

			// Set Positions of elements which do not have a position set.
			for (final DiagramNode dn : nodesToLayout) {
				if (dn instanceof DiagramElement) {
					final DiagramElement de = (DiagramElement) dn;
					if (!de.hasPosition()) {
						if (de.getDockArea() == null) {
							mod.setPosition(de, new Point(0.0, 0.0));
						} else if (de.getDockArea() != DockArea.GROUP && de.getParent() instanceof DiagramElement) {
							final DiagramElement parent = (DiagramElement) de.getParent();
							final DockingPosition defaultDockingPosition = de.getGraphicalConfiguration()
									.getDefaultDockingPosition();
							final DockArea defaultDockArea = DockArea.fromDockingPosition(defaultDockingPosition);

							if (parent.hasSize()) {
								final Stream<DiagramElement> otherElementsAlongSide = parent.getChildren()
										.stream()
										.filter(c -> c.hasPosition() && c.hasSize()
												&& c.getDockArea() == defaultDockArea);

								// Determine the position of the new element along it's preferred docking position.
								double locationAlongSide;
								if (defaultDockingPosition == DockingPosition.TOP
										|| defaultDockingPosition == DockingPosition.BOTTOM) {
									locationAlongSide = otherElementsAlongSide
											.max(Comparator.comparingDouble(c -> c.getY()))
											.map(c -> c.getX() + c.getWidth())
											.orElse(0.0);
								} else {
									locationAlongSide = otherElementsAlongSide
											.max(Comparator.comparingDouble(c -> c.getY()))
											.map(c -> c.getY() + c.getHeight())
											.orElse(0.0);
								}

								// Set position based on the docking position
								switch (defaultDockingPosition) {
								case TOP:
									mod.setPosition(de, new Point(locationAlongSide, 0));
									break;
								case BOTTOM:
									mod.setPosition(de, new Point(locationAlongSide, parent.getHeight()));
									break;
								case LEFT:
									mod.setPosition(de, new Point(0, locationAlongSide));
									break;
								case RIGHT:
									mod.setPosition(de, new Point(parent.getWidth(), locationAlongSide));
									break;
								default:
									break;
								}
							}

							mod.setDockArea(de, defaultDockArea);
						}
					}
				}
			}
		}
	}

	/**
	 * Sets the bendpoints for an element. The element should be a feature self loop connection element.
	 * @param connectionElement the element to update.
	 * @param mod the modification to use to set the bendpoints
	 * @param layoutInfoProvider the layout information provider to help calculate positions
	 */
	private static void layoutFeatureSelfLoopConnection(final DiagramElement connectionElement,
			final DiagramModification mod, final LayoutInfoProvider layoutInfoProvider) {
		final DockArea dockArea = getNonGroupDockArea(connectionElement.getStartElement());
		final Point featurePosition = getPortAnchorOffset(connectionElement.getStartElement(), dockArea,
				new Point(0, 0), layoutInfoProvider);

		// Calculate new bendpoints
		final List<Point> newBendpoints;
		final double majorOffset = 50; // Offset towards the inside of the port in the dock area dependent direction. The horizontal direction for Left/Right
		final double minorOffset = 8; // Offset used in both directions in the other axis.
		if (dockArea == DockArea.LEFT) {
			newBendpoints = ImmutableList.of(
					new Point(featurePosition.x + majorOffset, featurePosition.y - minorOffset),
					new Point(featurePosition.x + majorOffset, featurePosition.y + minorOffset));
		} else if (dockArea == DockArea.RIGHT) {
			newBendpoints = ImmutableList.of(
					new Point(featurePosition.x - majorOffset, featurePosition.y - minorOffset),
					new Point(featurePosition.x - majorOffset, featurePosition.y + minorOffset));
		} else if (dockArea == DockArea.TOP) {
			newBendpoints = ImmutableList.of(
					new Point(featurePosition.x - minorOffset, featurePosition.y + majorOffset),
					new Point(featurePosition.x + minorOffset, featurePosition.y + majorOffset));
		} else { // BOTTOM
			newBendpoints = ImmutableList.of(
					new Point(featurePosition.x - minorOffset, featurePosition.y - majorOffset),
					new Point(featurePosition.x + minorOffset, featurePosition.y - majorOffset));
		}

		// Update the bendpoints
		mod.setBendpoints(connectionElement, newBendpoints);
	}

	/**
	 * Returns true if the specified diagram node is a diagram element which represents a connection which both ends are connected to the same
	 * docked element
	 * @param n is the node to check.
	 * @return
	 */
	private static boolean isFeatureSelfLoopConnection(final DiagramNode n) {
		if (!(n instanceof DiagramElement)) {
			return false;
		}

		final DiagramElement de = (DiagramElement) n;
		if (DiagramElementPredicates.isConnection(de) && de.getStartElement() == de.getEndElement()
				&& de.getStartElement() != null && de.getStartElement().getDockArea() != null) {
			return true;
		}

		return false;
	}

	/**
	 * Gets the position of a port anchor relative to another point
	 * @param element the docked element for which to get the anchor offset
	 * @param nonGroupDockArea dock are of the group. Must not be group. For grouped docked elements,
	 * it should be the first non group dock area in the hierarchy.
	 * @param referencePosition is the absolute position to use as the reference point for the returned position.
	 * @param layoutInfoProvider the layout info provider to use to calculate sized.
	 * @return the position of the anchor relative to the specified point.
	 */
	private static Point getPortAnchorOffset(final DiagramElement element, final DockArea nonGroupDockArea,
			final Point referencePosition, final LayoutInfoProvider layoutInfoProvider) {
		// Find offset based on orientation and nature of the diagram element
		final Dimension labelsSize = layoutInfoProvider.getDockedElementLabelsSize(element);
		final double anchorOffset;
		if (DiagramElementPredicates.isResizeable(element) && element.hasSize()) {
			// Feature groups
			anchorOffset = nonGroupDockArea.isLeftOrRight() ? element.getHeight() - labelsSize.height
					: element.getWidth() - labelsSize.width;
		} else {
			anchorOffset = layoutInfoProvider.getPortGraphicSize(element).height;
		}

		// Find the position of the element
		final Point elementAbsPosition = getAbsolutePosition(element);

		final Point anchorPosition;
		if (nonGroupDockArea == DockArea.LEFT) {
			anchorPosition = new Point(elementAbsPosition.x - referencePosition.x,
					elementAbsPosition.y - referencePosition.y + labelsSize.height + anchorOffset / 2.0);
		} else if (nonGroupDockArea == DockArea.RIGHT) {
			anchorPosition = new Point(elementAbsPosition.x - referencePosition.x + element.getWidth(),
					elementAbsPosition.y - referencePosition.y + labelsSize.height + anchorOffset / 2.0);
		} else if (nonGroupDockArea == DockArea.TOP) {
			anchorPosition = new Point(
					elementAbsPosition.x - referencePosition.x + labelsSize.width + anchorOffset / 2.0,
					elementAbsPosition.y - referencePosition.y);
		} else { // BOTTOM
			anchorPosition = new Point(
					elementAbsPosition.x - referencePosition.x + labelsSize.width + anchorOffset / 2.0,
					elementAbsPosition.y - referencePosition.y + element.getHeight());
		}

		return anchorPosition;
	}

	private static Set<DiagramNode> getNodesToLayoutIncrementally(final DiagramNode node,
			final IncrementalLayoutMode mode, final Set<DiagramNode> results) {
		final boolean alwaysLayoutContainer = mode != IncrementalLayoutMode.LAYOUT_CONTENTS;

		for (final DiagramElement child : node.getChildren()) {
			if (DiagramElementPredicates.isShape(child)) {
				final boolean positionIsSet = child.hasPosition() || !DiagramElementPredicates.isMoveableShape(child);
				final boolean sizeIsSet = child.hasSize() || !DiagramElementPredicates.isResizeable(child);

				// The position is set but the size isn't, then layout the child.
				// This occurs when a user has created an element using the palette
				if (positionIsSet && !sizeIsSet) {
					results.add(child);
				} else {
					if (sizeIsSet && positionIsSet) {
						getNodesToLayoutIncrementally(child, mode, results);
					} else {
						// If always layout container is specified, layout container
						// If container does not have any layed out shapes, layout container.
						final boolean layoutContainer = alwaysLayoutContainer
								|| !hasLayedOutShapes(node.getChildren());
						if (layoutContainer) {
							results.add(node);
							break;
						} else {
							results.add(child);
						}
					}
				}
			} else if (DiagramElementPredicates.isConnection(child)) {
				final AgeConnection c = (AgeConnection) child.getGraphic();
				if (c.isFlowIndicator) {
					// Layout the flow indicator's not docked container if its position has not been set
					if (child.getStartElement() != null && (!child.isBendpointsSet() || !child.hasPosition())) {
						// If we should lay out container than lay out the container of the flow indicator
						if (alwaysLayoutContainer) {
							DiagramElement undockedContainer = DiagramElementUtil
									.getUndockedDiagramElement(child.getStartElement().getParent());
							if (undockedContainer != null) {
								results.add(undockedContainer);
							}
						} else {
							// Otherwise, layout the flow indicator
							results.add(child);
						}
					}
				} else if (alwaysLayoutContainer) {
					// Only layout the connection if its bendpoints have not been set regardless of whether it has any bendpoints.
					if (child.getStartElement() != null && child.getEndElement() != null && !child.isBendpointsSet()) {
						final Optional<BusinessObjectContext> ancestor = BusinessObjectContext.getFirstCommonAncestor(
								child.getStartElement().getParent(), child.getEndElement().getParent());
						if (ancestor.isPresent()) {
							results.add((DiagramNode) ancestor.get());
						}
					}
				} else if (isFeatureSelfLoopConnection(child) && !child.isBendpointsSet()) {
					results.add(child);
				}
			}
		}

		return results;
	}

	private static boolean hasLayedOutShapes(final Collection<DiagramElement> diagramElements) {
		return diagramElements.stream().anyMatch(de -> {
			final boolean moveable = DiagramElementPredicates.isMoveableShape(de);
			final boolean resizable = DiagramElementPredicates.isResizeable(de);
			return ((de.hasPosition() && moveable) || !moveable) && ((de.hasSize() && resizable) || !resizable)
					&& (moveable || resizable);
		});
	}

	/**
	 * Sets the position and bendpoints of all specified flow indicators. Intended to provide a default position for flow indicators in cases
	 * where only contents are being layed out. This layout will be replaced by the ELK produced layout when the flow indicator's container
	 * is layed out.
	 *
	 * This function simply positions flow indicators such that they are in a after existing indicators.
	 * @param m is the modification to use to modify the diagram.
	 * @param flowIndicatorsToLayout is the stream of flow indicators to layout.
	 * @param layoutInfoProvider is the source for layout info needed to determine source anchor points.
	 */
	public static void layoutFlowIndicators(final DiagramModification m,
			final Stream<DiagramElement> flowIndicatorsToLayout, final LayoutInfoProvider layoutInfoProvider) {
		Objects.requireNonNull(flowIndicatorsToLayout, "flowIndicators must not be null");

		// Create set of a start elements in which we are interested.
		final Set<DiagramNode> startElements = flowIndicatorsToLayout.map(n -> n.getStartElement())
				.collect(Collectors.toSet());
		if (startElements.isEmpty()) {
			return;
		}

		// Search diagram and build a multimap mapping start elements to the flow indicators which reference them.
		final ArrayListMultimap<DiagramElement, DiagramElement> startElementToFlowIndicators = ArrayListMultimap
				.create();
		m.getDiagram()
		.getAllDescendants()
		.filter(q -> q instanceof DiagramElement
				&& DiagramElementPredicates.isFlowIndicator((DiagramElement) q))
		.forEachOrdered(q -> {
			final DiagramElement e = (DiagramElement) q;
			final DiagramElement start = e.getStartElement();
			if (startElements.contains(start)) {
				startElementToFlowIndicators.put(start, e);
			}
		});

		// Process each start element
		for (DiagramElement startElement : startElementToFlowIndicators.keySet()) {
			// Skip start elements that haven't been positioned
			if (!startElement.hasPosition()) {
				continue;
			}

			// Skip if unable to determine what side the start element is on. Flow indicators are only supported when there is a source element which is docked.
			final DockArea dockArea = getNonGroupDockArea(startElement);
			if (dockArea == null) {
				continue;
			}

			// Sort by X or Y based on dock area. Flow indicators without a position are sorted at the end.
			final List<DiagramElement> flowIndicatorsForStartElement = startElementToFlowIndicators.get(startElement);
			flowIndicatorsForStartElement.sort((e1, e2) -> {
				if (e1.hasPosition() && e2.hasPosition()) {
					if (dockArea.isLeftOrRight()) {
						return Double.compare(e1.getY(), e2.getY());
					} else {
						return Double.compare(e1.getX(), e2.getX());
					}
				} else if (e1.hasPosition()) {
					return -1;
				} else if (e2.hasPosition()) {
					return 1;
				} else {
					return 0;
				}

			});

			// Flow indicators are positions based on the first undocked container. Need to find that container.
			final DiagramElement indicatorContainer = DiagramElementUtil
					.getUndockedDiagramElement(flowIndicatorsForStartElement.get(0).getParent());

			// Skip if we are unable to determine the container or if the container doesn't have a size.
			if (indicatorContainer == null || !indicatorContainer.hasSize()) {
				continue;
			}

			final Point containerAbsPosition = getAbsolutePosition(indicatorContainer);

			//
			// Determine how to adjust position of bendpoints and indicator positions based on the dock area
			//
			final Point startAnchorPosition = getPortAnchorOffset(startElement, dockArea, containerAbsPosition,
					layoutInfoProvider);

			final double initialPositionOffsetX;
			final double initialPositionOffsetY;
			final double positionIncrementX;
			final double positionIncrementY;

			switch (dockArea) {
			case LEFT:
				initialPositionOffsetX = INCREMENTAL_FLOW_INDICATOR_PRIMARY_OFFSET;
				initialPositionOffsetY = 0;
				positionIncrementX = 0.0;
				positionIncrementY = INCREMENTAL_FLOW_INDICATOR_SECONDARY_INCREMENT;

				break;
			case RIGHT:
				initialPositionOffsetX = -INCREMENTAL_FLOW_INDICATOR_PRIMARY_OFFSET;
				initialPositionOffsetY = 0;
				positionIncrementX = 0;
				positionIncrementY = INCREMENTAL_FLOW_INDICATOR_SECONDARY_INCREMENT;
				break;

			case TOP:

				initialPositionOffsetX = 0;
				initialPositionOffsetY = INCREMENTAL_FLOW_INDICATOR_PRIMARY_OFFSET;
				positionIncrementX = INCREMENTAL_FLOW_INDICATOR_SECONDARY_INCREMENT;
				positionIncrementY = 0;
				break;

			case BOTTOM:
				initialPositionOffsetX = 0;
				initialPositionOffsetY = -INCREMENTAL_FLOW_INDICATOR_PRIMARY_OFFSET;
				positionIncrementX = INCREMENTAL_FLOW_INDICATOR_SECONDARY_INCREMENT;
				positionIncrementY = 0;
				break;

			case GROUP:
			default:
				// Our dock area should never have the group value and all other values should be handled
				throw new GraphicalEditorException("Unexpected case: " + dockArea);
			}

			// Calculate absolute position for the start anchor. Used for bendpoints
			final Point startAnchorAbsPosition = new Point(containerAbsPosition.x + startAnchorPosition.x,
					containerAbsPosition.y + startAnchorPosition.y);

			// Determine initial of the first flow indicator relative to it's container.
			// This is only used when there all flow indicators do not have a position
			final Point firstPosition = new Point(startAnchorPosition.x + initialPositionOffsetX,
					startAnchorPosition.y + initialPositionOffsetY);
			Point nextPosition = firstPosition;

			for (DiagramElement indicator : flowIndicatorsForStartElement) {
				final Point currentPosition;
				if (indicator.hasPosition()) {
					currentPosition = indicator.getPosition();
				} else {
					// Set the position
					currentPosition = nextPosition;
					m.setPosition(indicator, nextPosition);

					// The first flow indicator should not need bendpoints, to reset them
					if (nextPosition.equals(firstPosition)) {
						m.setBendpoints(indicator, ImmutableList.of());
					} else {
						// Set bendpoints
						final Point bp1 = new Point(
								startAnchorAbsPosition.x
								+ (initialPositionOffsetX * INCREMENTAL_FLOW_INDICATOR_BENDPOINT_OFFSET_SCALING),
								startAnchorAbsPosition.y
								+ (initialPositionOffsetY * INCREMENTAL_FLOW_INDICATOR_BENDPOINT_OFFSET_SCALING));

						final Point positionAbs = new Point(containerAbsPosition.x + nextPosition.x,
								+containerAbsPosition.y + nextPosition.y);
						final Point bp2 = new Point(positionAbs.x
								- (initialPositionOffsetX * (1.0 - INCREMENTAL_FLOW_INDICATOR_BENDPOINT_OFFSET_SCALING)),
								positionAbs.y - (initialPositionOffsetY
										* (1.0 - INCREMENTAL_FLOW_INDICATOR_BENDPOINT_OFFSET_SCALING)));

						m.setBendpoints(indicator, ImmutableList.of(bp1, bp2));
					}
				}

				// Determine the next position
				nextPosition = new Point(currentPosition.x + positionIncrementX,
						currentPosition.y + positionIncrementY);
			}
		}
	}

	private static void resetFlowIndicatorsWithStartElementsPositions(final DiagramModification m,
			final Stream<? extends DiagramNode> startElementsStream) {
		getFlowIndicatorsWithStartElements(m.getDiagram(), startElementsStream).forEach(de -> {
			m.setPosition(de, null);
		});
	}

	private static Stream<DiagramElement> getFlowIndicatorsWithStartElements(final AgeDiagram diagram,
			final Stream<? extends DiagramNode> startElementsStream) {
		Objects.requireNonNull(startElementsStream, "startElementsStream must not be null");

		// Create set of a start elements in which we are interested.
		final Set<DiagramNode> startElements = startElementsStream.collect(Collectors.toSet());
		if (startElements.isEmpty()) {
			return Stream.empty();
		}

		return diagram.getAllDescendants()
				.filter(q -> q instanceof DiagramElement && DiagramElementPredicates.isFlowIndicator((DiagramElement) q)
						&& startElements.contains(((DiagramElement) q).getStartElement()))
				.map(DiagramElement.class::cast);
	}

	/**
	 * Sets the ELK properties of elements in the specified layout mapping based on the layout options.
	 * @param layoutMapping
	 */
	private static void applyProperties(final DiagramNode rootDiagramNode, final LayoutMapping layoutMapping) {
		// Set the minimum node size based on the ports and their assigned sides.
		final IGraphElementVisitor minNodeSizeVisitor = element -> {
			if (element instanceof ElkNode) {
				final ElkNode n = (ElkNode) element;

				final double maxLabelWidth = n.getLabels().stream().mapToDouble(l -> l.getWidth()).max().orElse(0.0);
				final double labelHeightSum = n.getLabels().stream().mapToDouble(l -> l.getHeight()).sum();

				// Determine max width for ports on the left and right sides
				final double maxLeftPortWidth = n.getPorts()
						.stream()
						.filter(p -> p.getProperty(CoreOptions.PORT_SIDE) == PortSide.WEST)
						.mapToDouble(p -> p.getWidth())
						.max()
						.orElse(0.0);
				final double maxRightPortWidth = n.getPorts()
						.stream()
						.filter(p -> p.getProperty(CoreOptions.PORT_SIDE) == PortSide.EAST)
						.mapToDouble(p -> p.getWidth())
						.max()
						.orElse(0.0);

				final DiagramNode dn = (DiagramNode) layoutMapping.getGraphMap().get(n);

				double minWidth = 0;
				if (n.getProperty(CoreOptions.NODE_LABELS_PLACEMENT).contains(NodeLabelPlacement.H_CENTER)) {
					// Ensure the minimum width is such that the label can be centered without overlapping with ports.
					// This happens because ports are inside the node due to the PORT_BORDER_OFFSET and ELK centers the labels in the remaining space.
					final double widthForPorts = 2 * Math.max(maxLeftPortWidth, maxRightPortWidth);
					minWidth = Math.max(40, maxLabelWidth + widthForPorts + PORT_WIDTH_PADDING);
				} else {
					final double widthForPorts = maxLeftPortWidth + maxRightPortWidth + PORT_WIDTH_PADDING;
					minWidth = Math.max(40, Math.max(maxLabelWidth, widthForPorts));
				}

				double minHeight = Math.max(35, labelHeightSum);

				if (dn instanceof DiagramElement) {
					final DiagramElement de = ((DiagramElement) dn);

					// Special min height handling for initial modes
					final Graphic graphic = de.getGraphic();

					if (graphic instanceof AgeShape && !((AgeShape) graphic).isResizeable() && de.hasSize()) {
						final Dimension dim = de.getSize();
						minHeight = dim.height;
						minWidth = dim.width;

						// Adjust size constraints for fixed sized shapes which do not have contents.
						if (n.getChildren().size() == 0 || n.getLabels().size() == 0 && n.getPorts().size() == 0) {
							final EnumSet<SizeConstraint> nodeSizeConstraints = EnumSet.of(SizeConstraint.MINIMUM_SIZE);
							n.setProperty(CoreOptions.NODE_SIZE_CONSTRAINTS, nodeSizeConstraints);
						}
					}

					if (graphic instanceof ModeGraphic && ((ModeGraphic) graphic).isInitialMode) {
						minHeight += ModeGraphic.INITIAL_MODE_AREA_HEIGHT;
					}

					// Special min size handling for elements shown as image
					final Style style = de.getStyle();
					if (style != null && Boolean.TRUE.equals(style.getShowAsImage())) {
						final Dimension dim = ((DiagramElement) dn).getSize();
						minHeight = dim.height;
						minWidth = dim.width;
					}
				}

				// Increase min width and min height for top level nodes.
				if (dn != null && dn.getParent() instanceof AgeDiagram) {
					minWidth = Math.max(minWidth, 200);
					minHeight = Math.max(minHeight, 100);
				}

				n.setProperty(CoreOptions.NODE_SIZE_MINIMUM, new KVector(minWidth, minHeight));
			}
		};

		ElkUtil.applyVisitors(layoutMapping.getLayoutGraph(), minNodeSizeVisitor);

		// If the top level element has a size set, don't shrink it.
		if (rootDiagramNode instanceof DiagramElement) {
			final DiagramElement rootDiagramElement = (DiagramElement) rootDiagramNode;
			final ElkGraphElement rootGraphElement = layoutMapping.getGraphMap().inverse().get(rootDiagramNode);

			if (rootGraphElement != null && rootDiagramElement.hasSize()
					&& DiagramElementPredicates.isResizeable(rootDiagramElement)) {
				final KVector minSize = rootGraphElement.getProperty(CoreOptions.NODE_SIZE_MINIMUM);
				final double newMinWidth = Math.max(rootDiagramElement.getWidth(), minSize == null ? 0.0 : minSize.x);
				final double newMinHeight = Math.max(rootDiagramElement.getHeight(), minSize == null ? 0.0 : minSize.y);
				rootGraphElement.setProperty(CoreOptions.NODE_SIZE_MINIMUM, new KVector(newMinWidth, newMinHeight));
			}
		}
	}

	private static boolean isTopLevel(final ElkGraphElement ge) {
		if (ge instanceof ElkPort) {
			final ElkPort p = (ElkPort) ge;
			return p.getParent() == null || p.getParent().getParent() == null;
		} else if (ge instanceof ElkNode) {
			final ElkNode n = (ElkNode) ge;
			return n.getParent() == null || n.getParent().getParent() == null;
		} else {
			return false;
		}
	}

	/**
	 * Returns a list which contains the specified diagram nodes with unnecessary nodes removed.
	 * It removes nodes which are:
	 *   Not a diagram.
	 *   Not shapes
	 *   Elements which have an ancestor in the specified list.
	 *   Children of a docked element unless the current diagram mode is layout diagram.
	 * @param diagramNodes the diagram nodes to filter
	 * @return a list which contains the specified diagram nodes with unnecessary nodes removed.
	 */
	private static Collection<DiagramNode> filterUnnecessaryNodes(final Collection<? extends DiagramNode> diagramNodes,
			final boolean includeGroupChildren) {
		return diagramNodes.stream()
				.filter(dn -> dn instanceof AgeDiagram
						|| (dn instanceof DiagramElement && DiagramElementPredicates.isShape((DiagramElement) dn)
								&& !containsAnyAncestor(diagramNodes, dn)
								&& (includeGroupChildren || ((DiagramElement) dn).getDockArea() != DockArea.GROUP)))
				.collect(Collectors.toList());
	}

	/**
	 * Returns true if the specified collection contains any ancestor for the specified diagram node
	 * @param c
	 * @param e
	 * @return
	 */
	private static boolean containsAnyAncestor(final Collection<? extends DiagramNode> c, final DiagramNode n) {
		for (DiagramNode ancestor = n.getParent(); ancestor != null; ancestor = ancestor.getParent()) {
			if (c.contains(ancestor)) {
				return true;
			}
		}

		return false;
	}

	private static void applyShapeLayout(final LayoutMapping mapping, final DiagramModification m) {
		// Modify shapes
		for (Entry<ElkGraphElement, Object> e : mapping.getGraphMap().entrySet()) {
			final ElkGraphElement elkElement = e.getKey();
			final Object mappedValue = e.getValue();
			final boolean isTopLevelElement = isTopLevel(elkElement);

			if (!(elkElement instanceof ElkShape)) {
				continue;
			}
			final ElkShape elkShape = (ElkShape) elkElement;

			if (!(mappedValue instanceof DiagramElement)) {
				continue;
			}

			final DiagramElement de = (DiagramElement) mappedValue;
			if (!(de.getGraphic() instanceof AgeShape)) {
				continue;
			}

			if (de.getGraphic() instanceof Label) {
				continue;
			}

			// Set Position. Don't set the position of top level elements
			if (!isTopLevelElement && DiagramElementPredicates.isMoveableShape(de)) {
				// Determine position for the element
				double x = elkShape.getX();
				double y = elkShape.getY();

				// If the diagram element has a parent port, subtract the parent port position from the ELK port position to determine the relative position
				if (de.getDockArea() == DockArea.GROUP) {
					final ElkPort parentPort = (ElkPort) mapping.getGraphMap().inverse().get(de.getParent());
					if (parentPort != null) {
						final PortSide side = parentPort.getProperty(CoreOptions.PORT_SIDE);
						if (PortSide.SIDES_NORTH_SOUTH.contains(side)) {
							x = elkShape.getX() - parentPort.getX();
						} else if (PortSide.SIDES_EAST_WEST.contains(side)) {
							y = elkShape.getY() - parentPort.getY();
						} else {
							throw new GraphicalEditorException("Unexpected side: " + side);
						}
					}
				}

				DiagramElementLayoutUtil.moveElement(m, de, new Point(x, y));

				// Set the dock area
				if (de.getDockArea() != DockArea.GROUP && de.getDockArea() != null) {
					final DockArea newDockArea = PortSideUtil.getDockArea(elkShape.getProperty(CoreOptions.PORT_SIDE));
					if (newDockArea != null) {
						m.setDockArea(de, newDockArea);
					}
				}
			}

			// Set the size
			if (DiagramElementPredicates.isResizeable(de)) {
				m.setSize(de, new Dimension(elkShape.getWidth(), elkShape.getHeight()));
			}
		}
	}

	private static void applyConnectionLayout(final LayoutMapping mapping, final DiagramModification m) {
		// Modify Connections
		for (Entry<ElkGraphElement, Object> e : mapping.getGraphMap().entrySet()) {
			final ElkGraphElement elkElement = e.getKey();
			final Object mappedValue = e.getValue();

			if (!(elkElement instanceof ElkEdge)) {
				continue;
			}
			final ElkEdge edge = (ElkEdge) elkElement;

			// Ignore edges which do not have exactly one section. This is usually the case where it is a long hierarchical connection that has 0 sections
			if (edge.getSections().size() != 1) {
				continue;
			}
			final ElkEdgeSection edgeSection = edge.getSections().get(0);

			if (!(mappedValue instanceof DiagramElement)) {
				continue;
			}

			final DiagramElement de = (DiagramElement) mappedValue;
			if (!(de.getGraphic() instanceof AgeConnection)) {
				continue;
			}

			final AgeConnection connection = (AgeConnection) de.getGraphic();

			// Flow indicators have a position representing where the indicator ends.
			if (connection.isFlowIndicator && edge.getTargets().size() == 1) {
				final ElkPort flowIndicatorEndPort = (ElkPort) edge.getTargets().get(0);
				final ElkShape flowIndicatorEndPortShape = (ElkShape) flowIndicatorEndPort.eContainer();
				m.setPosition(de, new Point(flowIndicatorEndPortShape.getX(), flowIndicatorEndPortShape.getY()));
			}

			// Don't update connections if it wasn't updated. This prevents updating bendpoints to invalid values if an edge is not layed out.
			if (edgeSection.eIsSet(ElkGraphPackage.eINSTANCE.getElkEdgeSection_StartX())
					&& edgeSection.eIsSet(ElkGraphPackage.eINSTANCE.getElkEdgeSection_EndX())) {
				final List<Point> bendpointsInParentCoordinateSystem = edgeSection.getBendPoints()
						.stream()
						.map(bp -> new Point(bp.getX(), bp.getY()))
						.collect(Collectors.toCollection(LinkedList::new));

				//
				// Set bendpoints
				//

				// Add the start and end points to the bendpoints list if the the start/end element is not a port.
				// For ports the start and end points are unnecessary and will actually be located inside the port graphic.
				final boolean srcIsPort = edge.getSources().size() == 1 ? edge.getSources().get(0) instanceof ElkPort
						: false;
				final boolean dstIsPort = edge.getTargets().size() == 1 ? edge.getTargets().get(0) instanceof ElkPort
						: false;

				if (!srcIsPort) {
					bendpointsInParentCoordinateSystem.add(0,
							new Point(edgeSection.getStartX(), edgeSection.getStartY()));
				}

				if (!dstIsPort) {
					bendpointsInParentCoordinateSystem.add(new Point(edgeSection.getEndX(), edgeSection.getEndY()));
				}

				// Adjust newly added bendpoints so that the connection arrows will face the appropriate direction
				if (!srcIsPort && bendpointsInParentCoordinateSystem.size() >= 2) {
					bendpointsInParentCoordinateSystem.set(0,
							getAdjacentPoint(bendpointsInParentCoordinateSystem.get(0),
									bendpointsInParentCoordinateSystem.get(1), START_AND_END_BENDPOINT_DISTANCE));
				}

				if (!dstIsPort && bendpointsInParentCoordinateSystem.size() >= 2) {
					bendpointsInParentCoordinateSystem.set(bendpointsInParentCoordinateSystem.size() - 1,
							getAdjacentPoint(
									bendpointsInParentCoordinateSystem
									.get(bendpointsInParentCoordinateSystem.size() - 1),
									bendpointsInParentCoordinateSystem
									.get(bendpointsInParentCoordinateSystem.size() - 2),
									START_AND_END_BENDPOINT_DISTANCE));
				}

				// Get the absolute coordinate in the diagram of the edge's ELK container.
				final Point elkContainerPosition;
				if (edge.getContainingNode() == mapping.getLayoutGraph()) {
					// Special handling for edges that are children of the ELK root. Usually occurs when Layout Contents is used. In that case there isn't a
					// Diagram
					// Node available. Use the first and only child of the top level ELK node.
					if (mapping.getLayoutGraph().getChildren().size() == 1) {
						final ElkNode topLayoutElkNode = mapping.getLayoutGraph().getChildren().get(0);
						final Point topLayoutElkNodePosition = getAbsolutePosition(
								(DiagramNode) mapping.getGraphMap().get(topLayoutElkNode));
						elkContainerPosition = new Point(topLayoutElkNodePosition.x - topLayoutElkNode.getX(),
								topLayoutElkNodePosition.y - topLayoutElkNode.getY());
					} else {
						elkContainerPosition = new Point(0, 0);
					}
				} else {
					elkContainerPosition = getAbsolutePosition(
							(DiagramNode) mapping.getGraphMap().get(edge.getContainingNode()));
				}

				final List<Point> bendpointsInAbsoluteCoordinateSystem = bendpointsInParentCoordinateSystem.stream()
						.map(p -> new Point(p.x + elkContainerPosition.x, p.y + elkContainerPosition.y))
						.collect(Collectors.toList());
				m.setBendpoints(de, bendpointsInAbsoluteCoordinateSystem);

				// For the midpoint calculation, the start and end points are needed. Add them if they have not already been added.
				if (srcIsPort) {
					bendpointsInParentCoordinateSystem.add(0,
							new Point(edgeSection.getStartX(), edgeSection.getStartY()));
				}

				if (dstIsPort) {
					bendpointsInParentCoordinateSystem.add(new Point(edgeSection.getEndX(), edgeSection.getEndY()));
				}

				// Set Label Positions
				setLabelPositionsForEdge(mapping, m, edge, findMidpoint(bendpointsInParentCoordinateSystem));
			}
		}
	}

	/**
	 *
	 * @param mapping
	 * @param m
	 * @param edge
	 * @param edgeMidpoint must be relative to the edge's container
	 */
	private static void setLabelPositionsForEdge(final LayoutMapping mapping, DiagramModification m, final ElkEdge edge,
			final Point edgeMidpoint) {
		// Handle labels
		for (final ElkLabel edgeLabel : edge.getLabels()) {
			final Object labelValue = mapping.getGraphMap().get(edgeLabel);
			if (labelValue instanceof ConnectionLabelReference) {
				final ConnectionLabelReference labelRef = (ConnectionLabelReference) labelValue;
				if (Boolean.TRUE.equals(edgeLabel.getProperty(CoreOptions.NO_LAYOUT))) {
					labelRef.setPosition(m, null);
				} else {
					final double lx = edgeLabel.getX() - edgeMidpoint.x;
					final double ly = edgeLabel.getY() - edgeMidpoint.y;
					labelRef.setPosition(m, new Point(lx, ly));
				}
			}
		}
	}

	private static Point findMidpoint(final List<Point> points) {
		if (points.size() < 2) {
			throw new IllegalArgumentException("At least two points must be specified");
		}

		final double totalLength = length(points);
		double lengthToTarget = totalLength / 2.0;

		for (int i = 1; i < points.size(); i++) {
			final Point p1 = points.get(i - 1);
			final Point p2 = points.get(i);

			final double segmentLength = length(p1, p2);
			if (lengthToTarget > segmentLength) {
				lengthToTarget -= segmentLength;
			} else {
				final double frac = lengthToTarget / segmentLength;
				return new Point(p1.x + (p2.x - p1.x) * frac, p1.y + (p2.y - p1.y) * frac);
			}
		}

		throw new GraphicalEditorException("Unexpected case: midpoint not found");
	}

	private static double length(final List<Point> points) {
		double totalLength = 0;
		for (int i = 1; i < points.size(); i++) {
			totalLength += length(points.get(i - 1), points.get(i));
		}

		return totalLength;
	}

	private static double length(final Point p1, final Point p2) {
		final double dx = p1.x - p2.x;
		final double dy = p1.y - p2.y;
		return Math.sqrt(dx * dx + dy * dy);
	}

	/**
	 * Returns a point next to p1 which is on the line segment between p2 and p1.
	 * @param p1
	 * @param p2
	 * @return
	 */
	private static Point getAdjacentPoint(final Point p1, final Point p2, double d) {
		final double dx = p2.x - p1.x;
		final double dy = p2.y - p1.y;
		final double l = Math.sqrt(dx * dx + dy * dy);
		final double ux = dx / l;
		final double uy = dy / l;

		// Ensure that d is less than l. Otherwise the produced point will be further away than the actual point.
		d = Math.min(d, l);

		return new Point(p1.x + d * ux, p1.y + d * uy);
	}

	/**
	 * Gets the absolute position of a node. This absolute position only considers the positions of shapes.
	 * Connections are ignored.
	 * @param dn the node for which to get the absolute position.
	 * @return the absolute position.
	 */
	public static Point getAbsolutePosition(final DiagramNode dn) {
		int x = 0;
		int y = 0;
		for (DiagramNode tmp = dn; tmp instanceof DiagramElement; tmp = tmp.getParent()) {
			final DiagramElement tmpDe = (DiagramElement) tmp;
			if (tmpDe.getGraphic() instanceof AgeShape) { // Ignore connections in case the X and Y values are not 0.
				x += tmpDe.getX();
				y += tmpDe.getY();
			}
		}

		return new Point(x, y);
	}

	/**
	 * Shifts the bendpoints of all connections for which both endpoints are contained within the specified elements.
	 * Shifts bendpoints of flow indicators if start elements are contained within the specified elements.
	 * Shifts position of flow indicators if start elements are contained within the specified elements and the flow indicator container is not
	 * is not in movedElements.
	 *
	 * @param movedElements are the element which have been moved.
	 * @param delta the amount to shift the bendpoints
	 * @param m the modification that will be used to update the bendpoints
	 * @param shiftBendpoints whether to shift bendpoints
	 * @param shiftFlowIndicatorPositions whether to shift flow indicator positions.
	 * @param checkDescendants whether to check descendants of the specified elements when looking for connections
	 */
	public static void shiftRelatedConnections(final Stream<DiagramElement> movedElements,
			final org.osate.ge.graphics.Point delta, final DiagramModification m, boolean shiftBendpoints,
			boolean shiftFlowIndicatorPositions, final boolean checkDescendants) {
		final Set<BusinessObjectContext> movedElementsSet = movedElements.collect(Collectors.toSet());

		// Build a set containing the moved elements and all of their descendant which are represented as shapes
		final Set<BusinessObjectContext> diagramElements = checkDescendants
				? movedElementsSet.stream()
						.flatMap(de -> Stream.concat(Stream.of(de), de.getAllDescendants()))
						.collect(Collectors.toSet())
						: movedElementsSet;
		final Stream<DiagramElement> connections = m.getDiagram()
				.getAllDiagramNodes()
				.filter(q -> q instanceof DiagramElement && DiagramElementPredicates.isConnection((DiagramElement) q))
				.map(DiagramElement.class::cast);

		// Iterate over all the connections in the diagram and update their bendpoints if their ends are in the set above.
		connections.forEachOrdered(connection -> {
			final DiagramElement startElement = connection.getStartElement();
			final DiagramElement endElement = connection.getEndElement();
			final boolean isFlowIndicator = ((AgeConnection) connection.getGraphic()).isFlowIndicator;
			if (diagramElements.contains(startElement) && (diagramElements.contains(endElement) || isFlowIndicator)) {
				if (shiftBendpoints) {
					shiftBendpoints(connection, delta, m);
				}

				// Shift flow indicator positions
				if (shiftFlowIndicatorPositions && isFlowIndicator && connection.hasPosition()) {
					// Flow indicator positions are relative to the container of the flow indicator.
					// If the flow indicator's ancestor has moved, then do not shift the flow indicator's position
					boolean ancestorHasMoved = false;
					for (DiagramNode tmp = connection.getParent(); tmp != null; tmp = tmp.getParent()) {
						if (movedElementsSet.contains(tmp)) {
							ancestorHasMoved = true;
						}
					}

					if (!ancestorHasMoved) {
						final DockArea startDockArea = getNonGroupDockArea(startElement);
						m.setPosition(connection, new org.osate.ge.graphics.Point(
								connection.getX()
								+ (startDockArea == null || !startDockArea.isLeftOrRight() ? delta.x : 0),
								connection.getY()
								+ (startDockArea == null || startDockArea.isLeftOrRight() ? delta.y : 0)));
					}
				}
			}
		});
	}

	/**
	 * Returns the connections which are affected by moving the specified elements
	 * @param movedElement is the element which to get the affected connections
	 * @param diagram is the diagram which contains the connections.
	 * @param checkDescendants whether to check descendants of the specified elements when looking for connections
	 * @return he connections which are affected by moving the specified elements
	 */
	public static Stream<DiagramElement> getConnectionsAffectedByMove(final DiagramElement movedElement,
			final AgeDiagram diagram, final boolean checkDescendants) {
		// Build a set containing the moved elements and all of their descendant which are represented as shapes
		final Set<BusinessObjectContext> diagramElements = checkDescendants
				? movedElement.getAllDescendants().collect(Collectors.toSet())
						: Collections.singleton(movedElement);
		final Stream<DiagramElement> connections = diagram.getAllDiagramNodes()
				.filter(q -> q instanceof DiagramElement && DiagramElementPredicates.isConnection((DiagramElement) q))
				.map(DiagramElement.class::cast);

		// Iterate over all the connections in the diagram and update their bendpoints if their ends are in the set above.
		return connections.filter(c -> {
			final DiagramElement startElement = c.getStartElement();
			final DiagramElement endElement = c.getEndElement();
			final boolean isFlowIndicator = ((AgeConnection) c.getGraphic()).isFlowIndicator;
			return diagramElements.contains(startElement) && (diagramElements.contains(endElement) || isFlowIndicator);
		});
	}

	private static void shiftBendpoints(final DiagramElement connection, final org.osate.ge.graphics.Point delta,
			final DiagramModification m) {
		// Set new bendpoint locations
		final List<org.osate.ge.graphics.Point> bendpoints = Lists.newArrayList(connection.getBendpoints());
		for (int i = 0; i < bendpoints.size(); i++) {
			final org.osate.ge.graphics.Point bendpoint = bendpoints.get(i);
			bendpoints.set(i, new org.osate.ge.graphics.Point(bendpoint.x + delta.x, bendpoint.y + delta.y));
		}

		m.setBendpoints(connection, bendpoints);
	}

	/**
	 * Walks up the tree from the given node and returns the first dock area that isn't the group dock area.
	 * Checks the specified diagram node and then ancestors.
	 * @param diagramNode is the diagram for which to return the non group docker area.
	 * @return the first dock area that isn't the group dock area.
	 */
	public static DockArea getNonGroupDockArea(DiagramNode diagramNode) {
		DockArea result = null;
		do {
			if (!(diagramNode instanceof DiagramElement)) {
				result = null;
				break;
			}

			result = ((DiagramElement) diagramNode).getDockArea();
			diagramNode = diagramNode.getParent();
		} while (result != null && result == DockArea.GROUP);

		return result;
	}

	//
	// The following methods are used to move elements and make related changes appropriate
	//
	/**
	 * Sets the position of a diagram element. Updates dock area, bendpoints, and flow indicators
	 * @param modification the diagram modification to use to modify the diagram
	 * @param e the element for which to set the position
	 * @param value the new position of the element
	 * @see #moveElement(DiagramModification, DiagramElement, Point, boolean, boolean, boolean)
	 */
	public static void moveElement(final DiagramModification modification, final DiagramElement e, final Point value) {
		moveElement(modification, e, value, true, true);
	}

	/**
	 * Sets the position of a diagram element. Updates the flow indicators
	 * @param modification the diagram modification to use to modify the diagram
	 * @param e the element for which to set the position
	 * @param value the new position of the element
	 * @param updateDockArea whether the dock area should be updated based on the set position.
	 * @param updateBendpoints whether to shift bendpoints of related connections
	 * @see #moveElement(DiagramModification, DiagramElement, Point, boolean, boolean, boolean)
	 */
	public static void moveElement(final DiagramModification modification, final DiagramElement e, final Point value,
			final boolean updateDockArea, final boolean updateBendpoints) {
		moveElement(modification, e, value, updateDockArea, updateBendpoints, true);
	}

	/**
	 * Sets the position of a diagram element
	 * @param modification the diagram modification to use to modify the diagram
	 * @param e the element for which to set the position
	 * @param value the new position of the element
	 * @param updateDockArea whether the dock area should be updated based on the set position.
	 * @param updateBendpoints whether to shift bendpoints of related connections
	 * @param updateFlowIndicators if related flow indicators should be moved. If dock area has changed the position of the dock areas will be reset to allow for a new layout.
	 */
	public static void moveElement(final DiagramModification modification, final DiagramElement e, final Point value,
			final boolean updateDockArea, final boolean updateBendpoints, final boolean updateFlowIndicators) {
		if (!Objects.equals(e.getPosition(), value)) {

			// Determine the different between X and Y
			final Point delta = value == null ? null : new Point(value.x - e.getX(), value.y - e.getY());

			modification.setPosition(e, value);

			// Only update dock area and bendpoints if position is being set to an actual value
			if (delta != null) {
				final DockArea originalDockArea = e.getDockArea();

				if (updateDockArea) {
					// Update the dock area based on the position
					if (originalDockArea != null) {
						if (originalDockArea != DockArea.GROUP) {
							modification.setDockArea(e, calculateDockArea(e));
						}
					}
				}

				if (updateBendpoints || updateFlowIndicators) {
					DiagramElementLayoutUtil.shiftRelatedConnections(Stream.of(e), new Point(delta.x, delta.y),
							modification, updateBendpoints, updateFlowIndicators, true);
				}

				// Reset flow indicators entirely if dock area has changed
				if (updateFlowIndicators && originalDockArea != e.getDockArea()) {
					final Stream<DiagramNode> affectedStartElements = e.getAllDiagramNodes();
					DiagramElementLayoutUtil.resetFlowIndicatorsWithStartElementsPositions(modification,
							affectedStartElements);
				}
			}
		}
	}

	private static DockArea calculateDockArea(final DiagramElement e) {
		return DockArea.fromDockingPosition(AgeDiagramUtil.determineDockingPosition(e.getParent(), e.getX(), e.getY(),
				e.getWidth(), e.getHeight()));
	}

}