GefAgeDiagram.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.gef.ui.diagram;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.gef.fx.anchors.IAnchor;
import org.eclipse.swt.widgets.Display;
import org.osate.ge.gef.AgeGefRuntimeException;
import org.osate.ge.gef.BaseConnectionNode;
import org.osate.ge.gef.ConnectionNode;
import org.osate.ge.gef.ContainerShape;
import org.osate.ge.gef.DiagramRootNode;
import org.osate.ge.gef.DockSide;
import org.osate.ge.gef.DockedShape;
import org.osate.ge.gef.FeatureConstants;
import org.osate.ge.gef.FeatureGroupNode;
import org.osate.ge.gef.FlowIndicatorNode;
import org.osate.ge.gef.FxStyle;
import org.osate.ge.gef.FxStyleApplier;
import org.osate.ge.gef.ImageManager;
import org.osate.ge.gef.LabelNode;
import org.osate.ge.gef.PreferredPosition;
import org.osate.ge.gef.StyleRoot;
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.StyleBuilder;
import org.osate.ge.graphics.internal.FeatureGraphic;
import org.osate.ge.graphics.internal.FeatureGraphicType;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.BeforeModificationsCompletedEvent;
import org.osate.ge.internal.diagram.runtime.DiagramConfigurationChangedEvent;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramElementPredicates;
import org.osate.ge.internal.diagram.runtime.DiagramModificationListener;
import org.osate.ge.internal.diagram.runtime.DiagramNode;
import org.osate.ge.internal.diagram.runtime.DockArea;
import org.osate.ge.internal.diagram.runtime.ElementAddedEvent;
import org.osate.ge.internal.diagram.runtime.ElementRemovedEvent;
import org.osate.ge.internal.diagram.runtime.ElementUpdatedEvent;
import org.osate.ge.internal.diagram.runtime.ModificationsCompletedEvent;
import org.osate.ge.internal.diagram.runtime.layout.DiagramElementLayoutUtil;
import org.osate.ge.internal.diagram.runtime.layout.LayoutInfoProvider;
import org.osate.ge.internal.diagram.runtime.styling.StyleCalculator;
import org.osate.ge.internal.diagram.runtime.styling.StyleProvider;
import org.osate.ge.internal.diagram.runtime.updating.Completeness;
import org.osate.ge.internal.services.ColoringService;

import com.google.common.base.Strings;

import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;

/**
 * Creates and updates a JavaFX node representing an {@link AgeDiagram}.
 * Handles updating the JavaFX nodes to reflect the changes in the AgeDiagram.
 */
public class GefAgeDiagram implements AutoCloseable, LayoutInfoProvider {
	private static final String INCOMPLETE_INDICATOR = "*";
	private static final Dimension FEATURE_GROUP_CIRCLE_SIZE = new Dimension(FeatureGroupNode.CIRCLE_DIAMETER,
			FeatureGroupNode.CIRCLE_DIAMETER);
	private static final Dimension DEFAULT_FEATURE_SIZE = new Dimension(FeatureConstants.WIDTH,
			FeatureConstants.HEIGHT);

	/**
	 * Contains the details regarding how a diagram element is represented in the scene graph.
	 */
	private static class GefDiagramElement {
		public GefDiagramElement(final DiagramElement diagramElement) {
			this.diagramElement = diagramElement;
		}

		final DiagramElement diagramElement;

		/**
		 * The node which represents the diagram element in the scene graph
		 */
		Node sceneNode;

		/**
		 * The graphic which was used to create the current scene node.
		 */
		Graphic sourceGraphic;

		/**
		 * The scene node which represents the diagram element's parent in the scene graph
		 */
		Node parentDiagramNodeSceneNode;

		LabelNode primaryLabel;
		LabelNode annotationLabel;
	}

	/**
	 * The diagram for which nodes are created and updated.
	 */
	private final AgeDiagram diagram;

	/**
	 * A mapping between the diagram elements and {@link GefDiagramElement} instances.
	 */
	private final Map<DiagramElement, GefDiagramElement> diagramElementToGefDiagramElementMap = new HashMap<>();

	/**
	 * A mapping between scene nodes and {@link GefDiagramElement} instances.
	 */
	private final Map<Node, GefDiagramElement> sceneNodeToGefDiagramElementMap = new HashMap<>();

	/**
	 * Root node which contains all the shape and connection nodes for the diagram.
	 */
	private final DiagramRootNode diagramNode = new DiagramRootNode();

	/**
	 * Image manager for the images referenced by the diagram.
	 */
	private final ImageManager imageManager = new ImageManager(path -> {
		final IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
		final IResource imageResource = workspaceRoot.findMember(path.toString());
		return imageResource == null ? null : imageResource.getRawLocation().makeAbsolute().toFile();
	});

	/**
	 * Converter for converting the renderer independent {@link Style} tp {@link FxStyle}.
	 */
	private final StyleToFx styleToFx = new StyleToFx(imageManager);

	/**
	 * Service for determining whether the style for a diagram element should be overridden.
	 */
	private ColoringService coloringService;

	/**
	 * True if the diagram is currently being updated based on the position and size of scene graph nodes. Avoids changing
	 * the scene graph nodes based on such changes to the diagram.
	 */
	private boolean updatingDiagramFromSceneGraph = false;

	/**
	 * Listener for modifications to the {@link AgeDiagram}. Updates scene graph when the diagram changes.
	 */
	private DiagramModificationListener modificationListener = new DiagramModificationListener() {
		private boolean inBeforeModificationsCompleted = false;
		private boolean needFullUpdate = false;
		private final Set<DiagramElement> elementsToUpdate = new LinkedHashSet<>();
		private final Set<DiagramElement> elementsToRemove = new LinkedHashSet<>();

		@Override
		public void diagramConfigurationChanged(final DiagramConfigurationChangedEvent e) {
			needFullUpdate = true;
		}

		@Override
		public void elementAdded(final ElementAddedEvent e) {
			if (!inBeforeModificationsCompleted) {
				onElementAdded(e.element);
			}
		}

		private void onElementAdded(final DiagramElement element) {
			elementsToRemove.remove(element);
			needFullUpdate = true;
			elementsToUpdate.clear(); // Clear all elements to update. They will not be processed if an element has been added.
		}

		@Override
		public void elementRemoved(final ElementRemovedEvent e) {
			if (!inBeforeModificationsCompleted) {
				elementsToRemove.add(e.element);
				elementsToUpdate.remove(e.element);
			}
		}

		@Override
		public void elementUpdated(final ElementUpdatedEvent e) {
			// If a full update is going to be performed, don't track the elements that have been updated
			// Additionally, if the element has been removed, don't store it as an update.
			if (!needFullUpdate && e.element.getGraphicalConfiguration() != null && !inBeforeModificationsCompleted
					&& !updatingDiagramFromSceneGraph && !elementsToRemove.contains(e.element)) {
					// If the element is already in the elements to update set, remove it so that it will be inserted at the end of the set
					if (elementsToUpdate.contains(e.element)) {
						elementsToUpdate.remove(e.element);
					}

					elementsToUpdate.add(e.element);
			}
		}

		@Override
		public void beforeModificationsCompleted(final BeforeModificationsCompletedEvent e) {
			if (updatingDiagramFromSceneGraph) {
				return;
			}

			// Ensure that the modification is executed in the user interface thread.
			Display.getDefault().syncExec(() -> {
				try {
					inBeforeModificationsCompleted = true;

					// Remove elements
					for (final DiagramElement de : elementsToRemove) {
						// Remove any contained connections first. Connections are stored at the diagram level so they need to be
						// deleted individually.
						removeContainedConnections(de);

						final GefDiagramElement ge = diagramElementToGefDiagramElementMap.get(de);
						if (ge != null && ge.sceneNode != null) {
							removeNode(ge.sceneNode);
						}

						// Remove the mapping
						removeMappingForBranch(de);
					}

					if (needFullUpdate) {
						updateSceneGraph();
					} else {
						// Refresh a new style provider to prepare to apply styles to updated elements
						final StyleProvider styleProvider = createStyleProvider();

						// Ensure that the scene graph nodes have been created and/or switched to the appropriate type
						boolean refreshConnections = false;
						for (final DiagramElement de : elementsToUpdate) {
							final GefDiagramElement ge = diagramElementToGefDiagramElementMap.get(de);
							if (ge != null) {
								final Node originalSceneNode = ge.sceneNode;
								ensureSceneNodeExists(ge, ge.parentDiagramNodeSceneNode);
								refreshConnections = refreshConnections || (originalSceneNode != ge.sceneNode);
							}
						}

						// Update modified elements
						for (final DiagramElement de : elementsToUpdate) {
							final GefDiagramElement ge = diagramElementToGefDiagramElementMap.get(de);
							updateSceneNode(ge);
							calculateAndApplyStyle(ge, styleProvider);
						}

						// Force connections to refresh when updating an element results in the scene node being
						// recreated. This will update the referenced anchors of all connections.
						if (refreshConnections) {
							for (final GefDiagramElement ge : diagramElementToGefDiagramElementMap.values()) {
								if (ge.sceneNode instanceof BaseConnectionNode) {
									updateSceneNode(ge);
								}
							}
						}
					}

					// Forces the scene graph layout and applies those changes to the diagram
					updateDiagramFromSceneGraph();

					// Force connections to refresh. This works around cases where a diagram element is moved
					// which causes an anchor to move and the connection itself isn't refreshed
					// automatically.
					for (final GefDiagramElement ge : diagramElementToGefDiagramElementMap.values()) {
						if (ge.sceneNode instanceof BaseConnectionNode) {
							final BaseConnectionNode cn = (BaseConnectionNode) ge.sceneNode;
							cn.refresh();
						}
					}

				} finally {
					inBeforeModificationsCompleted = false;
				}
			});
		}

		@Override
		public void modificationsCompleted(final ModificationsCompletedEvent e) {
			needFullUpdate = false;
			elementsToRemove.clear();
			elementsToUpdate.clear();
		}

		/**
		 * Removes the mapping for the specified diagram node and all children
		 * @param dn
		 */
		private void removeMappingForBranch(final DiagramElement de) {
			// Remove mapping for children
			for (final DiagramElement child : de.getChildren()) {
				removeMappingForBranch(child);
			}

			// Remove mapping for the element itself
			final GefDiagramElement removed = diagramElementToGefDiagramElementMap.remove(de);
			if (removed != null && removed.sceneNode != null) {
				sceneNodeToGefDiagramElementMap.remove(removed.sceneNode);
			}
		}

		/**
		 * Removes all connections contained in the specified element or its descendants.
		 */
		private void removeContainedConnections(final DiagramElement e) {
			for (final DiagramElement childDiagramElement : e.getChildren()) {
				final GefDiagramElement childGefDiagramElement = diagramElementToGefDiagramElementMap
						.get(childDiagramElement);
				removeContainedConnections(childDiagramElement);

				if (childGefDiagramElement != null && childGefDiagramElement.sceneNode instanceof BaseConnectionNode) {
					removeNode(childGefDiagramElement.sceneNode);
				}
			}
		}
	};

	/**
	 * Creates a new instance
	 * @param diagram the diagram for which JavaFX node will be created and updated.
	 * @param coloringService service to use to support determining the final style of nodes.
	 */
	public GefAgeDiagram(final AgeDiagram diagram, final ColoringService coloringService) {
		this.coloringService = Objects.requireNonNull(coloringService, "coloringService must not be null");
		this.diagram = Objects.requireNonNull(diagram, "diagram must not be null");

		updateSceneGraph();

		// Register our modification listener to update the scene graph based on changes
		diagram.addModificationListener(modificationListener);
	}

	@Override
	public void close() {
		coloringService = null;
		diagram.removeModificationListener(modificationListener);
		imageManager.close();
	}

	/**
	 * Finds the diagram element for a scene node. Only works for the node that serves as the root of the branch for the diagram element.
	 * @param sceneNode the node for which to find the diagram element
	 * @return the diagram element which the node represents or null if one cannot be found.
	 */
	public DiagramElement getDiagramElement(final Node sceneNode) {
		final GefDiagramElement ge = sceneNodeToGefDiagramElementMap.get(sceneNode);
		return ge == null ? null : ge.diagramElement;
	}

	/**
	 * Finds the scene node for a diagram element.
	 * @param diagramNode the diagram node for which to find the scene node
	 * @return the scene node which represents the diagram node or null if one cannot be found.
	 */
	public Node getSceneNode(final DiagramNode diagramNode) {
		if (diagramNode == diagram) {
			return this.diagramNode;
		}

		final GefDiagramElement ge = diagramElementToGefDiagramElementMap.get(diagramNode);
		return ge == null ? null : ge.sceneNode;
	}

	/**
	 * Check for changes in referenced images and updates the diagram as appropriate.
	 */
	public void refreshImages() {
		imageManager.refreshImages();
	}

	/**
	 * Performs a full update of the scene graph based on the diagram. Ensures nodes exist, that they are updated, and have appropriate styles.
	 */
	public void updateSceneGraph() {
		ensureSceneNodesExistForChildren(diagram, diagramNode);
		updateSceneNodesForChildren(diagram);
		refreshDiagramStyles();
	}

	/**
	 * Ensures the scene node exists for the children of a diagram node.
	 * Creates or recreates scene graph nodes and adds to the scene graph as necessary. Populates {@link #diagramElementToGefDiagramElementMap}.
	 * @param parentDiagramNode the diagram node for which scene nodes will be created for its children.
	 * @param parentDiagramNodeSceneNode the scene node for the parent diagram node
	 * @return the created or updated JavaFX node
	 */
	private void ensureSceneNodesExistForChildren(final DiagramNode parentDiagramNode,
			final Node parentDiagramNodeSceneNode) {
		for (final DiagramElement childDiagramElement : parentDiagramNode.getChildren()) {
			final GefDiagramElement childGefDiagramElement = diagramElementToGefDiagramElementMap
					.computeIfAbsent(childDiagramElement, e -> new GefDiagramElement(childDiagramElement));
			final Node childSceneNode = ensureSceneNodeExists(childGefDiagramElement, parentDiagramNodeSceneNode);
			ensureSceneNodesExistForChildren(childDiagramElement, childSceneNode);
		}
	}

	/**
	 * Ensures that a scene node exists for the specified GEF diagram element.
	 * Creates or recreates scene nodes and adds to the scene graph as necessary. Updates the specified GEF diagram element.
	 * @param gefDiagramElement the GEF diagram element for which to ensure that the scene node exists.
	 * @param parentDiagramElementSceneNode the scene node for the parent of the GEF diagram element. This is specified instead of using
	 * the value contained in the GEF diagram element because it may not be up to date.
	 * @return the scene node for the diagram element. This specified GEF diagram element will be updated to hold this value.
	 */
	private Node ensureSceneNodeExists(GefDiagramElement gefDiagramElement, final Node parentDiagramElementSceneNode) {
		Objects.requireNonNull(parentDiagramElementSceneNode, "parentDiagramElementScenenNode must not be null");
		final Graphic graphic = Objects.requireNonNull(gefDiagramElement.diagramElement.getGraphic(),
				"graphic must not be null");

		final DiagramElement childDiagramElement = gefDiagramElement.diagramElement;

		//
		// The following final variables determine the operations that needs to be performed by the remainder of the function.
		// They are set by comparing the previous state with the current state of the diagram element.
		//
		final boolean docked = childDiagramElement.getDockArea() != null;
		final boolean parentIsConnection = parentDiagramElementSceneNode instanceof BaseConnectionNode;
		final boolean create = !Objects.equals(graphic, gefDiagramElement.sourceGraphic)
				|| docked != gefDiagramElement.sceneNode instanceof DockedShape
				|| parentIsConnection != gefDiagramElement.parentDiagramNodeSceneNode instanceof BaseConnectionNode;
		final boolean addToScene = create
				|| gefDiagramElement.parentDiagramNodeSceneNode != parentDiagramElementSceneNode;
		final boolean removeFromScene = addToScene && gefDiagramElement.sceneNode != null;

		// Update other fields
		gefDiagramElement.sourceGraphic = graphic;

		// Remove the node for the scene graph
		if (removeFromScene) {
			removeNode(gefDiagramElement.sceneNode);
		}

		//
		// Create a new node for the diagram element
		//
		if (create) {
			// Remove mapping to old scene node
			if (gefDiagramElement.sceneNode != null) {
				sceneNodeToGefDiagramElementMap.remove(gefDiagramElement.sceneNode);
			}

			// Create the new node. Create a graphic and then a wrapper as appropriate
			final Node graphicNode = GraphicToFx.createNode(graphic);
			if (graphicNode instanceof BaseConnectionNode) {
				final BaseConnectionNode newConnectionNode = (BaseConnectionNode) graphicNode;
				gefDiagramElement.sceneNode = graphicNode;

				// Create the primary label node
				final LabelNode primaryLabel = new LabelNode();
				newConnectionNode.getPrimaryLabels().add(primaryLabel);
				gefDiagramElement.primaryLabel = primaryLabel;
			} else if (graphicNode instanceof LabelNode) {
				gefDiagramElement.sceneNode = graphicNode;
			} else if (parentIsConnection) {
				// NOTE: This should only occur for fixed sized graphics
				// Rotate midpoint decorations 180.0 degrees because our connection not expects midpoint decorations to be oriented as if
				// the connection was left to right and that is not how graphics are specified in the graphical editor.
				final Group rotationWrapper = new Group();
				rotationWrapper.getChildren().add(graphicNode);
				rotationWrapper.setRotate(180.0);
				gefDiagramElement.sceneNode = rotationWrapper;
			} else {
				if (docked) {
					final DockedShape newDockedShape = new DockedShape();
					newDockedShape.setGraphic(graphicNode);
					gefDiagramElement.sceneNode = newDockedShape;

					// Create the primary label node
					final LabelNode primaryLabel = new LabelNode();
					newDockedShape.getPrimaryLabels().add(primaryLabel);
					gefDiagramElement.primaryLabel = primaryLabel;

					// Create annotation node
					final LabelNode annotationLabel = new LabelNode();
					newDockedShape.getSecondaryLabels().add(annotationLabel);
					gefDiagramElement.annotationLabel = annotationLabel;
				} else {
					final ContainerShape newContainerShape = new ContainerShape();
					newContainerShape.setGraphic(graphicNode);
					gefDiagramElement.sceneNode = newContainerShape;

					// Create the primary label node
					final LabelNode primaryLabel = new LabelNode();
					newContainerShape.getPrimaryLabels().add(primaryLabel);
					gefDiagramElement.primaryLabel = primaryLabel;
				}
			}

			// Add mapping to scene node
			if (gefDiagramElement.sceneNode != null) {
				sceneNodeToGefDiagramElementMap.put(gefDiagramElement.sceneNode, gefDiagramElement);
			}

			StyleRoot.set(gefDiagramElement.sceneNode, true);
		}

		//
		// Add the node to the appropriate parent
		//
		if (addToScene) {
			if (gefDiagramElement.sceneNode instanceof BaseConnectionNode) {
				diagramNode.getChildren().add(gefDiagramElement.sceneNode);

				// Flow indicators are positioned relative to the scene node of the parent diagram element
				if (gefDiagramElement.sceneNode instanceof FlowIndicatorNode) {
					if (parentDiagramElementSceneNode instanceof ContainerShape) {
						((FlowIndicatorNode) gefDiagramElement.sceneNode)
								.setPositioningReference(parentDiagramElementSceneNode);
					} else {
						throw new AgeGefRuntimeException(
								"Unexpected parent diagram element scene node for flow indicator: "
										+ parentDiagramElementSceneNode);
					}
				}
			} else if (gefDiagramElement.sceneNode instanceof LabelNode) {
				// Add label to parent
				if (parentDiagramElementSceneNode instanceof ContainerShape) {
					((ContainerShape) parentDiagramElementSceneNode).getSecondaryLabels()
							.add(gefDiagramElement.sceneNode);
				} else if (parentDiagramElementSceneNode instanceof DockedShape) {
					((DockedShape) parentDiagramElementSceneNode).getSecondaryLabels().add(gefDiagramElement.sceneNode);
				} else if (parentDiagramElementSceneNode instanceof BaseConnectionNode) {
					((BaseConnectionNode) parentDiagramElementSceneNode).getSecondaryLabels()
							.add(gefDiagramElement.sceneNode);
				} else {
					throw new AgeGefRuntimeException(
							"Unexpected parent node for label: " + parentDiagramElementSceneNode);
				}
			} else if (parentIsConnection) {
				((BaseConnectionNode) parentDiagramElementSceneNode).getMidpointDecorations()
						.add(gefDiagramElement.sceneNode);
			} else {
				final DockArea dockArea = childDiagramElement.getDockArea();
				if (gefDiagramElement.sceneNode instanceof DockedShape) {
					final DockedShape dockedShape = (DockedShape) gefDiagramElement.sceneNode;

					// Add the docked shape to the appropriate list
					if (parentDiagramElementSceneNode instanceof ContainerShape) {
						final ContainerShape containerShapeParent = (ContainerShape) parentDiagramElementSceneNode;
						containerShapeParent.addOrUpdateDockedChild(dockedShape,
								GefAgeDiagramUtil.toDockSide(dockArea));
					} else if (parentDiagramElementSceneNode instanceof DockedShape) {
						final DockedShape dockedShapeParent = (DockedShape) parentDiagramElementSceneNode;
						dockedShapeParent.getNestedChildren().add(dockedShape);
					} else {
						throw new AgeGefRuntimeException(
								"Unexpected parent for docked shape: " + parentDiagramElementSceneNode);
					}
				} else {
					if (parentDiagramElementSceneNode instanceof ContainerShape) {
						final ContainerShape containerShapeParent = (ContainerShape) parentDiagramElementSceneNode;
						containerShapeParent.getFreeChildren().add(gefDiagramElement.sceneNode);
					} else if (parentDiagramElementSceneNode instanceof Group) {
						((Group) parentDiagramElementSceneNode).getChildren().add(gefDiagramElement.sceneNode);
					} else {
						throw new AgeGefRuntimeException(
								"Unexpected parent node for container shape: " + parentDiagramElementSceneNode);
					}
				}
			}

			gefDiagramElement.parentDiagramNodeSceneNode = parentDiagramElementSceneNode;
		}

		return gefDiagramElement.sceneNode;
	}

	private void removeNode(final Node node) {
		// Remove the old node from the scene graph. In order to remove the old node the parent is assumed to be a child of a Group
		final Parent graphParent = node.getParent();
		if (graphParent instanceof Group) {
			((Group) graphParent).getChildren().remove(node);
		} else {
			throw new AgeGefRuntimeException("Unexpected case. Parent is not a group. Child: " + node);
		}
	}

	/**
	 * Updates the properties of the scene nodes for the children of the specified diagram node.
	 * Only updates properties which do not effect the structure of the scene graph. Recursive
	 * @param parentDiagramNode the diagram node for which scene nodes associated with its children will be updated.
	 */
	private void updateSceneNodesForChildren(final DiagramNode parentDiagramNode) {
		for (final DiagramElement childDiagramElement : parentDiagramNode.getChildren()) {
			updateSceneNode(diagramElementToGefDiagramElementMap.get(childDiagramElement));
			updateSceneNodesForChildren(childDiagramElement);
		}
	}

	/**
	 * Updates the scene nodes related to the specified GEF diagram element based on the diagram element.
	 * Only updates properties which do not effect the structure of the scene graph. Not-recursive
	 * @param gefDiagramElement is the GEF diagram element for which scene nodes will be updated.
	 */
	private void updateSceneNode(final GefDiagramElement gefDiagramElement) {
		final DiagramElement diagramElement = gefDiagramElement.diagramElement;
		final Node sceneNode = gefDiagramElement.sceneNode;

		// Update connections
		if (sceneNode instanceof BaseConnectionNode) {
			final BaseConnectionNode connectionNode = (BaseConnectionNode) sceneNode;
			final Point controlPointOrigin = getControlPointOriginFromDiagram(diagramElement, sceneNode);
			if (sceneNode instanceof FlowIndicatorNode) {
				PreferredPosition.set(sceneNode, convertPoint(diagramElement.getPosition()));
			}

			// Update the connection anchor
			updateConnectionAnchors(diagramElement, (BaseConnectionNode) sceneNode);

			// Set control points. Coordinates are specified in the diagram model relative to the diagram. The need to be specified relative to the
			// connection position. For regular connection this is the same because the node's parent is the diagram node.
			// However, flow indicators have a position and have parent nodes other than the diagram.
			connectionNode.getInnerConnection()
					.setControlPoints(diagramElement.getBendpoints()
							.stream()
							.map(p -> new org.eclipse.gef.geometry.planar.Point(p.x - controlPointOrigin.x,
									p.y - controlPointOrigin.y))
							.collect(Collectors.toList()));

			PreferredPosition.set(gefDiagramElement.primaryLabel,
					convertPoint(diagramElement.getConnectionPrimaryLabelPosition()));

		} else if (sceneNode instanceof LabelNode) {
			// Such a label represents a secondary label
			final LabelNode label = (LabelNode) sceneNode;
			label.setText(Strings.nullToEmpty(diagramElement.getLabelName()));
			setLabelVisibility(label);

			// Update element position
			if (gefDiagramElement.parentDiagramNodeSceneNode instanceof BaseConnectionNode) {
				PreferredPosition.set(label, convertPoint(diagramElement.getPosition()));
			}
		} else if (sceneNode instanceof ContainerShape) {
			final ContainerShape containerShape = (ContainerShape) sceneNode;

			PreferredPosition.set(sceneNode, convertPoint(diagramElement.getPosition()));

			// Set configured size
			final Dimension size = diagramElement.getSize();
			if (size == null) {
				containerShape.setConfiguredWidth(ContainerShape.NOT_SPECIFIED);
				containerShape.setConfiguredHeight(ContainerShape.NOT_SPECIFIED);
			} else {
				containerShape.setConfiguredWidth(size.width);
				containerShape.setConfiguredHeight(size.height);
			}
		} else if (sceneNode instanceof DockedShape) {
			final DockedShape n = (DockedShape) sceneNode;

			PreferredPosition.set(sceneNode, convertPoint(diagramElement.getPosition()));

			// Set configured size
			final Dimension size = diagramElement.getSize();
			if (size == null) {
				n.setConfiguredWidth(ContainerShape.NOT_SPECIFIED);
				n.setConfiguredHeight(ContainerShape.NOT_SPECIFIED);
			} else {
				n.setConfiguredWidth(size.width);
				n.setConfiguredHeight(size.height);
			}

			final DockArea dockArea = diagramElement.getDockArea();
			if (dockArea != null && dockArea != DockArea.GROUP
					&& gefDiagramElement.parentDiagramNodeSceneNode instanceof ContainerShape) {
				final DockSide side = GefAgeDiagramUtil.toDockSide(dockArea);
				final ContainerShape cs = (ContainerShape) gefDiagramElement.parentDiagramNodeSceneNode;
				cs.addOrUpdateDockedChild(n, side);
			}
		}

		// Update the primary label
		if (gefDiagramElement.primaryLabel != null) {
			gefDiagramElement.primaryLabel.setText(getPrimaryLabelText(diagramElement));
			setLabelVisibility(gefDiagramElement.primaryLabel);
			gefDiagramElement.primaryLabel
					.setWrapText(diagramElement.getGraphicalConfiguration().isPrimaryLabelIsMultiline());
		}

		// Update the secondary label
		if (gefDiagramElement.annotationLabel != null) {
			final String annotation = diagramElement.getGraphicalConfiguration().getAnnotation();
			gefDiagramElement.annotationLabel.setText(Strings.nullToEmpty(annotation));
			setLabelVisibility(gefDiagramElement.annotationLabel);
		}
	}

	private void updateConnectionAnchors(final DiagramElement de, final BaseConnectionNode node) {
		if (node instanceof FlowIndicatorNode) {
			final FlowIndicatorNode fi = (FlowIndicatorNode) node;
			final IAnchor anchor = GefAgeDiagramUtil.getAnchor(this, de.getStartElement(), null);
			if (anchor != null) {
				fi.setStartAnchor(anchor);
			}
		} else if (node instanceof ConnectionNode) {
			final ConnectionNode cn = (ConnectionNode) node;
			final IAnchor startAnchor = GefAgeDiagramUtil.getAnchor(this, de.getStartElement(), de.getEndElement());
			final IAnchor endAnchor = GefAgeDiagramUtil.getAnchor(this, de.getEndElement(), de.getStartElement());
			if (startAnchor != null && endAnchor != null) {
				cn.setStartAnchor(startAnchor);
				cn.setEndAnchor(endAnchor);
			}
		} else {
			throw new AgeGefRuntimeException("Unexpected node: " + node);
		}
	}

	/**
	 * Determines the text to use for the primary label.
	 * @param de the diagram element to determine the primary label text for
	 * @return the primary label text. Guaranteed to be non-null.
	 */
	private String getPrimaryLabelText(final DiagramElement de) {
		final String completenessSuffix = de.getCompleteness() == Completeness.INCOMPLETE ? INCOMPLETE_INDICATOR : "";
		final String labelName = de.getLabelName();
		return labelName == null ? "" : (labelName + completenessSuffix);
	}

	/**
	 * Refreshes the style of all elements in the diagram
	 */
	public void refreshDiagramStyles() {
		// Refresh Coloring
		final StyleProvider styleProvider = createStyleProvider();
		calculateAndApplyStylesForChildren(diagram, styleProvider);
	}

	/**
	 * Rebuilds the override style information using the {@link #refreshOverrideStyles()} and then calculates and applies the final style of the
	 * specified elements. Does not refresh the style of descendant diagram elements.
	 * If the override style information is up to date, it is better to call {@link #calculateAndApplyStyle} for each element
	 * @param elements the element for which to refresh the style.
	 */
	public void refreshStyle(final Collection<DiagramElement> elements) {
		final StyleProvider styleProvider = createStyleProvider();
		for (final DiagramElement de : elements) {
			calculateAndApplyStyle(diagramElementToGefDiagramElementMap.get(de), styleProvider);
		}
	}

	/**
	 * Determines the final style and applies it to the diagram element
	 * Assumes override style information has been updated using {@link #refreshOverrideStyles()}
	 * @param gefDiagramElement the GEF diagram element for which to refresh the style
	 * @param styleProvider the style provider which will be used to determine the final style for the diagram element
	 */
	private void calculateAndApplyStyle(final GefDiagramElement gefDiagramElement,
			final StyleProvider styleProvider) {
		if (gefDiagramElement != null && gefDiagramElement.sceneNode != null) {
			final Style style = styleProvider.getStyle(gefDiagramElement.diagramElement);
			final FxStyle fxStyle = styleToFx.createStyle(style);
			FxStyleApplier.applyStyle(gefDiagramElement.sceneNode, fxStyle);
		}
	}

	/**
	 * Visits the children of the specified diagram node and their descendants and calculates and applies the style
	 * Assumes override style information has been updated using {@link #refreshOverrideStyles()}
	 * using {@link #calculateAndApplyStyle(DiagramElement)}.
	 * @param n the node to calculate apply the style for
	 * @param styleProvider the style provider which will be used to determine the final style for the diagram elements
	 */
	private void calculateAndApplyStylesForChildren(final DiagramNode n, final StyleProvider styleProvider) {
		for (final DiagramElement childDiagramElement : n.getChildren()) {
			calculateAndApplyStylesForChildren(childDiagramElement, styleProvider);
			calculateAndApplyStyle(diagramElementToGefDiagramElementMap.get(childDiagramElement), styleProvider);
		}
	}

	/**
	 * Returns a new style provider which will return the appropriate style for the diagram element. The style provider will use colors from the
	 * coloring service as appropriate to determine styles.
	 * @return the new style provider
	 */
	private StyleProvider createStyleProvider() {
		final Map<DiagramElement, Style> overrideStyles;
		if (coloringService == null) {
			overrideStyles = Collections.emptyMap();
		} else {
			overrideStyles = coloringService.buildForegroundColorMap()
					.entrySet()
					.stream()
					.collect(Collectors.toMap(Entry::getKey,
							v -> StyleBuilder.create().foregroundColor(v.getValue()).build()));
		}

		return new StyleCalculator(diagram.getConfiguration(), de -> overrideStyles.get(de));
	};

	/**
	 * Triggers an immediate layout
	 */
	private void forceSceneGraphLayout() {
		diagramNode.applyCss();
		diagramNode.layout();
	}

	/**
	 * Triggers a layout of the scene graph nodes and then updates the diagram based on the layout of the scene graph nodes.
	 * Updates position, size, and bendpoints from the scene graph.
	 * Should only be called after the root node has been added to a scene.
	 */
	public void updateDiagramFromSceneGraph() {
		updateDiagramFromSceneGraph(true);
	}

	/**
	 * Triggers a layout of the scene graph nodes and then updates the diagram based on the layout of the scene graph nodes.
	 * Updates position and size. Optionally updates bendpoints.
	 * Should only be called after the root node has been added to a scene.
	 * @param updateBendpoints whether to update bendpoints in addition to the position and size of elements.
	 */
	public void updateDiagramFromSceneGraph(final boolean updateBendpoints) {
		updatingDiagramFromSceneGraph = true;
		forceSceneGraphLayout();
		diagram.modify("Update Diagram from Scene Graph", m -> {
			for (final Entry<DiagramElement, GefDiagramElement> e : this.diagramElementToGefDiagramElementMap
					.entrySet()) {
				final DiagramElement de = e.getKey();
				final GefDiagramElement ge = e.getValue();
				final Node sceneNode = ge.sceneNode;

				final DiagramNode parent = de.getParent();
				if (DiagramElementPredicates.isMoveable(de)) {
					if (parent instanceof DiagramElement
							&& DiagramElementPredicates.isConnection((DiagramElement) parent)) {
						// Store the preferred position.
						final Point2D p = PreferredPosition.get(sceneNode);
						m.setPosition(de, GefAgeDiagramUtil.toAgePoint(p));
					} else {
						final double newX = sceneNode.getLayoutX();
						final double newY = sceneNode.getLayoutY();
						if (de.hasPosition() || (newX != 0.0 || newY != 0)) {
							m.setPosition(de, new Point(newX, newY));
						}
					}

					if (sceneNode instanceof DockedShape && ge.parentDiagramNodeSceneNode instanceof ContainerShape) {
						final DockedShape ds = (DockedShape) sceneNode;
						final DockSide side = ds.getSide();
						if (side != null) {
							m.setDockArea(de, GefAgeDiagramUtil.toDockArea(side));
						}
					}
				}

				// Set the size for all elements. Even for non-resizable elements, the layout engine uses the sizes in the diagram.
				// This is important for secondary labels of connections.
				final Bounds layoutBounds = sceneNode.getLayoutBounds();
				if (de.hasSize() || (layoutBounds.getWidth() != 0.0 || layoutBounds.getHeight() != 0)) {
					m.setSize(de, new Dimension(layoutBounds.getWidth(), layoutBounds.getHeight()));
				}

				if (DiagramElementPredicates.isConnection(de) && sceneNode instanceof BaseConnectionNode) {
					final BaseConnectionNode cn = (BaseConnectionNode) sceneNode;

					// Primary label position
					if (!cn.getPrimaryLabels().isEmpty()) {
						// Store the preferred position of the connection label
						final Node primaryLabel = cn.getPrimaryLabels().get(0);
						final Point2D p = PreferredPosition.get(primaryLabel);
						m.setConnectionPrimaryLabelPosition(de, GefAgeDiagramUtil.toAgePoint(p));
					}

					// Bendpoints
					if (updateBendpoints) {
						final List<org.eclipse.gef.geometry.planar.Point> controlPoints = cn.getInnerConnection()
								.getControlPoints();
						if (!controlPoints.isEmpty() || !de.getBendpoints().isEmpty()) {
							final Point controlPointOrigin = getControlPointOriginFromSceneGraph(sceneNode);
							m.setBendpoints(de,
									cn.getInnerConnection()
											.getControlPoints()
											.stream()
											.map(p -> new Point(p.x + controlPointOrigin.x, p.y + controlPointOrigin.y))
											.collect(Collectors.toList()));
						}
					}
				}
			}
		});
		updatingDiagramFromSceneGraph = false;
	}

	/**
	 * Determines the origin of the control points to allow converting to absolute coordinates. This function should not be used
	 * when updating the diagram element because the containing diagram elements may not be updated.
	 * @param de the diagram element for the connection to get the control point origin.
	 * @param sceneNode the scene node for the specified diagram element
	 * @return the control point origin
	 */
	private Point getControlPointOriginFromDiagram(final DiagramElement de, final Node sceneNode) {
		if (sceneNode instanceof FlowIndicatorNode) {
			final Point parentPosition = DiagramElementLayoutUtil.getAbsolutePosition(de);
			return new Point(parentPosition.x + de.getX(), parentPosition.y + de.getY());
		} else {
			return Point.ZERO;
		}
	}

	/**
	 * Determines the origin of the control points to allow converting control points to absolute coordinates.
	 * @param sceneNode the scene node for the specified diagram element
	 * @return the control point origin
	 */
	private Point getControlPointOriginFromSceneGraph(final Node sceneNode) {
		if (sceneNode instanceof FlowIndicatorNode) {
			return GefAgeDiagramUtil.toAgePoint(diagramNode.getSceneToLocalTransform()
					.transform(sceneNode.getLocalToSceneTransform().transform(new Point2D(0, 0))));
		} else {
			return Point.ZERO;
		}
	}

	/**
	 * Return the JavaFX node for the diagram.
	 * @return the root scene graph node for the diagram.
	 */
	public DiagramRootNode getSceneNode() {
		return diagramNode;
	}

	/**
	 * Provides access to the diagram represented by the scene graph.
	 * @return the diagram which is represented by the scene graph.
	 */
	public AgeDiagram getDiagram() {
		return diagram;
	}

	/**
	 * Returns the scene graph node for a diagram element's primary label. If the diagram element does not have a primary label, but the root scene graph node
	 * for the diagram element is a label, then that node will be returned.
	 * @param de the diagram element for which to provide the primary label
	 * @return the scene graph node for the diagram element's primary label. Returns null if the label could not be retrieved.
	 */
	public LabelNode getPrimaryLabelSceneNode(final DiagramElement de) {
		final GefDiagramElement ge = diagramElementToGefDiagramElementMap.get(de);
		if (ge == null) {
			return null;
		}

		if (ge.primaryLabel == null) {
			if (ge.sceneNode.isManaged() && ge.sceneNode instanceof LabelNode) {
				return (LabelNode) ge.sceneNode;
			}
		} else {
			if (ge.primaryLabel.isManaged()) {
				return ge.primaryLabel;
			}
		}

		return null;
	}

	@Override
	public Dimension getPrimaryLabelSize(final DiagramElement de) {
		final LabelNode primaryLabel = getPrimaryLabelSceneNode(de);
		if (primaryLabel == null) {
			return Dimension.ZERO;
		}

		final double width = primaryLabel.computePrefWidth(-1);
		return new Dimension(width, primaryLabel.computePrefHeight(width));
	}

	@Override
	public Dimension getAnnotationLabelSize(final DiagramElement de) {
		final GefDiagramElement ge = diagramElementToGefDiagramElementMap.get(de);
		if (ge == null || ge.annotationLabel == null || !ge.annotationLabel.isManaged()) {
			return Dimension.ZERO;
		}

		return new Dimension(ge.annotationLabel.computePrefWidth(-1), ge.annotationLabel.computePrefHeight(-1));
	}

	@Override
	public Dimension getPortGraphicSize(final DiagramElement dockedElement) {
		final Graphic g = dockedElement.getGraphic();
		if (g instanceof FeatureGraphic) {
			final FeatureGraphic fg = (FeatureGraphic) g;
			if (fg.featureType == FeatureGraphicType.FEATURE_GROUP) {
				return FEATURE_GROUP_CIRCLE_SIZE;
			}
		}

		// Return the default feature size regardless of the type of graphic
		return DEFAULT_FEATURE_SIZE;
	}

	@Override
	public Dimension getDockedElementLabelsSize(final DiagramElement dockedDiagramElement) {
		final GefDiagramElement dockedGefDiagramElement = diagramElementToGefDiagramElementMap
				.get(dockedDiagramElement);
		if (dockedGefDiagramElement != null && dockedGefDiagramElement.sceneNode instanceof DockedShape) {
			final DockedShape ds = (DockedShape) dockedGefDiagramElement.sceneNode;
			return new Dimension(ds.getMaxPrefLabelWidth(), ds.getTotalLabelHeight());
		} else {
			return Dimension.ZERO;
		}
	}

	/**
	 * Sets whether the label is managed and visible based on whether the label is empty.
	 * @param label the label to update.
	 */
	private static void setLabelVisibility(final LabelNode label) {
		final boolean visible = !Strings.isNullOrEmpty(label.getText());
		label.setVisible(visible);
		label.setManaged(visible);
	}

	/**
	 * Converts a graphical editor point to a JavaFX point object.
	 * @param p the graphical editor point
	 * @return the JavaFX point.
	 */
	private static Point2D convertPoint(final Point p) {
		return p == null ? null : new Point2D(p.x, p.y);
	}
}