All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.sprotty.layout.ElkLayoutEngine Maven / Gradle / Ivy

/********************************************************************************
 * Copyright (c) 2017-2019 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/
package org.eclipse.sprotty.layout;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.eclipse.elk.core.IGraphLayoutEngine;
import org.eclipse.elk.core.RecursiveGraphLayoutEngine;
import org.eclipse.elk.core.data.ILayoutMetaDataProvider;
import org.eclipse.elk.core.data.LayoutMetaDataService;
import org.eclipse.elk.core.math.ElkPadding;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.elk.core.util.BasicProgressMonitor;
import org.eclipse.elk.core.util.ElkUtil;
import org.eclipse.elk.graph.ElkBendPoint;
import org.eclipse.elk.graph.ElkConnectableShape;
import org.eclipse.elk.graph.ElkEdge;
import org.eclipse.elk.graph.ElkEdgeSection;
import org.eclipse.elk.graph.ElkGraphElement;
import org.eclipse.elk.graph.ElkGraphFactory;
import org.eclipse.elk.graph.ElkLabel;
import org.eclipse.elk.graph.ElkNode;
import org.eclipse.elk.graph.ElkPort;
import org.eclipse.elk.graph.ElkShape;
import org.eclipse.elk.graph.properties.IProperty;
import org.eclipse.elk.graph.properties.Property;
import org.eclipse.elk.graph.util.ElkGraphUtil;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.sprotty.Action;
import org.eclipse.sprotty.BoundsAware;
import org.eclipse.sprotty.Dimension;
import org.eclipse.sprotty.EdgeLayoutable;
import org.eclipse.sprotty.ILayoutEngine;
import org.eclipse.sprotty.LayoutContainer;
import org.eclipse.sprotty.Point;
import org.eclipse.sprotty.SEdge;
import org.eclipse.sprotty.SGraph;
import org.eclipse.sprotty.SLabel;
import org.eclipse.sprotty.SModelElement;
import org.eclipse.sprotty.SModelRoot;
import org.eclipse.sprotty.SNode;
import org.eclipse.sprotty.SPort;

import com.google.common.collect.Maps;

/**
 * Layout engine that uses the Eclipse Layout Kernel (ELK).
 * 
 * 

The layout engine must be initialized once during the lifecycle of the application by calling * {@link #initialize(ILayoutMetaDataProvider...)}. The arguments of that method should be all meta data * providers of the layout algorithms that should be used by this layout engine, * e.g. org.eclipse.elk.alg.layered.options.LayeredMetaDataProvider.

*/ public class ElkLayoutEngine implements ILayoutEngine { public static final IProperty P_TYPE = new Property<>("org.eclipse.sprotty.layout.type"); public static void initialize(ILayoutMetaDataProvider ...providers) { LayoutMetaDataService.getInstance().registerLayoutMetaDataProviders(providers); } private IGraphLayoutEngine engine = new RecursiveGraphLayoutEngine(); protected final ElkGraphFactory factory = ElkGraphFactory.eINSTANCE; /** * Compute a layout for a graph. The default implementation uses only default settings for all layout * options (see layout options reference). * Override this in a subclass in order to customize the layout for your model using a * {@link SprottyLayoutConfigurator}. */ @Override public void layout(SModelRoot root, Action cause) { if (root instanceof SGraph) { layout((SGraph) root, null, cause); } } /** * Compute a layout for a graph with the given configurator (or {@code null} to use only default settings). */ public void layout(SGraph sgraph, SprottyLayoutConfigurator configurator, Action cause) { LayoutContext context = transformGraph(sgraph, cause); if (configurator != null) { ElkUtil.applyVisitors(context.elkGraph, configurator); } applyEngine(context.elkGraph); transferLayout(context); } /** * Transform a sprotty graph to an ELK graph, including all contents. */ protected LayoutContext transformGraph(SGraph sgraph, Action cause) { LayoutContext context = new LayoutContext(cause); context.sgraph = sgraph; ElkNode rootNode = createGraph(sgraph, context); context.elkGraph = rootNode; context.shapeMap.put(sgraph, rootNode); processChildren(sgraph, rootNode, context); resolveReferences(context); return context; } /** * Create a root ELK node for the given sprotty graph. */ protected ElkNode createGraph(SGraph sgraph, LayoutContext context) { ElkNode elkGraph = factory.createElkNode(); elkGraph.setIdentifier(SprottyLayoutConfigurator.toElkId(sgraph.getId())); elkGraph.setProperty(P_TYPE, sgraph.getType()); return elkGraph; } /** * Transform the children of a sprotty model element to their ELK graph counterparts. */ protected int processChildren(SModelElement sParent, ElkGraphElement elkParent, LayoutContext context) { int childrenCount = 0; if (sParent.getChildren() != null) { for (SModelElement schild : sParent.getChildren()) { context.parentMap.put(schild, sParent); ElkGraphElement elkChild = null; if (shouldInclude(schild, sParent, elkParent, context)) { if (schild instanceof SNode) { SNode snode = (SNode) schild; ElkNode elkNode = createNode(snode); if (elkParent instanceof ElkNode) { elkNode.setParent((ElkNode) elkParent); childrenCount++; } context.shapeMap.put(snode, elkNode); elkChild = elkNode; } else if (schild instanceof SPort) { SPort sport = (SPort) schild; ElkPort elkPort = createPort(sport); if (elkParent instanceof ElkNode) { elkPort.setParent((ElkNode) elkParent); childrenCount++; } context.shapeMap.put(sport, elkPort); elkChild = elkPort; } else if (schild instanceof SEdge) { SEdge sedge = (SEdge) schild; ElkEdge elkEdge = createEdge(sedge); // The most suitable container for the edge is determined later childrenCount++; context.edgeMap.put(sedge, elkEdge); elkChild = elkEdge; } else if (schild instanceof SLabel) { SLabel slabel = (SLabel) schild; ElkLabel elkLabel = createLabel(slabel); elkLabel.setParent(elkParent); childrenCount++; context.shapeMap.put(slabel, elkLabel); elkChild = elkLabel; } } int grandChildrenCount = processChildren(schild, elkChild != null ? elkChild : elkParent, context); childrenCount += grandChildrenCount; if (grandChildrenCount > 0 && sParent instanceof LayoutContainer && schild instanceof BoundsAware) { handleClientLayout((BoundsAware) schild, (LayoutContainer) sParent, elkParent, context); } } } return childrenCount; } /** * Return true if the given model element should be included in the layout computation. */ protected boolean shouldInclude(SModelElement element, SModelElement sParent, ElkGraphElement elkParent, LayoutContext context) { if (element instanceof SNode || element instanceof SPort) // Nodes and ports can only be contained in a node return elkParent instanceof ElkNode; else if (element instanceof SEdge) // Edges are automatically put into their most suitable container return true; else if (sParent instanceof LayoutContainer) { // If the parent has configured a client layout, we ignore its direct children in the server layout String layout = ((LayoutContainer) sParent).getLayout(); if (layout != null && !layout.isEmpty()) return false; } else if (element instanceof EdgeLayoutable && ((EdgeLayoutable)element).getEdgePlacement() != null) return false; return true; } /** * Consider the layout computed by the client by configuring appropriate ELK layout options. */ protected void handleClientLayout(BoundsAware element, LayoutContainer sParent, ElkGraphElement elkParent, LayoutContext context) { String layout = sParent.getLayout(); if (layout != null && !layout.isEmpty()) { Point position = element.getPosition(); if (position == null) position = new Point(); Dimension size = element.getSize(); if (size == null) size = new Dimension(); ElkPadding padding = new ElkPadding(); padding.setLeft(position.getX()); padding.setTop(position.getY()); if (sParent instanceof BoundsAware) { Dimension parentSize = ((BoundsAware) sParent).getSize(); if (parentSize != null) { padding.setRight(parentSize.getWidth() - position.getX() - size.getWidth()); padding.setBottom(parentSize.getHeight() - position.getY() - size.getHeight()); } } if (elkParent.hasProperty(CoreOptions.PADDING)) { // Add the previously computed padding to the current one. // NOTE: This makes sense only if there are multiple _nested_ layouting containers of which the deepest // one contains actual graph elements. Multiple compartments that contain graph elements but are // not nested into each other cannot be mapped properly to the ELK graph format. padding.add(elkParent.getProperty(CoreOptions.PADDING)); } elkParent.setProperty(CoreOptions.PADDING, padding); } } /** * Resolve cross-references in the ELK graph. */ protected void resolveReferences(LayoutContext context) { Map id2NodeMap = Maps.newHashMapWithExpectedSize(context.shapeMap.size()); for (Map.Entry entry : context.shapeMap.entrySet()) { String id = entry.getKey().getId(); if (id != null && entry.getValue() instanceof ElkConnectableShape) id2NodeMap.put(id, (ElkConnectableShape) entry.getValue()); } for (Map.Entry entry : context.edgeMap.entrySet()) { resolveReferences(entry.getValue(), entry.getKey(), id2NodeMap, context); } } /** * Resolve the source and target cross-references for the given ELK edge. */ protected void resolveReferences(ElkEdge elkEdge, SEdge sedge, Map id2NodeMap, LayoutContext context) { ElkConnectableShape source = id2NodeMap.get(sedge.getSourceId()); ElkConnectableShape target = id2NodeMap.get(sedge.getTargetId()); if (source != null && target != null) { elkEdge.getSources().add(source); elkEdge.getTargets().add(target); ElkNode container = ElkGraphUtil.findBestEdgeContainment(elkEdge); if (container != null) elkEdge.setContainingNode(container); else elkEdge.setContainingNode(context.elkGraph); } } /** * Create an ELK node for the given sprotty node. */ protected ElkNode createNode(SNode snode) { ElkNode elkNode = factory.createElkNode(); elkNode.setIdentifier(SprottyLayoutConfigurator.toElkId(snode.getId())); elkNode.setProperty(P_TYPE, snode.getType()); applyBounds(snode, elkNode); return elkNode; } /** * Create an ELK port for the given sprotty port. */ protected ElkPort createPort(SPort sport) { ElkPort elkPort = factory.createElkPort(); elkPort.setIdentifier(SprottyLayoutConfigurator.toElkId(sport.getId())); elkPort.setProperty(P_TYPE, sport.getType()); applyBounds(sport, elkPort); return elkPort; } /** * Create an ELK edge for the given sprotty edge. */ protected ElkEdge createEdge(SEdge sedge) { ElkEdge elkEdge = factory.createElkEdge(); elkEdge.setIdentifier(SprottyLayoutConfigurator.toElkId(sedge.getId())); elkEdge.setProperty(P_TYPE, sedge.getType()); // The source and target of the edge are resolved later return elkEdge; } /** * Create an ELK label for the given sprotty label. */ protected ElkLabel createLabel(SLabel slabel) { ElkLabel elkLabel = factory.createElkLabel(); elkLabel.setIdentifier(SprottyLayoutConfigurator.toElkId(slabel.getId())); elkLabel.setProperty(P_TYPE, slabel.getType()); elkLabel.setText(slabel.getText()); applyBounds(slabel, elkLabel); return elkLabel; } /** * Apply the bounds of the given bounds-aware element to an ELK shape (node, port, or label). */ protected void applyBounds(BoundsAware bounds, ElkShape elkShape) { Point position = bounds.getPosition(); if (position != null) { elkShape.setX(position.getX()); elkShape.setY(position.getY()); } Dimension size = bounds.getSize(); if (size != null) { if (size.getWidth() >= 0) elkShape.setWidth(size.getWidth()); if (size.getHeight() >= 0) elkShape.setHeight(size.getHeight()); } } /** * Set the graph layout engine to invoke in {@link #applyEngine(ElkNode)}. The default is * the {@link RecursiveGraphLayoutEngine}, which determines the layout algorithm to apply to each * composite node based on the {@link org.eclipse.elk.core.options.CoreOptions#ALGORITHM} option. * This requires the meta data providers of the referenced algorithms to be registered * using {@link #initialize(ILayoutMetaDataProvider...)} before any layout is performed, e.g. on * application start. Alternatively, you can use a specific layout algorithm directly, e.g. * org.eclipse.elk.alg.layered.LayeredLayoutProvider. */ public void setEngine(IGraphLayoutEngine engine) { if (engine == null) throw new NullPointerException(); this.engine = engine; } public IGraphLayoutEngine getEngine() { return engine; } /** * Apply the layout engine that has been configured with {@link #setEngine(IGraphLayoutEngine)}. */ protected void applyEngine(ElkNode elkGraph) { getEngine().layout(elkGraph, new BasicProgressMonitor()); } /** * Transfer the computed ELK layout back to the original sprotty graph. */ protected void transferLayout(LayoutContext context) { transferLayout(context.sgraph, context); } /** * Apply the computed ELK layout to the given model element. */ protected void transferLayout(SModelElement element, LayoutContext context) { if (element instanceof SGraph) { transferGraphLayout((SGraph) element, context.elkGraph, context); } else if (element instanceof SNode) { SNode snode = (SNode) element; ElkNode elkNode = (ElkNode) context.shapeMap.get(snode); if (elkNode != null) transferNodeLayout(snode, elkNode, context); } else if (element instanceof SPort) { SPort sport = (SPort) element; ElkPort elkPort = (ElkPort) context.shapeMap.get(sport); if (elkPort != null) transferPortLayout(sport, elkPort, context); } else if (element instanceof SEdge) { SEdge sedge = (SEdge) element; ElkEdge elkEdge = context.edgeMap.get(sedge); if (elkEdge != null) transferEdgeLayout(sedge, elkEdge, context); } else if (element instanceof SLabel) { SLabel slabel = (SLabel) element; ElkLabel elkLabel = (ElkLabel) context.shapeMap.get(slabel); if (elkLabel != null) transferLabelLayout(slabel, elkLabel, context); } if (element.getChildren() != null) { for (SModelElement child: element.getChildren()) { transferLayout(child, context); } } } /** * Apply the computed ELK layout to the given sprotty graph. */ protected void transferGraphLayout(SGraph sgraph, ElkNode elkGraph, LayoutContext context) { sgraph.setPosition(new Point(elkGraph.getX(), elkGraph.getY())); sgraph.setSize(new Dimension(elkGraph.getWidth(), elkGraph.getHeight())); } /** * Apply the computed ELK layout to the given sprotty node. */ protected void transferNodeLayout(SNode snode, ElkNode elkNode, LayoutContext context) { Point offset = getOffset(snode, elkNode, context); snode.setPosition(new Point(elkNode.getX() + offset.getX(), elkNode.getY() + offset.getY())); snode.setSize(new Dimension(elkNode.getWidth(), elkNode.getHeight())); } /** * Apply the computed ELK layout to the given sprotty port. */ protected void transferPortLayout(SPort sport, ElkPort elkPort, LayoutContext context) { Point offset = getOffset(sport, elkPort, context); sport.setPosition(new Point(elkPort.getX() + offset.getX(), elkPort.getY() + offset.getY())); sport.setSize(new Dimension(elkPort.getWidth(), elkPort.getHeight())); } /** * Apply the computed ELK layout to the given sprotty label. */ protected void transferLabelLayout(SLabel slabel, ElkLabel elkLabel, LayoutContext context) { Point offset = getOffset(slabel, elkLabel, context); slabel.setPosition(new Point(elkLabel.getX() + offset.getX(), elkLabel.getY() + offset.getY())); slabel.setSize(new Dimension(elkLabel.getWidth(), elkLabel.getHeight())); } /** * Apply the computed ELK layout to the given sprotty edge. */ protected void transferEdgeLayout(SEdge sedge, ElkEdge elkEdge, LayoutContext context) { if (!elkEdge.getSections().isEmpty()) { Point offset = getOffset(sedge, elkEdge, context); ElkEdgeSection section = elkEdge.getSections().get(0); List routingPoints = new ArrayList<>(); Point p1 = new Point(section.getStartX() + offset.getX(), section.getStartY() + offset.getY()); routingPoints.add(p1); for (ElkBendPoint bendPoint : section.getBendPoints()) { Point p2 = new Point(bendPoint.getX() + offset.getX(), bendPoint.getY() + offset.getY()); routingPoints.add(p2); } Point p3 = new Point(section.getEndX() + offset.getX(), section.getEndY() + offset.getY()); routingPoints.add(p3); sedge.setRoutingPoints(routingPoints); } } /** * Compute the offset for applying a computed ELK layout to a sprotty model element. Such an offset can * occur when the two elements are put into containers with different coordinate systems. */ protected Point getOffset(SModelElement selem, ElkGraphElement elkElem, LayoutContext context) { // Build a list of parents of the sprotty model element LinkedList sParents = null; SModelElement currentSParent = selem; while (currentSParent != null) { currentSParent = context.parentMap.get(currentSParent); if (currentSParent != null) { ElkShape shapeForSParent = context.shapeMap.get(currentSParent); if (shapeForSParent == elkElem.eContainer()) { // Shortcut: the current sprotty parent matches the ELK container double x = 0, y = 0; if (sParents != null) { for (SModelElement sParent : sParents) { if (sParent instanceof BoundsAware) { Point position = ((BoundsAware) sParent).getPosition(); if (position != null) { x -= position.getX(); y -= position.getY(); } } } } return new Point(x, y); } if (sParents == null) sParents = new LinkedList<>(); sParents.addFirst(currentSParent); } } // Build a list of parents of the ELK graph element LinkedList elkParents = new LinkedList<>(); EObject currentElkParent = elkElem; while (currentElkParent != null) { currentElkParent = currentElkParent.eContainer(); if (currentElkParent != null) { elkParents.addFirst(currentElkParent); } } boolean foundMismatch = false; do { // Find the next sprotty parent that is connected to a shape ElkShape shapeForSParent = null; int nextSParentIndex = 0; while (shapeForSParent == null && nextSParentIndex < sParents.size()) { shapeForSParent = context.shapeMap.get(sParents.get(nextSParentIndex++)); } // Find the next ELK parent that is a shape ElkShape elkParentShape = null; while (elkParentShape == null && !elkParents.isEmpty()) { EObject elkParent = elkParents.getFirst(); if (elkParent instanceof ElkShape) elkParentShape = (ElkShape) elkParent; else elkParents.removeFirst(); } // Remove the current parents if they match if (shapeForSParent != null && shapeForSParent == elkParentShape) { for (int i = 0; i < nextSParentIndex; i++) { sParents.removeFirst(); } elkParents.removeFirst(); } else { foundMismatch = true; } } while (!foundMismatch && !sParents.isEmpty() && !elkParents.isEmpty()); double x = 0, y = 0; // Add the remaining ELK shapes to the offset for (EObject elkParent : elkParents) { if (elkParent instanceof ElkShape) { ElkShape elkShape = (ElkShape) elkParent; x += elkShape.getX(); y += elkShape.getY(); } } // Subtract the remaining sprotty shapes from the offset for (SModelElement sParent : sParents) { if (sParent instanceof BoundsAware) { Point position = ((BoundsAware) sParent).getPosition(); if (position != null) { x -= position.getX(); y -= position.getY(); } } } return new Point(x, y); } /** * Data required for applying the computed ELK layout to the original sprotty model. */ public static class LayoutContext { public SGraph sgraph; public ElkNode elkGraph; public final Map parentMap = Maps.newHashMap(); public final Map shapeMap = Maps.newLinkedHashMap(); public final Map edgeMap = Maps.newLinkedHashMap(); public final Action cause; public LayoutContext(Action cause) { this.cause = cause; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy